package hygiene import ( "bufio" "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) }) t.Run("split_module_contract", func(t *testing.T) { checkSplitModuleContract(t, root, 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) } } } // checkSplitModuleContract runs one orchestration or CLI step. // Signature: checkSplitModuleContract(t *testing.T, root string, files []string). // Why: The long-term test layout requirement is top-level `testing/`; this // guard freezes the remaining in-tree baseline so new root-module tests cannot // slip in while the migration continues incrementally and safely. func checkSplitModuleContract(t *testing.T, root string, files []string) { t.Helper() allowlistPath := filepath.Join(root, "testing", "hygiene", "in_tree_test_allowlist.txt") allowed := loadInTreeTestAllowlist(t, allowlistPath) actual := make([]string, 0, len(files)) for _, file := range files { if !strings.HasSuffix(file, "_test.go") { continue } actual = append(actual, mustRepoRelativePath(t, root, file)) } sort.Strings(actual) allowedSet := make(map[string]struct{}, len(allowed)) for _, file := range allowed { allowedSet[file] = struct{}{} } actualSet := make(map[string]struct{}, len(actual)) for _, file := range actual { actualSet[file] = struct{}{} } unexpected := make([]string, 0) for _, file := range actual { if _, ok := allowedSet[file]; !ok { unexpected = append(unexpected, file) } } missing := make([]string, 0) for _, file := range allowed { if _, ok := actualSet[file]; !ok { missing = append(missing, file) } } if len(unexpected) == 0 && len(missing) == 0 { return } var report strings.Builder report.WriteString("split-module contract mismatch\n") if len(unexpected) > 0 { report.WriteString("unexpected in-tree tests (move them under testing/ or explicitly rebaseline if this is an intentional migration checkpoint):\n") for _, file := range unexpected { report.WriteString("- " + file + "\n") } } if len(missing) > 0 { report.WriteString("stale allowlist entries (remove them from testing/hygiene/in_tree_test_allowlist.txt after a migration):\n") for _, file := range missing { report.WriteString("- " + file + "\n") } } t.Fatal(report.String()) } // loadInTreeTestAllowlist runs one orchestration or CLI step. // Signature: loadInTreeTestAllowlist(t *testing.T, path string) []string. // Why: The split-module baseline needs one canonical source of truth so the // gate can distinguish legacy in-tree tests from accidental new ones. func loadInTreeTestAllowlist(t *testing.T, path string) []string { t.Helper() f, err := os.Open(path) if err != nil { t.Fatalf("open in-tree test allowlist %s: %v", path, err) } defer f.Close() entries := make([]string, 0, 64) s := bufio.NewScanner(f) for s.Scan() { line := strings.TrimSpace(s.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } entries = append(entries, filepath.ToSlash(line)) } if err := s.Err(); err != nil { t.Fatalf("scan in-tree test allowlist %s: %v", path, err) } sort.Strings(entries) return entries } // mustRepoRelativePath runs one orchestration or CLI step. // Signature: mustRepoRelativePath(t *testing.T, root string, path string) string. // Why: Relative slash-normalized paths keep split-module hygiene output stable // across machines and make allowlist diffs easy to review. func mustRepoRelativePath(t *testing.T, root string, path string) string { t.Helper() rel, err := filepath.Rel(root, path) if err != nil { t.Fatalf("make %s relative to %s: %v", path, root, err) } return filepath.ToSlash(rel) }