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 json
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import xml.etree.ElementTree as ET
|
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}")
|
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:
|
def main() -> int:
|
||||||
coverage_path = os.getenv("COVERAGE_JSON", "build/coverage.json")
|
coverage_path = os.getenv("COVERAGE_JSON", "build/coverage.json")
|
||||||
junit_path = os.getenv("JUNIT_XML", "build/junit.xml")
|
junit_path = os.getenv("JUNIT_XML", "build/junit.xml")
|
||||||
@ -92,6 +112,7 @@ def main() -> int:
|
|||||||
build_number = os.getenv("BUILD_NUMBER", "")
|
build_number = os.getenv("BUILD_NUMBER", "")
|
||||||
commit = os.getenv("GIT_COMMIT", "")
|
commit = os.getenv("GIT_COMMIT", "")
|
||||||
strict = os.getenv("METRICS_STRICT", "") == "1"
|
strict = os.getenv("METRICS_STRICT", "") == "1"
|
||||||
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
if not os.path.exists(coverage_path):
|
if not os.path.exists(coverage_path):
|
||||||
raise RuntimeError(f"missing coverage file {coverage_path}")
|
raise RuntimeError(f"missing coverage file {coverage_path}")
|
||||||
@ -101,6 +122,7 @@ def main() -> int:
|
|||||||
coverage = _load_coverage(coverage_path)
|
coverage = _load_coverage(coverage_path)
|
||||||
totals = _load_junit(junit_path)
|
totals = _load_junit(junit_path)
|
||||||
test_exit_code = _load_exit_code(test_exit_code_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)
|
passed = max(totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"], 0)
|
||||||
|
|
||||||
outcome = "ok"
|
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}',
|
f'metis_quality_gate_run_status{{suite="{suite}",status="failed"}} {1 if outcome == "failed" else 0}',
|
||||||
"# TYPE metis_quality_gate_coverage_percent gauge",
|
"# TYPE metis_quality_gate_coverage_percent gauge",
|
||||||
f'metis_quality_gate_coverage_percent{{suite="{suite}"}} {coverage:.3f}',
|
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",
|
"# TYPE metis_quality_gate_build_info gauge",
|
||||||
f"metis_quality_gate_build_info{_label_str(labels)} 1",
|
f"metis_quality_gate_build_info{_label_str(labels)} 1",
|
||||||
]
|
]
|
||||||
@ -154,6 +180,7 @@ def main() -> int:
|
|||||||
"tests_errors": totals["errors"],
|
"tests_errors": totals["errors"],
|
||||||
"tests_skipped": totals["skipped"],
|
"tests_skipped": totals["skipped"],
|
||||||
"coverage_percent": round(coverage, 3),
|
"coverage_percent": round(coverage, 3),
|
||||||
|
"source_lines_over_500": source_lines_over_500,
|
||||||
"test_exit_code": test_exit_code,
|
"test_exit_code": test_exit_code,
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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) {
|
func countLines(path string) (int, error) {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user