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
99 lines
2.8 KiB
Go
99 lines
2.8 KiB
Go
package analysis
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// clique adds n function nodes with the given id prefix and a call
|
|
// edge between every pair — a maximally cohesive cluster.
|
|
func clique(g *graph.Graph, prefix string, n int) []string {
|
|
ids := make([]string, n)
|
|
for i := 0; i < n; i++ {
|
|
id := fmt.Sprintf("%s%d", prefix, i)
|
|
ids[i] = id
|
|
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id, FilePath: prefix + ".go"})
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
for j := i + 1; j < n; j++ {
|
|
g.AddEdge(&graph.Edge{From: ids[i], To: ids[j], Kind: graph.EdgeCalls})
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func TestSpectralClusters_SeparatesTwoCliques(t *testing.T) {
|
|
g := graph.New()
|
|
a := clique(g, "a", 12)
|
|
b := clique(g, "b", 12)
|
|
// A single bridge edge — the Fiedler cut should run through it.
|
|
g.AddEdge(&graph.Edge{From: a[0], To: b[0], Kind: graph.EdgeCalls})
|
|
|
|
res := SpectralClusters(g)
|
|
if len(res.Communities) != 2 {
|
|
t.Fatalf("expected 2 communities, got %d: %+v", len(res.Communities), res.Communities)
|
|
}
|
|
|
|
// No community may mix an a-clique node with a b-clique node.
|
|
for _, c := range res.Communities {
|
|
var sawA, sawB bool
|
|
for _, m := range c.Members {
|
|
if m[0] == 'a' {
|
|
sawA = true
|
|
} else {
|
|
sawB = true
|
|
}
|
|
}
|
|
if sawA && sawB {
|
|
t.Errorf("community %s mixes the two cliques: %v", c.ID, c.Members)
|
|
}
|
|
}
|
|
|
|
// Every node is assigned.
|
|
if len(res.NodeToComm) != 24 {
|
|
t.Errorf("NodeToComm covers %d nodes, want 24", len(res.NodeToComm))
|
|
}
|
|
// A well-separated partition has clearly positive modularity.
|
|
if res.Modularity <= 0 {
|
|
t.Errorf("modularity = %f, want > 0 for two near-disjoint cliques", res.Modularity)
|
|
}
|
|
}
|
|
|
|
func TestSpectralClusters_EmptyGraph(t *testing.T) {
|
|
res := SpectralClusters(graph.New())
|
|
if len(res.Communities) != 0 {
|
|
t.Errorf("empty graph should yield no communities, got %d", len(res.Communities))
|
|
}
|
|
if res.NodeToComm == nil {
|
|
t.Error("NodeToComm must be non-nil even for an empty graph")
|
|
}
|
|
}
|
|
|
|
func TestSpectralClusters_SmallComponentNotSplit(t *testing.T) {
|
|
g := graph.New()
|
|
// A single 6-node clique — well below the bisection floor, so it
|
|
// must come back as exactly one community.
|
|
clique(g, "c", 6)
|
|
res := SpectralClusters(g)
|
|
if len(res.Communities) != 1 {
|
|
t.Fatalf("a small clique should be one community, got %d", len(res.Communities))
|
|
}
|
|
if res.Communities[0].Size != 6 {
|
|
t.Errorf("community size = %d, want 6", res.Communities[0].Size)
|
|
}
|
|
}
|
|
|
|
func TestSpectralClusters_DisconnectedComponents(t *testing.T) {
|
|
g := graph.New()
|
|
clique(g, "x", 5)
|
|
clique(g, "y", 5)
|
|
clique(g, "z", 5)
|
|
// No bridges — three disconnected cliques.
|
|
res := SpectralClusters(g)
|
|
if len(res.Communities) != 3 {
|
|
t.Fatalf("three disconnected cliques should be three communities, got %d", len(res.Communities))
|
|
}
|
|
}
|