quality: strengthen metis hygiene contracts and platform metrics
This commit is contained in:
parent
642b0606e2
commit
1b62d20320
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
@ -80,6 +81,25 @@ def _post_text(url: str, payload: str) -> None:
|
||||
raise RuntimeError(f"metrics push failed status={resp.status}")
|
||||
|
||||
|
||||
def _count_source_files_over_limit(repo_root: Path, max_lines: int = 500) -> int:
|
||||
"""Count source files above the configured line budget."""
|
||||
|
||||
count = 0
|
||||
for rel_root in ("cmd", "pkg", "scripts", "testing"):
|
||||
base = repo_root / rel_root
|
||||
if not base.exists():
|
||||
continue
|
||||
for path in base.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix not in {".go", ".py", ".sh"}:
|
||||
continue
|
||||
lines = len(path.read_text(encoding="utf-8", errors="ignore").splitlines())
|
||||
if lines > max_lines:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def main() -> int:
|
||||
coverage_path = os.getenv("COVERAGE_JSON", "build/coverage.json")
|
||||
junit_path = os.getenv("JUNIT_XML", "build/junit.xml")
|
||||
@ -92,6 +112,7 @@ def main() -> int:
|
||||
build_number = os.getenv("BUILD_NUMBER", "")
|
||||
commit = os.getenv("GIT_COMMIT", "")
|
||||
strict = os.getenv("METRICS_STRICT", "") == "1"
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
|
||||
if not os.path.exists(coverage_path):
|
||||
raise RuntimeError(f"missing coverage file {coverage_path}")
|
||||
@ -101,6 +122,7 @@ def main() -> int:
|
||||
coverage = _load_coverage(coverage_path)
|
||||
totals = _load_junit(junit_path)
|
||||
test_exit_code = _load_exit_code(test_exit_code_path)
|
||||
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
|
||||
passed = max(totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"], 0)
|
||||
|
||||
outcome = "ok"
|
||||
@ -131,6 +153,10 @@ def main() -> int:
|
||||
f'metis_quality_gate_run_status{{suite="{suite}",status="failed"}} {1 if outcome == "failed" else 0}',
|
||||
"# TYPE metis_quality_gate_coverage_percent gauge",
|
||||
f'metis_quality_gate_coverage_percent{{suite="{suite}"}} {coverage:.3f}',
|
||||
"# TYPE platform_quality_gate_workspace_line_coverage_percent gauge",
|
||||
f'platform_quality_gate_workspace_line_coverage_percent{{suite="{suite}"}} {coverage:.3f}',
|
||||
"# TYPE platform_quality_gate_source_lines_over_500_total gauge",
|
||||
f'platform_quality_gate_source_lines_over_500_total{{suite="{suite}"}} {source_lines_over_500}',
|
||||
"# TYPE metis_quality_gate_build_info gauge",
|
||||
f"metis_quality_gate_build_info{_label_str(labels)} 1",
|
||||
]
|
||||
@ -154,6 +180,7 @@ def main() -> int:
|
||||
"tests_errors": totals["errors"],
|
||||
"tests_skipped": totals["skipped"],
|
||||
"coverage_percent": round(coverage, 3),
|
||||
"source_lines_over_500": source_lines_over_500,
|
||||
"test_exit_code": test_exit_code,
|
||||
},
|
||||
indent=2,
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -151,6 +152,134 @@ func TestCoveragePolicy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user