lint/pkg/lint/tools_test.go
Virgil 48acea0ef4 refactor(lint): add semantic tracked comment API
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-01 11:18:16 +00:00

223 lines
6.6 KiB
Go

package lint
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// 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 TestNewToolkit(t *testing.T) {
tk := NewToolkit("/tmp")
assert.Equal(t, "/tmp", tk.Dir)
}
func TestToolkit_Coverage_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 := NewToolkit(t.TempDir())
reports, err := tk.Coverage("./...")
require.NoError(t, err)
require.Len(t, reports, 2)
assert.Equal(t, "example.com/pkg1", reports[0].Package)
assert.Equal(t, 85.0, reports[0].Percentage)
assert.Equal(t, "example.com/pkg2", reports[1].Package)
assert.Equal(t, 100.0, reports[1].Percentage)
}
func TestToolkit_Coverage_Bad(t *testing.T) {
setupMockCmd(t, "go", "FAIL\texample.com/broken [build failed]")
tk := NewToolkit(t.TempDir())
reports, err := tk.Coverage("./...")
require.NoError(t, err)
assert.Empty(t, reports)
}
func TestToolkit_GitLog_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 := NewToolkit(t.TempDir())
commits, err := tk.GitLog(2)
require.NoError(t, err)
require.Len(t, commits, 2)
assert.Equal(t, "abc123", commits[0].Hash)
assert.Equal(t, "Alice", commits[0].Author)
assert.Equal(t, "Fix the bug", commits[0].Message)
assert.True(t, commits[0].Date.Equal(now))
}
func TestToolkit_GitLog_Bad(t *testing.T) {
setupMockCmd(t, "git", "incomplete|line\nabc|Bob|2025-01-01T00:00:00Z|Good commit")
tk := NewToolkit(t.TempDir())
commits, err := tk.GitLog(5)
require.NoError(t, err)
assert.Len(t, commits, 1)
}
func TestToolkit_GocycloComplexity_Good(t *testing.T) {
output := "15 main ComplexFunc file.go:10:1\n20 pkg VeryComplex other.go:50:1"
setupMockCmd(t, "gocyclo", output)
tk := NewToolkit(t.TempDir())
funcs, err := tk.GocycloComplexity(10)
require.NoError(t, err)
require.Len(t, funcs, 2)
assert.Equal(t, 15, funcs[0].Score)
assert.Equal(t, "ComplexFunc", funcs[0].FuncName)
assert.Equal(t, "file.go", funcs[0].File)
assert.Equal(t, 10, funcs[0].Line)
assert.Equal(t, 20, funcs[1].Score)
assert.Equal(t, "pkg", funcs[1].Package)
}
func TestToolkit_GocycloComplexity_Bad(t *testing.T) {
setupMockCmd(t, "gocyclo", "")
tk := NewToolkit(t.TempDir())
funcs, err := tk.GocycloComplexity(50)
require.NoError(t, err)
assert.Empty(t, funcs)
}
func TestToolkit_DepGraph_Good(t *testing.T) {
output := "modA@v1 modB@v2\nmodA@v1 modC@v3\nmodB@v2 modD@v1"
setupMockCmd(t, "go", output)
tk := NewToolkit(t.TempDir())
graph, err := tk.DepGraph("./...")
require.NoError(t, err)
assert.Len(t, graph.Nodes, 4)
assert.Len(t, graph.Edges["modA@v1"], 2)
}
func TestToolkit_DepGraph_SortsNodesAndEdges(t *testing.T) {
output := "modB@v2 modD@v1\nmodA@v1 modC@v3\nmodA@v1 modB@v2"
setupMockCmd(t, "go", output)
tk := NewToolkit(t.TempDir())
graph, err := tk.DepGraph("./...")
require.NoError(t, err)
assert.Equal(t, []string{"modA@v1", "modB@v2", "modC@v3", "modD@v1"}, graph.Nodes)
assert.Equal(t, []string{"modB@v2", "modC@v3"}, graph.Edges["modA@v1"])
}
func TestToolkit_RaceDetect_Good(t *testing.T) {
setupMockCmd(t, "go", "ok\texample.com/safe\t0.1s")
tk := NewToolkit(t.TempDir())
races, err := tk.RaceDetect("./...")
require.NoError(t, err)
assert.Empty(t, races)
}
func TestToolkit_RaceDetect_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 := NewToolkit(t.TempDir())
races, err := tk.RaceDetect("./...")
require.NoError(t, err)
require.Len(t, races, 1)
assert.Equal(t, "/home/user/project/main.go", races[0].File)
assert.Equal(t, 42, races[0].Line)
}
func TestToolkit_DiffStat_Good(t *testing.T) {
output := ` file1.go | 10 +++++++---
file2.go | 5 +++++
2 files changed, 12 insertions(+), 3 deletions(-)`
setupMockCmd(t, "git", output)
tk := NewToolkit(t.TempDir())
s, err := tk.DiffStat()
require.NoError(t, err)
assert.Equal(t, 2, s.FilesChanged)
assert.Equal(t, 12, s.Insertions)
assert.Equal(t, 3, s.Deletions)
}
func TestToolkit_CheckPerms_Good(t *testing.T) {
dir := t.TempDir()
badFile := filepath.Join(dir, "bad.txt")
require.NoError(t, os.WriteFile(badFile, []byte("test"), 0644))
require.NoError(t, os.Chmod(badFile, 0666))
goodFile := filepath.Join(dir, "good.txt")
require.NoError(t, os.WriteFile(goodFile, []byte("test"), 0644))
tk := NewToolkit("/")
issues, err := tk.CheckPerms(dir)
require.NoError(t, err)
require.Len(t, issues, 1)
assert.Equal(t, "World-writable", issues[0].Issue)
}
func TestToolkit_FindTrackedComments_Compatibility(t *testing.T) {
output := "pkg/file.go:12:TODO: fix this\n"
setupMockCmd(t, "git", output)
tk := NewToolkit(t.TempDir())
comments, err := tk.FindTrackedComments("pkg")
require.NoError(t, err)
require.Len(t, comments, 1)
assert.Equal(t, "pkg/file.go", comments[0].File)
assert.Equal(t, 12, comments[0].Line)
assert.Equal(t, "TODO", comments[0].Type)
assert.Equal(t, "fix this", comments[0].Message)
legacyComments, err := tk.FindTODOs("pkg")
require.NoError(t, err)
assert.Equal(t, comments, legacyComments)
}