ci(R-1297 A): heavy suites by path on a branch push, all on a tag and nightly; no second full gate run
gates / gates (push) Successful in 3m23s

- script_tests_gate.py: test_gate_decoys / test_due_checks_gate / dooplex-offsite run on a CI branch push only
  when one of their paths changed (FELHOM_CHANGED_FILES); no list, an empty or unreadable list = every suite
- gates.yml: nightly schedule 03:20 UTC; the list is set only for push:refs/heads/*; a trimmed commits array
  (total_commits > listed) is an unknown list
- test_repo_gates.py and test_due_checks_gate.py group G drive the real runner with GATES narrowed to their
  labels, instead of the whole gate set a second time (130 s each in CI)
- red-proved: filter cases, workflow pins (old gates.yml), inert run_gate, unregistered due-checks
This commit is contained in:
admin committed 2026-10-26 18:14:23 +01:00
1 parent 8143a2c872
commit ecd82abab8
5 files changed
+194 -10

No files matched your search

+74 -2
View File
@@ -16,7 +16,8 @@ GATE = os.path.join(HERE, "script_tests_gate.py")
COVERS = {
"script-tests": ("a suite that PRINTS OK and exits 1; a failing suite three levels deep; an empty scripts/ "
"tree; plus the genuine article (must pass) and the nested-run mark (must step aside)"),
"tree; plus the genuine article (must pass) and the nested-run mark (must step aside); R-1297: a "
"failing heavy suite on a push that touched its own path, with no list, an empty or a missing list"),
}
@@ -54,8 +55,79 @@ def timeouts_ok():
return (g.timeout_for("scripts/test_gate_decoys.py") == 600 and g.timeout_for("scripts/test_other.py") == g.TIMEOUT_S == 300)
# R-1297 (A): a heavy suite runs on a CI push only when one of ITS paths changed; with no list (a tag, the nightly run,
# the pre-push hook) or an unreadable or empty list, every suite runs. Each case plants a FAILING heavy suite, so
# „it ran" is rc 1 and „it was skipped" is rc 0 — the verdict, not a log line, says which happened.
CHANGED = "FELHOM_CHANGED_FILES"
def gate_changed(root, changed):
"""changed: None (variable unset), "MISSING" (points at no file), or a list of pushed paths."""
env = {k: v for k, v in os.environ.items() if k not in ("FELHOM_SCRIPT_TESTS_GATE", CHANGED)}
if changed == "MISSING":
env[CHANGED] = os.path.join(root, "no-such-list.txt")
elif changed is not None:
lst = os.path.join(root, "pushed.txt")
open(lst, "w").write("".join(f + "\n" for f in changed))
env[CHANGED] = lst
p = subprocess.run([sys.executable, GATE, root], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return p.returncode, p.stdout.decode("utf-8", "replace")
HEAVY = {"test_ok.py": "pass\n", "test_gate_decoys.py": "raise SystemExit(1)\n",
"dooplex-offsite/test_dooplex_offsite.py": "raise SystemExit(1)\n",
"test_due_checks_gate.py": "raise SystemExit(1)\n"}
FILTER_CASES = [
# (name, files, changed, want rc, text the output must hold)
("filter: no list -> every suite runs", HEAVY, None, 1, "test_gate_decoys.py"),
("filter: docs-only push skips the 3", HEAVY, ["documentation/x.md", "STATUS.md"], 0, "SKIP"),
("filter: a gate script runs decoys", {"test_ok.py": "pass\n", "test_gate_decoys.py": "raise SystemExit(1)\n"},
["scripts/site_gates.py"], 1, "test_gate_decoys.py"),
("filter: the due gate runs its suite", {"test_ok.py": "pass\n", "test_due_checks_gate.py": "raise SystemExit(1)\n"},
["scripts/due_checks_gate.py"], 1, "test_due_checks_gate.py"),
("filter: offsite path runs offsite", {"test_ok.py": "pass\n", "dooplex-offsite/test_dooplex_offsite.py": "raise SystemExit(1)\n"},
["scripts/dooplex-offsite/dooplex-offsite.sh"], 1, "test_dooplex_offsite.py"),
("filter: hub push skips offsite", {"test_ok.py": "pass\n", "dooplex-offsite/test_dooplex_offsite.py": "raise SystemExit(1)\n"},
["hub/internal/web/hosts.go"], 0, "SKIP"),
("filter: empty list -> every suite", HEAVY, [], 1, "test_gate_decoys.py"),
("filter: missing list -> every suite", HEAVY, "MISSING", 1, "test_gate_decoys.py"),
("filter: other suites never skipped", {"test_other.py": "raise SystemExit(1)\n"}, ["documentation/x.md"], 1, "test_other.py"),
]
def workflow_pins():
"""R-1297: the filter is safe only while the nightly run exists and only a branch push sets the list. Read from the
real workflow and hook, so removing the schedule, or setting the list on every event, turns this red."""
root = os.path.dirname(HERE)
wf = open(os.path.join(root, ".gitea", "workflows", "gates.yml"), encoding="utf-8").read()
hook = open(os.path.join(root, ".githooks", "pre-push"), encoding="utf-8").read()
problems = []
if "schedule:" not in wf or "- cron:" not in wf:
problems.append("gates.yml has no nightly schedule — the filtered suites would run only when touched")
arm = wf.find("push:refs/heads/*)")
sets = [i for i in range(len(wf)) if wf.startswith('FELHOM_CHANGED_FILES=', i)]
if not sets or arm < 0 or any(i < arm or i > wf.find(";;", arm) for i in sets):
problems.append("gates.yml sets FELHOM_CHANGED_FILES outside the branch-push arm")
if "total_commits" not in wf:
problems.append("gates.yml does not treat a trimmed commits array as an unknown list")
if "FELHOM_CHANGED_FILES" in hook:
problems.append("the pre-push hook sets the suite filter — the hook must run every suite")
for p in problems:
print("FAIL workflow pin: " + p)
if not problems:
print("ok workflow pins (nightly run, branch-push-only list, trimmed payload, hook unfiltered)")
return not problems
def main():
bad = 0
bad += not workflow_pins()
for name, files, changed, want, text in FILTER_CASES:
got, out = gate_changed(fake_repo(files), changed)
ok = got == want and text in out
bad += not ok
print("%s %-38s want rc=%d got rc=%d%s" % ("ok " if ok else "FAIL", name, want, got,
"" if text in out else " (no %r in output)" % text))
t_ok = timeouts_ok()
bad += not t_ok
print("%s %-32s" % ("ok " if t_ok else "FAIL", "per-suite time limits (R-1209)"))
@@ -64,7 +136,7 @@ def main():
ok = got == want
bad += not ok
print("%s %-32s want rc=%d got rc=%d" % ("ok " if ok else "FAIL", name, want, got))
print("script-tests decoys: %d/%d" % (len(CASES) - bad, len(CASES)))
print("script-tests decoys: %d failed of %d" % (bad, len(CASES) + len(FILTER_CASES) + 1))
return 1 if bad else 0