c3e4bb18c7
--fast selects only gates that touch no network and no container runtime: gate 1 (check-image-pins) runs, image-resolvable and volume-persistence do NOT. Default behaviour with no flag is unchanged. The skip is ANNOUNCED with the reason and with what still owes a periodic run — a silently narrowed run reads as 'covered everything' when it did not. Why the runtime gates are never in a hook: a push that pulls images and starts containers gets bypassed within a week, and the bypass becomes the habit. They stay deliberate periodic runs at the start of a catalog campaign, before a publish train, and when a template's volumes: block or image tag changes — on a scratch host, never a customer box. .githooks/pre-push runs catalog_gates.py --fast and refuses the push. Per-clone and --no-verify-able, both stated in the hook itself. test_catalog_gates.py pins --fast's CONTENT, not just its exit code: the runtime gates must not run, the skip must be announced, and the no-flag path must still select all three. Red-proofed: an inert run_gate turns it red.
67 lines
2.9 KiB
Python
67 lines
2.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Seam test for scripts/catalog_gates.py --fast.
|
|
|
|
Run: python3 scripts/test_catalog_gates.py
|
|
|
|
WHY THIS EXISTS. An entry point is a seam by definition: a runner that LISTS a gate but never
|
|
executes it is inert and fully green. So the assertion is on the member gate's OWN distinctive
|
|
stdout — never on the runner's summary line — plus the exit code.
|
|
|
|
The second and third tests pin --fast's CONTENT, not just its exit code: the two runtime gates
|
|
must NOT run (a push that pulls images and starts containers gets bypassed within a week, and
|
|
the bypass becomes the habit), and the skip must be ANNOUNCED — a silently narrowed run reads as
|
|
"covered everything" when it did not.
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
ENTRY = os.path.join(ROOT, "scripts", "catalog_gates.py")
|
|
|
|
|
|
class CatalogGatesFastTest(unittest.TestCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
p = subprocess.run([sys.executable, ENTRY, "--fast"], cwd=ROOT,
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
cls.rc = p.returncode
|
|
cls.out = p.stdout.decode("utf-8", "replace")
|
|
|
|
def test_exit_code_is_zero(self):
|
|
self.assertEqual(self.rc, 0, self.out)
|
|
|
|
def test_static_gate_actually_ran(self):
|
|
self.assertIn("image-pin gate", self.out,
|
|
"check-image-pins is listed but its own output never appeared — an inert "
|
|
"runner prints the summary without calling anything:\n%s" % self.out)
|
|
|
|
def test_runtime_gates_did_not_run(self):
|
|
for fingerprint in ("resolvability gate", "volume-persistence gate", "canary"):
|
|
self.assertNotIn(fingerprint, self.out,
|
|
"a runtime gate ran under --fast (%r) — --fast must touch no network "
|
|
"and no container runtime:\n%s" % (fingerprint, self.out))
|
|
self.assertNotIn("image-resolvable OK", self.out)
|
|
self.assertNotIn("volume-persistence OK", self.out)
|
|
|
|
def test_skip_is_announced(self):
|
|
self.assertIn("--fast SKIPPED", self.out)
|
|
self.assertIn("image-resolvable", self.out)
|
|
self.assertIn("volume-persistence", self.out)
|
|
|
|
def test_default_run_still_selects_all_three(self):
|
|
"""--fast must not change the no-flag behaviour. Asserted on the GATES table rather than
|
|
by running it — the default run deploys every template and takes minutes per app."""
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("catalog_gates_under_test", ENTRY)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
self.assertEqual(len(mod.GATES), 3)
|
|
self.assertEqual([g[0] for g in mod.GATES if g[3]], ["image-pins"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|