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
258 lines
8.5 KiB
Go
258 lines
8.5 KiB
Go
package analysis
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// connNode is a single node in a connectivity test fixture.
|
|
type connNode struct {
|
|
id string
|
|
kind graph.NodeKind
|
|
file string
|
|
}
|
|
|
|
// connEdge is a single directed edge in a connectivity test fixture.
|
|
type connEdge struct {
|
|
from string
|
|
to string
|
|
}
|
|
|
|
// buildConnGraph assembles a graph from a node/edge fixture so each
|
|
// test pins exactly which nodes are isolated / leaf / connected.
|
|
func buildConnGraph(nodes []connNode, edges []connEdge) (*graph.Graph, []*graph.Node) {
|
|
g := graph.New()
|
|
for _, n := range nodes {
|
|
g.AddNode(&graph.Node{ID: n.id, Kind: n.kind, Name: n.id, FilePath: n.file, Language: "go"})
|
|
}
|
|
for _, e := range edges {
|
|
g.AddEdge(&graph.Edge{From: e.from, To: e.to, Kind: graph.EdgeDefines, Confidence: 1})
|
|
}
|
|
return g, g.AllNodes()
|
|
}
|
|
|
|
func TestGraphConnectivity(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
// fixture
|
|
nodes []connNode
|
|
edges []connEdge
|
|
// expectations
|
|
wantNominal int
|
|
wantEffective int
|
|
wantRatio float64
|
|
wantIsolated int
|
|
wantLeaf int
|
|
wantSourceOnly int
|
|
wantSinkOnly int
|
|
}{
|
|
{
|
|
name: "empty graph",
|
|
nodes: nil,
|
|
edges: nil,
|
|
wantNominal: 0,
|
|
wantEffective: 0,
|
|
wantRatio: 0,
|
|
},
|
|
{
|
|
// file.go defines fn — fn is a leaf (one edge) and
|
|
// sink-only (only incoming); file.go is source-only.
|
|
// No node is isolated.
|
|
name: "fully connected pair",
|
|
nodes: []connNode{
|
|
{"a.go", graph.KindFile, "a.go"},
|
|
{"a.go::Fn", graph.KindFunction, "a.go"},
|
|
},
|
|
edges: []connEdge{{"a.go", "a.go::Fn"}},
|
|
wantNominal: 2,
|
|
wantEffective: 2,
|
|
wantRatio: 1.0,
|
|
wantIsolated: 0,
|
|
wantLeaf: 2, // both ends of the single edge have degree 1
|
|
wantSourceOnly: 1, // a.go
|
|
wantSinkOnly: 1, // a.go::Fn
|
|
},
|
|
{
|
|
// One isolated function: zero edges of any kind. This is
|
|
// the headline extraction-gap signal — NOT dead code.
|
|
name: "one isolated node",
|
|
nodes: []connNode{
|
|
{"a.go", graph.KindFile, "a.go"},
|
|
{"a.go::Fn", graph.KindFunction, "a.go"},
|
|
{"orphan.go::Lost", graph.KindFunction, "orphan.go"},
|
|
},
|
|
edges: []connEdge{{"a.go", "a.go::Fn"}},
|
|
wantNominal: 3,
|
|
wantEffective: 2,
|
|
wantRatio: 2.0 / 3.0,
|
|
wantIsolated: 1, // orphan.go::Lost
|
|
wantLeaf: 2, // a.go and a.go::Fn
|
|
wantSourceOnly: 1,
|
|
wantSinkOnly: 1,
|
|
},
|
|
{
|
|
// A chain a -> b -> c. b has in+out (degree 2, neither
|
|
// leaf nor source/sink-only). a is source-only, c is
|
|
// sink-only; both are leaves.
|
|
name: "three-node chain",
|
|
nodes: []connNode{
|
|
{"a.go::A", graph.KindFunction, "a.go"},
|
|
{"a.go::B", graph.KindFunction, "a.go"},
|
|
{"a.go::C", graph.KindFunction, "a.go"},
|
|
},
|
|
edges: []connEdge{
|
|
{"a.go::A", "a.go::B"},
|
|
{"a.go::B", "a.go::C"},
|
|
},
|
|
wantNominal: 3,
|
|
wantEffective: 3,
|
|
wantRatio: 1.0,
|
|
wantIsolated: 0,
|
|
wantLeaf: 2, // A and C
|
|
wantSourceOnly: 1, // A
|
|
wantSinkOnly: 1, // C
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g, nodes := buildConnGraph(tt.nodes, tt.edges)
|
|
report := GraphConnectivity(g, nodes, 0)
|
|
|
|
assert.Equal(t, tt.wantNominal, report.NominalNodes, "nominal_nodes")
|
|
assert.Equal(t, tt.wantEffective, report.EffectiveNodes, "effective_nodes")
|
|
assert.InDelta(t, tt.wantRatio, report.EffectiveRatio, 1e-9, "effective_ratio")
|
|
assert.Equal(t, tt.wantIsolated, report.Isolated, "isolated")
|
|
assert.Equal(t, tt.wantLeaf, report.Leaf, "leaf")
|
|
assert.Equal(t, tt.wantSourceOnly, report.SourceOnly, "source_only")
|
|
assert.Equal(t, tt.wantSinkOnly, report.SinkOnly, "sink_only")
|
|
assert.NotEmpty(t, report.Note, "report must carry the extraction-vs-dead-code note")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGraphConnectivity_DeadWeightByFile asserts the per-file
|
|
// dead-weight attribution ranks files by isolated+leaf contribution
|
|
// so an extraction gap can be localised.
|
|
func TestGraphConnectivity_DeadWeightByFile(t *testing.T) {
|
|
// gappy.go contributes 3 isolated nodes; ok.go contributes a
|
|
// single connected pair (two leaves, zero isolated).
|
|
nodes := []connNode{
|
|
{"ok.go", graph.KindFile, "ok.go"},
|
|
{"ok.go::Fn", graph.KindFunction, "ok.go"},
|
|
{"gappy.go::L1", graph.KindFunction, "gappy.go"},
|
|
{"gappy.go::L2", graph.KindFunction, "gappy.go"},
|
|
{"gappy.go::L3", graph.KindFunction, "gappy.go"},
|
|
}
|
|
edges := []connEdge{{"ok.go", "ok.go::Fn"}}
|
|
|
|
g, allNodes := buildConnGraph(nodes, edges)
|
|
report := GraphConnectivity(g, allNodes, 0)
|
|
|
|
require.Len(t, report.DeadWeightByFile, 2, "both files contribute dead-weight nodes")
|
|
|
|
// gappy.go ranks first: 3 isolated nodes > ok.go's 2 leaf nodes.
|
|
top := report.DeadWeightByFile[0]
|
|
assert.Equal(t, "gappy.go", top.FilePath)
|
|
assert.Equal(t, 3, top.Isolated)
|
|
assert.Equal(t, 0, top.Leaf)
|
|
assert.Equal(t, 3, top.DeadWeight)
|
|
|
|
second := report.DeadWeightByFile[1]
|
|
assert.Equal(t, "ok.go", second.FilePath)
|
|
assert.Equal(t, 0, second.Isolated)
|
|
assert.Equal(t, 2, second.Leaf)
|
|
assert.Equal(t, 2, second.DeadWeight)
|
|
}
|
|
|
|
// TestGraphConnectivity_FileLimit asserts the fileLimit argument
|
|
// truncates the dead-weight ranking to the top-N files.
|
|
func TestGraphConnectivity_FileLimit(t *testing.T) {
|
|
nodes := []connNode{
|
|
{"a.go::A", graph.KindFunction, "a.go"},
|
|
{"b.go::B", graph.KindFunction, "b.go"},
|
|
{"c.go::C", graph.KindFunction, "c.go"},
|
|
}
|
|
g, allNodes := buildConnGraph(nodes, nil) // all three isolated
|
|
|
|
report := GraphConnectivity(g, allNodes, 2)
|
|
assert.Len(t, report.DeadWeightByFile, 2, "fileLimit=2 must cap the ranking at 2 files")
|
|
assert.Equal(t, 3, report.Isolated, "the isolated count is not affected by fileLimit")
|
|
}
|
|
|
|
// TestGraphConnectivity_ByKind asserts the per-node-kind breakdown
|
|
// tallies isolated / leaf counts separately per kind.
|
|
func TestGraphConnectivity_ByKind(t *testing.T) {
|
|
// One connected file->function pair, plus an isolated type.
|
|
nodes := []connNode{
|
|
{"a.go", graph.KindFile, "a.go"},
|
|
{"a.go::Fn", graph.KindFunction, "a.go"},
|
|
{"a.go::Orphan", graph.KindType, "a.go"},
|
|
}
|
|
edges := []connEdge{{"a.go", "a.go::Fn"}}
|
|
|
|
g, allNodes := buildConnGraph(nodes, edges)
|
|
report := GraphConnectivity(g, allNodes, 0)
|
|
|
|
byKind := map[string]ConnectivityKindEntry{}
|
|
for _, k := range report.ByKind {
|
|
byKind[k.Kind] = k
|
|
}
|
|
|
|
require.Contains(t, byKind, "type")
|
|
assert.Equal(t, 1, byKind["type"].Total)
|
|
assert.Equal(t, 1, byKind["type"].Isolated, "the orphan type is isolated")
|
|
assert.Equal(t, 0, byKind["type"].Leaf)
|
|
|
|
require.Contains(t, byKind, "function")
|
|
assert.Equal(t, 1, byKind["function"].Total)
|
|
assert.Equal(t, 0, byKind["function"].Isolated)
|
|
assert.Equal(t, 1, byKind["function"].Leaf, "the defined function has degree 1")
|
|
}
|
|
|
|
// TestGraphConnectivity_IsolatedIsNotDeadCode pins the load-bearing
|
|
// distinction: an isolated node (zero edges of ANY kind) is an
|
|
// extraction-gap signal, whereas a dead-code node still carries a
|
|
// structural edge. A node that is `defines`-linked from its file but
|
|
// has no incoming usage edge is dead code, NOT isolated — this
|
|
// analyzer must not count it as isolated.
|
|
func TestGraphConnectivity_IsolatedIsNotDeadCode(t *testing.T) {
|
|
nodes := []connNode{
|
|
{"a.go", graph.KindFile, "a.go"},
|
|
// Defined by its file but never used — classic dead code.
|
|
{"a.go::Unused", graph.KindFunction, "a.go"},
|
|
// No edges at all — an extraction gap.
|
|
{"b.go::Missing", graph.KindFunction, "b.go"},
|
|
}
|
|
edges := []connEdge{{"a.go", "a.go::Unused"}}
|
|
|
|
g, allNodes := buildConnGraph(nodes, edges)
|
|
report := GraphConnectivity(g, allNodes, 0)
|
|
|
|
// Only the zero-edge node counts as isolated; the dead-code node
|
|
// has a structural edge and does not.
|
|
assert.Equal(t, 1, report.Isolated, "only the zero-edge node is isolated")
|
|
|
|
// Cross-check against the shared classifier the analyzer reuses.
|
|
assert.Equal(t, graph.ZeroEdgePossibleExtractionGap,
|
|
graph.ClassifyZeroEdge(g, "b.go::Missing"),
|
|
"the zero-edge node classifies as an extraction gap")
|
|
assert.NotEqual(t, graph.ZeroEdgePossibleExtractionGap,
|
|
graph.ClassifyZeroEdge(g, "a.go::Unused"),
|
|
"the structurally-linked dead-code node is NOT an extraction gap")
|
|
}
|
|
|
|
// TestGraphConnectivity_NilGraph asserts a nil graph yields a zero
|
|
// report rather than panicking.
|
|
func TestGraphConnectivity_NilGraph(t *testing.T) {
|
|
report := GraphConnectivity(nil, nil, 0)
|
|
assert.Equal(t, 0, report.NominalNodes)
|
|
assert.Equal(t, 0, report.EffectiveNodes)
|
|
assert.True(t, math.IsNaN(report.EffectiveRatio) == false, "ratio stays a real number")
|
|
}
|