ananke/testing/hygiene/hygiene_test.go

140 lines
3.8 KiB
Go

package hygiene
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"testing"
)
const maxGoFileLOC = 500
var goFileNamePattern = regexp.MustCompile(`^[a-z0-9]+(_[a-z0-9]+)*(_test)?\.go$`)
func repoRoot(tb testing.TB) string {
tb.Helper()
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
tb.Fatalf("unable to resolve caller path")
}
return filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", ".."))
}
func collectGoFiles(tb testing.TB, roots ...string) []string {
tb.Helper()
files := make([]string, 0, 128)
for _, root := range roots {
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case ".git", "vendor", "dist", "artifacts", "testdata":
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(path, ".go") {
files = append(files, path)
}
return nil
})
if err != nil {
tb.Fatalf("walk %s: %v", root, err)
}
}
return files
}
// TestHygieneContracts runs one orchestration or CLI step.
// Signature: TestHygieneContracts(t *testing.T).
// Why: Enforces an explicit quality order: doc contracts first, then naming,
// then file-size constraints. That keeps maintainability rules deterministic.
func TestHygieneContracts(t *testing.T) {
root := repoRoot(t)
files := collectGoFiles(t, filepath.Join(root, "cmd"), filepath.Join(root, "internal"))
sort.Strings(files)
t.Run("doc_contract", func(t *testing.T) {
checkDocContracts(t, files)
})
t.Run("naming_contract", func(t *testing.T) {
checkNamingContracts(t, files)
})
t.Run("loc_limit", func(t *testing.T) {
checkFileLOCLimits(t, files)
})
}
// checkDocContracts runs one orchestration or CLI step.
// Signature: checkDocContracts(t *testing.T, files []string).
// Why: The project requires function-level intent to be explicit so future edits
// preserve behavior and avoid reintroducing drill-time regressions.
func checkDocContracts(t *testing.T, files []string) {
t.Helper()
fset := token.NewFileSet()
for _, file := range files {
af, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
if err != nil {
t.Errorf("parse %s: %v", file, err)
continue
}
for _, decl := range af.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
if fn.Doc == nil {
t.Errorf("%s: function %s is missing doc comment block", file, fn.Name.Name)
continue
}
doc := fn.Doc.Text()
if !strings.Contains(doc, "Signature:") {
t.Errorf("%s: function %s doc is missing Signature: contract", file, fn.Name.Name)
}
if !strings.Contains(doc, "Why:") {
t.Errorf("%s: function %s doc is missing Why: contract", file, fn.Name.Name)
}
}
}
}
// checkNamingContracts runs one orchestration or CLI step.
// Signature: checkNamingContracts(t *testing.T, files []string).
// Why: File/module naming consistency is part of maintainability and helps new
// contributors find behavior quickly as the lab and codebase expand.
func checkNamingContracts(t *testing.T, files []string) {
t.Helper()
for _, file := range files {
base := filepath.Base(file)
if !goFileNamePattern.MatchString(base) {
t.Errorf("%s: filename %q violates naming contract %s", file, base, goFileNamePattern.String())
}
}
}
// checkFileLOCLimits runs one orchestration or CLI step.
// Signature: checkFileLOCLimits(t *testing.T, files []string).
// Why: A strict LOC cap forces focused files and keeps refactors manageable.
func checkFileLOCLimits(t *testing.T, files []string) {
t.Helper()
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
t.Errorf("read %s: %v", file, err)
continue
}
lineCount := strings.Count(string(data), "\n") + 1
if lineCount > maxGoFileLOC {
t.Errorf("%s has %d lines, exceeds %d", file, lineCount, maxGoFileLOC)
}
}
}