Co-authored-by: Charon (snider-linux) <charon@lethean.io> Co-authored-by: Snider <snider@host.uk.com> Co-authored-by: Virgil <virgil@lethean.io> Co-authored-by: Claude <developers@lethean.io> Reviewed-on: #2 Co-authored-by: Snider <snider@lethean.io> Co-committed-by: Snider <snider@lethean.io>
270 lines
7.5 KiB
Go
270 lines
7.5 KiB
Go
// 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
|