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
122 lines
4.4 KiB
Go
122 lines
4.4 KiB
Go
package graph
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// buildClassifyGraph wires three functions in one file, each with a
|
|
// distinct edge profile, so ClassifyZeroEdge can be exercised against
|
|
// every classification:
|
|
//
|
|
// - Used — the file defines it AND a caller calls it.
|
|
// - Unused — the file defines it, it makes an outgoing call, but
|
|
// nothing calls it back. Indexed, dead.
|
|
// - Orphaned — added with no edges at all, mimicking a symbol the
|
|
// extractor never processed.
|
|
func buildClassifyGraph() *Graph {
|
|
g := New()
|
|
g.AddNode(&Node{ID: "svc.go", Kind: KindFile, Name: "svc.go", FilePath: "svc.go"})
|
|
g.AddNode(&Node{ID: "svc.go::Caller", Kind: KindFunction, Name: "Caller", FilePath: "svc.go"})
|
|
g.AddNode(&Node{ID: "svc.go::Used", Kind: KindFunction, Name: "Used", FilePath: "svc.go"})
|
|
g.AddNode(&Node{ID: "svc.go::Unused", Kind: KindFunction, Name: "Unused", FilePath: "svc.go"})
|
|
g.AddNode(&Node{ID: "svc.go::Helper", Kind: KindFunction, Name: "Helper", FilePath: "svc.go"})
|
|
|
|
// The file defines every function except the orphan.
|
|
g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Caller", Kind: EdgeDefines})
|
|
g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Used", Kind: EdgeDefines})
|
|
g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Unused", Kind: EdgeDefines})
|
|
g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Helper", Kind: EdgeDefines})
|
|
|
|
// Used has an incoming call edge.
|
|
g.AddEdge(&Edge{From: "svc.go::Caller", To: "svc.go::Used", Kind: EdgeCalls})
|
|
// Unused has only an outgoing call — no incoming usage edge.
|
|
g.AddEdge(&Edge{From: "svc.go::Unused", To: "svc.go::Helper", Kind: EdgeCalls})
|
|
return g
|
|
}
|
|
|
|
func TestClassifyZeroEdge(t *testing.T) {
|
|
g := buildClassifyGraph()
|
|
// A method node with only its member_of structural edge — indexed,
|
|
// but nothing references it.
|
|
g.AddNode(&Node{ID: "svc.go::T", Kind: KindType, Name: "T", FilePath: "svc.go"})
|
|
g.AddNode(&Node{ID: "svc.go::T.Method", Kind: KindMethod, Name: "Method", FilePath: "svc.go"})
|
|
g.AddEdge(&Edge{From: "svc.go::T.Method", To: "svc.go::T", Kind: EdgeMemberOf})
|
|
// An orphan: in the graph, but carrying no edges of any kind.
|
|
g.AddNode(&Node{ID: "svc.go::Orphan", Kind: KindFunction, Name: "Orphan", FilePath: "svc.go"})
|
|
|
|
tests := []struct {
|
|
name string
|
|
symbolID string
|
|
want ZeroEdgeClass
|
|
}{
|
|
{
|
|
name: "incoming call edge yields none",
|
|
symbolID: "svc.go::Used",
|
|
want: ZeroEdgeNone,
|
|
},
|
|
{
|
|
name: "only structural defines plus outgoing call yields likely_unused",
|
|
symbolID: "svc.go::Unused",
|
|
want: ZeroEdgeLikelyUnused,
|
|
},
|
|
{
|
|
name: "method with only member_of yields likely_unused",
|
|
symbolID: "svc.go::T.Method",
|
|
want: ZeroEdgeLikelyUnused,
|
|
},
|
|
{
|
|
name: "zero edges of any kind yields possible_extraction_gap",
|
|
symbolID: "svc.go::Orphan",
|
|
want: ZeroEdgePossibleExtractionGap,
|
|
},
|
|
{
|
|
name: "unknown symbol id yields possible_extraction_gap",
|
|
symbolID: "svc.go::DoesNotExist",
|
|
want: ZeroEdgePossibleExtractionGap,
|
|
},
|
|
{
|
|
name: "empty symbol id yields possible_extraction_gap",
|
|
symbolID: "",
|
|
want: ZeroEdgePossibleExtractionGap,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ClassifyZeroEdge(g, tt.symbolID)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyZeroEdge_NilGraph(t *testing.T) {
|
|
assert.Equal(t, ZeroEdgePossibleExtractionGap, ClassifyZeroEdge(nil, "svc.go::Used"))
|
|
}
|
|
|
|
func TestCaveatForZeroEdge(t *testing.T) {
|
|
g := buildClassifyGraph()
|
|
g.AddNode(&Node{ID: "svc.go::Orphan", Kind: KindFunction, Name: "Orphan", FilePath: "svc.go"})
|
|
|
|
// A used symbol carries no caveat — nil is returned so callers can
|
|
// attach it unconditionally.
|
|
assert.Nil(t, CaveatForZeroEdge(g, "svc.go::Used"))
|
|
|
|
// Likely-unused: classification plus a non-empty message.
|
|
unused := CaveatForZeroEdge(g, "svc.go::Unused")
|
|
require.NotNil(t, unused)
|
|
assert.Equal(t, ZeroEdgeLikelyUnused, unused.Class)
|
|
assert.NotEmpty(t, unused.Message)
|
|
|
|
// Extraction gap: classification plus a non-empty message.
|
|
gap := CaveatForZeroEdge(g, "svc.go::Orphan")
|
|
require.NotNil(t, gap)
|
|
assert.Equal(t, ZeroEdgePossibleExtractionGap, gap.Class)
|
|
assert.NotEmpty(t, gap.Message)
|
|
|
|
// The two caveat messages must differ — they describe opposite
|
|
// situations and an agent has to be able to tell them apart.
|
|
assert.NotEqual(t, unused.Message, gap.Message)
|
|
}
|