diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 13:07:40 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 13:07:40 -0500 |
| commit | 47ca509e69b6a1472a735a4b9521a952e7434491 (patch) | |
| tree | 9101eb376d233c455f653ad857474b3a0dae767f /languages/go/tests | |
| parent | af478a42b18c4d5e0712c4cb43036126d36c56b5 (diff) | |
| download | rulesets-47ca509e69b6a1472a735a4b9521a952e7434491.tar.gz rulesets-47ca509e69b6a1472a735a4b9521a952e7434491.zip | |
feat(go): add coverage-summary as a Go bundle coverage slice
Third language in the coverage-summary fan-out, after Elisp and Python. Same kernel: count every source file on disk that's absent from the coverage profile as 0% and weight the project number by file, so an untested file stays visible instead of being averaged away.
The script at languages/go/claude/scripts/coverage-summary.go parses a cover.out profile, maps each import-path-qualified entry back to an on-disk relative path using the module path from go.mod, and reports a file-weighted number plus the missing files. It's standard library only, so it runs anywhere via go run, and it doesn't reimplement the per-function table that go tool cover -func already prints. I proved it against a real go test -coverprofile run, not just a synthetic fixture, since the Go toolchain is installed here.
Two findings to flag. Modern go test ./... already lists every module package in the profile at 0% even when untested, so for in-module code the missing-file list is usually empty. The detection earns its keep on build-tagged files and dirs outside ./.... And this is a coverage-only slice of a Go bundle that doesn't otherwise exist yet: there's no go.md rule file, so sync-language-bundle.sh can't fingerprint it (detection keys on a bundle's own .claude/rules). The script installs via make install-lang LANG=go but won't be sync-maintained until the Go bundle gets real rules and a CLAUDE.md. Building that out is the natural companion task.
Tests are black-box: a Go test in its own throwaway module runs the script via go run against temp fixtures and checks output, so the shipped script dir stays test-free. They cover missing-file detection, all-tracked, _test.go exclusion, and the missing-report error. make test gained a go test discovery path for languages/*/tests, guarded so environments without Go skip it cleanly.
Diffstat (limited to 'languages/go/tests')
| -rw-r--r-- | languages/go/tests/coverage_summary_test.go | 142 | ||||
| -rw-r--r-- | languages/go/tests/go.mod | 3 |
2 files changed, 145 insertions, 0 deletions
diff --git a/languages/go/tests/coverage_summary_test.go b/languages/go/tests/coverage_summary_test.go new file mode 100644 index 0000000..6b2fb44 --- /dev/null +++ b/languages/go/tests/coverage_summary_test.go @@ -0,0 +1,142 @@ +// Black-box tests for the Go bundle coverage-summary script. +// +// The script ships into a project's .claude/scripts/ and runs via `go run`, so +// these tests exercise the real CLI rather than importing it: build a throwaway +// module (go.mod + source files + a cover.out profile), run the script against +// it, and assert on stdout. Keeping the test in its own module here means the +// shipped script dir stays test-free. +// +// Normal / Boundary / Error coverage at the behavior level: missing-file +// detection, the file-weighted number, all-tracked, ignoring _test.go, and a +// missing-report error. +package gocovtest + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +// scriptPath resolves coverage-summary.go relative to this test file. +func scriptPath(t *testing.T) string { + t.Helper() + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("cannot locate test file") + } + p := filepath.Join(filepath.Dir(thisFile), "..", "claude", "scripts", "coverage-summary.go") + abs, err := filepath.Abs(p) + if err != nil { + t.Fatal(err) + } + return abs +} + +// fixture writes a go.mod, the given source files, and a cover.out under a temp +// module root. sources maps relpath -> contents; profile is the raw cover.out. +func fixture(t *testing.T, sources map[string]string, profile string) string { + t.Helper() + root := t.TempDir() + write := func(rel, body string) { + full := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + write("go.mod", "module example.com/m\n\ngo 1.26\n") + for rel, body := range sources { + write(rel, body) + } + write("cover.out", profile) + return root +} + +// run executes the script against a fixture root, returning combined output + err. +func run(t *testing.T, root, sourceDir string) (string, error) { + t.Helper() + cmd := exec.Command("go", "run", scriptPath(t), + filepath.Join(root, "cover.out"), filepath.Join(root, sourceDir), root) + out, err := cmd.CombinedOutput() + return string(out), err +} + +func TestMissingFileSurfacedAndCountedZero(t *testing.T) { + root := fixture(t, + map[string]string{ + "calc/calc.go": "package calc\n\nfunc Add(a, b int) int { return a + b }\n", + "calc/untested.go": "package calc\n\nfunc Mul(a, b int) int { return a * b }\n", + }, + // calc.go fully covered (2/2 stmts); untested.go absent from the profile. + "mode: set\nexample.com/m/calc/calc.go:3.40,3.56 2 1\n", + ) + out, err := run(t, root, ".") + if err != nil { + t.Fatalf("script failed: %v\n%s", err, out) + } + if !strings.Contains(out, "calc/untested.go") { + t.Errorf("missing file not surfaced:\n%s", out) + } + if !strings.Contains(out, "0%") { + t.Errorf("missing-as-0%% note absent:\n%s", out) + } + // calc.go = 100%, untested.go missing = 0% -> 50.0% + if !strings.Contains(out, "50.0%") { + t.Errorf("expected project number 50.0%%:\n%s", out) + } +} + +func TestAllTrackedNoMissing(t *testing.T) { + root := fixture(t, + map[string]string{"calc/calc.go": "package calc\n\nfunc Add(a, b int) int { return a + b }\n"}, + "mode: set\nexample.com/m/calc/calc.go:3.40,3.56 2 1\n", + ) + out, err := run(t, root, ".") + if err != nil { + t.Fatalf("script failed: %v\n%s", err, out) + } + if !strings.Contains(out, "Not in coverage report: 0 file") { + t.Errorf("expected zero missing files:\n%s", out) + } + if !strings.Contains(out, "100.0%") { + t.Errorf("expected 100.0%% project number:\n%s", out) + } +} + +func TestTestFilesAreNotCountedAsSource(t *testing.T) { + root := fixture(t, + map[string]string{ + "calc/calc.go": "package calc\n\nfunc Add(a, b int) int { return a + b }\n", + "calc/calc_test.go": "package calc\n\nimport \"testing\"\n\nfunc TestX(t *testing.T) {}\n", + }, + "mode: set\nexample.com/m/calc/calc.go:3.40,3.56 2 1\n", + ) + out, err := run(t, root, ".") + if err != nil { + t.Fatalf("script failed: %v\n%s", err, out) + } + if strings.Contains(out, "calc_test.go") { + t.Errorf("_test.go wrongly counted as source:\n%s", out) + } + if !strings.Contains(out, "100.0%") { + t.Errorf("expected 100.0%% (test file excluded):\n%s", out) + } +} + +func TestMissingReportErrors(t *testing.T) { + root := fixture(t, + map[string]string{"calc/calc.go": "package calc\n"}, + "mode: set\n", + ) + cmd := exec.Command("go", "run", scriptPath(t), + filepath.Join(root, "does-not-exist.out"), root, root) + out, err := cmd.CombinedOutput() + if err == nil { + t.Errorf("expected non-zero exit for missing report; output:\n%s", out) + } +} diff --git a/languages/go/tests/go.mod b/languages/go/tests/go.mod new file mode 100644 index 0000000..db72cb1 --- /dev/null +++ b/languages/go/tests/go.mod @@ -0,0 +1,3 @@ +module gocovtest + +go 1.26 |
