package testing_test import ( "bufio" "encoding/json" "fmt" "go/ast" "go/parser" "go/token" "math" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" "testing" ) type coveragePolicy struct { TargetPercent float64 `json:"target_percent"` Files map[string]float64 `json:"files"` } func TestSourceFileLineLimit(t *testing.T) { root := repoRoot(t) var offenders []string for _, relRoot := range []string{"cmd", "pkg", "scripts", "testing"} { walkSourceFiles(t, filepath.Join(root, relRoot), func(path string, info os.DirEntry) error { if info.IsDir() { return nil } switch filepath.Ext(path) { case ".go", ".py", ".sh": lines, err := countLines(path) if err != nil { return err } if lines > 500 { offenders = append(offenders, fmt.Sprintf("%s:%d", rel(root, path), lines)) } } return nil }) } if len(offenders) > 0 { sort.Strings(offenders) t.Fatalf("source files exceed 500 LOC: %s", strings.Join(offenders, ", ")) } } func TestExportedDocs(t *testing.T) { root := repoRoot(t) var missing []string fset := token.NewFileSet() walkSourceFiles(t, root, func(path string, info os.DirEntry) error { if info.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") { return nil } if !strings.HasPrefix(rel(root, path), "cmd/") && !strings.HasPrefix(rel(root, path), "pkg/") { return nil } file, err := parser.ParseFile(fset, path, nil, parser.ParseComments) if err != nil { return err } for _, decl := range file.Decls { switch d := decl.(type) { case *ast.FuncDecl: if d.Name.IsExported() && !hasUsefulDoc(d.Doc, d.Name.Name) { missing = append(missing, fmt.Sprintf("%s:%s", rel(root, path), d.Name.Name)) } case *ast.GenDecl: for _, spec := range d.Specs { switch s := spec.(type) { case *ast.TypeSpec: if s.Name.IsExported() && !hasUsefulDoc(d.Doc, s.Name.Name) { missing = append(missing, fmt.Sprintf("%s:%s", rel(root, path), s.Name.Name)) } } } } } return nil }) if len(missing) > 0 { sort.Strings(missing) t.Fatalf("exported declarations without useful docs: %s", strings.Join(missing, ", ")) } } func TestGoFmtAndVet(t *testing.T) { root := repoRoot(t) gofmt := exec.Command("gofmt", "-l", "cmd", "pkg", "testing") gofmt.Dir = root out, err := gofmt.CombinedOutput() if err != nil { t.Fatalf("gofmt check failed: %v\n%s", err, out) } if trimmed := strings.TrimSpace(string(out)); trimmed != "" { t.Fatalf("gofmt -l reported files:\n%s", trimmed) } vet := exec.Command("go", "vet", "./...") vet.Dir = root out, err = vet.CombinedOutput() if err != nil { t.Fatalf("go vet failed: %v\n%s", err, out) } } func TestCoveragePolicy(t *testing.T) { root := repoRoot(t) coveragePath := filepath.Join(root, "build", "coverage.out") if err := os.MkdirAll(filepath.Dir(coveragePath), 0o755); err != nil { t.Fatalf("create coverage dir: %v", err) } useExisting := os.Getenv("METIS_USE_EXISTING_COVERAGE") == "1" if !useExisting { if err := os.Remove(coveragePath); err != nil && !os.IsNotExist(err) { t.Fatalf("clear stale coverage profile: %v", err) } } if _, err := os.Stat(coveragePath); err != nil { if !os.IsNotExist(err) { t.Fatalf("check coverage profile: %v", err) } cmd := exec.Command("go", "test", "-count=1", "./...", "-coverprofile=build/coverage.out") cmd.Dir = root out, runErr := cmd.CombinedOutput() if runErr != nil { t.Fatalf("root coverage run failed: %v\n%s", runErr, out) } } policyPath := filepath.Join(root, "testing", "coverage_policy.json") policy := loadCoveragePolicy(t, policyPath) actual := readCoverageProfile(t, coveragePath) var regressions []string for file, min := range policy.Files { got, ok := actual[file] if !ok { regressions = append(regressions, fmt.Sprintf("%s missing from coverage", file)) continue } if got+0.05 < min { regressions = append(regressions, fmt.Sprintf("%s %.1f < %.1f", file, got, min)) } } if len(regressions) > 0 { sort.Strings(regressions) t.Fatalf("coverage regressed: %s", strings.Join(regressions, ", ")) } } func TestStructureHygiene(t *testing.T) { root := repoRoot(t) genericNames := map[string]struct{}{ "tmp": {}, "temp": {}, "foo": {}, "bar": {}, "baz": {}, "misc": {}, "new": {}, "old": {}, "final": {}, "wip": {}, } maxDepth := 8 var violations []string for _, relRoot := range []string{"cmd", "pkg", "scripts", "testing"} { base := filepath.Join(root, relRoot) walkSourceFiles(t, base, func(path string, info os.DirEntry) error { if info.IsDir() { return nil } switch filepath.Ext(path) { case ".go", ".py", ".sh": default: return nil } relative := rel(root, path) depth := len(strings.Split(relative, "/")) if depth > maxDepth { violations = append(violations, fmt.Sprintf("%s: depth %d > %d", relative, depth, maxDepth)) } baseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) for _, tok := range strings.FieldsFunc(baseName, func(r rune) bool { return r == '_' || r == '-' }) { if _, found := genericNames[strings.ToLower(tok)]; found { violations = append(violations, fmt.Sprintf("%s: non-descriptive token %q", relative, tok)) } } return nil }) } if len(violations) > 0 { sort.Strings(violations) t.Fatalf("structure hygiene violations: %s", strings.Join(violations, ", ")) } } func TestCodeSmellContracts(t *testing.T) { root := repoRoot(t) patterns := []struct { re *regexp.Regexp scope []string message string }{ { re: regexp.MustCompile(`\bpanic\(`), scope: []string{"cmd", "pkg"}, message: "avoid panic in production Go code", }, { re: regexp.MustCompile(`\blog\.Fatalf\(`), scope: []string{"pkg"}, message: "avoid log.Fatalf in pkg code", }, { re: regexp.MustCompile(`\bfmt\.Print(f|ln)?\(`), scope: []string{"pkg"}, message: "avoid fmt.Print* in pkg code", }, } type hit struct { path string line int text string msg string } var hits []hit for _, rule := range patterns { for _, relRoot := range rule.scope { base := filepath.Join(root, relRoot) walkSourceFiles(t, base, func(path string, info os.DirEntry) error { if info.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") { return nil } file, err := os.Open(path) if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) lineNo := 0 for scanner.Scan() { lineNo++ line := scanner.Text() if rule.re.MatchString(line) { hits = append(hits, hit{ path: rel(root, path), line: lineNo, text: strings.TrimSpace(line), msg: rule.message, }) } } return scanner.Err() }) } } if len(hits) > 0 { sort.Slice(hits, func(i, j int) bool { if hits[i].path == hits[j].path { return hits[i].line < hits[j].line } return hits[i].path < hits[j].path }) lines := make([]string, 0, len(hits)) for _, h := range hits { lines = append(lines, fmt.Sprintf("%s:%d (%s) %s", h.path, h.line, h.msg, h.text)) } t.Fatalf("code smell violations: %s", strings.Join(lines, ", ")) } } func countLines(path string) (int, error) { f, err := os.Open(path) if err != nil { return 0, err } defer f.Close() s := bufio.NewScanner(f) count := 0 for s.Scan() { count++ } return count, s.Err() } func rel(root, path string) string { out, err := filepath.Rel(root, path) if err != nil { return path } return filepath.ToSlash(out) } func hasUsefulDoc(comment *ast.CommentGroup, name string) bool { if comment == nil { return false } text := strings.TrimSpace(comment.Text()) if text == "" { return false } if len(strings.Fields(text)) < 4 { return false } return strings.Contains(strings.ToLower(text), strings.ToLower(name[:1])) || len(text) > len(name)+12 } func loadCoveragePolicy(t *testing.T, path string) coveragePolicy { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var policy coveragePolicy if err := json.Unmarshal(data, &policy); err != nil { t.Fatal(err) } if policy.TargetPercent == 0 { policy.TargetPercent = 95 } if policy.Files == nil { policy.Files = map[string]float64{} } return policy } func readCoverageProfile(t *testing.T, path string) map[string]float64 { t.Helper() f, err := os.Open(path) if err != nil { t.Fatal(err) } defer f.Close() stats := map[string]struct{ covered, total int }{} s := bufio.NewScanner(f) for s.Scan() { line := strings.TrimSpace(s.Text()) if line == "" || strings.HasPrefix(line, "mode:") { continue } parts := strings.Fields(line) if len(parts) != 3 { continue } file := strings.SplitN(parts[0], ":", 2)[0] stmts, err := strconv.Atoi(parts[1]) if err != nil { continue } count, err := strconv.Atoi(parts[2]) if err != nil { continue } entry := stats[file] entry.total += stmts if count > 0 { entry.covered += stmts } stats[file] = entry } if err := s.Err(); err != nil { t.Fatal(err) } out := map[string]float64{} for file, stat := range stats { if stat.total == 0 { continue } out[file] = math.Round((float64(stat.covered)/float64(stat.total))*1000) / 10 } return out }