chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:33:42 +08:00
commit a06f331eb8
3186 changed files with 689843 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
// Package coverage parses Go's cover.out profile format and stamps
// per-function coverage percentages onto graph nodes. The result is
// the per-symbol meta.coverage_pct field that lets agents answer
// "which symbols are untested" with real numbers rather than the
// reverse-edge-empty heuristic the existing get_untested_symbols
// tool relies on.
//
// Profile format (one segment per non-header line):
//
// mode: set | count | atomic
// <importpath>/<file.go>:<startLine>.<startCol>,<endLine>.<endCol> <numStmt> <count>
//
// startLine/endLine are 1-based; numStmt is the number of source
// statements in the segment; count is execution count (or 0/1 for
// `mode: set`). To compute a function's coverage we sum numStmt
// over segments that fall fully within the function's line range,
// then sum the same numStmt over segments where count > 0; the
// ratio is the percentage.
package coverage
import (
"bufio"
"bytes"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// Segment is one parsed entry from a cover profile. Lines are
// 1-based; columns are kept verbatim from the file but unused by
// the projection — Go's cover output puts the boundary on a line
// number that always belongs to the enclosing function.
type Segment struct {
File string
StartLine int
EndLine int
NumStmt int
Count int
}
// Parse reads cover-profile content and returns one Segment per
// non-header line. Malformed lines are skipped silently — the
// enrichment pass is best-effort like blame.
func Parse(profile []byte) []Segment {
var out []Segment
scanner := bufio.NewScanner(bytes.NewReader(profile))
scanner.Buffer(make([]byte, 64*1024), 16*1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// Skip the mode header (and any other lines that aren't
// segment shapes).
if strings.HasPrefix(line, "mode:") {
continue
}
seg, ok := parseSegment(line)
if !ok {
continue
}
out = append(out, seg)
}
return out
}
// ParseFile is a small convenience wrapper that reads the profile
// from disk and delegates to Parse.
func ParseFile(path string) ([]Segment, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return Parse(data), nil
}
// parseSegment splits one profile line into a Segment. Format:
// `<file>:<sl>.<sc>,<el>.<ec> <numStmt> <count>` — the last two
// are simple ints; the file:range prefix is split on `:`,`,` and
// `.` boundaries.
func parseSegment(line string) (Segment, bool) {
colon := strings.LastIndex(line, ":")
if colon < 0 {
return Segment{}, false
}
file := line[:colon]
rest := line[colon+1:]
// rest is `<sl>.<sc>,<el>.<ec> <numStmt> <count>`.
fields := strings.Fields(rest)
if len(fields) != 3 {
return Segment{}, false
}
rng := fields[0]
comma := strings.Index(rng, ",")
if comma < 0 {
return Segment{}, false
}
startSpec := rng[:comma]
endSpec := rng[comma+1:]
startLine, ok := parseLineCol(startSpec)
if !ok {
return Segment{}, false
}
endLine, ok := parseLineCol(endSpec)
if !ok {
return Segment{}, false
}
numStmt, err := strconv.Atoi(fields[1])
if err != nil {
return Segment{}, false
}
count, err := strconv.Atoi(fields[2])
if err != nil {
return Segment{}, false
}
return Segment{
File: file,
StartLine: startLine,
EndLine: endLine,
NumStmt: numStmt,
Count: count,
}, true
}
// parseLineCol returns just the line component of a `<line>.<col>`
// pair. Cover-profile columns aren't useful for the line-range
// projection so we keep the parser focused.
func parseLineCol(spec string) (int, bool) {
dot := strings.Index(spec, ".")
if dot < 0 {
return 0, false
}
v, err := strconv.Atoi(spec[:dot])
if err != nil {
return 0, false
}
return v, true
}
// CoverageStats is the accumulated coverage for one node range.
type CoverageStats struct {
NumStmt int // total statements counted
Hit int // statements with count > 0
}
// Percent returns coverage as a 0100 float, or -1 when no
// statements were counted (so callers can distinguish "uncovered"
// from "no measurement").
func (s CoverageStats) Percent() float64 {
if s.NumStmt == 0 {
return -1
}
return float64(s.Hit) / float64(s.NumStmt) * 100
}
// EnrichGraph projects parsed segments onto every function /
// method / closure / generic_param node by line range and stamps
// meta.coverage_pct (rounded to 2 decimals) plus meta.coverage =
// {num_stmt, hit}. Returns the number of nodes that received a
// measurement (segments with NumStmt > 0).
//
// modulePath is the Go module path of the indexed repo (read from
// go.mod) — needed because cover-profile file paths are
// module-prefixed (`github.com/foo/bar/pkg/file.go`) while graph
// file paths are repo-relative (`pkg/file.go`). Pass "" to skip
// the prefix-strip, useful when the profile was generated against
// raw paths.
func EnrichGraph(g graph.Store, segments []Segment, modulePath string) int {
if g == nil || len(segments) == 0 {
return 0
}
// Group segments by repo-relative file path so each file is
// projected once even when the profile lists thousands of
// segments per package.
byFile := make(map[string][]Segment)
for _, s := range segments {
path := stripModulePrefix(s.File, modulePath)
byFile[path] = append(byFile[path], s)
}
enriched := 0
// Collect every node whose Meta we stamp so we can round-trip it
// back through the store at the end. On the in-memory backend the
// in-place mutation already persists (n is the canonical node); on
// disk backends (SQLite) n is a per-call GetNode/AllNodes
// reconstruction, so without the write-back the coverage_pct stamp
// is silently discarded the moment AllNodes' slice goes out of
// scope — leaving analyze:coverage_gaps / health_score's coverage
// axis empty on the disk backend. Mirrors releases.EnrichGraph and
// the reach index, which already round-trip Meta through
// AddNode/AddBatch.
var stamped []*graph.Node
covWriter, useCovSidecar := g.(graph.CoverageEnrichmentWriter)
var covRows []graph.CoverageEnrichment
for _, n := range g.AllNodes() {
if !shouldEnrichCoverage(n.Kind) {
continue
}
if n.FilePath == "" || n.StartLine == 0 {
continue
}
segs, ok := byFile[n.FilePath]
if !ok {
continue
}
stats := projectStats(segs, n.StartLine, n.EndLine)
if stats.NumStmt == 0 {
continue
}
if n.Meta == nil {
n.Meta = map[string]any{}
}
pct := roundTwo(stats.Percent())
n.Meta["coverage_pct"] = pct
n.Meta["coverage"] = map[string]any{
"num_stmt": stats.NumStmt,
"hit": stats.Hit,
}
stamped = append(stamped, n)
if useCovSidecar {
covRows = append(covRows, graph.CoverageEnrichment{
NodeID: n.ID, RepoPrefix: n.RepoPrefix,
CoveragePct: pct, NumStmt: stats.NumStmt, Hit: stats.Hit,
})
}
enriched++
// EdgeCoveredBy: invert each EdgeTests pointing at this
// node so agents can ask "which tests cover X" with the
// coverage metric attached, without re-deriving it from
// meta.coverage_pct + a second EdgeTests walk. Skip when
// pct == 0 — uncovered code has no test relation worth
// advertising. Dedup on the test ID because the same test
// may call the subject multiple times.
if pct == 0 {
continue
}
seen := map[string]bool{}
for _, in := range g.GetInEdges(n.ID) {
if in == nil || in.Kind != graph.EdgeTests {
continue
}
if seen[in.From] {
continue
}
seen[in.From] = true
g.AddEdge(&graph.Edge{
From: n.ID,
To: in.From,
Kind: graph.EdgeCoveredBy,
FilePath: n.FilePath,
Line: n.StartLine,
Origin: graph.OriginASTInferred,
Meta: map[string]any{
"coverage_pct": pct,
},
})
}
}
// Persist the stamped node Meta back through the store in one batch
// (a no-op-ish re-insert on the in-memory backend, the durable write
// on disk backends). Without this the coverage_pct stamps never
// survive on the disk backend.
// Persist coverage. Prefer the typed sidecar (change A); on success
// strip the Meta stamps so the node blob stays lean and skip the
// AddBatch. On sidecar write failure, fall back to persisting Meta via
// AddBatch so coverage is never lost (the readers' Meta fallback then
// serves it).
if useCovSidecar && len(covRows) > 0 {
persisted := true
byPrefix := map[string][]graph.CoverageEnrichment{}
for _, r := range covRows {
byPrefix[r.RepoPrefix] = append(byPrefix[r.RepoPrefix], r)
}
for prefix, rr := range byPrefix {
if err := covWriter.BulkSetCoverage(prefix, rr); err != nil {
persisted = false
break
}
}
if persisted {
for _, n := range stamped {
delete(n.Meta, "coverage_pct")
delete(n.Meta, "coverage")
}
} else if len(stamped) > 0 {
g.AddBatch(stamped, nil)
}
} else if len(stamped) > 0 {
g.AddBatch(stamped, nil)
}
return enriched
}
// projectStats sums numStmt and hit-count for segments whose start
// line falls inside [startLine, endLine] inclusive. Using the
// segment start-line as the inclusion test matches the way Go's
// cover tool reports per-function counts via `go tool cover -func`
// — segments are scoped to the immediately enclosing block, so a
// start-line containment check is the correct projection.
func projectStats(segments []Segment, startLine, endLine int) CoverageStats {
if endLine < startLine {
endLine = startLine
}
var stats CoverageStats
for _, s := range segments {
if s.StartLine < startLine || s.StartLine > endLine {
continue
}
stats.NumStmt += s.NumStmt
if s.Count > 0 {
stats.Hit += s.NumStmt
}
}
return stats
}
// shouldEnrichCoverage limits enrichment to executable-symbol
// kinds. Variables, fields, types — the structural kinds — have
// no coverage signal of their own.
func shouldEnrichCoverage(kind graph.NodeKind) bool {
switch kind {
case graph.KindFunction, graph.KindMethod, graph.KindClosure:
return true
}
return false
}
// stripModulePrefix turns `github.com/foo/bar/pkg/file.go` into
// `pkg/file.go` when modulePath is `github.com/foo/bar`. Always
// strips a leading `./` regardless of modulePath — some profiles
// (notably ones generated outside a module-aware build) emit
// relative paths.
func stripModulePrefix(file, modulePath string) string {
file = strings.TrimPrefix(file, "./")
if modulePath == "" {
return file
}
prefix := modulePath + "/"
if strings.HasPrefix(file, prefix) {
return file[len(prefix):]
}
return file
}
// ReadModulePath extracts the module path declared by go.mod at
// repoRoot. Returns "" when go.mod is missing or malformed —
// EnrichGraph treats "" as "skip prefix-strip", which still
// produces correct output for profiles generated against raw
// paths.
func ReadModulePath(repoRoot string) string {
data, err := os.ReadFile(filepath.Join(repoRoot, "go.mod"))
if err != nil {
return ""
}
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "module ") {
return strings.TrimSpace(strings.TrimPrefix(line, "module "))
}
}
return ""
}
// roundTwo rounds a float to 2 decimal places. Used for the
// coverage_pct field to keep the meta payload tidy in JSON
// responses.
func roundTwo(v float64) float64 {
if v < 0 {
return v
}
scaled := int64(v*100 + 0.5)
return float64(scaled) / 100
}
+275
View File
@@ -0,0 +1,275 @@
package coverage
import (
"os"
"path/filepath"
"testing"
"github.com/zzet/gortex/internal/graph"
)
func TestParse_BasicProfile(t *testing.T) {
profile := []byte(`mode: set
github.com/foo/bar/pkg/a.go:5.13,8.2 2 1
github.com/foo/bar/pkg/a.go:10.13,15.2 4 0
github.com/foo/bar/pkg/b.go:1.13,3.2 1 1
`)
got := Parse(profile)
if len(got) != 3 {
t.Fatalf("expected 3 segments, got %d: %+v", len(got), got)
}
if got[0].File != "github.com/foo/bar/pkg/a.go" {
t.Errorf("file = %q", got[0].File)
}
if got[0].StartLine != 5 || got[0].EndLine != 8 {
t.Errorf("range = %d-%d", got[0].StartLine, got[0].EndLine)
}
if got[0].NumStmt != 2 || got[0].Count != 1 {
t.Errorf("stmt/count = %d/%d", got[0].NumStmt, got[0].Count)
}
// Second segment is the uncovered block.
if got[1].Count != 0 || got[1].NumStmt != 4 {
t.Errorf("uncovered segment wrong: %+v", got[1])
}
}
func TestParse_SkipsMalformed(t *testing.T) {
profile := []byte(`mode: set
github.com/x/y/pkg/a.go:5.13,8.2 2 1
this line is not a segment
github.com/x/y/pkg/a.go:bad 1 1
github.com/x/y/pkg/b.go:1.13,3.2 1 1
`)
got := Parse(profile)
if len(got) != 2 {
t.Errorf("expected 2 valid segments (malformed skipped), got %d", len(got))
}
}
func TestProjectStats(t *testing.T) {
segments := []Segment{
{StartLine: 5, EndLine: 8, NumStmt: 2, Count: 1}, // covered
{StartLine: 10, EndLine: 15, NumStmt: 4, Count: 0}, // uncovered
{StartLine: 20, EndLine: 22, NumStmt: 1, Count: 1}, // outside range
}
stats := projectStats(segments, 1, 16)
if stats.NumStmt != 6 {
t.Errorf("num_stmt = %d, want 6", stats.NumStmt)
}
if stats.Hit != 2 {
t.Errorf("hit = %d, want 2 (only first segment is covered)", stats.Hit)
}
if pct := stats.Percent(); pct < 33.32 || pct > 33.34 {
t.Errorf("percent = %f, want ~33.33", pct)
}
}
func TestProjectStats_NoCoverage(t *testing.T) {
stats := projectStats(nil, 1, 100)
if stats.NumStmt != 0 {
t.Errorf("empty segments should yield zero stats")
}
if stats.Percent() != -1 {
t.Errorf("no-measurement percent should be -1, got %f", stats.Percent())
}
}
func TestEnrichGraph_StampsMetaCoveragePct(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/a.go::Foo",
Kind: graph.KindFunction,
FilePath: "pkg/a.go",
StartLine: 1,
EndLine: 20,
})
g.AddNode(&graph.Node{
ID: "pkg/a.go::Bar",
Kind: graph.KindFunction,
FilePath: "pkg/a.go",
StartLine: 25,
EndLine: 30,
})
segs := []Segment{
{File: "github.com/foo/bar/pkg/a.go", StartLine: 5, EndLine: 8, NumStmt: 2, Count: 1},
{File: "github.com/foo/bar/pkg/a.go", StartLine: 10, EndLine: 15, NumStmt: 4, Count: 0},
{File: "github.com/foo/bar/pkg/a.go", StartLine: 27, EndLine: 28, NumStmt: 1, Count: 1},
}
enriched := EnrichGraph(g, segs, "github.com/foo/bar")
if enriched != 2 {
t.Errorf("expected 2 enriched, got %d", enriched)
}
// Coverage now persists in the typed sidecar (change A), not Node.Meta.
byID := map[string]graph.CoverageEnrichment{}
for _, e := range g.CoverageRows("") {
byID[e.NodeID] = e
}
if pct := byID["pkg/a.go::Foo"].CoveragePct; pct < 33.32 || pct > 33.34 {
t.Errorf("Foo pct = %v, want ~33.33", pct)
}
if pct := byID["pkg/a.go::Bar"].CoveragePct; pct != 100 {
t.Errorf("Bar pct = %v, want 100", pct)
}
if _, present := g.GetNode("pkg/a.go::Foo").Meta["coverage_pct"]; present {
t.Errorf("coverage_pct must not remain in Node.Meta after sidecar migration")
}
}
func TestEnrichGraph_EmitsCoveredByForExistingTestEdges(t *testing.T) {
g := graph.New()
subj := &graph.Node{
ID: "pkg/a.go::Foo", Kind: graph.KindFunction,
FilePath: "pkg/a.go", StartLine: 1, EndLine: 20,
}
test := &graph.Node{
ID: "pkg/a_test.go::TestFoo", Kind: graph.KindFunction,
FilePath: "pkg/a_test.go", StartLine: 1, EndLine: 5,
Meta: map[string]any{"is_test": true},
}
g.AddNode(subj)
g.AddNode(test)
g.AddEdge(&graph.Edge{
From: test.ID, To: subj.ID, Kind: graph.EdgeTests,
FilePath: test.FilePath, Line: 2, Origin: graph.OriginASTInferred,
})
segs := []Segment{
{File: "pkg/a.go", StartLine: 5, EndLine: 8, NumStmt: 4, Count: 1},
}
if got := EnrichGraph(g, segs, ""); got != 1 {
t.Fatalf("expected 1 enriched, got %d", got)
}
var coveredBy *graph.Edge
for _, e := range g.GetOutEdges(subj.ID) {
if e.Kind == graph.EdgeCoveredBy && e.To == test.ID {
coveredBy = e
break
}
}
if coveredBy == nil {
t.Fatalf("EdgeCoveredBy from %s to %s not emitted", subj.ID, test.ID)
}
if pct, _ := coveredBy.Meta["coverage_pct"].(float64); pct != 100 {
t.Errorf("coveredBy.Meta.coverage_pct = %v, want 100", pct)
}
}
func TestEnrichGraph_SkipsCoveredByWhenZeroPct(t *testing.T) {
g := graph.New()
subj := &graph.Node{
ID: "pkg/a.go::Foo", Kind: graph.KindFunction,
FilePath: "pkg/a.go", StartLine: 1, EndLine: 20,
}
test := &graph.Node{
ID: "pkg/a_test.go::TestFoo", Kind: graph.KindFunction,
FilePath: "pkg/a_test.go", StartLine: 1, EndLine: 5,
Meta: map[string]any{"is_test": true},
}
g.AddNode(subj)
g.AddNode(test)
g.AddEdge(&graph.Edge{
From: test.ID, To: subj.ID, Kind: graph.EdgeTests,
FilePath: test.FilePath, Line: 2, Origin: graph.OriginASTInferred,
})
// All segments missed.
segs := []Segment{
{File: "pkg/a.go", StartLine: 5, EndLine: 8, NumStmt: 4, Count: 0},
}
EnrichGraph(g, segs, "")
for _, e := range g.GetOutEdges(subj.ID) {
if e.Kind == graph.EdgeCoveredBy {
t.Fatalf("EdgeCoveredBy unexpectedly emitted for 0%%-covered subject")
}
}
}
func TestEnrichGraph_SkipsNonExecutable(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/a.go::T", Kind: graph.KindType,
FilePath: "pkg/a.go", StartLine: 1, EndLine: 10,
})
segs := []Segment{
{File: "pkg/a.go", StartLine: 5, EndLine: 8, NumStmt: 2, Count: 1},
}
if got := EnrichGraph(g, segs, ""); got != 0 {
t.Errorf("KindType should not be enriched, got %d", got)
}
}
func TestEnrichGraph_HandlesUnprefixedPaths(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/a.go::Foo",
Kind: graph.KindFunction,
FilePath: "pkg/a.go",
StartLine: 1,
EndLine: 10,
})
// Profile without a module prefix.
segs := []Segment{
{File: "pkg/a.go", StartLine: 5, EndLine: 8, NumStmt: 2, Count: 1},
}
if got := EnrichGraph(g, segs, ""); got != 1 {
t.Errorf("expected 1 enriched (no prefix-strip path), got %d", got)
}
}
func TestStripModulePrefix(t *testing.T) {
cases := []struct {
file, mod, want string
}{
{"github.com/foo/bar/pkg/a.go", "github.com/foo/bar", "pkg/a.go"},
{"pkg/a.go", "", "pkg/a.go"},
{"./pkg/a.go", "", "pkg/a.go"},
{"github.com/x/y/a.go", "github.com/foo/bar", "github.com/x/y/a.go"},
}
for _, c := range cases {
got := stripModulePrefix(c.file, c.mod)
if got != c.want {
t.Errorf("stripModulePrefix(%q,%q) = %q, want %q", c.file, c.mod, got, c.want)
}
}
}
func TestReadModulePath(t *testing.T) {
dir := t.TempDir()
if err := writeFile(filepath.Join(dir, "go.mod"), "module github.com/foo/bar\n\ngo 1.22\n"); err != nil {
t.Fatal(err)
}
got := ReadModulePath(dir)
if got != "github.com/foo/bar" {
t.Errorf("got %q", got)
}
}
func TestReadModulePath_NoFile(t *testing.T) {
if got := ReadModulePath(t.TempDir()); got != "" {
t.Errorf("expected empty string for missing go.mod, got %q", got)
}
}
func TestRoundTwo(t *testing.T) {
cases := []struct{ in, want float64 }{
{33.3333, 33.33},
{99.999, 100},
{0, 0},
{-1, -1},
}
for _, c := range cases {
if got := roundTwo(c.in); got != c.want {
t.Errorf("roundTwo(%v) = %v, want %v", c.in, got, c.want)
}
}
}
// helpers
func writeFile(path, content string) error {
return os.WriteFile(path, []byte(content), 0o644)
}