- harness/generic.py: recipe-agnostic assert_serving (converged + real HTTP, 404-excluded + not Traefik 404 body + CA-verified trusted wildcard cert), op helpers, backup_capable detect - harness/discovery.py: per-op overlay resolution (repo-local > cc-ci > generic), custom + hook - tests/_generic/: assertion-only tiers (install/upgrade/backup/restore) on the shared deployment - run_recipe_ci.py: deploy-ONCE orchestrator, per-op summary, deploy-count guard (DG4.1) - conftest live_app fixture; lifecycle deploy-count + install-steps hook + pin DOMAIN to run domain DG1 cold-verified green on hedgedoc (pure generic, deploy-count=1, clean teardown). G0 CLAIMED. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.9 KiB
Python
72 lines
2.9 KiB
Python
"""Overlay / custom-test / install-steps discovery + precedence (Phase 1d, plan §2.5, DG4/DG5).
|
|
|
|
The generic is the default for each lifecycle op; a recipe's `test_<op>.py` OVERRIDES it. Sources,
|
|
in precedence order (machine-docs/DECISIONS.md):
|
|
|
|
lifecycle op (install/upgrade/backup/restore) — exactly ONE assertion file runs:
|
|
repo-local tests/test_<op>.py (upstream-authoritative, wins same-name collisions)
|
|
> cc-ci tests/<recipe>/test_<op>.py
|
|
> generic tests/_generic/test_<op>.py <- always present; the floor
|
|
|
|
custom (non-lifecycle) test_*.py — ALL run, additively, from BOTH locations (opt-in).
|
|
|
|
install-steps hook — install_steps.sh: repo-local > cc-ci, or none.
|
|
|
|
Repo-local = the recipe repo's own tests/ dir, snapshotted after fetch (it survives abra
|
|
re-checking-out the recipe to a version tag — see the run orchestrator).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import glob
|
|
import os
|
|
|
|
LIFECYCLE_OPS = ("install", "upgrade", "backup", "restore")
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
GENERIC_DIR = os.path.join(ROOT, "tests", "_generic")
|
|
|
|
|
|
def cc_ci_dir(recipe: str) -> str:
|
|
return os.path.join(ROOT, "tests", recipe)
|
|
|
|
|
|
def resolve_op(recipe: str, op: str, repo_local_dir: str | None) -> tuple[str, str]:
|
|
"""Return (source, path) for the single assertion file to run for `op`:
|
|
source in {"repo-local","cc-ci","generic"}. The generic file is the floor and always exists."""
|
|
fname = f"test_{op}.py"
|
|
if repo_local_dir:
|
|
p = os.path.join(repo_local_dir, fname)
|
|
if os.path.isfile(p):
|
|
return ("repo-local", p)
|
|
p = os.path.join(cc_ci_dir(recipe), fname)
|
|
if os.path.isfile(p):
|
|
return ("cc-ci", p)
|
|
return ("generic", os.path.join(GENERIC_DIR, fname))
|
|
|
|
|
|
def custom_tests(recipe: str, repo_local_dir: str | None) -> list[tuple[str, str]]:
|
|
"""All non-lifecycle test_*.py from cc-ci's tests/<recipe>/ and the recipe's repo-local tests/.
|
|
These have no generic equivalent and run only when present (opt-in), additively from both."""
|
|
lifecycle_names = {f"test_{op}.py" for op in LIFECYCLE_OPS}
|
|
found: list[tuple[str, str]] = []
|
|
for source, d in (("cc-ci", cc_ci_dir(recipe)), ("repo-local", repo_local_dir)):
|
|
if not d or not os.path.isdir(d):
|
|
continue
|
|
for p in sorted(glob.glob(os.path.join(d, "test_*.py"))):
|
|
if os.path.basename(p) not in lifecycle_names:
|
|
found.append((source, p))
|
|
return found
|
|
|
|
|
|
def install_steps(recipe: str, repo_local_dir: str | None) -> tuple[str, str] | None:
|
|
"""The custom install-steps hook (install_steps.sh) for a recipe, or None. repo-local > cc-ci."""
|
|
if repo_local_dir:
|
|
p = os.path.join(repo_local_dir, "install_steps.sh")
|
|
if os.path.isfile(p):
|
|
return ("repo-local", p)
|
|
p = os.path.join(cc_ci_dir(recipe), "install_steps.sh")
|
|
if os.path.isfile(p):
|
|
return ("cc-ci", p)
|
|
return None
|