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
305 lines
11 KiB
Go
305 lines
11 KiB
Go
package query
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// buildTestGraph creates a small realistic graph:
|
|
//
|
|
// main.go::main -> calls -> pkg/server.go::Start
|
|
// pkg/server.go::Start -> calls -> pkg/db.go::Connect
|
|
// pkg/db.go::Connect -> calls -> pkg/db.go::Ping
|
|
// pkg/server.go -> imports -> pkg/db.go
|
|
// pkg/db.go::DBImpl -> implements -> pkg/db.go::DB (interface)
|
|
func buildTestGraph() *graph.Graph {
|
|
g := graph.New()
|
|
|
|
nodes := []*graph.Node{
|
|
{ID: "main.go", Kind: graph.KindFile, Name: "main.go", FilePath: "main.go", Language: "go"},
|
|
{ID: "main.go::main", Kind: graph.KindFunction, Name: "main", FilePath: "main.go", Language: "go",
|
|
StartLine: 5, EndLine: 10, Meta: map[string]any{"signature": "func main()"}},
|
|
{ID: "pkg/server.go", Kind: graph.KindFile, Name: "server.go", FilePath: "pkg/server.go", Language: "go"},
|
|
{ID: "pkg/server.go::Start", Kind: graph.KindFunction, Name: "Start", FilePath: "pkg/server.go", Language: "go",
|
|
StartLine: 10, EndLine: 20},
|
|
{ID: "pkg/db.go", Kind: graph.KindFile, Name: "db.go", FilePath: "pkg/db.go", Language: "go"},
|
|
{ID: "pkg/db.go::Connect", Kind: graph.KindFunction, Name: "Connect", FilePath: "pkg/db.go", Language: "go",
|
|
StartLine: 5, EndLine: 15},
|
|
{ID: "pkg/db.go::Ping", Kind: graph.KindFunction, Name: "Ping", FilePath: "pkg/db.go", Language: "go",
|
|
StartLine: 17, EndLine: 22},
|
|
{ID: "pkg/db.go::DB", Kind: graph.KindInterface, Name: "DB", FilePath: "pkg/db.go", Language: "go",
|
|
StartLine: 1, EndLine: 4},
|
|
{ID: "pkg/db.go::DBImpl", Kind: graph.KindType, Name: "DBImpl", FilePath: "pkg/db.go", Language: "go",
|
|
StartLine: 24, EndLine: 30},
|
|
}
|
|
for _, n := range nodes {
|
|
g.AddNode(n)
|
|
}
|
|
|
|
edges := []*graph.Edge{
|
|
{From: "main.go::main", To: "pkg/server.go::Start", Kind: graph.EdgeCalls, FilePath: "main.go", Line: 7},
|
|
{From: "pkg/server.go::Start", To: "pkg/db.go::Connect", Kind: graph.EdgeCalls, FilePath: "pkg/server.go", Line: 12},
|
|
{From: "pkg/db.go::Connect", To: "pkg/db.go::Ping", Kind: graph.EdgeCalls, FilePath: "pkg/db.go", Line: 10},
|
|
{From: "pkg/server.go", To: "pkg/db.go", Kind: graph.EdgeImports, FilePath: "pkg/server.go", Line: 3},
|
|
{From: "pkg/db.go::DBImpl", To: "pkg/db.go::DB", Kind: graph.EdgeImplements, FilePath: "pkg/db.go", Line: 24},
|
|
{From: "main.go::main", To: "pkg/db.go::DBImpl", Kind: graph.EdgeReferences, FilePath: "main.go", Line: 8},
|
|
}
|
|
for _, e := range edges {
|
|
g.AddEdge(e)
|
|
}
|
|
|
|
return g
|
|
}
|
|
|
|
func TestGetSymbol(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
n := e.GetSymbol("pkg/db.go::Connect")
|
|
require.NotNil(t, n)
|
|
assert.Equal(t, "Connect", n.Name)
|
|
|
|
assert.Nil(t, e.GetSymbol("nonexistent"))
|
|
}
|
|
|
|
func TestFindSymbols(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
results := e.FindSymbols("Connect")
|
|
require.Len(t, results, 1)
|
|
|
|
results = e.FindSymbols("Connect", graph.KindFunction)
|
|
require.Len(t, results, 1)
|
|
|
|
results = e.FindSymbols("Connect", graph.KindType)
|
|
assert.Empty(t, results)
|
|
}
|
|
|
|
func TestGetFileSymbols(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
sg := e.GetFileSymbols("pkg/db.go")
|
|
assert.GreaterOrEqual(t, len(sg.Nodes), 4) // file + Connect + Ping + DB + DBImpl
|
|
}
|
|
|
|
func TestGetDependencies(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
// main calls Start (depth 1).
|
|
sg := e.GetDependencies("main.go::main", QueryOptions{Depth: 1, Limit: 50, Detail: "full"})
|
|
assert.GreaterOrEqual(t, len(sg.Nodes), 2) // main + Start
|
|
|
|
// Depth 2 should also reach Connect.
|
|
sg = e.GetDependencies("main.go::main", QueryOptions{Depth: 2, Limit: 50, Detail: "full"})
|
|
ids := nodeIDs(sg.Nodes)
|
|
assert.Contains(t, ids, "pkg/server.go::Start")
|
|
assert.Contains(t, ids, "pkg/db.go::Connect")
|
|
}
|
|
|
|
func TestGetDependents(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
// Who depends on Connect?
|
|
sg := e.GetDependents("pkg/db.go::Connect", QueryOptions{Depth: 2, Limit: 50})
|
|
ids := nodeIDs(sg.Nodes)
|
|
assert.Contains(t, ids, "pkg/server.go::Start")
|
|
}
|
|
|
|
func TestGetCallChain(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
sg := e.GetCallChain("main.go::main", QueryOptions{Depth: 3, Limit: 50})
|
|
ids := nodeIDs(sg.Nodes)
|
|
assert.Contains(t, ids, "pkg/server.go::Start")
|
|
assert.Contains(t, ids, "pkg/db.go::Connect")
|
|
assert.Contains(t, ids, "pkg/db.go::Ping")
|
|
}
|
|
|
|
func TestGetCallers(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
sg := e.GetCallers("pkg/db.go::Ping", QueryOptions{Depth: 3, Limit: 50})
|
|
ids := nodeIDs(sg.Nodes)
|
|
assert.Contains(t, ids, "pkg/db.go::Connect")
|
|
}
|
|
|
|
// Regression: methods wired in by reference (e.g. HTTP handler registration
|
|
// like `mux.HandleFunc("/p", h.foo)`) carry an EdgeReferences edge from the
|
|
// registration site to the method — they're never reached by a direct
|
|
// EdgeCalls. Before the fix GetCallers ignored EdgeReferences and these
|
|
// methods looked dead in the graph.
|
|
func TestGetCallers_IncludesMethodValueReferences(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "routes.go::Register", Kind: graph.KindFunction, Name: "Register", FilePath: "routes.go", Language: "go"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "handler.go::Handler.HandleHealth", Kind: graph.KindMethod, Name: "HandleHealth",
|
|
FilePath: "handler.go", Language: "go", Meta: map[string]any{"receiver": "Handler"},
|
|
})
|
|
// `mux.HandleFunc("/health", h.HandleHealth)` after the resolver:
|
|
g.AddEdge(&graph.Edge{
|
|
From: "routes.go::Register", To: "handler.go::Handler.HandleHealth",
|
|
Kind: graph.EdgeReferences, FilePath: "routes.go", Line: 12,
|
|
})
|
|
|
|
e := NewEngine(g)
|
|
sg := e.GetCallers("handler.go::Handler.HandleHealth", QueryOptions{Depth: 1, Limit: 50})
|
|
ids := nodeIDs(sg.Nodes)
|
|
assert.Contains(t, ids, "routes.go::Register",
|
|
"method-value registration site should surface as a caller via EdgeReferences")
|
|
}
|
|
|
|
func TestFindImplementations(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
impls := e.FindImplementations("pkg/db.go::DB")
|
|
require.Len(t, impls, 1)
|
|
assert.Equal(t, "DBImpl", impls[0].Name)
|
|
}
|
|
|
|
func TestFindUsages(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
sg := e.FindUsages("pkg/db.go::DBImpl")
|
|
ids := nodeIDs(sg.Nodes)
|
|
assert.Contains(t, ids, "main.go::main") // references DBImpl
|
|
}
|
|
|
|
func TestGetCluster(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
sg := e.GetCluster("pkg/server.go::Start", QueryOptions{Depth: 1, Limit: 50})
|
|
assert.GreaterOrEqual(t, len(sg.Nodes), 3) // Start + main + Connect
|
|
}
|
|
|
|
func TestTruncation(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
|
|
sg := e.GetCallChain("main.go::main", QueryOptions{Depth: 10, Limit: 2})
|
|
assert.True(t, sg.Truncated)
|
|
assert.LessOrEqual(t, len(sg.Nodes), 2)
|
|
}
|
|
|
|
func TestCycleHandling(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "a", Kind: graph.KindFunction, Name: "a", FilePath: "a.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "b", Kind: graph.KindFunction, Name: "b", FilePath: "b.go", Language: "go"})
|
|
g.AddEdge(&graph.Edge{From: "a", To: "b", Kind: graph.EdgeCalls, FilePath: "a.go", Line: 1})
|
|
g.AddEdge(&graph.Edge{From: "b", To: "a", Kind: graph.EdgeCalls, FilePath: "b.go", Line: 1})
|
|
|
|
e := NewEngine(g)
|
|
sg := e.GetCallChain("a", QueryOptions{Depth: 10, Limit: 50})
|
|
// Should terminate without infinite loop.
|
|
assert.LessOrEqual(t, len(sg.Nodes), 2)
|
|
}
|
|
|
|
func TestStats(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
s := e.Stats()
|
|
assert.Equal(t, 9, s.TotalNodes)
|
|
assert.Equal(t, 6, s.TotalEdges)
|
|
assert.Equal(t, 9, s.ByLanguage["go"])
|
|
}
|
|
|
|
func TestBriefModeStripsMeta(t *testing.T) {
|
|
e := NewEngine(buildTestGraph())
|
|
sg := e.GetDependencies("main.go::main", QueryOptions{Depth: 1, Limit: 50, Detail: "brief"})
|
|
for _, n := range sg.Nodes {
|
|
assert.Nil(t, n.Meta)
|
|
}
|
|
}
|
|
|
|
// buildMultiRepoTestGraph creates a graph with nodes from two repos for testing repo-scoped queries.
|
|
func buildMultiRepoTestGraph() *graph.Graph {
|
|
g := graph.New()
|
|
|
|
nodes := []*graph.Node{
|
|
{ID: "repoA/main.go::main", Kind: graph.KindFunction, Name: "main", FilePath: "repoA/main.go", Language: "go", RepoPrefix: "repoA", StartLine: 1, EndLine: 10},
|
|
{ID: "repoA/pkg/util.go::Helper", Kind: graph.KindFunction, Name: "Helper", FilePath: "repoA/pkg/util.go", Language: "go", RepoPrefix: "repoA", StartLine: 1, EndLine: 5},
|
|
{ID: "repoB/lib.go::Helper", Kind: graph.KindFunction, Name: "Helper", FilePath: "repoB/lib.go", Language: "go", RepoPrefix: "repoB", StartLine: 1, EndLine: 8},
|
|
{ID: "repoB/lib.go::Process", Kind: graph.KindFunction, Name: "Process", FilePath: "repoB/lib.go", Language: "go", RepoPrefix: "repoB", StartLine: 10, EndLine: 20},
|
|
}
|
|
for _, n := range nodes {
|
|
g.AddNode(n)
|
|
}
|
|
|
|
edges := []*graph.Edge{
|
|
{From: "repoA/main.go::main", To: "repoA/pkg/util.go::Helper", Kind: graph.EdgeCalls, FilePath: "repoA/main.go", Line: 5},
|
|
{From: "repoA/main.go::main", To: "repoB/lib.go::Process", Kind: graph.EdgeCalls, FilePath: "repoA/main.go", Line: 7, CrossRepo: true},
|
|
{From: "repoB/lib.go::Process", To: "repoB/lib.go::Helper", Kind: graph.EdgeCalls, FilePath: "repoB/lib.go", Line: 12},
|
|
}
|
|
for _, e := range edges {
|
|
g.AddEdge(e)
|
|
}
|
|
|
|
return g
|
|
}
|
|
|
|
func TestSearchSymbolsInRepo(t *testing.T) {
|
|
g := buildMultiRepoTestGraph()
|
|
e := NewEngine(g)
|
|
|
|
// Search for "Helper" scoped to repoA — should only return repoA's Helper.
|
|
results := e.SearchSymbolsInRepo("Helper", "repoA", 10)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "repoA/pkg/util.go::Helper", results[0].ID)
|
|
|
|
// Search for "Helper" scoped to repoB — should only return repoB's Helper.
|
|
results = e.SearchSymbolsInRepo("Helper", "repoB", 10)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, "repoB/lib.go::Helper", results[0].ID)
|
|
|
|
// Search for "Process" scoped to repoA — should return nothing.
|
|
results = e.SearchSymbolsInRepo("Process", "repoA", 10)
|
|
assert.Empty(t, results)
|
|
|
|
// Search for a non-existent repo — should return nothing.
|
|
results = e.SearchSymbolsInRepo("Helper", "repoC", 10)
|
|
assert.Empty(t, results)
|
|
}
|
|
|
|
func TestSearchSymbolsInRepo_Limit(t *testing.T) {
|
|
g := buildMultiRepoTestGraph()
|
|
e := NewEngine(g)
|
|
|
|
// With limit=1, should return at most 1 result.
|
|
results := e.SearchSymbolsInRepo("Helper", "repoA", 1)
|
|
assert.Len(t, results, 1)
|
|
}
|
|
|
|
func TestGetFileSymbolsInRepo(t *testing.T) {
|
|
g := buildMultiRepoTestGraph()
|
|
e := NewEngine(g)
|
|
|
|
// Get symbols for repoB/lib.go scoped to repoB.
|
|
sg := e.GetFileSymbolsInRepo("repoB/lib.go", "repoB")
|
|
assert.Len(t, sg.Nodes, 2) // Helper and Process
|
|
ids := nodeIDs(sg.Nodes)
|
|
assert.Contains(t, ids, "repoB/lib.go::Helper")
|
|
assert.Contains(t, ids, "repoB/lib.go::Process")
|
|
|
|
// Get symbols for repoB/lib.go scoped to repoA — should return nothing.
|
|
sg = e.GetFileSymbolsInRepo("repoB/lib.go", "repoA")
|
|
assert.Empty(t, sg.Nodes)
|
|
|
|
// Get symbols for a non-existent file — should return empty.
|
|
sg = e.GetFileSymbolsInRepo("nonexistent.go", "repoA")
|
|
assert.Empty(t, sg.Nodes)
|
|
}
|
|
|
|
func TestGetFileSymbolsInRepo_Edges(t *testing.T) {
|
|
g := buildMultiRepoTestGraph()
|
|
e := NewEngine(g)
|
|
|
|
// Edges should be included when at least one endpoint is in the filtered set.
|
|
sg := e.GetFileSymbolsInRepo("repoB/lib.go", "repoB")
|
|
assert.Greater(t, len(sg.Edges), 0)
|
|
}
|
|
|
|
func nodeIDs(nodes []*graph.Node) []string {
|
|
ids := make([]string, len(nodes))
|
|
for i, n := range nodes {
|
|
ids[i] = n.ID
|
|
}
|
|
return ids
|
|
}
|