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

412 lines
14 KiB
Go

package analysis
import (
"fmt"
"math"
"testing"
"github.com/zzet/gortex/internal/graph"
)
// pathGraph builds a directed path a0 -> a1 -> ... -> a(n-1) over
// EdgeCalls. On a path of length n the interior node at index i has
// an analytic betweenness of i * (n-1-i): every source at index < i
// reaches every target at index > i through it.
func pathGraph(n int) *graph.Graph {
g := graph.New()
for i := 0; i < n; i++ {
id := fmt.Sprintf("p%d", i)
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
}
for i := 0; i < n-1; i++ {
g.AddEdge(&graph.Edge{
From: fmt.Sprintf("p%d", i),
To: fmt.Sprintf("p%d", i+1),
Kind: graph.EdgeCalls,
})
}
return g
}
// relayStar builds a directed star where every leaf calls the hub and
// the hub calls every leaf. The only path between two distinct leaves
// runs leaf -> hub -> leaf, so the hub's analytic betweenness is
// k*(k-1) for k leaves and every leaf scores 0.
func relayStar(leaves int) *graph.Graph {
g := graph.New()
g.AddNode(&graph.Node{ID: "hub", Kind: graph.KindFunction, Name: "hub"})
for i := 0; i < leaves; i++ {
id := fmt.Sprintf("leaf%d", i)
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
g.AddEdge(&graph.Edge{From: id, To: "hub", Kind: graph.EdgeCalls})
g.AddEdge(&graph.Edge{From: "hub", To: id, Kind: graph.EdgeCalls})
}
return g
}
func TestComputeBetweenness_EmptyGraph(t *testing.T) {
r := ComputeBetweenness(graph.New())
if len(r.Scores) != 0 {
t.Errorf("empty graph should yield no scores, got %d", len(r.Scores))
}
if r.Max != 0 {
t.Errorf("Max = %f, want 0", r.Max)
}
if r.Sampled {
t.Errorf("empty graph should not report Sampled")
}
}
func TestComputeBetweenness_NilGraph(t *testing.T) {
r := ComputeBetweenness(nil)
if r == nil || len(r.Scores) != 0 {
t.Fatalf("nil graph should yield an empty result, got %+v", r)
}
}
// TestComputeBetweenness_ExactPathGraph checks exact Brandes' against
// the closed-form betweenness of a directed path. Every node's score
// is hand-checkable: index i on a path of n nodes scores i*(n-1-i).
func TestComputeBetweenness_ExactPathGraph(t *testing.T) {
tests := []struct {
name string
n int
want map[string]float64
}{
{
name: "path of 5",
n: 5,
// p0,p4 are endpoints (0). p1: 1*3=3. p2: 2*2=4. p3: 3*1=3.
want: map[string]float64{"p0": 0, "p1": 3, "p2": 4, "p3": 3, "p4": 0},
},
{
name: "path of 4",
n: 4,
// p1: 1*2=2. p2: 2*1=2.
want: map[string]float64{"p0": 0, "p1": 2, "p2": 2, "p3": 0},
},
{
name: "path of 3",
n: 3,
// only p1 is interior: 1*1=1.
want: map[string]float64{"p0": 0, "p1": 1, "p2": 0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := ComputeBetweenness(pathGraph(tt.n))
if r.Sampled {
t.Fatalf("small graph (%d nodes) must use the exact path", tt.n)
}
if r.Pivots != tt.n {
t.Errorf("exact path should run from every node: Pivots=%d, want %d", r.Pivots, tt.n)
}
for id, want := range tt.want {
if got := r.ScoreOf(id); math.Abs(got-want) > 1e-9 {
t.Errorf("betweenness(%s) = %v, want %v", id, got, want)
}
}
})
}
}
// TestComputeBetweenness_ExactStarGraph checks exact Brandes' against
// the closed-form betweenness of a relay star: the hub scores
// k*(k-1), every leaf scores 0.
func TestComputeBetweenness_ExactStarGraph(t *testing.T) {
tests := []struct {
name string
leaves int
wantHub float64
}{
{name: "3 leaves", leaves: 3, wantHub: 6}, // 3*2
{name: "4 leaves", leaves: 4, wantHub: 12}, // 4*3
{name: "6 leaves", leaves: 6, wantHub: 30}, // 6*5
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := ComputeBetweenness(relayStar(tt.leaves))
if r.Sampled {
t.Fatalf("small graph must use the exact path")
}
if got := r.ScoreOf("hub"); math.Abs(got-tt.wantHub) > 1e-9 {
t.Errorf("hub betweenness = %v, want %v", got, tt.wantHub)
}
if r.Max != r.ScoreOf("hub") {
t.Errorf("hub should hold the max score: max=%v hub=%v", r.Max, r.ScoreOf("hub"))
}
for i := 0; i < tt.leaves; i++ {
leaf := fmt.Sprintf("leaf%d", i)
if got := r.ScoreOf(leaf); math.Abs(got) > 1e-9 {
t.Errorf("leaf %s should have zero betweenness, got %v", leaf, got)
}
}
})
}
}
// TestComputeBetweenness_AdaptiveThreshold verifies the fast path
// switch: at or below betweennessExactThreshold every node is a
// source; above it the sampled path runs from a bounded pivot set.
func TestComputeBetweenness_AdaptiveThreshold(t *testing.T) {
tests := []struct {
name string
nodes int
wantSampled bool
wantPivots int
}{
{name: "below threshold stays exact", nodes: betweennessExactThreshold - 1, wantSampled: false, wantPivots: betweennessExactThreshold - 1},
{name: "at threshold stays exact", nodes: betweennessExactThreshold, wantSampled: false, wantPivots: betweennessExactThreshold},
{name: "above threshold goes sampled", nodes: betweennessExactThreshold + 1, wantSampled: true, wantPivots: betweennessPivots},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := ComputeBetweenness(pathGraph(tt.nodes))
if r.Sampled != tt.wantSampled {
t.Errorf("Sampled = %v, want %v (nodes=%d)", r.Sampled, tt.wantSampled, tt.nodes)
}
if r.Pivots != tt.wantPivots {
t.Errorf("Pivots = %d, want %d (nodes=%d)", r.Pivots, tt.wantPivots, tt.nodes)
}
})
}
}
// TestComputeBetweenness_SampledApproximatesExact builds a graph past
// the exact threshold and checks the sampled estimate tracks the
// analytic betweenness of a long directed path. On a path the score
// of index i is i*(n-1-i); the sampled, V/k-rescaled estimate should
// land within a modest relative tolerance for the high-centrality
// interior nodes.
func TestComputeBetweenness_SampledApproximatesExact(t *testing.T) {
n := betweennessExactThreshold + 1500
g := pathGraph(n)
r := ComputeBetweenness(g)
if !r.Sampled {
t.Fatalf("graph of %d nodes should use the sampled path", n)
}
// Check the middle of the path, where betweenness is largest and
// the relative sampling error is smallest.
mid := n / 2
id := fmt.Sprintf("p%d", mid)
want := float64(mid) * float64(n-1-mid)
got := r.ScoreOf(id)
relErr := math.Abs(got-want) / want
const tolerance = 0.20 // 20% — a 256-pivot sample on a 3500-node path
if relErr > tolerance {
t.Errorf("sampled betweenness(%s) = %.0f, want ~%.0f (rel err %.3f > %.2f)",
id, got, want, relErr, tolerance)
}
// The endpoints are never intermediate — they must stay at zero
// regardless of which pivots were sampled.
if got := r.ScoreOf("p0"); got != 0 {
t.Errorf("path endpoint p0 betweenness = %v, want 0", got)
}
if got := r.ScoreOf(fmt.Sprintf("p%d", n-1)); got != 0 {
t.Errorf("path endpoint p%d betweenness = %v, want 0", n-1, got)
}
}
// TestComputeBetweenness_SampledIsDeterministic verifies the
// fixed-seed pivot sampling produces byte-identical scores across
// repeated runs on the same graph.
func TestComputeBetweenness_SampledIsDeterministic(t *testing.T) {
n := betweennessExactThreshold + 800
g := pathGraph(n)
first := ComputeBetweenness(g)
if !first.Sampled {
t.Fatalf("graph of %d nodes should use the sampled path", n)
}
for run := 0; run < 5; run++ {
again := ComputeBetweenness(g)
if again.Pivots != first.Pivots {
t.Fatalf("run %d: Pivots = %d, want %d", run, again.Pivots, first.Pivots)
}
if again.Max != first.Max {
t.Errorf("run %d: Max = %v, want %v", run, again.Max, first.Max)
}
for id, want := range first.Scores {
if got := again.Scores[id]; got != want {
t.Errorf("run %d: score(%s) = %v, want %v — sampling not deterministic", run, id, got, want)
}
}
}
}
// TestComputeBetweenness_LargeGraphCompletes builds a graph well past
// the exact threshold and asserts the sampled fast path returns a
// well-formed result. This exercises the O(k*E) structural fast path
// without a wall-clock bound.
func TestComputeBetweenness_LargeGraphCompletes(t *testing.T) {
n := 12000
g := graph.New()
for i := 0; i < n; i++ {
id := fmt.Sprintf("n%d", i)
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
}
// A wide directed mesh: each node calls the next three. Plenty of
// shortest paths cross the interior so betweenness is non-trivial.
for i := 0; i < n; i++ {
for d := 1; d <= 3 && i+d < n; d++ {
g.AddEdge(&graph.Edge{
From: fmt.Sprintf("n%d", i),
To: fmt.Sprintf("n%d", i+d),
Kind: graph.EdgeCalls,
})
}
}
r := ComputeBetweenness(g)
if !r.Sampled {
t.Fatalf("graph of %d nodes should use the sampled path", n)
}
if r.Pivots != betweennessPivots {
t.Errorf("Pivots = %d, want %d", r.Pivots, betweennessPivots)
}
if len(r.Scores) != n {
t.Errorf("Scores should cover every node: got %d, want %d", len(r.Scores), n)
}
if r.Max <= 0 {
t.Errorf("a connected mesh should have a positive max betweenness, got %v", r.Max)
}
}
// TestComputeBetweenness_OnlyCallAndReferenceEdges verifies that
// structural edges are ignored — a path wired with EdgeDefines
// carries no betweenness.
func TestComputeBetweenness_OnlyCallAndReferenceEdges(t *testing.T) {
g := graph.New()
for _, id := range []string{"x", "y", "z"} {
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
}
g.AddEdge(&graph.Edge{From: "x", To: "y", Kind: graph.EdgeDefines})
g.AddEdge(&graph.Edge{From: "y", To: "z", Kind: graph.EdgeDefines})
r := ComputeBetweenness(g)
if r.Max != 0 {
t.Errorf("structural edges should carry no betweenness, max=%v", r.Max)
}
// References participate exactly like calls.
g2 := graph.New()
for _, id := range []string{"x", "y", "z"} {
g2.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
}
g2.AddEdge(&graph.Edge{From: "x", To: "y", Kind: graph.EdgeReferences})
g2.AddEdge(&graph.Edge{From: "y", To: "z", Kind: graph.EdgeReferences})
r2 := ComputeBetweenness(g2)
if got := r2.ScoreOf("y"); math.Abs(got-1) > 1e-9 {
t.Errorf("reference-edge path: betweenness(y) = %v, want 1", got)
}
}
// TestFindHotspots_BetweennessComponent verifies the hotspot scorer
// surfaces a pure bottleneck. The relay hub has modest fan-in/out
// relative to a separately wired high-fan-in node, but it sits on
// every leaf-to-leaf shortest path — its Betweenness field must be
// populated and non-zero, and it must rank as a hotspot.
func TestFindHotspots_BetweennessComponent(t *testing.T) {
g := relayStar(8)
// Pad with extra unrelated functions so the graph clears the
// 10-symbol floor the MCP handler enforces.
for i := 0; i < 6; i++ {
id := fmt.Sprintf("extra%d", i)
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
}
communities := &CommunityResult{NodeToComm: map[string]string{}}
result := FindHotspots(g, communities, 0)
var hub *HotspotEntry
for i := range result {
if result[i].ID == "hub" {
hub = &result[i]
break
}
}
if hub == nil {
t.Fatalf("relay hub should be reported as a hotspot, got %d entries", len(result))
}
if hub.Betweenness <= 0 {
t.Errorf("relay hub should carry a positive betweenness component, got %v", hub.Betweenness)
}
// The hub is the single highest-betweenness node, so its
// normalized betweenness should be the 0-100 ceiling.
if math.Abs(hub.Betweenness-100) > 0.01 {
t.Errorf("relay hub betweenness = %v, want 100 (it holds the graph max)", hub.Betweenness)
}
// A leaf is never an intermediate vertex — if it surfaces at all
// its betweenness component is zero.
for i := range result {
if result[i].ID == "leaf0" && result[i].Betweenness != 0 {
t.Errorf("leaf0 betweenness = %v, want 0", result[i].Betweenness)
}
}
}
// TestFindHotspots_BetweennessRaisesRank verifies the betweenness
// term augments — not replaces — the legacy fan-in/out signal: adding
// a bottleneck role to a node strictly raises its complexity score.
func TestFindHotspots_BetweennessRaisesRank(t *testing.T) {
// Baseline: a plain 3-hop chain bridge -> via -> sink, plus
// padding to clear the symbol floor.
build := func(withBottleneck bool) []HotspotEntry {
g := graph.New()
ids := []string{"src", "via", "sink"}
for _, id := range ids {
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
}
g.AddEdge(&graph.Edge{From: "src", To: "via", Kind: graph.EdgeCalls})
g.AddEdge(&graph.Edge{From: "via", To: "sink", Kind: graph.EdgeCalls})
if withBottleneck {
// Route extra callers and callees through `via` so it
// becomes a genuine shortest-path bottleneck.
for i := 0; i < 4; i++ {
in := fmt.Sprintf("in%d", i)
out := fmt.Sprintf("out%d", i)
g.AddNode(&graph.Node{ID: in, Kind: graph.KindFunction, Name: in})
g.AddNode(&graph.Node{ID: out, Kind: graph.KindFunction, Name: out})
g.AddEdge(&graph.Edge{From: in, To: "via", Kind: graph.EdgeCalls})
g.AddEdge(&graph.Edge{From: "via", To: out, Kind: graph.EdgeCalls})
}
}
for i := 0; i < 8; i++ {
id := fmt.Sprintf("pad%d", i)
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id})
}
return FindHotspots(g, &CommunityResult{NodeToComm: map[string]string{}}, 0)
}
scoreOf := func(entries []HotspotEntry, id string) (float64, bool) {
for _, e := range entries {
if e.ID == id {
return e.ComplexityScore, true
}
}
return 0, false
}
withBottleneck := build(true)
viaScore, ok := scoreOf(withBottleneck, "via")
if !ok {
t.Fatalf("bottleneck node `via` should be reported as a hotspot")
}
if viaScore <= 0 {
t.Errorf("bottleneck node should have a positive complexity score, got %v", viaScore)
}
// The bottleneck node carries both fan-in/out and betweenness, so
// it must outrank the inert padding functions.
if padScore, ok := scoreOf(withBottleneck, "pad0"); ok && viaScore <= padScore {
t.Errorf("bottleneck node (%.2f) should outrank inert padding (%.2f)", viaScore, padScore)
}
}