hub 0.165.0 source: a household's mail list is never overwritten by a box default, a box reads it back (R-1199 reopened); host_swap_short only when swap is really short (R-1216, D28 = A); host-swap-rule gate with 4 decoys; 03/05/08 updated
gates / gates (push) Successful in 15m54s
gates / gates (push) Successful in 15m54s
This commit is contained in:
1 parent
a92837410a
commit
5ae5984cf9
18 files changed
+653
-24
No files matched your search
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""host_swap_rule_gate.py — the hub and the box judge „too little host swap" by ONE rule (R-1216, D28 = A).
|
||||
|
||||
Run from the repo root: python3 scripts/host_swap_rule_gate.py
|
||||
Exit 0 clean · 1 convicted · 2 inconclusive (the felhom-controller clone is not beside this one — never a silent pass).
|
||||
|
||||
WHY. The rule lives twice: the hub raises `host_swap_short` for the operator (hub/internal/api/host_swap.go), and the
|
||||
controller's install page tells the household „A gépen nincs elég cserehely (swap)…" (internal/web/
|
||||
system_memory_handlers.go). Two Go modules cannot share a function, so they share a CASE TABLE: each repo carries
|
||||
`testdata/host_swap_rule_cases.json` and a Go test that runs its own implementation over every row. This gate is the
|
||||
contract between them:
|
||||
1. the two tables are BYTE-IDENTICAL (a row changed on one side only is drift);
|
||||
2. each table is READ by a Go test in its own package (a table no test opens proves nothing — the label without the
|
||||
fact: the file name present, the run absent).
|
||||
The decoys are in scripts/test_host_swap_rule_gate.py, run from scripts/test_gate_decoys.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.environ.get("HOST_SWAP_RULE_REPO") or os.path.dirname(HERE)
|
||||
WORKSPACE = os.path.dirname(REPO)
|
||||
NAME = "host_swap_rule_cases.json"
|
||||
HUB_DIR = os.path.join(REPO, "hub", "internal", "api")
|
||||
CTRL_DIR = os.path.join(WORKSPACE, "felhom-controller", "controller", "internal", "web")
|
||||
|
||||
|
||||
def read_by_a_test(pkg_dir: str) -> bool:
|
||||
for f in glob.glob(os.path.join(pkg_dir, "*_test.go")):
|
||||
if NAME in io.open(f, encoding="utf-8").read():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
hub = os.path.join(HUB_DIR, "testdata", NAME)
|
||||
ctrl = os.path.join(CTRL_DIR, "testdata", NAME)
|
||||
if not os.path.exists(hub):
|
||||
print("host-swap-rule: CONVICTS — the hub's case table is gone (%s)" % os.path.relpath(hub, REPO))
|
||||
return 1
|
||||
if not os.path.isdir(os.path.join(WORKSPACE, "felhom-controller")):
|
||||
print("host-swap-rule: INCONCLUSIVE — no felhom-controller clone beside this one; the contract cannot be read",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
problems = []
|
||||
if not os.path.exists(ctrl):
|
||||
problems.append("the controller's case table is gone (felhom-controller/controller/internal/web/testdata/%s)" % NAME)
|
||||
else:
|
||||
a, b = open(hub, "rb").read(), open(ctrl, "rb").read()
|
||||
if a != b:
|
||||
problems.append("the two case tables differ (%d vs %d bytes) — the hub and the box no longer judge host "
|
||||
"swap by the same rule" % (len(a), len(b)))
|
||||
for label, d in (("hub", HUB_DIR), ("controller", CTRL_DIR)):
|
||||
if not read_by_a_test(d):
|
||||
problems.append("no Go test in the %s package reads %s — a table nothing runs proves nothing" % (label, NAME))
|
||||
print("host-swap-rule: compared the hub's and the controller's %s, and looked for the test that runs each" % NAME)
|
||||
if problems:
|
||||
print("host-swap-rule gate CONVICTS (%d):" % len(problems))
|
||||
for p in problems:
|
||||
print(" " + p)
|
||||
return 1
|
||||
print("host-swap-rule gate OK: one rule, byte-identical tables, both run by a test.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -19,6 +19,7 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
|
||||
8. wire-contract every emitted field is decodable by its receiver (G-1)
|
||||
9. hub-copy the hub's customer-facing words, against the retired-name list (R-324)
|
||||
9b. guide-quote the English volunteer guide, against the controller's English bundle (R-596)
|
||||
9b. host-swap-rule the hub's and the box's host-swap rule, one byte-identical case table (R-1216)
|
||||
10. due-checks a dated check in OPEN-ITEMS.md that has come due (R-341)
|
||||
11. one-register open work living outside OPEN-ITEMS.md (R-369)
|
||||
12. closed-register a CLOSED row whose verdict still reads open, or an id in both (R-405)
|
||||
@@ -160,6 +161,10 @@ GATES = [
|
||||
# Hungarian shipped, and would have quoted it after the fix too. INCONCLUSIVE (exit 2, reported
|
||||
# not swallowed) when the controller clone is absent. Fast: two file reads.
|
||||
("guide-quote", os.path.join(SCRIPTS, "guide_quote_gate.py"), [], True, False),
|
||||
# R-1216 (D28 = A) — the hub's host_swap_short and the controller's install-page swap sentence judge by ONE rule,
|
||||
# held as a byte-identical case table in both repos, each run by a Go test. INCONCLUSIVE without the controller
|
||||
# clone. Fast: file reads.
|
||||
("host-swap-rule", os.path.join(SCRIPTS, "host_swap_rule_gate.py"), [], True, False),
|
||||
# R-341 — dated checks in the register were prose that nothing read. Fast: stdlib file read.
|
||||
("due-checks", os.path.join(SCRIPTS, "due_checks_gate.py"), [], True, False),
|
||||
# R-369 — two files held open work and only one called itself the source of truth, so a READY
|
||||
|
||||
@@ -95,6 +95,10 @@ COVERS = {
|
||||
"script-tests": ("R-885: FIVE cases in scripts/test_script_tests_gate.py, run from here: a suite that PRINTS "
|
||||
"OK and exits 1 (label without fact), a failing suite three levels deep (scope is a walk), "
|
||||
"an empty scripts/ tree (checked nothing), the genuine article (must pass), the nested mark"),
|
||||
"host-swap-rule": ("R-1216: FOUR cases in scripts/test_host_swap_rule_gate.py, run from here: one verdict "
|
||||
"flipped on the controller side only (every case NAME still matches — name-for-fact), "
|
||||
"identical tables that no controller test reads any more (the label without the run), "
|
||||
"no controller clone (must be INCONCLUSIVE), and the genuine article (must pass)"),
|
||||
"guide-quote": ("R-596: SEVEN cases in scripts/test_guide_quote_gate.py, run from here so "
|
||||
"this suite stays the single entry point. The load-bearing one is "
|
||||
"name-for-fact: a guide that lists every key in a table and quotes none of "
|
||||
@@ -505,6 +509,15 @@ else:
|
||||
_n = _gq.stdout.strip().splitlines()[-1] if _gq.stdout.strip() else "?"
|
||||
print(" ok %-20s %s" % ("guide-quote", _n))
|
||||
|
||||
# ── host-swap-rule (R-1216) ── built fake workspaces, guide-quote's shape: its own file, run from here.
|
||||
ran += 1
|
||||
_hs = subprocess.run([sys.executable, os.path.join("scripts", "test_host_swap_rule_gate.py")],
|
||||
cwd=ROOT, capture_output=True, text=True)
|
||||
if _hs.returncode != 0:
|
||||
fails.append("host-swap-rule: its decoy suite FAILED — a decoy did not convict\n%s" % (_hs.stdout + _hs.stderr)[-800:])
|
||||
else:
|
||||
print(" ok %-20s %s" % ("host-swap-rule", (_hs.stdout.strip().splitlines() or ["?"])[-1]))
|
||||
|
||||
# ── iso-bootstrap (R-502) ────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Its decoys need a fake docker on a one-directory PATH, so they live in their own file and are RUN from
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decoys for host_swap_rule_gate (R-421). Run: python3 scripts/test_host_swap_rule_gate.py
|
||||
|
||||
Each case builds a fake workspace (felhom.eu + a felhom-controller clone) and runs the real gate against it:
|
||||
1. one row's verdict flipped on the controller side only — drift (the files share every NAME; the fact is a value);
|
||||
2. identical tables, but no controller test reads the table any more — the label without the fact;
|
||||
3. no controller clone — INCONCLUSIVE (2), never a pass;
|
||||
4. the genuine article — must pass.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.dirname(HERE)
|
||||
GATE = os.path.join(HERE, "host_swap_rule_gate.py")
|
||||
REAL = os.path.join(REPO, "hub", "internal", "api", "testdata", "host_swap_rule_cases.json")
|
||||
NAME = "host_swap_rule_cases.json"
|
||||
|
||||
|
||||
def build(tmp, ctrl_table=None, ctrl_test=True, with_ctrl=True):
|
||||
repo = os.path.join(tmp, "felhom.eu")
|
||||
hub = os.path.join(repo, "hub", "internal", "api")
|
||||
os.makedirs(os.path.join(hub, "testdata"))
|
||||
shutil.copy(REAL, os.path.join(hub, "testdata", NAME))
|
||||
open(os.path.join(hub, "x_test.go"), "w").write('package api\n// os.ReadFile("testdata/%s")\n' % NAME)
|
||||
if with_ctrl:
|
||||
web = os.path.join(tmp, "felhom-controller", "controller", "internal", "web")
|
||||
os.makedirs(os.path.join(web, "testdata"))
|
||||
data = open(REAL, encoding="utf-8").read() if ctrl_table is None else ctrl_table
|
||||
open(os.path.join(web, "testdata", NAME), "w", encoding="utf-8").write(data)
|
||||
body = 'package web\n// os.ReadFile("testdata/%s")\n' % NAME if ctrl_test else "package web\n"
|
||||
open(os.path.join(web, "y_test.go"), "w").write(body)
|
||||
return repo
|
||||
|
||||
|
||||
def run(repo):
|
||||
env = dict(os.environ, HOST_SWAP_RULE_REPO=repo)
|
||||
return subprocess.run([sys.executable, GATE], env=env, capture_output=True, text=True).returncode
|
||||
|
||||
|
||||
def main():
|
||||
real = open(REAL, encoding="utf-8").read()
|
||||
flipped = real.replace('"guest_swap_mb": 12288, "short": false', '"guest_swap_mb": 12288, "short": true', 1)
|
||||
assert flipped != real, "the decoy's row is gone from the real table"
|
||||
cases = [
|
||||
("one verdict flipped on the controller side", dict(ctrl_table=flipped), 1),
|
||||
("no controller test reads the table", dict(ctrl_test=False), 1),
|
||||
("no controller clone", dict(with_ctrl=False), 2),
|
||||
("the genuine article", dict(), 0),
|
||||
]
|
||||
fails = 0
|
||||
for label, kw, want in cases:
|
||||
tmp = tempfile.mkdtemp()
|
||||
try:
|
||||
got = run(build(tmp, **kw))
|
||||
finally:
|
||||
shutil.rmtree(tmp)
|
||||
ok = got == want
|
||||
fails += 0 if ok else 1
|
||||
print(" %s %-45s rc=%d want %d" % ("ok" if ok else "FAIL", label, got, want))
|
||||
print("host-swap-rule decoys: %d/%d" % (len(cases) - fails, len(cases)))
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user