Files
wehub-resource-sync a06f331eb8
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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

339 lines
18 KiB
Go

package resolver
import (
"fmt"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// storeNodeSet returns the store's nodes as a sorted, comparable set of
// (id, kind) tuples — used alongside storeEdgeSet so the tail-pass equivalence
// tests also catch a node the scoped pass materialised but the full pass did
// not (or vice-versa).
func storeNodeSet(s graph.Store) []string {
var out []string
for _, n := range s.AllNodes() {
if n == nil {
continue
}
out = append(out, n.ID+"\t"+string(n.Kind))
}
sort.Strings(out)
return out
}
// buildGoTailFixture populates a store with the warm-restart-of-repo-A state
// that exercises every post-resolve Go attribution pass. Repo A's edges start
// in the freshly-re-stubbed / pre-classification shape a reindex leaves behind;
// repo B is already in its post-full-resolve steady state (nothing the tail
// passes should touch), matching a warm restart where only repo A re-indexed.
func buildGoTailFixture(s graph.Store) {
// --- files ---
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddNode(&graph.Node{ID: "repoa/pkg/a2.go", Kind: graph.KindFile, Name: "a2.go", FilePath: "repoa/pkg/a2.go", Language: "go", RepoPrefix: "repoa"})
s.AddNode(&graph.Node{ID: "repob/lib/b.go", Kind: graph.KindFile, Name: "b.go", FilePath: "repob/lib/b.go", Language: "go", RepoPrefix: "repob"})
// (0) same-repo call — resolved by the compute loop, not a tail pass.
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::Foo", Kind: graph.KindFunction, Name: "Foo", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::CallerA", Kind: graph.KindFunction, Name: "CallerA", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddEdge(&graph.Edge{From: "repoa/pkg/a.go::CallerA", To: "unresolved::Foo", Kind: graph.EdgeCalls, FilePath: "repoa/pkg/a.go", Line: 2})
// (1) builtin attribution: unresolved::len from a repoa function.
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::UsesLen", Kind: graph.KindFunction, Name: "UsesLen", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddEdge(&graph.Edge{From: "repoa/pkg/a.go::UsesLen", To: "unresolved::len", Kind: graph.EdgeCalls, FilePath: "repoa/pkg/a.go", Line: 3})
// (2) external-call materialisation: a pre-classified stdlib target (the
// shape resolveExtern leaves in the compute loop; not an unresolved:: stub).
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::UsesFmt", Kind: graph.KindFunction, Name: "UsesFmt", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddEdge(&graph.Edge{From: "repoa/pkg/a.go::UsesFmt", To: "stdlib::fmt::Errorf", Kind: graph.EdgeCalls, FilePath: "repoa/pkg/a.go", Line: 4})
// (3) method-receiver rebind: a phantom <methodfile>::Type receiver for a
// method declared in a different file of the same package.
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::Widget", Kind: graph.KindType, Name: "Widget", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddNode(&graph.Node{ID: "repoa/pkg/a2.go::Widget.Do", Kind: graph.KindMethod, Name: "Do", FilePath: "repoa/pkg/a2.go", Language: "go", RepoPrefix: "repoa"})
s.AddEdge(&graph.Edge{From: "repoa/pkg/a2.go::Widget.Do", To: "repoa/pkg/a2.go::Widget", Kind: graph.EdgeMemberOf, FilePath: "repoa/pkg/a2.go", Line: 2})
// (4) bare-name local binding: unresolved::x with an in-scope local.
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::Fn", Kind: graph.KindFunction, Name: "Fn", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::Fn#local:x", Kind: graph.KindLocal, Name: "x", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa", StartLine: 6})
s.AddEdge(&graph.Edge{From: "repoa/pkg/a.go::Fn", To: "unresolved::x", Kind: graph.EdgeReads, FilePath: "repoa/pkg/a.go", Line: 7})
// (5) generic-param binding: unresolved::T with an in-scope tparam.
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::Gen", Kind: graph.KindFunction, Name: "Gen", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddNode(&graph.Node{ID: "repoa/pkg/a.go::Gen#tparam:T", Kind: graph.KindGenericParam, Name: "T", FilePath: "repoa/pkg/a.go", Language: "go", RepoPrefix: "repoa"})
s.AddEdge(&graph.Edge{From: "repoa/pkg/a.go::Gen", To: "unresolved::T", Kind: graph.EdgeReferences, FilePath: "repoa/pkg/a.go", Line: 9})
// --- repo B: steady state (already-resolved call + a foreign stub) ---
s.AddNode(&graph.Node{ID: "repob/lib/b.go::Bar", Kind: graph.KindFunction, Name: "Bar", FilePath: "repob/lib/b.go", Language: "go", RepoPrefix: "repob"})
s.AddNode(&graph.Node{ID: "repob/lib/b.go::CallerB", Kind: graph.KindFunction, Name: "CallerB", FilePath: "repob/lib/b.go", Language: "go", RepoPrefix: "repob"})
s.AddEdge(&graph.Edge{From: "repob/lib/b.go::CallerB", To: "repob/lib/b.go::Bar", Kind: graph.EdgeCalls, FilePath: "repob/lib/b.go", Line: 7, Origin: graph.OriginASTResolved})
s.AddEdge(&graph.Edge{From: "repob/lib/b.go::CallerB", To: "repob::unresolved::Ghost", Kind: graph.EdgeCalls, FilePath: "repob/lib/b.go", Line: 9})
}
// TestResolveAll_ScopedEqualsFull_GoTailPasses is the equivalence gate for the
// scoped post-resolve Go attribution passes (method-receiver rebind, bare-name
// and generic-param binding, builtin + external-call materialisation). A scoped
// ResolveAll over changed repo A and a full ResolveAll over an identical twin
// must land the same node + edge set — and the scoped pass must actually have
// run the tail passes over repo A.
func TestResolveAll_ScopedEqualsFull_GoTailPasses(t *testing.T) {
scopedStore := graph.New()
buildGoTailFixture(scopedStore)
twinStore := graph.New()
buildGoTailFixture(twinStore)
scoped := New(scopedStore)
scoped.SetScope(map[string]struct{}{"repoa": {}})
require.NotNil(t, scoped.ResolveAll())
full := New(twinStore)
require.NotNil(t, full.ResolveAll())
assert.Equal(t, storeEdgeSet(twinStore), storeEdgeSet(scopedStore),
"scoped and full resolves must produce identical final edge sets")
assert.Equal(t, storeNodeSet(twinStore), storeNodeSet(scopedStore),
"scoped and full resolves must materialise identical node sets")
// The scoped tail passes ran over repo A: the builtin was attributed off the
// unresolved stub, the phantom method receiver was lifted onto the canonical
// type, and the stdlib symbol/module nodes were materialised.
assert.False(t, hasCallEdge(scopedStore, "repoa/pkg/a.go::UsesLen", "unresolved::len"),
"repo A's builtin call must no longer be unresolved")
assert.True(t, hasEdgeKind(scopedStore, "repoa/pkg/a2.go::Widget.Do", "repoa/pkg/a.go::Widget", graph.EdgeMemberOf),
"repo A's phantom method receiver must rebind onto the canonical type")
assert.True(t, hasBuiltinNode(scopedStore, "len"),
"the len builtin node must be materialised in the scoped store")
assert.True(t, hasGoModuleNode(scopedStore, "fmt"),
"the fmt stdlib module node must be materialised in the scoped store")
// Repo B stayed exactly as the fixture left it — the scoped pass skipped it.
assert.True(t, hasCallEdge(scopedStore, "repob/lib/b.go::CallerB", "repob/lib/b.go::Bar"),
"repo B's resolved call must stay resolved")
assert.True(t, hasCallEdge(scopedStore, "repob/lib/b.go::CallerB", "repob::unresolved::Ghost"),
"repo B's foreign stub must stay unresolved")
}
// TestResolveAll_ScopedAllReposEqualsFull is the degenerate check: putting
// every repo in scope must reproduce the full whole-graph resolve exactly, so
// the per-file attribution dispatch and the repo-filtered language passes are
// faithful reimplementations of their whole-graph forms.
func TestResolveAll_ScopedAllReposEqualsFull(t *testing.T) {
scopedStore := graph.New()
buildGoTailFixture(scopedStore)
twinStore := graph.New()
buildGoTailFixture(twinStore)
scoped := New(scopedStore)
scoped.SetScope(map[string]struct{}{"repoa": {}, "repob": {}})
require.NotNil(t, scoped.ResolveAll())
full := New(twinStore)
require.NotNil(t, full.ResolveAll())
assert.Equal(t, storeEdgeSet(twinStore), storeEdgeSet(scopedStore))
assert.Equal(t, storeNodeSet(twinStore), storeNodeSet(scopedStore))
}
// TestResolveAll_ScopedEqualsFull_JavaOverrideDispatch covers the scoped Java
// override-dispatch pass (one of the filtered post-resolve passes with no
// per-file sibling): an ambiguous member call in the changed repo must fan out
// to the override family identically under a scoped and a full resolve.
func TestResolveAll_ScopedEqualsFull_JavaOverrideDispatch(t *testing.T) {
baseF := "repoa/model/NamedEntity.java"
ownerF := "repoa/owner/Owner.java"
sysF := "repoa/system/PropertiesLogger.java"
caller := sysF + "::PropertiesLogger.printProperties"
build := func(s graph.Store) {
for _, f := range []string{baseF, ownerF, sysF} {
s.AddNode(&graph.Node{ID: f, Kind: graph.KindFile, Name: f, FilePath: f, Language: "java", RepoPrefix: "repoa"})
}
// Both NamedEntity and Owner extend BaseEntity (Owner via Person), so
// their toString overrides share a common ancestor and fan out together.
s.AddNode(&graph.Node{ID: baseF + "::NamedEntity", Kind: graph.KindType, Name: "NamedEntity", FilePath: baseF, Language: "java", RepoPrefix: "repoa", Meta: map[string]any{"scope_parent": "BaseEntity"}})
s.AddNode(&graph.Node{ID: ownerF + "::Owner", Kind: graph.KindType, Name: "Owner", FilePath: ownerF, Language: "java", RepoPrefix: "repoa", Meta: map[string]any{"scope_parent": "Person"}})
s.AddNode(&graph.Node{ID: ownerF + "::Person", Kind: graph.KindType, Name: "Person", FilePath: ownerF, Language: "java", RepoPrefix: "repoa", Meta: map[string]any{"scope_parent": "BaseEntity"}})
s.AddNode(&graph.Node{ID: baseF + "::NamedEntity.toString", Kind: graph.KindMethod, Name: "toString", FilePath: baseF, Language: "java", RepoPrefix: "repoa", Meta: map[string]any{"receiver": "NamedEntity"}})
s.AddNode(&graph.Node{ID: ownerF + "::Owner.toString", Kind: graph.KindMethod, Name: "toString", FilePath: ownerF, Language: "java", RepoPrefix: "repoa", Meta: map[string]any{"receiver": "Owner"}})
s.AddNode(&graph.Node{ID: caller, Kind: graph.KindMethod, Name: "printProperties", FilePath: sysF, Language: "java", RepoPrefix: "repoa", Meta: map[string]any{"receiver": "PropertiesLogger"}})
s.AddEdge(&graph.Edge{From: caller, To: "unresolved::*.toString", Kind: graph.EdgeCalls, FilePath: sysF, Line: 125})
}
scopedStore := graph.New()
build(scopedStore)
twinStore := graph.New()
build(twinStore)
scoped := New(scopedStore)
scoped.SetScope(map[string]struct{}{"repoa": {}})
require.NotNil(t, scoped.ResolveAll())
full := New(twinStore)
require.NotNil(t, full.ResolveAll())
assert.Equal(t, storeEdgeSet(twinStore), storeEdgeSet(scopedStore),
"scoped and full Java override dispatch must produce identical edge sets")
// The dispatch actually fanned out to both overrides under the scoped pass.
assert.True(t, hasEdgeKind(scopedStore, caller, baseF+"::NamedEntity.toString", graph.EdgeCalls))
assert.True(t, hasEdgeKind(scopedStore, caller, ownerF+"::Owner.toString", graph.EdgeCalls))
for _, e := range scopedStore.GetOutEdges(caller) {
assert.NotEqual(t, "unresolved::*.toString", e.To, "no toString call should remain ambiguous after fan-out")
}
}
// TestResolveAll_ScopedEqualsFull_RelativeImports covers the scoped
// relative-import pass (a second filtered post-resolve pass): a Python relative
// import in the changed repo must resolve to the same internal file node under
// a scoped and a full resolve.
func TestResolveAll_ScopedEqualsFull_RelativeImports(t *testing.T) {
build := func(s graph.Store) {
s.AddNode(&graph.Node{ID: "repoa/pkg/app.py", Kind: graph.KindFile, Name: "app.py", FilePath: "repoa/pkg/app.py", Language: "python", RepoPrefix: "repoa"})
s.AddNode(&graph.Node{ID: "repoa/pkg/util.py", Kind: graph.KindFile, Name: "util.py", FilePath: "repoa/pkg/util.py", Language: "python", RepoPrefix: "repoa"})
s.AddEdge(&graph.Edge{From: "repoa/pkg/app.py", To: "unresolved::pyrel::repoa/pkg/util", Kind: graph.EdgeImports, FilePath: "repoa/pkg/app.py", Line: 1})
}
scopedStore := graph.New()
build(scopedStore)
twinStore := graph.New()
build(twinStore)
scoped := New(scopedStore)
scoped.SetScope(map[string]struct{}{"repoa": {}})
require.NotNil(t, scoped.ResolveAll())
full := New(twinStore)
require.NotNil(t, full.ResolveAll())
assert.Equal(t, storeEdgeSet(twinStore), storeEdgeSet(scopedStore),
"scoped and full relative-import resolution must produce identical edge sets")
assert.True(t, hasEdgeKind(scopedStore, "repoa/pkg/app.py", "repoa/pkg/util.py", graph.EdgeImports),
"the python relative import must resolve to the internal file under the scoped pass")
}
// TestScopedTailExceedsFileBudget pins the size gate that routes a large changed
// repo away from the per-file attribution storm: the gate fires when any repo in
// scope holds more KindFile nodes than the budget, and reads the already-built
// dirIndex rather than re-materializing the repo just to count.
func TestScopedTailExceedsFileBudget(t *testing.T) {
s := graph.New()
for i := 0; i < 3; i++ {
f := fmt.Sprintf("repoa/pkg/f%d.go", i)
s.AddNode(&graph.Node{ID: f, Kind: graph.KindFile, Name: fmt.Sprintf("f%d.go", i), FilePath: f, Language: "go", RepoPrefix: "repoa"})
}
s.AddNode(&graph.Node{ID: "repob/x/b.go", Kind: graph.KindFile, Name: "b.go", FilePath: "repob/x/b.go", Language: "go", RepoPrefix: "repob"})
r := New(s)
r.buildDirIndexes()
defer r.clearDirIndexes()
orig := scopedTailFileBudget
defer func() { scopedTailFileBudget = orig }()
r.SetScope(map[string]struct{}{"repoa": {}})
scopedTailFileBudget = 2
assert.True(t, r.scopedTailExceedsFileBudget(), "repoa's 3 files exceed a budget of 2")
scopedTailFileBudget = 3
assert.False(t, r.scopedTailExceedsFileBudget(), "repoa's 3 files do not exceed a budget of 3")
// A small changed repo stays on the per-file path even when a large sibling
// exists, because only in-scope repos are counted.
r.SetScope(map[string]struct{}{"repob": {}})
scopedTailFileBudget = 2
assert.False(t, r.scopedTailExceedsFileBudget(), "repob's single file is within budget")
}
// TestResolveAll_ScopedLargeRepoEqualsFull is the correctness gate for the size
// guard: when a changed repo exceeds the file budget the scoped tail runs the
// whole-graph streaming passes instead of the per-file dispatch, and must still
// land the identical edge + node set a full resolve produces. buildGoTailFixture
// gives repo A two files, so a budget of 1 forces the streaming branch.
func TestResolveAll_ScopedLargeRepoEqualsFull(t *testing.T) {
orig := scopedTailFileBudget
scopedTailFileBudget = 1
defer func() { scopedTailFileBudget = orig }()
scopedStore := graph.New()
buildGoTailFixture(scopedStore)
twinStore := graph.New()
buildGoTailFixture(twinStore)
scoped := New(scopedStore)
scoped.SetScope(map[string]struct{}{"repoa": {}})
require.NotNil(t, scoped.ResolveAll())
full := New(twinStore)
require.NotNil(t, full.ResolveAll())
assert.Equal(t, storeEdgeSet(twinStore), storeEdgeSet(scopedStore),
"a scoped resolve that streams the large repo's tail must equal the full resolve")
assert.Equal(t, storeNodeSet(twinStore), storeNodeSet(scopedStore))
// The tail still attributed repo A's edges under the streaming branch.
assert.False(t, hasCallEdge(scopedStore, "repoa/pkg/a.go::UsesLen", "unresolved::len"),
"repo A's builtin call must be attributed under the streaming tail")
assert.True(t, hasEdgeKind(scopedStore, "repoa/pkg/a2.go::Widget.Do", "repoa/pkg/a.go::Widget", graph.EdgeMemberOf),
"repo A's phantom method receiver must rebind under the streaming tail")
}
// TestBuildImportClosureFiltered_Equivalence pins the closure-scoping contract
// the cross-package guard relies on: for every caller file whose repo is in the
// filter set, the filtered closure entry is byte-identical to the whole-graph
// build, and a nil filter reproduces the whole-graph closure exactly.
func TestBuildImportClosureFiltered_Equivalence(t *testing.T) {
g := graph.New()
// repoa file imports a sibling repoa package.
g.AddNode(&graph.Node{ID: "repoa/x/a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "repoa/x/a.go", Language: "go", RepoPrefix: "repoa"})
g.AddNode(&graph.Node{ID: "repoa/y/dep.go", Kind: graph.KindFile, Name: "dep.go", FilePath: "repoa/y/dep.go", Language: "go", RepoPrefix: "repoa"})
g.AddEdge(&graph.Edge{From: "repoa/x/a.go", To: "repoa/y/dep.go", Kind: graph.EdgeImports, FilePath: "repoa/x/a.go", Line: 1, Origin: graph.OriginASTResolved})
// repob file imports a sibling repob package.
g.AddNode(&graph.Node{ID: "repob/m/b.go", Kind: graph.KindFile, Name: "b.go", FilePath: "repob/m/b.go", Language: "go", RepoPrefix: "repob"})
g.AddNode(&graph.Node{ID: "repob/n/dep.go", Kind: graph.KindFile, Name: "dep.go", FilePath: "repob/n/dep.go", Language: "go", RepoPrefix: "repob"})
g.AddEdge(&graph.Edge{From: "repob/m/b.go", To: "repob/n/dep.go", Kind: graph.EdgeImports, FilePath: "repob/m/b.go", Line: 1, Origin: graph.OriginASTResolved})
r := New(g)
full := r.buildImportClosure()
nilFiltered := r.buildImportClosureFiltered(nil)
assert.Equal(t, full, nilFiltered, "nil filter must reproduce the whole-graph closure")
scoped := r.buildImportClosureFiltered(map[string]struct{}{"repoa": {}})
// Every repoa caller entry is identical to the whole-graph build...
assert.Equal(t, full["repoa/x/a.go"], scoped["repoa/x/a.go"],
"a caller in the filter set keeps its whole-graph reachable-dir set")
require.Contains(t, scoped["repoa/x/a.go"], "repoa/y",
"the imported sibling dir must be reachable")
// ...and the out-of-scope repob caller is absent from the scoped closure.
_, ok := scoped["repob/m/b.go"]
assert.False(t, ok, "a caller outside the filter set is not seeded")
}
// hasEdgeKind reports whether s holds an edge of the given kind from → to.
func hasEdgeKind(s graph.Store, from, to string, kind graph.EdgeKind) bool {
for _, e := range s.GetOutEdges(from) {
if e.Kind == kind && e.To == to {
return true
}
}
return false
}
// hasBuiltinNode reports whether a KindBuiltin node named name was materialised.
func hasBuiltinNode(s graph.Store, name string) bool {
for _, n := range s.AllNodes() {
if n != nil && n.Kind == graph.KindBuiltin && n.Name == name {
return true
}
}
return false
}
// hasGoModuleNode reports whether a KindModule node named name was materialised.
func hasGoModuleNode(s graph.Store, name string) bool {
for _, n := range s.AllNodes() {
if n != nil && n.Kind == graph.KindModule && n.Name == name {
return true
}
}
return false
}