go-devops/devkit/complexity_test.go
Snider e20083d51b feat(devkit): Phase 4 — vulnerability scanning, complexity analysis, coverage trending
- govulncheck JSON output parsing with structured VulnFinding types (13 tests)
- Cyclomatic complexity analysis via go/ast with configurable threshold (21 tests)
- Coverage snapshot persistence and regression detection with CoverageStore (19 tests)

53 new tests, 68 total devkit tests. All pass with -race.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 06:28:37 +00:00

430 lines
9.5 KiB
Go

// LEK-1 | lthn.ai | EUPL-1.2
package devkit
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyseComplexitySource_SimpleFunc_Good(t *testing.T) {
src := `package example
func simple() {
x := 1
_ = x
}
`
results, err := AnalyseComplexitySource(src, "simple.go", 1)
require.NoError(t, err)
// Complexity = 1 (just the function body, no branches), threshold = 1
assert.Len(t, results, 1)
assert.Equal(t, "simple", results[0].FuncName)
assert.Equal(t, "example", results[0].Package)
assert.Equal(t, 1, results[0].Complexity)
}
func TestAnalyseComplexitySource_IfElse_Good(t *testing.T) {
src := `package example
func branches(x int) string {
if x > 0 {
return "positive"
} else if x < 0 {
return "negative"
}
return "zero"
}
`
results, err := AnalyseComplexitySource(src, "branches.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
// 1 (base) + 1 (if) + 1 (else if) = 3
assert.Equal(t, 3, results[0].Complexity)
assert.Equal(t, "branches", results[0].FuncName)
}
func TestAnalyseComplexitySource_ForLoop_Good(t *testing.T) {
src := `package example
func loopy(items []int) int {
total := 0
for _, v := range items {
total += v
}
for i := 0; i < 10; i++ {
total += i
}
return total
}
`
results, err := AnalyseComplexitySource(src, "loops.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
// 1 (base) + 1 (range) + 1 (for) = 3
assert.Equal(t, 3, results[0].Complexity)
}
func TestAnalyseComplexitySource_SwitchCase_Good(t *testing.T) {
src := `package example
func switcher(x int) string {
switch x {
case 1:
return "one"
case 2:
return "two"
case 3:
return "three"
default:
return "other"
}
}
`
results, err := AnalyseComplexitySource(src, "switch.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
// 1 (base) + 3 (case 1, 2, 3; default has nil List) = 4
assert.Equal(t, 4, results[0].Complexity)
}
func TestAnalyseComplexitySource_LogicalOperators_Good(t *testing.T) {
src := `package example
func complex(a, b, c bool) bool {
if a && b || c {
return true
}
return false
}
`
results, err := AnalyseComplexitySource(src, "logical.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
// 1 (base) + 1 (if) + 1 (&&) + 1 (||) = 4
assert.Equal(t, 4, results[0].Complexity)
}
func TestAnalyseComplexitySource_MethodReceiver_Good(t *testing.T) {
src := `package example
type Server struct{}
func (s *Server) Handle(req int) string {
if req > 0 {
return "ok"
}
return "bad"
}
`
results, err := AnalyseComplexitySource(src, "method.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "Server.Handle", results[0].FuncName)
assert.Equal(t, 2, results[0].Complexity)
}
func TestAnalyseComplexitySource_HighComplexity_Good(t *testing.T) {
// Build a function with high complexity to test threshold filtering.
src := `package example
func monster(x, y, z int) int {
result := 0
if x > 0 {
if y > 0 {
if z > 0 {
result = 1
} else if z < -10 {
result = 2
}
} else if y < -5 {
result = 3
}
} else if x < -10 {
result = 4
}
for i := 0; i < x; i++ {
for j := 0; j < y; j++ {
if i > j && j > 0 {
result += i
} else if i == j || i < 0 {
result += j
}
}
}
switch result {
case 1:
result++
case 2:
result--
case 3:
result *= 2
}
return result
}
`
// With threshold 15 — should be above it
results, err := AnalyseComplexitySource(src, "monster.go", 15)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "monster", results[0].FuncName)
assert.GreaterOrEqual(t, results[0].Complexity, 15)
}
func TestAnalyseComplexitySource_BelowThreshold_Good(t *testing.T) {
src := `package example
func simple() int {
return 42
}
`
results, err := AnalyseComplexitySource(src, "simple.go", 5)
require.NoError(t, err)
assert.Empty(t, results) // Complexity 1, below threshold 5
}
func TestAnalyseComplexitySource_MultipleFuncs_Good(t *testing.T) {
src := `package example
func low() { }
func medium(x int) {
if x > 0 {
if x > 10 {
_ = x
}
}
}
func high(a, b, c, d int) int {
if a > 0 {
if b > 0 {
if c > 0 {
if d > 0 {
return 1
}
}
}
}
return 0
}
`
results, err := AnalyseComplexitySource(src, "multi.go", 3)
require.NoError(t, err)
// low: 1, medium: 3, high: 5
assert.Len(t, results, 2) // medium and high
assert.Equal(t, "medium", results[0].FuncName)
assert.Equal(t, 3, results[0].Complexity)
assert.Equal(t, "high", results[1].FuncName)
assert.Equal(t, 5, results[1].Complexity)
}
func TestAnalyseComplexitySource_SelectStmt_Good(t *testing.T) {
src := `package example
func selecter(ch1, ch2 chan int) int {
select {
case v := <-ch1:
return v
case v := <-ch2:
return v
}
}
`
results, err := AnalyseComplexitySource(src, "select.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
// 1 (base) + 1 (select) + 2 (comm clauses) = 4
assert.Equal(t, 4, results[0].Complexity)
}
func TestAnalyseComplexitySource_TypeSwitch_Good(t *testing.T) {
src := `package example
func typeSwitch(v interface{}) string {
switch v.(type) {
case int:
return "int"
case string:
return "string"
default:
return "unknown"
}
}
`
results, err := AnalyseComplexitySource(src, "typeswitch.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
// 1 (base) + 1 (type switch) + 2 (case int, case string; default has nil List) = 4
assert.Equal(t, 4, results[0].Complexity)
}
func TestAnalyseComplexitySource_EmptyBody_Good(t *testing.T) {
// Interface methods or abstract funcs have nil body
src := `package example
type Iface interface {
DoSomething(x int) error
}
`
results, err := AnalyseComplexitySource(src, "iface.go", 1)
require.NoError(t, err)
assert.Empty(t, results) // No FuncDecl in interface
}
func TestAnalyseComplexitySource_ParseError_Bad(t *testing.T) {
src := `this is not valid go code at all!!!`
_, err := AnalyseComplexitySource(src, "bad.go", 1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "parse")
}
func TestAnalyseComplexity_DefaultThreshold_Good(t *testing.T) {
cfg := DefaultComplexityConfig()
assert.Equal(t, 15, cfg.Threshold)
assert.Equal(t, ".", cfg.Path)
}
func TestAnalyseComplexity_ZeroThreshold_Good(t *testing.T) {
// Zero threshold should default to 15
src := `package example
func f() { }
`
results, err := AnalyseComplexitySource(src, "zero.go", 0)
require.NoError(t, err)
assert.Empty(t, results) // complexity 1, default threshold 15
}
func TestAnalyseComplexity_Directory_Good(t *testing.T) {
dir := t.TempDir()
// Write a Go file with a complex function
src := `package example
func complex(a, b, c, d, e int) int {
if a > 0 {
if b > 0 {
if c > 0 {
return 1
}
}
}
if d > 0 || e > 0 {
return 2
}
return 0
}
`
err := os.WriteFile(filepath.Join(dir, "example.go"), []byte(src), 0644)
require.NoError(t, err)
cfg := ComplexityConfig{Threshold: 3, Path: dir}
results, err := AnalyseComplexity(cfg)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "complex", results[0].FuncName)
// 1 (base) + 3 (if x>0, if y>0, if z>0) + 1 (if d>0||e>0) + 1 (||) = 6
assert.Equal(t, 6, results[0].Complexity)
}
func TestAnalyseComplexity_SingleFile_Good(t *testing.T) {
dir := t.TempDir()
src := `package example
func branchy(x int) {
if x > 0 { }
if x > 1 { }
if x > 2 { }
}
`
path := filepath.Join(dir, "single.go")
err := os.WriteFile(path, []byte(src), 0644)
require.NoError(t, err)
cfg := ComplexityConfig{Threshold: 1, Path: path}
results, err := AnalyseComplexity(cfg)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, 4, results[0].Complexity) // 1 + 3 ifs
}
func TestAnalyseComplexity_SkipsTestFiles_Good(t *testing.T) {
dir := t.TempDir()
// Production file — should be analysed
prod := `package example
func prodFunc(x int) {
if x > 0 { }
if x > 1 { }
}
`
err := os.WriteFile(filepath.Join(dir, "prod.go"), []byte(prod), 0644)
require.NoError(t, err)
// Test file — should be skipped
test := `package example
func TestHelper(x int) {
if x > 0 { }
if x > 1 { }
if x > 2 { }
if x > 3 { }
}
`
err = os.WriteFile(filepath.Join(dir, "prod_test.go"), []byte(test), 0644)
require.NoError(t, err)
cfg := ComplexityConfig{Threshold: 1, Path: dir}
results, err := AnalyseComplexity(cfg)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "prodFunc", results[0].FuncName)
}
func TestAnalyseComplexity_SkipsVendor_Good(t *testing.T) {
dir := t.TempDir()
// Create vendor dir with a Go file
vendorDir := filepath.Join(dir, "vendor")
err := os.MkdirAll(vendorDir, 0755)
require.NoError(t, err)
vendorSrc := `package lib
func VendorFunc(x int) {
if x > 0 { }
if x > 1 { }
}
`
err = os.WriteFile(filepath.Join(vendorDir, "lib.go"), []byte(vendorSrc), 0644)
require.NoError(t, err)
cfg := ComplexityConfig{Threshold: 1, Path: dir}
results, err := AnalyseComplexity(cfg)
require.NoError(t, err)
assert.Empty(t, results) // vendor dir should be skipped
}
func TestAnalyseComplexity_NonexistentPath_Bad(t *testing.T) {
cfg := ComplexityConfig{Threshold: 1, Path: "/nonexistent/path/xyz"}
_, err := AnalyseComplexity(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "stat")
}
func TestAnalyseComplexitySource_NestedLogicalOps_Good(t *testing.T) {
src := `package example
func nested(a, b, c, d bool) bool {
return (a && b) || (c && d)
}
`
results, err := AnalyseComplexitySource(src, "nested.go", 1)
require.NoError(t, err)
require.Len(t, results, 1)
// 1 (base) + 2 (&&) + 1 (||) = 4
assert.Equal(t, 4, results[0].Complexity)
}
// LEK-1 | lthn.ai | EUPL-1.2