95 lines
4.3 KiB
Python
95 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""script_tests_gate.py — every Python test suite under scripts/ runs on every push (R-885).
|
||
|
||
Usage: python3 scripts/script_tests_gate.py [<repo-root>]
|
||
Exit 0 every suite exited 0 · 1 a suite failed, timed out, or NO suite was found.
|
||
|
||
WHY. `repo_gates.py` ran gates, not the `test_*.py` suites beside them, so a change that broke one — the 15 tests of
|
||
the DooPlex hub-DB push/restore scripts, the 73 of the instructions gate — was caught only if someone ran it by hand.
|
||
|
||
SCOPE IS A FACT (R-421): the suites are FOUND by walking scripts/ (os.walk, every depth), never listed by hand, so a new
|
||
suite is covered the day it is added. Finding none is a FAILURE — a gate that checked nothing must not read as green.
|
||
A suite's verdict is its EXIT CODE, never its output: a suite that prints "OK" and exits 1 has failed.
|
||
|
||
NESTING. `test_repo_gates.py` runs `repo_gates.py`, which runs this gate, which would run `test_repo_gates.py` again.
|
||
The gate marks its children with FELHOM_SCRIPT_TESTS_GATE=1 and, when it finds the mark, steps aside (exit 0, said
|
||
out loud). Pinned by scripts/test_script_tests_gate.py (decoys).
|
||
"""
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
|
||
# A gate's own console must not decide its verdict. These scripts quote back Hungarian copy, file
|
||
# paths and arrow characters; a Windows console here is cp1250, and printing one of them raised
|
||
# UnicodeEncodeError *before the gate had decided anything* — reuse_refs_check.py died while printing
|
||
# a NOTE, which the runner then reported as a failure (2026-10-10). Same trap class as
|
||
# poster_facts_gate.py's, which was found by its own red-proof.
|
||
for _stream in (sys.stdout, sys.stderr):
|
||
try:
|
||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||
except (AttributeError, ValueError, OSError): # pragma: no cover - old Python, or a pipe
|
||
pass
|
||
|
||
|
||
MARK = "FELHOM_SCRIPT_TESTS_GATE"
|
||
TIMEOUT_S = 300
|
||
# R-1209 (2026-10-21 night): the decoy suite runs every gate's decoys and took 250–255 s on the CI runner on green runs,
|
||
# then over 300 s on every push that night (CI jobs 2177–2189) — ~155 s on DooPlex. A time-out is still a FAILURE (rc
|
||
# „timeout"); only this one suite gets a longer limit. Pinned by test_script_tests_gate.py (timeout_for).
|
||
SUITE_TIMEOUT_S = {"scripts/test_gate_decoys.py": 600}
|
||
|
||
|
||
def timeout_for(rel):
|
||
return SUITE_TIMEOUT_S.get(rel.replace(os.sep, "/"), TIMEOUT_S)
|
||
SKIP_DIRS = {"__pycache__", ".git", "node_modules"}
|
||
|
||
|
||
def find_suites(scripts_dir):
|
||
out = []
|
||
for dp, dns, fns in os.walk(scripts_dir):
|
||
dns[:] = sorted(d for d in dns if d not in SKIP_DIRS)
|
||
for f in sorted(fns):
|
||
if f.startswith("test_") and f.endswith(".py"):
|
||
out.append(os.path.join(dp, f))
|
||
return out
|
||
|
||
|
||
def main(argv):
|
||
if os.environ.get(MARK):
|
||
print("script-tests: nested run (inside a suite this gate started) — stepping aside")
|
||
return 0
|
||
root = os.path.abspath(argv[1]) if len(argv) > 1 else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
suites = find_suites(os.path.join(root, "scripts"))
|
||
if not suites:
|
||
print("script-tests: FAILED — no test_*.py found under %s/scripts (a gate that checked nothing is not green)" % root)
|
||
return 1
|
||
env = dict(os.environ, **{MARK: "1"})
|
||
failed = []
|
||
for s in suites:
|
||
rel = os.path.relpath(s, root)
|
||
t0 = time.time()
|
||
limit = timeout_for(rel)
|
||
try:
|
||
p = subprocess.run([sys.executable, "-W", "ignore", s], cwd=root, env=env,
|
||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=limit)
|
||
rc, tail = p.returncode, p.stdout.decode("utf-8", "replace").strip().splitlines()[-3:]
|
||
except subprocess.TimeoutExpired:
|
||
rc, tail = "timeout", ["(killed after %d s)" % limit]
|
||
verdict = "OK " if rc == 0 else "FAIL"
|
||
print(" %s %-55s %5.1fs rc=%s" % (verdict, rel, time.time() - t0, rc))
|
||
if rc != 0:
|
||
failed.append(rel)
|
||
for line in tail:
|
||
print(" | " + line[:200])
|
||
if failed:
|
||
print("script-tests: FAILED — %d of %d suite(s): %s" % (len(failed), len(suites), ", ".join(failed)))
|
||
return 1
|
||
print("script-tests: OK — %d suite(s), every one exited 0" % len(suites))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv))
|