metis/testing/gate_test.go

251 lines
6.0 KiB
Go

package testing_test
import (
"bufio"
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"math"
"os"
"os/exec"
"path/filepath"
"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.Stat(coveragePath); err != nil {
cmd := exec.Command("go", "test", "./...", "-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
var phased []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 got < policy.TargetPercent {
phased = append(phased, fmt.Sprintf("%s=%.1f", file, got))
}
}
if len(regressions) > 0 {
sort.Strings(regressions)
t.Fatalf("coverage regressed: %s", strings.Join(regressions, ", "))
}
if len(phased) > 0 {
sort.Strings(phased)
t.Fatalf("coverage below target %.1f%%: %s", policy.TargetPercent, strings.Join(phased, ", "))
}
}
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
}