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
108 lines
3.2 KiB
Go
108 lines
3.2 KiB
Go
package analysis
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// seedComponentTestGraph builds a hub-and-spoke graph: two SCC
|
|
// triangles + one hub every node points at. Gives predictable
|
|
// WCC + SCC answers.
|
|
func seedComponentTestGraph() *graph.Graph {
|
|
g := graph.New()
|
|
for _, id := range []string{"a", "b", "c", "d", "e", "f", "hub"} {
|
|
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id, FilePath: id + ".go"})
|
|
}
|
|
edges := [][2]string{
|
|
{"a", "b"}, {"b", "c"}, {"c", "a"}, // triangle 1
|
|
{"d", "e"}, {"e", "f"}, {"f", "d"}, // triangle 2
|
|
{"c", "d"}, // bridge
|
|
{"a", "hub"}, {"b", "hub"}, {"c", "hub"},
|
|
{"d", "hub"}, {"e", "hub"}, {"f", "hub"},
|
|
}
|
|
for _, e := range edges {
|
|
g.AddEdge(&graph.Edge{From: e[0], To: e[1], Kind: graph.EdgeCalls, FilePath: "x.go"})
|
|
}
|
|
return g
|
|
}
|
|
|
|
func TestComputeWCC_OneComponent(t *testing.T) {
|
|
g := seedComponentTestGraph()
|
|
res := ComputeWCC(g, ComponentOptions{})
|
|
require.Len(t, res, 1, "all 7 nodes form one WCC; got %v", res)
|
|
assert.Equal(t, 7, res[0].Size)
|
|
}
|
|
|
|
func TestComputeWCC_HonoursEdgeFilter(t *testing.T) {
|
|
g := seedComponentTestGraph()
|
|
// Filter out the call edges entirely → no surviving edges → every node
|
|
// becomes its own singleton component.
|
|
res := ComputeWCC(g, ComponentOptions{
|
|
EdgeKinds: []graph.EdgeKind{graph.EdgeReferences},
|
|
})
|
|
assert.Len(t, res, 7,
|
|
"with no surviving edges every node should be a singleton; got %v", res)
|
|
}
|
|
|
|
func TestComputeSCC_ThreeComponents(t *testing.T) {
|
|
g := seedComponentTestGraph()
|
|
res := ComputeSCC(g, ComponentOptions{})
|
|
// 7 SCCs: {a,b,c}, {d,e,f}, {hub} (singleton). But the hub is
|
|
// trivial — without MinSize, expect 3 with sizes [3, 3, 1].
|
|
require.GreaterOrEqual(t, len(res), 3)
|
|
|
|
bySize := map[int]int{}
|
|
for _, r := range res {
|
|
bySize[r.Size]++
|
|
}
|
|
assert.Equal(t, 2, bySize[3], "should find two 3-node SCCs (the triangles); got %v", res)
|
|
}
|
|
|
|
func TestComputeSCC_MinSize_DropsSingletons(t *testing.T) {
|
|
g := seedComponentTestGraph()
|
|
res := ComputeSCC(g, ComponentOptions{MinSize: 2})
|
|
for _, r := range res {
|
|
assert.GreaterOrEqual(t, r.Size, 2,
|
|
"MinSize=2 should drop singleton SCCs; got %v", r)
|
|
}
|
|
}
|
|
|
|
// TestComputeSCC_Iterative_NoStackOverflow constructs a deep
|
|
// straight-line graph (1 -> 2 -> 3 -> ... -> N) to make sure the
|
|
// iterative Tarjan stays in heap and doesn't blow the goroutine
|
|
// call stack. N = 10k; recursive Tarjan would fall over.
|
|
func TestComputeSCC_Iterative_NoStackOverflow(t *testing.T) {
|
|
const n = 10000
|
|
g := graph.New()
|
|
for i := 0; i < n; i++ {
|
|
id := charID(i)
|
|
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id, FilePath: "x.go"})
|
|
}
|
|
for i := 0; i < n-1; i++ {
|
|
g.AddEdge(&graph.Edge{
|
|
From: charID(i), To: charID(i + 1), Kind: graph.EdgeCalls, FilePath: "x.go",
|
|
})
|
|
}
|
|
res := ComputeSCC(g, ComponentOptions{})
|
|
// A DAG of N nodes has N singleton SCCs.
|
|
assert.Equal(t, n, len(res))
|
|
}
|
|
|
|
func charID(i int) string {
|
|
// fmt.Sprintf is fine but we want zero allocs in the loop body — just
|
|
// build a deterministic string ID.
|
|
const hex = "0123456789abcdef"
|
|
out := make([]byte, 0, 8)
|
|
for x := i; ; x /= 16 {
|
|
out = append([]byte{hex[x%16]}, out...)
|
|
if x < 16 {
|
|
break
|
|
}
|
|
}
|
|
return "n_" + string(out)
|
|
}
|