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
188 lines
5.9 KiB
Go
188 lines
5.9 KiB
Go
package analysis
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// fanInGraph builds a hub symbol with `callers` inbound call edges plus a
|
|
// covering test, and returns the graph.
|
|
func fanInGraph(t *testing.T, hubID, hubFile string, callers int) *graph.Graph {
|
|
t.Helper()
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: hubID, Kind: graph.KindFunction, Name: "Hub", FilePath: hubFile, StartLine: 1, EndLine: 10})
|
|
for i := 0; i < callers; i++ {
|
|
cid := hubFile + "::caller" + itoaTest(i)
|
|
g.AddNode(&graph.Node{ID: cid, Kind: graph.KindFunction, Name: "caller" + itoaTest(i), FilePath: hubFile})
|
|
g.AddEdge(&graph.Edge{From: cid, To: hubID, Kind: graph.EdgeCalls})
|
|
}
|
|
return g
|
|
}
|
|
|
|
func itoaTest(n int) string {
|
|
const digits = "0123456789"
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var b []byte
|
|
for n > 0 {
|
|
b = append([]byte{digits[n%10]}, b...)
|
|
n /= 10
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func factorByAxis(factors []PRRiskFactor, axis string) (PRRiskFactor, bool) {
|
|
for _, f := range factors {
|
|
if f.Axis == axis {
|
|
return f, true
|
|
}
|
|
}
|
|
return PRRiskFactor{}, false
|
|
}
|
|
|
|
func TestScorePRRisk_WideFanInDrivesFlow(t *testing.T) {
|
|
hubID := "pkg/hub.go::Hub"
|
|
g := fanInGraph(t, hubID, "pkg/hub.go", 25)
|
|
|
|
res := ScorePRRisk(g, PRRiskInput{
|
|
SymbolIDs: []string{hubID},
|
|
ChangedFiles: []string{"pkg/hub.go"},
|
|
})
|
|
|
|
if res.TotalAffected < 20 {
|
|
t.Fatalf("expected wide blast radius, got TotalAffected=%d", res.TotalAffected)
|
|
}
|
|
flow, ok := factorByAxis(res.Factors, "flow")
|
|
if !ok {
|
|
t.Fatal("flow factor missing")
|
|
}
|
|
if flow.Score < 50 {
|
|
t.Fatalf("expected high flow axis for a 25-caller hub, got %.1f", flow.Score)
|
|
}
|
|
callers, ok := factorByAxis(res.Factors, "callers")
|
|
if !ok {
|
|
t.Fatal("callers factor missing")
|
|
}
|
|
if callers.Score < 50 {
|
|
t.Fatalf("expected high caller axis for a 25-caller hub, got %.1f", callers.Score)
|
|
}
|
|
// A 25-caller, untested hub should land at least HIGH.
|
|
if res.Risk != RiskHigh && res.Risk != RiskCritical {
|
|
t.Fatalf("expected >=HIGH for a wide-fan-in untested hub, got %s (score %.1f)", res.Risk, res.Score)
|
|
}
|
|
}
|
|
|
|
func TestScorePRRisk_SecurityKeywordRaisesRisk(t *testing.T) {
|
|
// A small change with no callers, but in an auth path.
|
|
symID := "internal/auth/login.go::ValidateToken"
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: symID, Kind: graph.KindFunction, Name: "ValidateToken", FilePath: "internal/auth/login.go", StartLine: 1, EndLine: 5})
|
|
|
|
res := ScorePRRisk(g, PRRiskInput{
|
|
SymbolIDs: []string{symID},
|
|
ChangedFiles: []string{"internal/auth/login.go"},
|
|
})
|
|
|
|
if len(res.SecurityHits) == 0 {
|
|
t.Fatalf("expected security hits for an auth path, got none")
|
|
}
|
|
sec, ok := factorByAxis(res.Factors, "security")
|
|
if !ok {
|
|
t.Fatal("security factor missing")
|
|
}
|
|
if sec.Score < 70 {
|
|
t.Fatalf("expected high security axis, got %.1f", sec.Score)
|
|
}
|
|
if res.Risk != RiskHigh && res.Risk != RiskCritical {
|
|
t.Fatalf("expected >=HIGH when a changed path is security-sensitive, got %s (score %.1f)", res.Risk, res.Score)
|
|
}
|
|
}
|
|
|
|
func TestScorePRRisk_ZeroImpactIsLow(t *testing.T) {
|
|
// A leaf symbol with no callers, no security keyword, and a covering
|
|
// test should read as LOW.
|
|
symID := "pkg/util.go::Helper"
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: symID, Kind: graph.KindFunction, Name: "Helper", FilePath: "pkg/util.go", StartLine: 1, EndLine: 5})
|
|
g.AddNode(&graph.Node{ID: "pkg/util_test.go::TestHelper", Kind: graph.KindFunction, Name: "TestHelper", FilePath: "pkg/util_test.go"})
|
|
g.AddEdge(&graph.Edge{From: "pkg/util_test.go::TestHelper", To: symID, Kind: graph.EdgeTests})
|
|
|
|
res := ScorePRRisk(g, PRRiskInput{
|
|
SymbolIDs: []string{symID},
|
|
ChangedFiles: []string{"pkg/util.go"},
|
|
})
|
|
|
|
if res.Risk != RiskLow {
|
|
t.Fatalf("expected LOW for a covered leaf change, got %s (score %.1f)", res.Risk, res.Score)
|
|
}
|
|
if res.UncoveredSymbols != 0 {
|
|
t.Fatalf("expected 0 uncovered symbols, got %d", res.UncoveredSymbols)
|
|
}
|
|
}
|
|
|
|
func TestScorePRRisk_FactorsNonEmptyAndSortedDesc(t *testing.T) {
|
|
hubID := "pkg/hub.go::Hub"
|
|
g := fanInGraph(t, hubID, "internal/auth/hub.go", 10)
|
|
|
|
res := ScorePRRisk(g, PRRiskInput{
|
|
SymbolIDs: []string{hubID},
|
|
ChangedFiles: []string{"internal/auth/hub.go"},
|
|
})
|
|
|
|
if len(res.Factors) == 0 {
|
|
t.Fatal("review_priorities must be non-empty")
|
|
}
|
|
if !sort.SliceIsSorted(res.Factors, func(i, j int) bool {
|
|
return res.Factors[i].Score > res.Factors[j].Score
|
|
}) {
|
|
t.Fatalf("review_priorities must be sorted descending by score: %+v", res.Factors)
|
|
}
|
|
// Every factor must carry a non-empty reason.
|
|
for _, f := range res.Factors {
|
|
if f.Reason == "" {
|
|
t.Fatalf("factor %s has empty reason", f.Axis)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScorePRRisk_UncoveredCounted(t *testing.T) {
|
|
covered := "pkg/a.go::Covered"
|
|
uncovered := "pkg/b.go::Uncovered"
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: covered, Kind: graph.KindFunction, Name: "Covered", FilePath: "pkg/a.go"})
|
|
g.AddNode(&graph.Node{ID: uncovered, Kind: graph.KindFunction, Name: "Uncovered", FilePath: "pkg/b.go"})
|
|
g.AddNode(&graph.Node{ID: "pkg/a_test.go::TestCovered", Kind: graph.KindFunction, Name: "TestCovered", FilePath: "pkg/a_test.go"})
|
|
g.AddEdge(&graph.Edge{From: "pkg/a_test.go::TestCovered", To: covered, Kind: graph.EdgeTests})
|
|
|
|
res := ScorePRRisk(g, PRRiskInput{
|
|
SymbolIDs: []string{covered, uncovered},
|
|
ChangedFiles: []string{"pkg/a.go", "pkg/b.go"},
|
|
})
|
|
if res.UncoveredSymbols != 1 {
|
|
t.Fatalf("expected exactly 1 uncovered symbol, got %d", res.UncoveredSymbols)
|
|
}
|
|
}
|
|
|
|
func TestSecurityKeywordHits(t *testing.T) {
|
|
hits := securityKeywordHits(
|
|
[]string{"internal/auth/session.go", "pkg/util.go"},
|
|
[]string{"EncryptPayload", "PlainHelper"},
|
|
)
|
|
got := map[string]bool{}
|
|
for _, h := range hits {
|
|
got[h] = true
|
|
}
|
|
for _, want := range []string{"auth", "session", "encrypt"} {
|
|
if !got[want] {
|
|
t.Fatalf("expected hit %q in %v", want, hits)
|
|
}
|
|
}
|
|
// No false positives from a clean path/name.
|
|
if len(securityKeywordHits([]string{"pkg/util.go"}, []string{"Helper"})) != 0 {
|
|
t.Fatal("expected no hits for clean inputs")
|
|
}
|
|
}
|