All checks were successful
continuous-integration/drone/push Build is passing
a) compose.ccci.yml is FIRST-CLASS: the harness auto-copies tests/<recipe>/ compose.ccci.yml into the run's recipe checkout (ABRA_DIR-aware, lifecycle. provide_ccci_overlay) and auto-chaoses the pinned base deploy on its presence (kills the R7 implicit coupling). ghost/discourse install_steps.sh (copy-only boilerplate) deleted; CHAOS_BASE_DEPLOY removed from both metas + the registry. b) install-time deps wiring is the ONLY mode: deps with DEPS provision BEFORE the single deploy; legacy post-deploy provisioning + the setup_custom_tests.sh invocation machinery deleted. lasuite-docs migrated to install_steps.sh OIDC wiring (same env names/values as the old hook — only the timing moved); lasuite-drive's remaining post-deploy MinIO bucket one-shot moved to ops.py pre_install; both setup_custom_tests.sh files deleted; OIDC_AT_INSTALL removed from drive/meet metas + the registry. c) SKIP_GENERIC meta key deleted (zero users). Env form CCCI_SKIP_GENERIC* stays as the documented dev-only escape hatch; when active in a drone CI run the orchestrator prints a loud !! warning (manifest embedding lands in P5). d) conftest cleanup: dead pre-deploy-once fixtures deployed/deployed_app deleted (zero users), app_domain + _short + _wait_healthy dropped (only users were the deleted fixtures); deps_apps+deps_creds consolidated into ONE deps fixture (entries expose .domain etc. as attributes; dict access intact); the 6 lasuite test files renamed deps_creds->deps (fixture name only — assertions and flows byte-identical). requires_deps marker + F2-11 skip-report plumbing unchanged. Registry is now exactly the 14 final keys; docs §4 table regenerated. Stale setup_custom_tests/OIDC_AT_INSTALL prose in docstrings/comments/assert MESSAGES updated (no assert logic or expected value touched). Verified on cc-ci: cc-ci-run -m pytest tests/unit -q -> 175 passed; scripts/lint.sh -> PASS.
204 lines
7.9 KiB
Python
204 lines
7.9 KiB
Python
"""Unit tests for the single recipe-meta loader + key registry (rcust P1; spec §8 R1/R6).
|
|
|
|
Covers: every in-repo recipe_meta.py loads clean through the registry (THE typo gate), validation
|
|
hard-errors (unknown key, wrong type, callable on a data key), the zero-config baseline defaults
|
|
(spec §2), the underscore exemption for recipe-private constants, and the registry↔generated-doc
|
|
sync (P1.5; drift fails CI). Run: cc-ci-run -m pytest tests/unit/test_meta.py -q
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
|
from harness import meta as meta_mod # noqa: E402
|
|
from harness.meta import KEYS, MetaError, RecipeMeta # noqa: E402
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
def _recipes_with_meta() -> list[str]:
|
|
tests_dir = os.path.join(ROOT, "tests")
|
|
return sorted(
|
|
n
|
|
for n in os.listdir(tests_dir)
|
|
if os.path.isfile(os.path.join(tests_dir, n, "recipe_meta.py"))
|
|
)
|
|
|
|
|
|
# ---- the typo gate: every in-repo recipe meta must validate against the registry --------------
|
|
|
|
|
|
@pytest.mark.parametrize("recipe", _recipes_with_meta())
|
|
def test_every_recipe_meta_loads_clean(recipe):
|
|
"""All tests/*/recipe_meta.py in the repo load + validate through the registry. A typo'd or
|
|
unregistered ALL-CAPS key in any recipe meta fails HERE, at PR time — not silently at run
|
|
time (the R6 failure mode this restructure kills)."""
|
|
meta = meta_mod.load(recipe)
|
|
assert isinstance(meta, RecipeMeta)
|
|
# sanity: the 4 base keys always materialize with usable types
|
|
assert isinstance(meta.HEALTH_PATH, str)
|
|
assert isinstance(meta.HEALTH_OK, tuple) and meta.HEALTH_OK
|
|
assert isinstance(meta.DEPLOY_TIMEOUT, int) and isinstance(meta.HTTP_TIMEOUT, int)
|
|
|
|
|
|
# ---- zero-config baseline (spec §2) ------------------------------------------------------------
|
|
|
|
|
|
def test_missing_meta_yields_spec_baseline(tmp_path):
|
|
meta = meta_mod.load("no-such-recipe", tests_dir=str(tmp_path))
|
|
assert meta.HEALTH_PATH == "/"
|
|
assert meta.HEALTH_OK == (200, 301, 302)
|
|
assert meta.DEPLOY_TIMEOUT == 600
|
|
assert meta.HTTP_TIMEOUT == 300
|
|
assert meta.BACKUP_CAPABLE is None # None = auto-detect (tri-state, not False)
|
|
assert meta.EXPECTED_NA is None
|
|
assert meta.READY_PROBE is None
|
|
assert meta.UPGRADE_BASE_VERSION is None
|
|
assert meta.BACKUP_VERIFY is None
|
|
assert meta.UPGRADE_EXTRA_ENV is None
|
|
assert meta.EXTRA_ENV == {}
|
|
assert meta.DEPS == []
|
|
assert meta.WARM_CANONICAL is False
|
|
assert meta.SCREENSHOT is None
|
|
assert meta_mod.non_default(meta) == {}
|
|
|
|
|
|
def test_registry_field_set_matches_dataclass():
|
|
"""The RecipeMeta field set is generated from KEYS — no drift possible, pinned anyway."""
|
|
import dataclasses
|
|
|
|
assert [f.name for f in dataclasses.fields(RecipeMeta)] == [k.name for k in KEYS]
|
|
# the 14 final keys, no more (the 3 P2-deleted legacy keys are gone from the registry,
|
|
# so any recipe_meta still setting them hard-fails the typo gate)
|
|
assert len(KEYS) == 14
|
|
assert not [k for k in KEYS if k.deprecated]
|
|
for gone in ("CHAOS_BASE_DEPLOY", "OIDC_AT_INSTALL", "SKIP_GENERIC"):
|
|
assert gone not in {k.name for k in KEYS}
|
|
|
|
|
|
# ---- validation hard errors (locked decision: fail fast at load) -------------------------------
|
|
|
|
|
|
def _write_meta(tmp_path, body: str, recipe: str = "r") -> str:
|
|
d = tmp_path / recipe
|
|
d.mkdir(exist_ok=True)
|
|
(d / "recipe_meta.py").write_text(body)
|
|
return recipe
|
|
|
|
|
|
def test_unknown_key_raises_with_suggestion(tmp_path):
|
|
r = _write_meta(tmp_path, "READINESS_PROBE = None\n") # the R6 typo example
|
|
with pytest.raises(MetaError) as ei:
|
|
meta_mod.load(r, tests_dir=str(tmp_path))
|
|
msg = str(ei.value)
|
|
assert "READINESS_PROBE" in msg and "READY_PROBE" in msg # names the typo + nearest key
|
|
|
|
|
|
def test_unknown_key_without_near_match_lists_registry(tmp_path):
|
|
r = _write_meta(tmp_path, "TOTALLY_BOGUS_KNOB = 1\n")
|
|
with pytest.raises(MetaError) as ei:
|
|
meta_mod.load(r, tests_dir=str(tmp_path))
|
|
assert "HEALTH_PATH" in str(ei.value) # registered keys listed for the reader
|
|
|
|
|
|
def test_wrong_type_raises(tmp_path):
|
|
r = _write_meta(tmp_path, 'DEPLOY_TIMEOUT = "900"\n')
|
|
with pytest.raises(MetaError, match="DEPLOY_TIMEOUT"):
|
|
meta_mod.load(r, tests_dir=str(tmp_path))
|
|
|
|
|
|
def test_bool_not_accepted_as_int(tmp_path):
|
|
r = _write_meta(tmp_path, "DEPLOY_TIMEOUT = True\n")
|
|
with pytest.raises(MetaError, match="DEPLOY_TIMEOUT"):
|
|
meta_mod.load(r, tests_dir=str(tmp_path))
|
|
|
|
|
|
def test_callable_on_data_key_rejected(tmp_path):
|
|
r = _write_meta(tmp_path, "def HEALTH_PATH():\n return '/'\n")
|
|
with pytest.raises(MetaError, match="hook-typed"):
|
|
meta_mod.load(r, tests_dir=str(tmp_path))
|
|
|
|
|
|
def test_non_callable_on_hook_key_rejected(tmp_path):
|
|
r = _write_meta(tmp_path, "READY_PROBE = ['not', 'a', 'callable']\n")
|
|
with pytest.raises(MetaError, match="READY_PROBE"):
|
|
meta_mod.load(r, tests_dir=str(tmp_path))
|
|
|
|
|
|
def test_underscore_names_are_private_and_exempt(tmp_path):
|
|
r = _write_meta(
|
|
tmp_path,
|
|
"_WELCOME_TEXT_MARKER = 'marker-xyz'\n_MAX_USERS = 42\n"
|
|
"EXTRA_ENV = {'WELCOME_TEXT': _WELCOME_TEXT_MARKER, 'USERS': str(_MAX_USERS)}\n",
|
|
)
|
|
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
|
assert meta.EXTRA_ENV == {"WELCOME_TEXT": "marker-xyz", "USERS": "42"}
|
|
|
|
|
|
def test_lowercase_helpers_ignored(tmp_path):
|
|
r = _write_meta(
|
|
tmp_path,
|
|
"def _helper(d):\n return {'K': d}\n\ndef EXTRA_ENV(domain):\n return _helper(domain)\n",
|
|
)
|
|
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
|
assert meta_mod.extra_env(meta, "x.example") == {"K": "x.example"}
|
|
|
|
|
|
# ---- normalization + helpers --------------------------------------------------------------------
|
|
|
|
|
|
def test_health_ok_list_normalized_to_tuple(tmp_path):
|
|
r = _write_meta(tmp_path, "HEALTH_OK = [200, 302]\n")
|
|
assert meta_mod.load(r, tests_dir=str(tmp_path)).HEALTH_OK == (200, 302)
|
|
|
|
|
|
def test_extra_env_dict_and_callable_forms(tmp_path):
|
|
r = _write_meta(tmp_path, "EXTRA_ENV = {'A': 1}\n")
|
|
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
|
assert meta_mod.extra_env(meta, "d") == {"A": "1"} # values stringified
|
|
r2 = _write_meta(
|
|
tmp_path, "UPGRADE_EXTRA_ENV = lambda domain: {'COMPOSE_FILE': domain}\n", recipe="r2"
|
|
)
|
|
meta2 = meta_mod.load(r2, tests_dir=str(tmp_path))
|
|
assert meta_mod.upgrade_extra_env(meta2, "dom.x") == {"COMPOSE_FILE": "dom.x"}
|
|
assert meta_mod.extra_env(meta2, "dom.x") == {} # unset EXTRA_ENV resolves to {}
|
|
|
|
|
|
def test_non_default_reports_only_customized_keys(tmp_path):
|
|
r = _write_meta(tmp_path, "DEPLOY_TIMEOUT = 1500\nDEPS = ['keycloak']\n")
|
|
nd = meta_mod.non_default(meta_mod.load(r, tests_dir=str(tmp_path)))
|
|
assert nd == {"DEPLOY_TIMEOUT": 1500, "DEPS": ["keycloak"]}
|
|
|
|
|
|
def test_meta_is_frozen():
|
|
import dataclasses
|
|
|
|
meta = meta_mod.load("custom-html")
|
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
meta.DEPLOY_TIMEOUT = 1
|
|
|
|
|
|
# ---- doc generation sync (P1.5: the committed §4 table == the registry rendering) ---------------
|
|
|
|
|
|
def test_generated_doc_table_in_sync():
|
|
"""docs/recipe-customization.md's key reference table is GENERATED from the registry
|
|
(scripts/gen-meta-docs.py). If this fails: re-run `python3 scripts/gen-meta-docs.py` and
|
|
commit the result — the table must never drift from the registry (R5)."""
|
|
gen = os.path.join(ROOT, "scripts", "gen-meta-docs.py")
|
|
doc = os.path.join(ROOT, "docs", "recipe-customization.md")
|
|
rendered = subprocess.run(
|
|
[sys.executable, gen, "--print"], capture_output=True, text=True, check=True
|
|
).stdout
|
|
with open(doc) as f:
|
|
committed = f.read()
|
|
assert rendered.strip() in committed, (
|
|
"docs/recipe-customization.md key table is out of sync with the harness.meta registry — "
|
|
"run `python3 scripts/gen-meta-docs.py` and commit"
|
|
)
|