package coverage import ( "bufio" "bytes" "fmt" "os" "os/exec" "path/filepath" "runtime" "sort" "strconv" "strings" "testing" ) type coverageBlock struct { file string stmts float64 hitCount float64 } type fileCoverage struct { covered float64 total float64 } func repoRoot(tb testing.TB) string { tb.Helper() _, thisFile, _, ok := runtime.Caller(0) if !ok { tb.Fatalf("unable to resolve caller path") } return filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..")) } // runCoverageCommand runs one orchestration or CLI step. // Signature: runCoverageCommand(t *testing.T, dir string, coverFile string, args ...string). // Why: coverage is now merged from both root and top-level testing modules, so // command execution needs one shared helper to avoid drift between invocations. func runCoverageCommand(t *testing.T, dir string, coverFile string, args ...string) { t.Helper() cmdArgs := append([]string{"test"}, args...) cmdArgs = append(cmdArgs, "-coverprofile="+coverFile) cmd := exec.Command("go", cmdArgs...) cmd.Dir = dir cmd.Env = os.Environ() out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("go test coverage run failed in %s: %v\n%s", dir, err, out) } } // parseCoverageProfile runs one orchestration or CLI step. // Signature: parseCoverageProfile(t *testing.T, path string, blocks map[string]coverageBlock). // Why: merged coverage requires block-level union logic so hits from either // module count toward per-file coverage without double-counting statements. func parseCoverageProfile(t *testing.T, path string, blocks map[string]coverageBlock) { t.Helper() f, err := os.Open(path) if err != nil { t.Fatalf("open coverprofile %s: %v", path, err) } defer f.Close() s := bufio.NewScanner(f) for s.Scan() { line := strings.TrimSpace(s.Text()) if line == "" || strings.HasPrefix(line, "mode:") { continue } parts := strings.Fields(line) if len(parts) != 3 { t.Fatalf("unexpected coverprofile line: %q", line) } loc := parts[0] stmts, err := strconv.ParseFloat(parts[1], 64) if err != nil { t.Fatalf("parse statement count %q: %v", parts[1], err) } hitCount, err := strconv.ParseFloat(parts[2], 64) if err != nil { t.Fatalf("parse hit count %q: %v", parts[2], err) } file := strings.SplitN(loc, ":", 2)[0] key := loc + "|" + parts[1] block := blocks[key] block.file = file block.stmts = stmts if hitCount > block.hitCount { block.hitCount = hitCount } blocks[key] = block } if err := s.Err(); err != nil { t.Fatalf("scan coverprofile %s: %v", path, err) } } func TestPerFileCoverageReport(t *testing.T) { root := repoRoot(t) tmp := t.TempDir() rootCover := filepath.Join(tmp, "ananke.root.cover.out") configCover := filepath.Join(tmp, "ananke.testing.config.cover.out") testingCover := filepath.Join(tmp, "ananke.testing.cover.out") runCoverageCommand(t, root, rootCover, "./...") runCoverageCommand( t, filepath.Join(root, "testing"), configCover, "./config", "-coverpkg=scm.bstein.dev/bstein/ananke/...", ) runCoverageCommand( t, filepath.Join(root, "testing"), testingCover, "./execx", "./metrics", "./hygiene", "./orchestrator", "./state", "./sshutil", "./ups", "-coverpkg=scm.bstein.dev/bstein/ananke/...", ) blocks := map[string]coverageBlock{} parseCoverageProfile(t, rootCover, blocks) parseCoverageProfile(t, configCover, blocks) parseCoverageProfile(t, testingCover, blocks) byFile := map[string]*fileCoverage{} for _, block := range blocks { fc := byFile[block.file] if fc == nil { fc = &fileCoverage{} byFile[block.file] = fc } fc.total += block.stmts if block.hitCount > 0 { fc.covered += block.stmts } } target := 95.0 if raw := strings.TrimSpace(os.Getenv("ANANKE_PER_FILE_COVERAGE_TARGET")); raw != "" { parsed, err := strconv.ParseFloat(raw, 64) if err != nil { t.Fatalf("invalid ANANKE_PER_FILE_COVERAGE_TARGET=%q: %v", raw, err) } target = parsed } enforce := strings.TrimSpace(os.Getenv("ANANKE_ENFORCE_COVERAGE")) == "1" keys := make([]string, 0, len(byFile)) for file := range byFile { if strings.Contains(file, "/testing/") { continue } keys = append(keys, file) } sort.Strings(keys) var report bytes.Buffer report.WriteString("per-file coverage\n") under := make([]string, 0) for _, file := range keys { fc := byFile[file] pct := 100.0 if fc.total > 0 { pct = (fc.covered / fc.total) * 100.0 } report.WriteString(fmt.Sprintf("- %s: %.1f%%\n", file, pct)) if pct < target { under = append(under, fmt.Sprintf("%s (%.1f%% < %.1f%%)", file, pct, target)) } } t.Log(report.String()) if len(under) > 0 && enforce { t.Fatalf("files below coverage target:\n%s", strings.Join(under, "\n")) } if len(under) > 0 && !enforce { t.Logf("coverage target %.1f%% is not enforced yet; %d files are below target", target, len(under)) } }