4"""Gate: first-party Go verification (vet, static analysis, tests, coverage).
6Scope is derived, not hardcoded
7-------------------------------
8``git ls-files`` enumerates every tracked or untracked-but-not-ignored,
9present ``*.go`` file, so a new module is covered the day it is added with no
10allowlist to forget. Build-output trees (``build/``, ``_deps/``) are dropped
11through :mod:`lint_targets`, matching every other provider.
15- **Lint / Static Analysis** (default): ``go vet ./...`` checks correctness,
16 and ``staticcheck ./...`` runs if available on PATH. Formatting (``gofmt``)
17 is enforced by the format gate (``format_tree.sh``), never here.
18- **Test execution** (``--test``): runs ``go test -race -v ./...`` across every
19 discovered module root.
20- **Coverage gating** (``--coverage``): runs ``go test -cover`` (with ``-coverpkg=./...``)
21 and fails if statement coverage falls below ``--floor <pct>`` (defaulting to 85.0%).
25``--selftest`` feeds the tools deliberately non-conforming fixtures and asserts they
26fire, clean fixtures and asserts silence, tests the race detector, exercises coverage
27floor enforcement, and verifies the worktree-scope exclusion check. A collapsed scope
28trips the file floor instead of reporting a clean tree.
32 check_go.py # lint gate (vet, staticcheck)
33 check_go.py --test # run tests with race detection
34 check_go.py --coverage # run coverage against floor (default 85%)
35 check_go.py --coverage --floor 90 # run coverage against 90% floor
36 check_go.py --test --coverage # test + coverage
37 check_go.py --require # fail (not skip) if go is absent
38 check_go.py --selftest # prove verification tools fire and stay quiet
40Exit 0 if clean, exit 1 on findings or test/coverage failures, exit 2 on a tool
41error or a scope that collapsed below the file floor.
44from __future__
import annotations
52from pathlib
import Path
54sys.path.insert(0, str(Path(__file__).resolve().parent))
56from lint_targets
import is_build_output_path
59def _repo_root() -> Path:
60 return Path(__file__).resolve().parents[2]
63def _find_go() -> str | None:
64 env = os.environ.get(
"GO")
65 if env
and Path(env).exists():
67 return shutil.which(
"go")
70def _find_staticcheck() -> str | None:
71 """Path to staticcheck on PATH or in STATICCHECK env var, if present."""
72 env = os.environ.get(
"STATICCHECK")
73 if env
and Path(env).exists():
75 return shutil.which(
"staticcheck")
78def _git_ls_files(*pathspec: str) -> list[str]:
79 """Return tracked or untracked/non-ignored paths matching `pathspec`."""
80 proc = subprocess.run(
96 if proc.returncode != 0:
97 sys.stderr.write(proc.stderr)
98 sys.stderr.write(
"check_go.py: FATAL -- `git ls-files` failed\n")
100 return [p
for p
in proc.stdout.split(
"\0")
if p]
103def _present_files(files: list[str], root: Path |
None =
None) -> list[str]:
104 """Return candidate paths that still exist in the worktree."""
105 base = _repo_root()
if root
is None else root
106 return [rel
for rel
in files
if (base / rel).is_file()]
109def _tracked_go_files() -> list[str]:
110 """Every candidate-worktree Go file, minus build-output trees."""
111 by_extension = _present_files(_git_ls_files(
"*.go"))
112 return sorted(rel
for rel
in by_extension
if not is_build_output_path(rel))
115def _module_roots(files: list[str]) -> list[Path]:
116 """Distinct directories holding a go.mod above each of `files`."""
117 roots: set[Path] = set()
119 directory = (_repo_root() / rel).parent
120 while directory != directory.parent:
121 if (directory /
"go.mod").is_file():
124 if directory == _repo_root():
126 directory = directory.parent
130def _run_vet(go: str, roots: list[Path]) -> dict[str, str]:
131 """Run `go vet ./...` per module root; map root to stderr on failure."""
132 findings: dict[str, str] = {}
134 proc = subprocess.run(
135 [go,
"vet",
"./..."],
141 if proc.returncode != 0:
143 str(root.relative_to(_repo_root()))
144 if root.is_relative_to(_repo_root())
147 findings[rel] = (proc.stdout + proc.stderr).strip()
151def _run_staticcheck(staticcheck: str, roots: list[Path]) -> dict[str, str]:
152 """Run `staticcheck ./...` per module root; map root to output on failure."""
153 findings: dict[str, str] = {}
155 proc = subprocess.run(
156 [staticcheck,
"./..."],
162 if proc.returncode != 0:
164 str(root.relative_to(_repo_root()))
165 if root.is_relative_to(_repo_root())
168 findings[rel] = (proc.stdout + proc.stderr).strip()
172def _run_tests(go: str, roots: list[Path]) -> dict[str, str]:
173 failures: dict[str, str] = {}
174 env = os.environ.copy()
175 env[
"GOWORK"] =
"off"
177 proc = subprocess.run(
178 [go,
"test",
"-race",
"-v",
"./..."],
185 if proc.returncode != 0:
187 str(root.relative_to(_repo_root()))
188 if root.is_relative_to(_repo_root())
191 failures[rel] = (proc.stdout + proc.stderr).strip()
195def _parse_coverage(stdout: str) -> list[tuple[str, float, bool]]:
196 """Parse (package_or_scope, coverage_pct, is_module_wide) from `go test -cover` output."""
197 results: list[tuple[str, float, bool]] = []
198 for raw
in stdout.splitlines():
200 match = re.search(
r"coverage:\s+([0-9]+(?:\.[0-9]+)?)\%\s+of\s+statements", line)
203 pct = float(match.group(1))
205 if not raw.startswith((
"ok\t",
"ok "))
and pct == 0.0:
208 pkg = parts[1]
if len(parts) > 1
and parts[0]
in (
"ok",
"FAIL")
else parts[0]
209 is_module_wide =
" in ./..." in line
210 results.append((pkg, pct, is_module_wide))
215 go: str, roots: list[Path], floor: float
216) -> tuple[dict[str, str], dict[str, str]]:
217 """Run `go test -cover` across module roots, verifying coverage >= floor."""
218 successes: dict[str, str] = {}
219 failures: dict[str, str] = {}
220 env = os.environ.copy()
221 env[
"GOWORK"] =
"off"
224 str(root.relative_to(_repo_root()))
if root.is_relative_to(_repo_root())
else str(root)
226 proc = subprocess.run(
227 [go,
"test",
"-cover",
"-coverpkg=./...",
"./..."],
234 if proc.returncode != 0:
235 failures[rel] = (proc.stdout + proc.stderr).strip()
238 results = _parse_coverage(proc.stdout)
240 failures[rel] =
"no statement coverage reported (0 tests or statements executed)"
243 module_wide = [pct
for (_, pct, is_mod)
in results
if is_mod]
245 effective_pct = max(module_wide)
246 if effective_pct < floor:
248 f
"coverage {effective_pct:.1f}% is below floor {floor:.1f}% "
249 f
"(target: {floor:.1f}%)"
252 successes[rel] = f
"coverage {effective_pct:.1f}% (floor {floor:.1f}%)"
254 below_floor = [(pkg, pct)
for (pkg, pct, _)
in results
if pct < floor]
256 details =
", ".join(f
"{pkg}: {pct:.1f}%" for pkg, pct
in below_floor)
257 failures[rel] = f
"package(s) below floor {floor:.1f}%: {details}"
259 min_pct =
min(pct
for (_, pct, _)
in results)
260 successes[rel] = f
"coverage {min_pct:.1f}% (floor {floor:.1f}%)"
262 return successes, failures
265def _parse_floor(args: list[str]) -> float:
266 """Parse --floor <pct> or --floor=<pct>, defaulting to 85.0."""
268 for i, arg
in enumerate(args):
270 if i + 1 < len(args):
272 return float(args[i + 1])
274 sys.stderr.write(f
"check_go.py: invalid --floor value: {args[i + 1]}\n")
277 sys.stderr.write(
"check_go.py: --floor requires a numeric argument\n")
279 elif arg.startswith(
"--floor="):
280 val = arg.split(
"=", 1)[1]
284 sys.stderr.write(f
"check_go.py: invalid --floor value: {val}\n")
291 staticcheck: dict[str, str] |
None =
None,
294 sys.stderr.write(
"check_go.py: `go vet` finding(s):\n")
295 for relroot
in sorted(vet):
296 sys.stderr.write(f
" {relroot}:\n")
297 for line
in vet[relroot].splitlines():
298 sys.stderr.write(f
" {line}\n")
300 sys.stderr.write(
"check_go.py: `staticcheck` finding(s):\n")
301 for relroot
in sorted(staticcheck):
302 sys.stderr.write(f
" {relroot}:\n")
303 for line
in staticcheck[relroot].splitlines():
304 sys.stderr.write(f
" {line}\n")
305 sys.stderr.write(
"\nFix the finding.\n")
317def _vet_fails(go: str, source: str) -> bool:
318 """True when `go vet` rejects a scratch module holding `source`."""
319 with tempfile.TemporaryDirectory()
as tmp:
321 (root /
"go.mod").write_text(
"module selftest\n\ngo 1.24\n", encoding=
"utf-8")
322 (root /
"fixture.go").write_text(source, encoding=
"utf-8")
323 proc = subprocess.run(
324 [go,
"vet",
"./..."],
330 return proc.returncode != 0
333def _staticcheck_fails(staticcheck: str, source: str) -> bool:
334 """True when `staticcheck ./...` rejects a scratch module holding `source`."""
335 with tempfile.TemporaryDirectory()
as tmp:
337 (root /
"go.mod").write_text(
"module selftest\n\ngo 1.24\n", encoding=
"utf-8")
338 (root /
"fixture.go").write_text(source, encoding=
"utf-8")
339 proc = subprocess.run(
340 [staticcheck,
"./..."],
346 return proc.returncode != 0
349def _selftest_vet(go: str, failures: list[str]) ->
None:
350 vet_bad =
'package selftest\n\nfunc f() int {\n\treturn "not an int"\n}\n'
351 vet_good =
"package selftest\n\nfunc f() int {\n\treturn 1\n}\n"
352 if not _vet_fails(go, vet_bad):
353 failures.append(
" must-fire: `go vet` accepted a mistyped return")
354 if _vet_fails(go, vet_good):
355 failures.append(
" must-stay-quiet: `go vet` rejected the clean fixture")
358def _selftest_staticcheck_fn(staticcheck: str, failures: list[str]) ->
None:
359 sc_bad =
"package selftest\n\nfunc dead() {\n\treturn\n\tprintln(1)\n}\n"
361 "package selftest\n\n// Add returns sum.\nfunc Add(a, b int) int {\n\treturn a + b\n}\n"
363 if not _staticcheck_fails(staticcheck, sc_bad):
364 failures.append(
" must-fire: `staticcheck` accepted dead code / unused function")
365 if _staticcheck_fails(staticcheck, sc_good):
366 failures.append(
" must-stay-quiet: `staticcheck` rejected clean exported function")
369def _selftest_tests(go: str, root: Path, failures: list[str]) ->
None:
370 root = root.resolve()
371 (root /
"go.mod").write_text(
"module testrun\n\ngo 1.24\n", encoding=
"utf-8")
372 (root /
"calc.go").write_text(
373 "package testrun\n\nfunc Add(a, b int) int { return a + b }\n",
376 (root /
"calc_test.go").write_text(
377 'package testrun\n\nimport "testing"\n\nfunc TestAdd(t *testing.T) {\n\tif Add(1, 2) != 3 { t.Fatal("fail") }\n}\n',
380 pass_findings = _run_tests(go, [root])
382 failures.append(f
" must-stay-quiet: `_run_tests` flagged a passing test: {pass_findings}")
384 (root /
"fail_test.go").write_text(
385 'package testrun\n\nimport "testing"\n\nfunc TestBoom(t *testing.T) {\n\tt.Fatal("boom")\n}\n',
388 fail_findings = _run_tests(go, [root])
389 if not fail_findings:
390 failures.append(
" must-fire: `_run_tests` did not report a failing test")
391 (root /
"fail_test.go").unlink()
394 (root /
"race_test.go").write_text(
399func TestDataRace(t *testing.T) {
401 ch := make(chan struct{})
413 race_findings = _run_tests(go, [root])
414 if not race_findings:
415 failures.append(
" must-fire: `_run_tests` (-race) did not detect a data race")
416 (root /
"race_test.go").unlink()
419def _selftest_coverage(go: str, root: Path, failures: list[str]) ->
None:
420 root = root.resolve()
421 (root /
"go.mod").write_text(
"module testcov\n\ngo 1.24\n", encoding=
"utf-8")
422 (root /
"branch.go").write_text(
425func Branch(x int) int {
434 (root /
"branch_test.go").write_text(
439func TestBranch(t *testing.T) {
447 _, cov_fail_85 = _run_coverage(go, [root], floor=85.0)
449 failures.append(
" must-fire: `_run_coverage` passed 66.7% coverage at floor 85.0%")
450 cov_succ_50, cov_fail_50 = _run_coverage(go, [root], floor=50.0)
452 failures.append(
" must-stay-quiet: `_run_coverage` failed 66.7% coverage at floor 50.0%")
454 failures.append(
" must-record: `_run_coverage` did not record success at floor 50.0%")
457def selftest(go: str, staticcheck: str |
None =
None) -> int:
458 """Prove all tools fire where they must and stay quiet where they must."""
459 failures: list[str] = []
461 _selftest_vet(go, failures)
463 _selftest_staticcheck_fn(staticcheck, failures)
465 with tempfile.TemporaryDirectory()
as tmp:
467 _selftest_tests(go, root, failures)
469 with tempfile.TemporaryDirectory()
as tmp:
471 _selftest_coverage(go, root, failures)
473 if _parse_floor([]) != 85.0:
474 failures.append(
" _parse_floor default was not 85.0")
475 if _parse_floor([
"--floor",
"72.5"]) != 72.5:
476 failures.append(
" _parse_floor did not parse --floor 72.5")
477 if _parse_floor([
"--floor=91.0"]) != 91.0:
478 failures.append(
" _parse_floor did not parse --floor=91.0")
480 parsed_wide = _parse_coverage(
"ok pkg/tests 0.1s coverage: 88.5% of statements in ./...")
481 if parsed_wide != [(
"pkg/tests", 88.5,
True)]:
482 failures.append(
" _parse_coverage failed on module-wide output")
484 parsed_blank = _parse_coverage(
"\tpkg\t\tcoverage: 0.0% of statements")
486 failures.append(
" _parse_coverage did not ignore non-test 0.0% package line")
489 with tempfile.TemporaryDirectory()
as tmp:
490 fixture_root = Path(tmp)
491 (fixture_root /
"present.go").touch()
492 present = _present_files([
"present.go",
"deleted.go"], fixture_root)
493 if present != [
"present.go"]:
494 failures.append(
" worktree scope did not exclude exactly the deleted fixture")
497 sys.stderr.write(
"check_go.py --selftest: FAILED\n")
498 sys.stderr.write(
"\n".join(failures) +
"\n")
501 sc_str =
"staticcheck, " if staticcheck
else ""
502 print(f
"check_go.py --selftest: OK (vet, {sc_str}test, race, coverage, floor).")
506def _lint_phase(go: str, roots: list[Path]) -> tuple[dict[str, str], dict[str, str], str |
None]:
507 """Run vet and staticcheck over `roots`."""
508 staticcheck_bin = _find_staticcheck()
509 vet_findings = _run_vet(go, roots)
510 staticcheck_findings = _run_staticcheck(staticcheck_bin, roots)
if staticcheck_bin
else {}
511 return vet_findings, staticcheck_findings, staticcheck_bin
514def _test_phase(go: str, roots: list[Path], do_test: bool) -> dict[str, str]:
515 """Run `go test -race` on every root; report and return any failures."""
518 test_failures = _run_tests(go, roots)
520 sys.stderr.write(
"check_go.py: `go test -race` failure(s):\n")
521 for relroot
in sorted(test_failures):
522 sys.stderr.write(f
" {relroot}:\n")
523 for line
in test_failures[relroot].splitlines():
524 sys.stderr.write(f
" {line}\n")
529 go: str, roots: list[Path], floor: float, do_coverage: bool
530) -> tuple[dict[str, str], dict[str, str]]:
531 """Run coverage against `floor`; report and return (successes, failures)."""
534 cov_successes, cov_failures = _run_coverage(go, roots, floor)
536 sys.stderr.write(
"check_go.py: coverage failure(s):\n")
537 for relroot
in sorted(cov_failures):
538 sys.stderr.write(f
" {relroot}: {cov_failures[relroot]}\n")
539 return cov_successes, cov_failures
542def _go_or_exit(go: str |
None, args: list[str]) -> str:
543 """Resolve the Go binary, exiting when absent unless a hook may skip."""
545 msg =
"check_go.py: go not found"
546 if "--require" in args
or "--selftest" in args:
547 sys.stderr.write(msg +
" -- required by --require/--selftest\n")
549 print(msg +
" -- skipping (install Go to enforce locally).")
554def _scope_or_error(tracked: list[str]) -> int |
None:
555 """Return an exit code when the Go scope collapsed below the file floor."""
557 if len(tracked) >= file_floor:
560 f
"check_go.py: FATAL -- only {len(tracked)} Go file(s) in scope, "
561 f
"floor is {file_floor}.\n"
562 " A collapsed scope reports a clean tree because it checked nothing.\n"
571 staticcheck_bin: str |
None,
572 cov_successes: dict[str, str],
574 """Print the per-mode clean summary on stdio."""
575 summary_parts: list[str] = []
577 sc_desc =
"vet/staticcheck" if staticcheck_bin
else "vet"
578 summary_parts.append(f
"{sc_desc} clean")
580 summary_parts.append(
"tests passed (race detector clean)")
581 if "coverage" in mode:
582 cov_str =
", ".join(f
"{k}: {v}" for k, v
in sorted(cov_successes.items()))
583 summary_parts.append(f
"{cov_str}")
585 f
"check_go.py: clean ({len(tracked)} files, {len(roots)} module(s), "
586 f
"{'; '.join(summary_parts)})."
590def _execute_lint_or_test(go: str, tracked: list[str], roots: list[Path], args: list[str]) -> int:
591 """Run lint/test/coverage, returning the process exit code directly."""
592 do_test =
"--test" in args
593 do_coverage =
"--coverage" in args
594 do_lint =
"--lint" in args
or (
not do_test
and not do_coverage)
595 floor = _parse_floor(args)
598 vet_findings, staticcheck_findings, staticcheck_bin = _lint_phase(go, roots)
599 if vet_findings
or staticcheck_findings:
600 _report(vet_findings, staticcheck_findings)
602 if not do_test
and not do_coverage:
603 sc_note =
", staticcheck" if staticcheck_bin
else ""
604 print(f
"check_go.py: clean ({len(tracked)} files, no vet{sc_note} findings).")
608 staticcheck_findings = {}
609 staticcheck_bin =
None
611 test_failures = _test_phase(go, roots, do_test)
612 cov_successes, cov_failures = _coverage_phase(go, roots, floor, do_coverage)
614 if test_failures
or cov_failures:
617 parts = (
"lint",
"test",
"coverage")
618 on = (do_lint, do_test, do_coverage)
619 mode =
"".join(part
for part, flag
in zip(parts, on, strict=
True)
if flag)
620 _render_summary(tracked, roots, mode, staticcheck_bin, cov_successes)
624def _selftest_or_scope(go: str, args: list[str]) -> int |
None:
625 """Run the selftest or return an error exit for a collapsed scope."""
626 if "--selftest" in args:
627 staticcheck = _find_staticcheck()
628 return selftest(go, staticcheck)
629 tracked = _tracked_go_files()
630 scope_error = _scope_or_error(tracked)
631 if scope_error
is not None:
636def _gate_main(go: str, args: list[str]) -> int:
637 """Dispatch the selftest or the lint/test/coverage phases for present Go."""
638 early = _selftest_or_scope(go, args)
639 if early
is not None:
641 tracked = _tracked_go_files()
642 return _execute_lint_or_test(go, tracked, _module_roots(tracked), args)
645def main(argv: list[str]) -> int:
646 """Run Go verification over every tracked first-party Go file.
648 A missing Go toolchain is handled two ways ON PURPOSE, mirroring
649 check_ruff.py. Bare, it prints a notice and exits 0, so a contributor
650 without Go is not blocked by a local hook. Under ``--require`` or
651 ``--selftest`` it exits 1 instead -- CI passes ``--require`` precisely so
652 that an absent toolchain fails the build rather than skipping silently.
654 ``--list-files`` exists for check_lint_coverage.py and needs no toolchain:
655 it reports exactly the files this gate would check.
657 Returns 0 when clean, 1 on any finding or test/coverage failure, 2 on tool
658 error or collapsed scope.
662 if "--list-files" in args:
665 print(
"\n".join(_tracked_go_files()))
668 go = _go_or_exit(_find_go(), args)
669 return _gate_main(go, args)
672if __name__ ==
"__main__":
673 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.