From f410d7e60dc86685f04de101e57cd3f356723815 Mon Sep 17 00:00:00 2001 From: packet-mover Date: Mon, 6 Apr 2026 12:38:43 +0200 Subject: [PATCH] refactor: streamline report format for large orgs - Compliant repos collapsed into clickable link list - Non-compliant repos collapsed, showing only failing rules as bullets - Clickable repo links (github.com/org/repo) everywhere - Overall compliance metric in header - Empty sections omitted - "No repos found" for empty results - Proper singular/plural ("1 repo" vs "2 repos") - Full golden-string tests with fixed timestamp Co-Authored-By: Claude Opus 4.6 (1M context) --- report.go | 93 ++++++++++++++---- report_test.go | 257 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 252 insertions(+), 98 deletions(-) diff --git a/report.go b/report.go index 3251b3c..ddcf563 100644 --- a/report.go +++ b/report.go @@ -8,26 +8,72 @@ import ( ) // GenerateReport produces a Markdown compliance report from scan results. -// The format matches the specification in README.md. func GenerateReport(org string, results []RepoResult) string { + return generateReport(org, results, time.Now()) +} + +func generateReport(org string, results []RepoResult, now time.Time) string { var b strings.Builder + compliant, nonCompliant := splitByCompliance(results) + b.WriteString("# Codatus - Org Compliance Report\n\n") fmt.Fprintf(&b, "**Org:** %s\n", org) - fmt.Fprintf(&b, "**Scanned:** %s\n", time.Now().UTC().Format("2006-01-02 15:04 UTC")) + fmt.Fprintf(&b, "**Scanned:** %s\n", now.UTC().Format("2006-01-02 15:04 UTC")) fmt.Fprintf(&b, "**Repos scanned:** %d\n", len(results)) + if len(results) > 0 { + fmt.Fprintf(&b, "**Compliant:** %d/%d (%d%%)\n", len(compliant), len(results), len(compliant)*100/len(results)) + } + + if len(results) == 0 { + b.WriteString("\nNo repos found.\n") + return b.String() + } b.WriteString("\n## Summary\n\n") writeSummaryTable(&b, results) - b.WriteString("\n## Results by repository\n") - for _, rr := range results { - writeRepoTable(&b, rr) + if len(compliant) > 0 { + writeCompliantSection(&b, org, compliant) + } + + if len(nonCompliant) > 0 { + writeNonCompliantSection(&b, org, nonCompliant) } return b.String() } +func splitByCompliance(results []RepoResult) (compliant, nonCompliant []RepoResult) { + for _, rr := range results { + if isFullyCompliant(rr) { + compliant = append(compliant, rr) + } else { + nonCompliant = append(nonCompliant, rr) + } + } + return +} + +func isFullyCompliant(rr RepoResult) bool { + for _, r := range rr.Results { + if !r.Passed { + return false + } + } + return true +} + +func failingRules(rr RepoResult) []string { + var names []string + for _, r := range rr.Results { + if !r.Passed { + names = append(names, r.RuleName) + } + } + return names +} + type ruleSummary struct { name string passing int @@ -40,7 +86,6 @@ func writeSummaryTable(b *strings.Builder, results []RepoResult) { return } - // Aggregate pass/fail counts per rule name. ruleOrder := make([]string, 0) counts := make(map[string]*ruleSummary) @@ -60,7 +105,6 @@ func writeSummaryTable(b *strings.Builder, results []RepoResult) { } } - // Calculate pass rates. summaries := make([]ruleSummary, 0, len(ruleOrder)) total := len(results) for _, name := range ruleOrder { @@ -69,7 +113,6 @@ func writeSummaryTable(b *strings.Builder, results []RepoResult) { summaries = append(summaries, *s) } - // Sort by pass rate ascending (worst compliance first). sort.SliceStable(summaries, func(i, j int) bool { return summaries[i].passRate < summaries[j].passRate }) @@ -81,15 +124,31 @@ func writeSummaryTable(b *strings.Builder, results []RepoResult) { } } -func writeRepoTable(b *strings.Builder, rr RepoResult) { - fmt.Fprintf(b, "\n### %s\n\n", rr.RepoName) - b.WriteString("| Rule | Result |\n") - b.WriteString("|------|--------|\n") - for _, result := range rr.Results { - icon := "❌" - if result.Passed { - icon = "✅" +func pluralRepos(n int) string { + if n == 1 { + return "1 repo" + } + return fmt.Sprintf("%d repos", n) +} + +func writeCompliantSection(b *strings.Builder, org string, compliant []RepoResult) { + fmt.Fprintf(b, "\n## ✅ Fully compliant (%s)\n\n", pluralRepos(len(compliant))) + b.WriteString("
\nAll rules passing\n\n") + for _, rr := range compliant { + fmt.Fprintf(b, "[%s](https://github.com/%s/%s)\n", rr.RepoName, org, rr.RepoName) + } + b.WriteString("\n
\n") +} + +func writeNonCompliantSection(b *strings.Builder, org string, nonCompliant []RepoResult) { + fmt.Fprintf(b, "\n## ❌ Non-compliant (%s)\n\n", pluralRepos(len(nonCompliant))) + for _, rr := range nonCompliant { + failing := failingRules(rr) + fmt.Fprintf(b, "
\n%s - %d failing\n\n", + org, rr.RepoName, rr.RepoName, len(failing)) + for _, name := range failing { + fmt.Fprintf(b, "- %s\n", name) } - fmt.Fprintf(b, "| %s | %s |\n", result.RuleName, icon) + b.WriteString("\n
\n\n") } } diff --git a/report_test.go b/report_test.go index 0014862..fb0ccc5 100644 --- a/report_test.go +++ b/report_test.go @@ -1,120 +1,215 @@ package scanner import ( - "strings" "testing" + "time" ) -func TestGenerateReport_Structure(t *testing.T) { +var testTime = time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC) + +func TestGenerateReport_MixedCompliance(t *testing.T) { results := []RepoResult{ - { - RepoName: "alpha", - Results: []RuleResult{ - {RuleName: "Has repo description", Passed: true}, - }, - }, - { - RepoName: "beta", - Results: []RuleResult{ - {RuleName: "Has repo description", Passed: false}, - }, - }, + {RepoName: "alpha", Results: []RuleResult{ + {RuleName: "Has repo description", Passed: true}, + {RuleName: "Has .gitignore", Passed: true}, + }}, + {RepoName: "beta", Results: []RuleResult{ + {RuleName: "Has repo description", Passed: false}, + {RuleName: "Has .gitignore", Passed: true}, + }}, } - report := GenerateReport("test-org", results) + got := generateReport("test-org", results, testTime) - // Header - if !strings.Contains(report, "# Codatus - Org Compliance Report") { - t.Error("missing report title") - } - if !strings.Contains(report, "**Org:** test-org") { - t.Error("missing org name") - } - if !strings.Contains(report, "**Repos scanned:** 2") { - t.Error("missing repo count") - } + want := `# Codatus - Org Compliance Report - // Summary table - if !strings.Contains(report, "## Summary") { - t.Error("missing summary section") - } - if !strings.Contains(report, "| Has repo description | 1 | 1 | 50% |") { - t.Error("missing or incorrect summary row") +**Org:** test-org +**Scanned:** 2026-04-05 12:00 UTC +**Repos scanned:** 2 +**Compliant:** 1/2 (50%) + +## Summary + +| Rule | Passing | Failing | Pass rate | +|------|---------|---------|----------| +| Has repo description | 1 | 1 | 50% | +| Has .gitignore | 2 | 0 | 100% | + +## ✅ Fully compliant (1 repo) + +
+All rules passing + +[alpha](https://github.com/test-org/alpha) + +
+ +## ❌ Non-compliant (1 repo) + +
+beta - 1 failing + +- Has repo description + +
+ +` + if got != want { + t.Errorf("report mismatch.\n\nGOT:\n%s\n\nWANT:\n%s", got, want) } +} - // Per-repo tables - if !strings.Contains(report, "### alpha") { - t.Error("missing alpha repo section") +func TestGenerateReport_AllCompliant(t *testing.T) { + results := []RepoResult{ + {RepoName: "alpha", Results: []RuleResult{{RuleName: "Rule A", Passed: true}}}, + {RepoName: "beta", Results: []RuleResult{{RuleName: "Rule A", Passed: true}}}, } - if !strings.Contains(report, "### beta") { - t.Error("missing beta repo section") + + got := generateReport("my-org", results, testTime) + + want := `# Codatus - Org Compliance Report + +**Org:** my-org +**Scanned:** 2026-04-05 12:00 UTC +**Repos scanned:** 2 +**Compliant:** 2/2 (100%) + +## Summary + +| Rule | Passing | Failing | Pass rate | +|------|---------|---------|----------| +| Rule A | 2 | 0 | 100% | + +## ✅ Fully compliant (2 repos) + +
+All rules passing + +[alpha](https://github.com/my-org/alpha) +[beta](https://github.com/my-org/beta) + +
+` + if got != want { + t.Errorf("report mismatch.\n\nGOT:\n%s\n\nWANT:\n%s", got, want) } } -func TestGenerateReport_PassFailIcons(t *testing.T) { +func TestGenerateReport_AllNonCompliant(t *testing.T) { results := []RepoResult{ - { - RepoName: "my-repo", - Results: []RuleResult{ - {RuleName: "Has repo description", Passed: true}, - }, - }, + {RepoName: "repo-1", Results: []RuleResult{ + {RuleName: "Rule A", Passed: false}, + {RuleName: "Rule B", Passed: false}, + }}, + {RepoName: "repo-2", Results: []RuleResult{ + {RuleName: "Rule A", Passed: true}, + {RuleName: "Rule B", Passed: false}, + }}, } - report := GenerateReport("test-org", results) + got := generateReport("test-org", results, testTime) - if !strings.Contains(report, "| Has repo description | ✅ |") { - t.Error("expected ✅ for passing rule") - } + want := `# Codatus - Org Compliance Report + +**Org:** test-org +**Scanned:** 2026-04-05 12:00 UTC +**Repos scanned:** 2 +**Compliant:** 0/2 (0%) + +## Summary + +| Rule | Passing | Failing | Pass rate | +|------|---------|---------|----------| +| Rule B | 0 | 2 | 0% | +| Rule A | 1 | 1 | 50% | + +## ❌ Non-compliant (2 repos) + +
+repo-1 - 2 failing - results[0].Results[0].Passed = false - report = GenerateReport("test-org", results) +- Rule A +- Rule B - if !strings.Contains(report, "| Has repo description | ❌ |") { - t.Error("expected ❌ for failing rule") +
+ +
+repo-2 - 1 failing + +- Rule B + +
+ +` + if got != want { + t.Errorf("report mismatch.\n\nGOT:\n%s\n\nWANT:\n%s", got, want) } } func TestGenerateReport_SummarySortedByPassRateAscending(t *testing.T) { results := []RepoResult{ - { - RepoName: "repo-1", - Results: []RuleResult{ - {RuleName: "Rule A", Passed: true}, - {RuleName: "Rule B", Passed: false}, - }, - }, - { - RepoName: "repo-2", - Results: []RuleResult{ - {RuleName: "Rule A", Passed: true}, - {RuleName: "Rule B", Passed: true}, - }, - }, + {RepoName: "repo-1", Results: []RuleResult{ + {RuleName: "Rule A", Passed: true}, + {RuleName: "Rule B", Passed: false}, + }}, + {RepoName: "repo-2", Results: []RuleResult{ + {RuleName: "Rule A", Passed: true}, + {RuleName: "Rule B", Passed: true}, + }}, } - report := GenerateReport("test-org", results) + got := generateReport("test-org", results, testTime) - // Rule B: 1 pass / 2 repos = 50%. Rule A: 2 pass / 2 repos = 100%. - // Sorted ascending: Rule B first, then Rule A. - posB := strings.Index(report, "| Rule B |") - posA := strings.Index(report, "| Rule A |") + want := `# Codatus - Org Compliance Report - if posB == -1 || posA == -1 { - t.Fatal("missing rule rows in summary") - } - if posB > posA { - t.Error("expected Rule B (50%) before Rule A (100%) in summary") +**Org:** test-org +**Scanned:** 2026-04-05 12:00 UTC +**Repos scanned:** 2 +**Compliant:** 1/2 (50%) + +## Summary + +| Rule | Passing | Failing | Pass rate | +|------|---------|---------|----------| +| Rule B | 1 | 1 | 50% | +| Rule A | 2 | 0 | 100% | + +## ✅ Fully compliant (1 repo) + +
+All rules passing + +[repo-2](https://github.com/test-org/repo-2) + +
+ +## ❌ Non-compliant (1 repo) + +
+repo-1 - 1 failing + +- Rule B + +
+ +` + if got != want { + t.Errorf("report mismatch.\n\nGOT:\n%s\n\nWANT:\n%s", got, want) } } func TestGenerateReport_EmptyResults(t *testing.T) { - report := GenerateReport("empty-org", nil) + got := generateReport("empty-org", nil, testTime) - if !strings.Contains(report, "**Repos scanned:** 0") { - t.Error("expected 0 repos scanned") - } - if !strings.Contains(report, "## Summary") { - t.Error("missing summary section") + want := `# Codatus - Org Compliance Report + +**Org:** empty-org +**Scanned:** 2026-04-05 12:00 UTC +**Repos scanned:** 0 + +No repos found. +` + if got != want { + t.Errorf("report mismatch.\n\nGOT:\n%s\n\nWANT:\n%s", got, want) } }