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
166 lines
5.3 KiB
Go
166 lines
5.3 KiB
Go
package query
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// distanceByID flattens a closure result into an id→distance map for
|
|
// terse assertions.
|
|
func distanceByID(res *ClosureResult) map[string]int {
|
|
out := make(map[string]int, len(res.Nodes))
|
|
for _, m := range res.Nodes {
|
|
out[m.Node.ID] = m.Distance
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestImportClosure_ExpandsOverDependencyEdges(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
// Default edge kinds (dependency allowlist) from main reach the
|
|
// whole call chain plus the referenced type.
|
|
res := e.ImportClosure([]string{"main.go::main"}, ClosureOptions{})
|
|
dist := distanceByID(res)
|
|
|
|
require.Contains(t, dist, "main.go::main")
|
|
assert.Equal(t, 0, dist["main.go::main"])
|
|
assert.Contains(t, dist, "pkg/server.go::Start")
|
|
assert.Contains(t, dist, "pkg/db.go::Connect")
|
|
assert.Contains(t, dist, "pkg/db.go::Ping")
|
|
// references edge is in the dependency allowlist.
|
|
assert.Contains(t, dist, "pkg/db.go::DBImpl")
|
|
}
|
|
|
|
func TestImportClosure_ExpandsOverImports(t *testing.T) {
|
|
g := buildTestGraph()
|
|
e := NewEngine(g)
|
|
|
|
// The pkg/server.go file imports pkg/db.go. Seeding the file node
|
|
// follows the imports edge into the dependency closure.
|
|
res := e.ImportClosure([]string{"pkg/server.go"}, ClosureOptions{
|
|
EdgeKinds: []graph.EdgeKind{graph.EdgeImports},
|
|
})
|
|
dist := distanceByID(res)
|
|
|
|
assert.Equal(t, 0, dist["pkg/server.go"])
|
|
if assert.Contains(t, dist, "pkg/db.go") {
|
|
assert.Equal(t, 1, dist["pkg/db.go"])
|
|
}
|
|
}
|
|
|
|
func TestImportClosure_DistanceRanking(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
res := e.ImportClosure([]string{"main.go::main"}, ClosureOptions{
|
|
EdgeKinds: []graph.EdgeKind{graph.EdgeCalls},
|
|
})
|
|
dist := distanceByID(res)
|
|
|
|
// main -> Start -> Connect -> Ping is a depth-3 call chain.
|
|
assert.Equal(t, 0, dist["main.go::main"])
|
|
assert.Equal(t, 1, dist["pkg/server.go::Start"])
|
|
assert.Equal(t, 2, dist["pkg/db.go::Connect"])
|
|
assert.Equal(t, 3, dist["pkg/db.go::Ping"])
|
|
|
|
// The result must be sorted by (distance, id) regardless of edge
|
|
// enumeration order.
|
|
for i := 1; i < len(res.Nodes); i++ {
|
|
prev, cur := res.Nodes[i-1], res.Nodes[i]
|
|
if prev.Distance == cur.Distance {
|
|
assert.LessOrEqual(t, prev.Node.ID, cur.Node.ID)
|
|
} else {
|
|
assert.Less(t, prev.Distance, cur.Distance)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestImportClosure_NodeBudgetCap(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
// Cap at two nodes: the seed plus the single nearest neighbour.
|
|
res := e.ImportClosure([]string{"main.go::main"}, ClosureOptions{
|
|
EdgeKinds: []graph.EdgeKind{graph.EdgeCalls},
|
|
MaxNodes: 2,
|
|
})
|
|
assert.LessOrEqual(t, len(res.Nodes), 2)
|
|
assert.True(t, res.Truncated, "expected truncation under a 2-node cap on a 4-node chain")
|
|
// The seed is always kept; the deepest node must have been dropped.
|
|
dist := distanceByID(res)
|
|
assert.Contains(t, dist, "main.go::main")
|
|
assert.NotContains(t, dist, "pkg/db.go::Ping")
|
|
}
|
|
|
|
func TestImportClosure_DepthCap(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
res := e.ImportClosure([]string{"main.go::main"}, ClosureOptions{
|
|
EdgeKinds: []graph.EdgeKind{graph.EdgeCalls},
|
|
MaxDepth: 1,
|
|
})
|
|
dist := distanceByID(res)
|
|
// Depth 1 reaches Start but not the deeper Connect / Ping.
|
|
assert.Contains(t, dist, "pkg/server.go::Start")
|
|
assert.NotContains(t, dist, "pkg/db.go::Connect")
|
|
assert.NotContains(t, dist, "pkg/db.go::Ping")
|
|
}
|
|
|
|
func TestImportClosure_MultiSeedMergeTakesNearest(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
// Seed both main (distance to Ping = 3) and Connect (distance to
|
|
// Ping = 1). The merged closure must record the NEAREST distance.
|
|
res := e.ImportClosure([]string{"main.go::main", "pkg/db.go::Connect"}, ClosureOptions{
|
|
EdgeKinds: []graph.EdgeKind{graph.EdgeCalls},
|
|
})
|
|
dist := distanceByID(res)
|
|
|
|
assert.Equal(t, 0, dist["main.go::main"])
|
|
assert.Equal(t, 0, dist["pkg/db.go::Connect"])
|
|
// Ping is 1 hop from Connect, 3 from main -> nearest wins.
|
|
assert.Equal(t, 1, dist["pkg/db.go::Ping"])
|
|
// Both seeds are recorded, de-duplicated and sorted.
|
|
assert.Equal(t, []string{"main.go::main", "pkg/db.go::Connect"}, res.SeedIDs)
|
|
}
|
|
|
|
func TestImportClosure_SkipsUnresolvedAndOutOfScope(t *testing.T) {
|
|
g := buildTestGraph()
|
|
for _, n := range g.AllNodes() {
|
|
n.WorkspaceID = "main"
|
|
}
|
|
g.AddNode(&graph.Node{
|
|
ID: "other/x.go::Foreign", Kind: graph.KindFunction, Name: "Foreign",
|
|
FilePath: "other/x.go", Language: "go", WorkspaceID: "other",
|
|
})
|
|
g.AddEdge(&graph.Edge{
|
|
From: "pkg/db.go::Ping", To: "other/x.go::Foreign",
|
|
Kind: graph.EdgeCalls, FilePath: "pkg/db.go", Line: 20,
|
|
})
|
|
// An unresolved target must never enter the closure.
|
|
g.AddEdge(&graph.Edge{
|
|
From: "main.go::main", To: "unresolved::Mystery",
|
|
Kind: graph.EdgeCalls, FilePath: "main.go", Line: 9,
|
|
})
|
|
e := NewEngine(g)
|
|
|
|
res := e.ImportClosure([]string{"main.go::main"}, ClosureOptions{
|
|
EdgeKinds: []graph.EdgeKind{graph.EdgeCalls},
|
|
WorkspaceID: "main",
|
|
})
|
|
dist := distanceByID(res)
|
|
assert.NotContains(t, dist, "other/x.go::Foreign", "out-of-scope neighbour must be dropped")
|
|
assert.NotContains(t, dist, "unresolved::Mystery", "unresolved target must be dropped")
|
|
}
|
|
|
|
func TestImportClosure_NoSeedsResolved(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
res := e.ImportClosure([]string{"does/not::Exist", "unresolved::Nope"}, ClosureOptions{})
|
|
assert.Empty(t, res.Nodes)
|
|
assert.Empty(t, res.SeedIDs)
|
|
}
|