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

141 lines
4.5 KiB
Go

package analysis
import (
"testing"
"github.com/zzet/gortex/internal/graph"
)
// TestLeidenMatchesLouvainOnSmallGraph confirms Leiden produces a
// sensible partition on the same toy graph the existing Louvain
// tests use. We don't require identical output (the algorithms can
// settle on different local maxima) — just that:
// - communities are non-empty
// - every multi-member community has cohesion ≥ 0
// - modularity is ≥ 0 (positive = better-than-random clustering)
func TestLeidenMatchesLouvainOnSmallGraph(t *testing.T) {
g := buildTestGraph()
leiden := DetectCommunitiesLeiden(g)
louvain := DetectCommunities(g)
if leiden == nil || louvain == nil {
t.Fatal("nil result")
}
if len(leiden.Communities) == 0 {
t.Fatal("leiden produced no communities")
}
if leiden.Modularity < 0 {
t.Fatalf("leiden modularity went negative: %v", leiden.Modularity)
}
for _, c := range leiden.Communities {
if c.Size < 1 {
t.Errorf("leiden community %q is empty", c.ID)
}
if c.Cohesion < 0 || c.Cohesion > 1 {
t.Errorf("leiden community %q has out-of-range cohesion %v", c.ID, c.Cohesion)
}
}
t.Logf("leiden: %d communities, modularity %.3f", len(leiden.Communities), leiden.Modularity)
t.Logf("louvain: %d communities, modularity %.3f", len(louvain.Communities), louvain.Modularity)
}
// TestLeidenReachableNodes guarantees every node ends up in some
// community in the final result — Leiden shouldn't lose anyone.
func TestLeidenReachableNodes(t *testing.T) {
g := buildTestGraph()
res := DetectCommunitiesLeiden(g)
// Count graph-relevant nodes — singletons (no in/out edges) are
// intentionally dropped from the result's NodeToComm map.
expected := 0
for _, n := range g.AllNodes() {
if n.Kind == graph.KindFile || n.Kind == graph.KindImport {
continue
}
for _, e := range g.AllEdges() {
if (e.From == n.ID || e.To == n.ID) && edgeWeight(e.Kind) > 0 {
expected++
break
}
}
}
if len(res.NodeToComm) < expected {
t.Fatalf("leiden lost nodes: NodeToComm has %d entries, expected at least %d graph-relevant nodes", len(res.NodeToComm), expected)
}
}
// TestLeidenConnectednessGuarantee is the *signature* benefit of
// Leiden over Louvain: every produced community is a connected
// induced subgraph. Build a graph with a structure that's known to
// trip Louvain into producing a disconnected community (rare in
// practice but reproducible), and confirm Leiden doesn't.
//
// The trip-up pattern: a "bridge" node X strongly connected to
// both clusters A and B. Louvain can move X into A's community
// while leaving A's connectivity weakened, so A's remaining
// members lose direct paths to each other in the induced subgraph.
//
// For our purposes we just verify connectedness as a property of
// every community in a synthetic dense graph.
func TestLeidenConnectednessGuarantee(t *testing.T) {
g := graph.New()
add := func(id string) { g.AddNode(&graph.Node{ID: id, Name: id, Kind: graph.KindFunction, FilePath: id + ".go"}) }
call := func(from, to string) { g.AddEdge(&graph.Edge{From: from, To: to, Kind: graph.EdgeCalls}) }
// Two dense triangles + bridge node
for _, id := range []string{"a1", "a2", "a3", "b1", "b2", "b3", "x"} {
add(id)
}
call("a1", "a2"); call("a2", "a3"); call("a3", "a1")
call("b1", "b2"); call("b2", "b3"); call("b3", "b1")
call("x", "a1"); call("x", "b1")
res := DetectCommunitiesLeiden(g)
// For each community, verify every member can reach every other
// member via intra-community edges only.
for _, c := range res.Communities {
if !isConnectedInGraph(g, c.Members) {
t.Errorf("leiden community %q (%v) is disconnected", c.ID, c.Members)
}
}
}
func isConnectedInGraph(g *graph.Graph, members []string) bool {
if len(members) <= 1 {
return true
}
memberSet := make(map[string]bool, len(members))
for _, m := range members {
memberSet[m] = true
}
adj := make(map[string]map[string]bool)
for _, e := range g.AllEdges() {
if !memberSet[e.From] || !memberSet[e.To] {
continue
}
if adj[e.From] == nil {
adj[e.From] = make(map[string]bool)
}
if adj[e.To] == nil {
adj[e.To] = make(map[string]bool)
}
adj[e.From][e.To] = true
adj[e.To][e.From] = true
}
// BFS from members[0]
visited := map[string]bool{members[0]: true}
queue := []string{members[0]}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
for nbr := range adj[node] {
if !visited[nbr] {
visited[nbr] = true
queue = append(queue, nbr)
}
}
}
return len(visited) == len(members)
}