go-devops/devkit/devkit_test.go

271 lines
7.5 KiB
Go
Raw Permalink Normal View History

// Designed by Gemini 3 Pro (Hypnos) + Claude Opus (Charon), signed LEK-1 | lthn.ai | EUPL-1.2
package devkit
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
// setupMockCmd creates a shell script in a temp dir that echoes predetermined
// content, and prepends that dir to PATH so Run() picks it up.
func setupMockCmd(t *testing.T, name, content string) {
t.Helper()
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, name)
script := fmt.Sprintf("#!/bin/sh\ncat <<'MOCK_EOF'\n%s\nMOCK_EOF\n", content)
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
t.Fatalf("failed to write mock command %s: %v", name, err)
}
oldPath := os.Getenv("PATH")
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)
}
// setupMockCmdExit creates a mock that echoes to stdout/stderr and exits with a code.
func setupMockCmdExit(t *testing.T, name, stdout, stderr string, exitCode int) {
t.Helper()
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, name)
script := fmt.Sprintf("#!/bin/sh\ncat <<'MOCK_EOF'\n%s\nMOCK_EOF\ncat <<'MOCK_ERR' >&2\n%s\nMOCK_ERR\nexit %d\n", stdout, stderr, exitCode)
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
t.Fatalf("failed to write mock command %s: %v", name, err)
}
oldPath := os.Getenv("PATH")
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)
}
func TestCoverage_Good(t *testing.T) {
output := `? example.com/skipped [no test files]
ok example.com/pkg1 0.5s coverage: 85.0% of statements
ok example.com/pkg2 0.2s coverage: 100.0% of statements`
setupMockCmd(t, "go", output)
tk := New(t.TempDir())
reports, err := tk.Coverage("./...")
if err != nil {
t.Fatalf("Coverage failed: %v", err)
}
if len(reports) != 2 {
t.Fatalf("expected 2 reports, got %d", len(reports))
}
if reports[0].Package != "example.com/pkg1" || reports[0].Percentage != 85.0 {
t.Errorf("report 0: want pkg1@85%%, got %s@%.1f%%", reports[0].Package, reports[0].Percentage)
}
if reports[1].Package != "example.com/pkg2" || reports[1].Percentage != 100.0 {
t.Errorf("report 1: want pkg2@100%%, got %s@%.1f%%", reports[1].Package, reports[1].Percentage)
}
}
func TestCoverage_Bad(t *testing.T) {
// No coverage lines in output
setupMockCmd(t, "go", "FAIL\texample.com/broken [build failed]")
tk := New(t.TempDir())
reports, err := tk.Coverage("./...")
if err != nil {
t.Fatalf("Coverage should not error on partial output: %v", err)
}
if len(reports) != 0 {
t.Errorf("expected 0 reports from failed build, got %d", len(reports))
}
}
func TestGitLog_Good(t *testing.T) {
now := time.Now().Truncate(time.Second)
nowStr := now.Format(time.RFC3339)
output := fmt.Sprintf("abc123|Alice|%s|Fix the bug\ndef456|Bob|%s|Add feature", nowStr, nowStr)
setupMockCmd(t, "git", output)
tk := New(t.TempDir())
commits, err := tk.GitLog(2)
if err != nil {
t.Fatalf("GitLog failed: %v", err)
}
if len(commits) != 2 {
t.Fatalf("expected 2 commits, got %d", len(commits))
}
if commits[0].Hash != "abc123" {
t.Errorf("hash: want abc123, got %s", commits[0].Hash)
}
if commits[0].Author != "Alice" {
t.Errorf("author: want Alice, got %s", commits[0].Author)
}
if commits[0].Message != "Fix the bug" {
t.Errorf("message: want 'Fix the bug', got %q", commits[0].Message)
}
if !commits[0].Date.Equal(now) {
t.Errorf("date: want %v, got %v", now, commits[0].Date)
}
}
func TestGitLog_Bad(t *testing.T) {
// Malformed lines should be skipped
setupMockCmd(t, "git", "incomplete|line\nabc|Bob|2025-01-01T00:00:00Z|Good commit")
tk := New(t.TempDir())
commits, err := tk.GitLog(5)
if err != nil {
t.Fatalf("GitLog failed: %v", err)
}
if len(commits) != 1 {
t.Errorf("expected 1 valid commit (skip malformed), got %d", len(commits))
}
}
func TestComplexity_Good(t *testing.T) {
output := "15 main ComplexFunc file.go:10:1\n20 pkg VeryComplex other.go:50:1"
setupMockCmd(t, "gocyclo", output)
tk := New(t.TempDir())
funcs, err := tk.Complexity(10)
if err != nil {
t.Fatalf("Complexity failed: %v", err)
}
if len(funcs) != 2 {
t.Fatalf("expected 2 funcs, got %d", len(funcs))
}
if funcs[0].Score != 15 || funcs[0].FuncName != "ComplexFunc" || funcs[0].File != "file.go" || funcs[0].Line != 10 {
t.Errorf("func 0: unexpected %+v", funcs[0])
}
if funcs[1].Score != 20 || funcs[1].Package != "pkg" {
t.Errorf("func 1: unexpected %+v", funcs[1])
}
}
func TestComplexity_Bad(t *testing.T) {
// No functions above threshold = empty output
setupMockCmd(t, "gocyclo", "")
tk := New(t.TempDir())
funcs, err := tk.Complexity(50)
if err != nil {
t.Fatalf("Complexity should not error on empty output: %v", err)
}
if len(funcs) != 0 {
t.Errorf("expected 0 funcs, got %d", len(funcs))
}
}
func TestDepGraph_Good(t *testing.T) {
output := "modA@v1 modB@v2\nmodA@v1 modC@v3\nmodB@v2 modD@v1"
setupMockCmd(t, "go", output)
tk := New(t.TempDir())
graph, err := tk.DepGraph("./...")
if err != nil {
t.Fatalf("DepGraph failed: %v", err)
}
if len(graph.Nodes) != 4 {
t.Errorf("expected 4 nodes, got %d: %v", len(graph.Nodes), graph.Nodes)
}
edgesA := graph.Edges["modA@v1"]
if len(edgesA) != 2 {
t.Errorf("expected 2 edges from modA@v1, got %d", len(edgesA))
}
}
func TestRaceDetect_Good(t *testing.T) {
// No races = clean run
setupMockCmd(t, "go", "ok\texample.com/safe\t0.1s")
tk := New(t.TempDir())
races, err := tk.RaceDetect("./...")
if err != nil {
t.Fatalf("RaceDetect failed on clean run: %v", err)
}
if len(races) != 0 {
t.Errorf("expected 0 races, got %d", len(races))
}
}
func TestRaceDetect_Bad(t *testing.T) {
stderrOut := `WARNING: DATA RACE
Read at 0x00c000123456 by goroutine 7:
/home/user/project/main.go:42
Previous write at 0x00c000123456 by goroutine 6:
/home/user/project/main.go:38`
setupMockCmdExit(t, "go", "", stderrOut, 1)
tk := New(t.TempDir())
races, err := tk.RaceDetect("./...")
if err != nil {
t.Fatalf("RaceDetect should parse races, not error: %v", err)
}
if len(races) != 1 {
t.Fatalf("expected 1 race, got %d", len(races))
}
if races[0].File != "/home/user/project/main.go" || races[0].Line != 42 {
t.Errorf("race: unexpected %+v", races[0])
}
}
func TestDiffStat_Good(t *testing.T) {
output := ` file1.go | 10 +++++++---
file2.go | 5 +++++
2 files changed, 12 insertions(+), 3 deletions(-)`
setupMockCmd(t, "git", output)
tk := New(t.TempDir())
s, err := tk.DiffStat()
if err != nil {
t.Fatalf("DiffStat failed: %v", err)
}
if s.FilesChanged != 2 {
t.Errorf("files: want 2, got %d", s.FilesChanged)
}
if s.Insertions != 12 {
t.Errorf("insertions: want 12, got %d", s.Insertions)
}
if s.Deletions != 3 {
t.Errorf("deletions: want 3, got %d", s.Deletions)
}
}
func TestCheckPerms_Good(t *testing.T) {
dir := t.TempDir()
// Create a world-writable file
badFile := filepath.Join(dir, "bad.txt")
if err := os.WriteFile(badFile, []byte("test"), 0644); err != nil {
t.Fatal(err)
}
if err := os.Chmod(badFile, 0666); err != nil {
t.Fatal(err)
}
// Create a safe file
goodFile := filepath.Join(dir, "good.txt")
if err := os.WriteFile(goodFile, []byte("test"), 0644); err != nil {
t.Fatal(err)
}
tk := New("/")
issues, err := tk.CheckPerms(dir)
if err != nil {
t.Fatalf("CheckPerms failed: %v", err)
}
if len(issues) != 1 {
t.Fatalf("expected 1 issue (world-writable), got %d", len(issues))
}
if issues[0].Issue != "World-writable" {
t.Errorf("issue: want 'World-writable', got %q", issues[0].Issue)
}
}
func TestNew(t *testing.T) {
tk := New("/tmp")
if tk.Dir != "/tmp" {
t.Errorf("Dir: want /tmp, got %s", tk.Dir)
}
}
// LEK-1 | lthn.ai | EUPL-1.2