Files
zzet--gortex/internal/resolver/scoped_global_passes_test.go
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

282 lines
11 KiB
Go

package resolver
import (
"iter"
"testing"
"github.com/zzet/gortex/internal/graph"
)
// countingStore wraps a graph.Store and counts the read paths the scoped global
// passes take, so a test can prove a scoped run drives off the per-repo readers
// (GetRepoEdges / GetRepoNodes) instead of the whole-graph scans and materialises
// fewer nodes. It deliberately does NOT re-expose the optional capabilities
// (FnValuePlaceholderScanner, NodesByKindsScanner, …) of the wrapped store, so
// both the scoped and unscoped runs under test take the same generic fallback
// paths and the comparison isolates the scope effect.
type countingStore struct {
graph.Store
edgesByKind int
repoEdges map[string]int
nodesReturned int // total *Node materialised via node-returning reads
}
func newCountingStore(s graph.Store) *countingStore {
return &countingStore{Store: s, repoEdges: map[string]int{}}
}
func (c *countingStore) EdgesByKind(k graph.EdgeKind) iter.Seq[*graph.Edge] {
c.edgesByKind++
return c.Store.EdgesByKind(k)
}
func (c *countingStore) GetRepoEdges(prefix string) []*graph.Edge {
c.repoEdges[prefix]++
return c.Store.GetRepoEdges(prefix)
}
func (c *countingStore) GetFileNodes(path string) []*graph.Node {
ns := c.Store.GetFileNodes(path)
c.nodesReturned += len(ns)
return ns
}
func (c *countingStore) FindNodesByName(name string) []*graph.Node {
ns := c.Store.FindNodesByName(name)
c.nodesReturned += len(ns)
return ns
}
func (c *countingStore) NodesByKind(k graph.NodeKind) iter.Seq[*graph.Node] {
inner := c.Store.NodesByKind(k)
return func(yield func(*graph.Node) bool) {
for n := range inner {
c.nodesReturned++
if !yield(n) {
return
}
}
}
}
func overrideEdgeSet(g graph.Store) map[string]bool {
out := map[string]bool{}
for _, e := range g.AllEdges() {
if e.Kind == graph.EdgeOverrides {
out[e.From+"->"+e.To] = true
}
}
return out
}
// crossRepoOverrideFixture builds a child type in repo A whose supertype lives
// in the (unchanged) repo B, so the override edge Derived.Do -> Base.Do spans a
// repo boundary. InferOverrides has no same-repo gate, so this pair is real.
func crossRepoOverrideFixture() *graph.Graph {
g := graph.New()
// Repo B (unchanged): the parent.
g.AddNode(&graph.Node{ID: "B::b.go::Base", Kind: graph.KindType, Name: "Base", RepoPrefix: "B", FilePath: "b.go"})
g.AddNode(&graph.Node{ID: "B::b.go::Base.Do", Kind: graph.KindMethod, Name: "Do", RepoPrefix: "B", FilePath: "b.go"})
g.AddEdge(&graph.Edge{From: "B::b.go::Base.Do", To: "B::b.go::Base", Kind: graph.EdgeMemberOf})
// Repo A (changed): the child extending the cross-repo parent.
g.AddNode(&graph.Node{ID: "A::a.go::Derived", Kind: graph.KindType, Name: "Derived", RepoPrefix: "A", FilePath: "a.go"})
g.AddNode(&graph.Node{ID: "A::a.go::Derived.Do", Kind: graph.KindMethod, Name: "Do", RepoPrefix: "A", FilePath: "a.go"})
g.AddEdge(&graph.Edge{From: "A::a.go::Derived.Do", To: "A::a.go::Derived", Kind: graph.EdgeMemberOf})
g.AddEdge(&graph.Edge{From: "A::a.go::Derived", To: "B::b.go::Base", Kind: graph.EdgeExtends, Origin: graph.OriginASTResolved})
return g
}
// TestInferOverridesScoped_CrossRepoBoundary asserts a scope that names ONLY the
// changed repo's types still re-derives a cross-repo override whose parent lives
// in an unchanged repo — the filter keeps a parent-edge row when EITHER endpoint
// is in scope, so the changed child is enough.
func TestInferOverridesScoped_CrossRepoBoundary(t *testing.T) {
full := crossRepoOverrideFixture()
New(full).InferOverrides()
want := overrideEdgeSet(full)
if len(want) != 1 {
t.Fatalf("setup: expected 1 cross-repo override from full pass, got %d: %v", len(want), want)
}
// Scope = repo A's type/interface IDs only (repo B did not change). The
// parent Base lives in B and is NOT in scope; the scoped pass must still
// re-derive Derived.Do -> Base.Do because the child Derived is in scope.
scoped := crossRepoOverrideFixture()
New(scoped).InferOverridesScoped(map[string]bool{"A::a.go::Derived": true})
got := overrideEdgeSet(scoped)
if len(got) != len(want) {
t.Fatalf("scoped cross-repo override = %v, want %v", got, want)
}
for k := range want {
if !got[k] {
t.Errorf("scoped run dropped cross-repo override %q", k)
}
}
}
func fnValueCandidate(from, filePath, name string) *graph.Edge {
return &graph.Edge{
From: from,
To: "unresolved::fnvalue::" + name,
Kind: graph.EdgeReferences,
FilePath: filePath,
Meta: map[string]any{
"via": fnValueCandidateVia,
metaFnValueName: name,
"fn_value_ungated": true,
},
}
}
func callbackEdgeSet(g graph.Store) map[string]bool {
out := map[string]bool{}
for _, e := range g.AllEdges() {
if e.Kind != graph.EdgeReferences || e.Meta == nil {
continue
}
if via, _ := e.Meta["via"].(string); via == fnValueRegistrationVia {
out[e.From+"->"+e.To] = true
}
}
return out
}
// crossRepoFnValueFixture builds a callback registration in repo A whose handler
// is a uniquely-named function in the (unchanged) repo B, plus a second
// registration wholly inside repo B. The scoped run over {A} must still bind
// A's callback to the B handler (resolution is whole-graph) while never touching
// B's own candidate.
func crossRepoFnValueFixture() *graph.Graph {
g := graph.New()
// Repo B: two handler functions plus a B-local callback registration.
g.AddNode(&graph.Node{ID: "B::b.go::HandlerB", Kind: graph.KindFunction, Name: "HandlerB", RepoPrefix: "B", FilePath: "b.go"})
g.AddNode(&graph.Node{ID: "B::b.go::HandlerB2", Kind: graph.KindFunction, Name: "HandlerB2", RepoPrefix: "B", FilePath: "b.go"})
g.AddNode(&graph.Node{ID: "B::b.go::RegisterB", Kind: graph.KindFunction, Name: "RegisterB", RepoPrefix: "B", FilePath: "b.go"})
g.AddEdge(fnValueCandidate("B::b.go::RegisterB", "b.go", "HandlerB2"))
// Repo A: a registration referencing the cross-repo handler by name.
g.AddNode(&graph.Node{ID: "A::a.go::RegisterA", Kind: graph.KindFunction, Name: "RegisterA", RepoPrefix: "A", FilePath: "a.go"})
g.AddEdge(fnValueCandidate("A::a.go::RegisterA", "a.go", "HandlerB"))
return g
}
// TestResolveFnValueCallbacksScoped_CrossRepoHandler asserts the scoped fn-value
// gate binds a changed repo's callback to a handler in an UNCHANGED repo (the
// resolution side stays whole-graph), drives its candidate scan off GetRepoEdges
// rather than the whole-graph EdgeReferences scan, and materialises fewer nodes
// than the unscoped run (it never resolves the unchanged repo's own candidate).
func TestResolveFnValueCallbacksScoped_CrossRepoHandler(t *testing.T) {
// Unscoped: both candidates bind.
full := newCountingStore(crossRepoFnValueFixture())
if n := ResolveFnValueCallbacks(full); n != 2 {
t.Fatalf("unscoped fn-value: expected 2 bound callbacks, got %d", n)
}
fullEdges := callbackEdgeSet(full)
if !fullEdges["A::a.go::RegisterA->B::b.go::HandlerB"] {
t.Fatalf("unscoped did not bind the cross-repo callback: %v", fullEdges)
}
if full.edgesByKind == 0 {
t.Errorf("unscoped fn-value should scan EdgesByKind(references) whole-graph")
}
// Scoped over {A}: only A's candidate is gated, but it still binds to the B
// handler via the whole-graph name resolution.
scoped := newCountingStore(crossRepoFnValueFixture())
if n := ResolveFnValueCallbacksScoped(scoped, map[string]bool{"A": true}); n != 1 {
t.Fatalf("scoped fn-value over {A}: expected 1 bound callback, got %d", n)
}
scopedEdges := callbackEdgeSet(scoped)
if !scopedEdges["A::a.go::RegisterA->B::b.go::HandlerB"] {
t.Errorf("scoped run dropped the cross-repo callback binding: %v", scopedEdges)
}
if scopedEdges["B::b.go::RegisterB->B::b.go::HandlerB2"] {
t.Errorf("scoped run must NOT re-bind the unchanged repo's own candidate: %v", scopedEdges)
}
// Candidate scan path: scoped drives off GetRepoEdges(A), never the
// whole-graph EdgeReferences scan.
if scoped.edgesByKind != 0 {
t.Errorf("scoped fn-value must not scan EdgesByKind whole-graph, got %d calls", scoped.edgesByKind)
}
if scoped.repoEdges["A"] == 0 {
t.Errorf("scoped fn-value must scan GetRepoEdges(\"A\")")
}
// Fewer node reads: scoped skips resolving B's candidate entirely.
if scoped.nodesReturned >= full.nodesReturned {
t.Errorf("scoped run should materialise fewer nodes than unscoped: scoped=%d full=%d",
scoped.nodesReturned, full.nodesReturned)
}
}
func mkValueRefCandidate(from, filePath, name string) *graph.Edge {
return &graph.Edge{
From: from,
To: "unresolved::value::" + name,
Kind: graph.EdgeReads,
FilePath: filePath,
Meta: map[string]any{"via": valueRefCandidateVia, "name": name},
}
}
// boundValueRefs maps reader -> constant for every value-ref read the pass bound
// (via == value_ref); a still-unbound candidate keeps the value_ref_candidate
// marker and is absent from the map.
func boundValueRefs(g graph.Store) map[string]string {
out := map[string]string{}
for _, e := range g.AllEdges() {
if e.Kind != graph.EdgeReads || e.Meta == nil {
continue
}
if via, _ := e.Meta["via"].(string); via == valueRefVia {
out[e.From] = e.To
}
}
return out
}
func crossRepoValueRefFixture() *graph.Graph {
g := graph.New()
// Repo A (changed): distinctive constant + a same-file reader candidate.
g.AddNode(&graph.Node{ID: "A::a.go::MAX_SIZE", Kind: graph.KindConstant, Name: "MAX_SIZE", RepoPrefix: "A", FilePath: "a.go", StartLine: 1})
g.AddNode(&graph.Node{ID: "A::a.go::useA", Kind: graph.KindFunction, Name: "useA", RepoPrefix: "A", FilePath: "a.go", StartLine: 5})
g.AddEdge(mkValueRefCandidate("A::a.go::useA", "a.go", "MAX_SIZE"))
// Repo B (unchanged): its own distinctive constant + same-file reader.
g.AddNode(&graph.Node{ID: "B::b.go::MIN_SIZE", Kind: graph.KindConstant, Name: "MIN_SIZE", RepoPrefix: "B", FilePath: "b.go", StartLine: 1})
g.AddNode(&graph.Node{ID: "B::b.go::useB", Kind: graph.KindFunction, Name: "useB", RepoPrefix: "B", FilePath: "b.go", StartLine: 5})
g.AddEdge(mkValueRefCandidate("B::b.go::useB", "b.go", "MIN_SIZE"))
return g
}
// TestResolveValueRefsScoped_CrossRepo asserts the scoped value-ref pass binds
// only the changed repo's candidate (leaving the unchanged repo's on-disk
// binding untouched), and drives its candidate scan off GetRepoEdges rather than
// the whole-graph EdgeReads scan. The unscoped pass binds both, proving the
// scoped pass is a strict narrowing of the same resolution.
func TestResolveValueRefsScoped_CrossRepo(t *testing.T) {
full := crossRepoValueRefFixture()
if n := ResolveValueRefs(full); n != 2 {
t.Fatalf("unscoped value-ref: expected 2 bound reads, got %d", n)
}
fb := boundValueRefs(full)
if fb["A::a.go::useA"] != "A::a.go::MAX_SIZE" || fb["B::b.go::useB"] != "B::b.go::MIN_SIZE" {
t.Fatalf("unscoped value-ref bindings wrong: %v", fb)
}
scoped := newCountingStore(crossRepoValueRefFixture())
if n := ResolveValueRefsScoped(scoped, map[string]bool{"A": true}); n != 1 {
t.Fatalf("scoped value-ref over {A}: expected 1 bound read, got %d", n)
}
sb := boundValueRefs(scoped)
if sb["A::a.go::useA"] != "A::a.go::MAX_SIZE" {
t.Errorf("scoped run dropped repo A's value-ref binding: %v", sb)
}
if _, ok := sb["B::b.go::useB"]; ok {
t.Errorf("scoped run must not bind the unchanged repo B's value-ref: %v", sb)
}
if scoped.edgesByKind != 0 {
t.Errorf("scoped value-ref must not scan EdgesByKind whole-graph, got %d calls", scoped.edgesByKind)
}
if scoped.repoEdges["A"] == 0 {
t.Errorf("scoped value-ref must scan GetRepoEdges(\"A\")")
}
}