// Command coverage-summary prints a whole-project coverage summary from a Go // coverage profile (cover.out). // // Batch helper for `make coverage-summary`. After `go test -coverprofile` writes // a profile, this prints a file-weighted project number and the source files // present on disk but absent from the profile. // // The value here is the missing-file detection: a package no test exercises // never appears in the profile, so a statement-weighted total silently skips it // and the suite looks healthier than it is. This counts such a file as 0% and // weights the project number by file rather than by statement, so untested // files stay visible. `go tool cover -func` already prints the per-function / // per-file table, so this deliberately doesn't reimplement that. // // Self-contained on the standard library, so it runs anywhere the bundle lands // via `go run coverage-summary.go`. The cover.out line shape it parses is: // // /.go:startLine.col,endLine.col numStmts count // // Profile paths are import-path-qualified; the module path from the project's // go.mod maps them back to on-disk relative paths. // // Usage: // // go run coverage-summary.go PROFILE SOURCE_DIR [PROJECT_ROOT] // PROFILE path to the cover.out coverage profile // SOURCE_DIR the directory to account for (walked recursively for *.go) // PROJECT_ROOT module root holding go.mod; defaults to the current directory package main import ( "bufio" "fmt" "os" "path/filepath" "sort" "strconv" "strings" ) type counts struct{ covered, total int } func die(format string, args ...any) { fmt.Fprintf(os.Stderr, "coverage-summary: "+format+"\n", args...) os.Exit(1) } // readModule returns the module path from PROJECT_ROOT/go.mod, or "" if absent. func readModule(root string) string { f, err := os.Open(filepath.Join(root, "go.mod")) if err != nil { return "" } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if rest, ok := strings.CutPrefix(line, "module "); ok { return strings.TrimSpace(rest) } } _ = sc.Err() // a read error just means an unknown module path; paths stay unstripped return "" } // stripModule turns an import-path-qualified file into a module-relative path. func stripModule(path, module string) string { if module != "" { if rest, ok := strings.CutPrefix(path, module+"/"); ok { return rest } } return path } // parseProfile reads a cover.out profile into module-relative path -> counts. // COVERED is the statements in blocks hit at least once; TOTAL is all statements. func parseProfile(profile, module string) map[string]counts { f, err := os.Open(profile) if err != nil { die("coverage profile not found: %s", profile) } defer f.Close() acc := map[string]counts{} sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "mode:") { continue } // ":.,. " fields := strings.Fields(line) if len(fields) != 3 { continue } idx := strings.LastIndex(fields[0], ":") if idx < 0 { continue } rel := stripModule(fields[0][:idx], module) ns, err1 := strconv.Atoi(fields[1]) cnt, err2 := strconv.Atoi(fields[2]) if err1 != nil || err2 != nil { continue } c := acc[rel] c.total += ns if cnt > 0 { c.covered += ns } acc[rel] = c } if err := sc.Err(); err != nil { die("reading %s: %v", profile, err) } return acc } // underDir keeps the entries whose path is at or below sourceRel. func underDir(table map[string]counts, sourceRel string) map[string]counts { if sourceRel == "." || sourceRel == "" { return table } prefix := sourceRel + "/" out := map[string]counts{} for path, c := range table { if path == sourceRel || strings.HasPrefix(path, prefix) { out[path] = c } } return out } // sourceFiles returns *.go files under sourceDir (relative to root), skipping // test files, vendored code, and the local tooling footprint. func sourceFiles(sourceDir, root string) []string { var out []string _ = filepath.WalkDir(sourceDir, func(path string, d os.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() { switch d.Name() { case "vendor", ".git", ".claude", "testdata": return filepath.SkipDir } return nil } name := d.Name() if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { return nil } rel, err := filepath.Rel(root, path) if err == nil { out = append(out, filepath.ToSlash(rel)) } return nil }) sort.Strings(out) return out } func missing(tracked map[string]counts, sourceDir, root string) []string { var out []string for _, f := range sourceFiles(sourceDir, root) { if _, ok := tracked[f]; !ok { out = append(out, f) } } return out } func filePct(c counts) float64 { if c.total > 0 { return 100.0 * float64(c.covered) / float64(c.total) } return 100.0 } func projectPct(tracked map[string]counts, miss []string) float64 { total := len(tracked) + len(miss) if total == 0 { return 0.0 } score := 0.0 for _, c := range tracked { score += filePct(c) } return score / float64(total) } func summaryText(tracked map[string]counts, miss []string, sourceRel string) string { total := len(tracked) + len(miss) pct := projectPct(tracked, miss) var b strings.Builder fmt.Fprintf(&b, "Coverage summary for %s\n\n", sourceRel) fmt.Fprintf(&b, "Project coverage: %.1f%% (%d tracked, %d missing, %d total; missing files count as 0%%)\n\n", pct, len(tracked), len(miss), total) plural := "s" if len(miss) == 1 { plural = "" } fmt.Fprintf(&b, "Not in coverage report: %d file%s\n", len(miss), plural) if len(miss) > 0 { b.WriteString("These files had no coverage entry; they count as 0% in project coverage.\n") sort.Strings(miss) for _, m := range miss { fmt.Fprintf(&b, " %s\n", m) } } else { b.WriteString("Every source file appears in the coverage report.\n") } b.WriteString("\n(Per-file table: run `go tool cover -func=`.)\n") return b.String() } func main() { args := os.Args[1:] if len(args) < 2 || len(args) > 3 { fmt.Fprintln(os.Stderr, "usage: coverage-summary.go PROFILE SOURCE_DIR [PROJECT_ROOT]") os.Exit(2) } profile, sourceDir := args[0], args[1] root := "." if len(args) == 3 { root = args[2] } absRoot, err := filepath.Abs(root) if err != nil { die("bad project root %s: %v", root, err) } absSource, err := filepath.Abs(sourceDir) if err != nil { die("bad source dir %s: %v", sourceDir, err) } sourceRel, err := filepath.Rel(absRoot, absSource) if err != nil { die("source dir %s is not under project root %s", sourceDir, root) } sourceRel = filepath.ToSlash(sourceRel) module := readModule(absRoot) tracked := underDir(parseProfile(profile, module), sourceRel) miss := missing(tracked, absSource, absRoot) fmt.Println() fmt.Print(summaryText(tracked, miss, sourceRel)) }