All checks were successful
continuous-integration/drone/push Build is passing
One loader: runner/harness/meta.py::load(recipe) -> RecipeMeta (frozen dataclass, attribute access), backed by the declarative KEYS registry (14 final keys + 3 P2-deprecated). The ONLY exec() of tests/<recipe>/recipe_meta.py. Validation per the locked decision: unknown ALL-CAPS top-level name or type mismatch = MetaError (hard error at load); underscore-prefixed names recipe-private; callables only on hook-typed keys. Migrated all six legacy loaders (spec §4 L1–L6): - run_recipe_ci.py::_load_meta deleted; orchestrator loads once, passes meta down - tests/conftest.py::_recipe_meta deleted; meta fixture returns full RecipeMeta (R3) - lifecycle.py::_recipe_extra_env/_recipe_meta_flag deleted; deploy_app takes meta - deps.py::declared_deps deleted; callers read meta.DEPS - canonical.py::is_enrolled reads through meta.load() - screenshot.py now actually receives SCREENSHOT through the orchestrator path (R2 fix; proven by unit test through the real load path) Mumble private constants underscore-prefixed (_WELCOME_TEXT_MARKER/_MAX_USERS) + importers fixed. New tests/unit/test_meta.py (all-recipes-load-clean typo gate, MetaError cases, spec §2 baseline defaults, underscore exemption, doc sync). Docs §4 key table now GENERATED from the registry (scripts/gen-meta-docs.py); drift fails CI. Verified on cc-ci: cc-ci-run -m pytest tests/unit -q -> 175 passed; scripts/lint.sh -> PASS.
82 lines
3.5 KiB
Python
82 lines
3.5 KiB
Python
"""Unit tests for runner/harness/deps.py (Phase 2 §4.2 / Q2.3).
|
|
|
|
Pure-Python: no real deploys. Tests the declarative parts of the dep resolver — DEPS declaration
|
|
(read through the single meta loader since rcust P1), the per-dep domain derivation, and write/load
|
|
of the run state file. The deploy_deps + teardown_deps integration is exercised by real e2e against
|
|
cc-ci (Q2.4 acceptance).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
|
from harness import deps # noqa: E402
|
|
from harness import meta as meta_mod # noqa: E402
|
|
|
|
|
|
def test_declared_deps_empty_for_no_meta(monkeypatch, tmp_path):
|
|
"""A recipe with no recipe_meta.py declares no deps (rcust P1: DEPS via meta.load)."""
|
|
monkeypatch.setattr(meta_mod, "TESTS_DIR", str(tmp_path / "tests"))
|
|
assert meta_mod.load("ccci-no-meta").DEPS == []
|
|
|
|
|
|
def test_declared_deps_reads_DEPS_list(tmp_path, monkeypatch):
|
|
"""A recipe_meta.py with `DEPS = [...]` surfaces the list on the loaded meta (the orchestrator
|
|
reads meta.DEPS — the successor of the deleted deps.declared_deps loader)."""
|
|
recipe_dir = tmp_path / "tests" / "ccci-with-deps"
|
|
recipe_dir.mkdir(parents=True)
|
|
(recipe_dir / "recipe_meta.py").write_text('HEALTH_PATH = "/"\nDEPS = ["keycloak", "redis"]\n')
|
|
monkeypatch.setattr(meta_mod, "TESTS_DIR", str(tmp_path / "tests"))
|
|
assert meta_mod.load("ccci-with-deps").DEPS == ["keycloak", "redis"]
|
|
|
|
|
|
def test_dep_domain_distinct_per_dep():
|
|
"""Two deps of different kinds (same parent/pr/ref) get distinct per-run domains."""
|
|
parent = "lasuite-docs"
|
|
pr = "42"
|
|
ref = "abc123"
|
|
d1 = deps.dep_domain(parent, pr, ref, "keycloak")
|
|
d2 = deps.dep_domain(parent, pr, ref, "redis")
|
|
assert d1 != d2
|
|
# Both must look like the standard run-app pattern: <recipe[:4]>-<6hex>.ci.commoninternet.net
|
|
assert d1.endswith(".ci.commoninternet.net")
|
|
assert d2.endswith(".ci.commoninternet.net")
|
|
# The hash is determined by (parent, pr, ref, dep) — same inputs = same domain (idempotent)
|
|
assert deps.dep_domain(parent, pr, ref, "keycloak") == d1
|
|
|
|
|
|
def test_dep_domain_distinct_per_parent():
|
|
"""The same dep deployed by two different parent recipes (same dep, pr, ref) gets distinct
|
|
domains — proves the dep is parent-scoped not just dep-name-scoped."""
|
|
d1 = deps.dep_domain("lasuite-docs", "42", "abc", "keycloak")
|
|
d2 = deps.dep_domain("cryptpad", "42", "abc", "keycloak")
|
|
assert d1 != d2
|
|
|
|
|
|
def test_write_and_load_run_state(tmp_path, monkeypatch):
|
|
"""write_run_state writes JSON to $CCCI_DEPS_FILE; load_run_state reads it back."""
|
|
state_path = tmp_path / "deps.json"
|
|
monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path))
|
|
state = [
|
|
{"recipe": "keycloak", "domain": "kc-deadbe.ci.commoninternet.net"},
|
|
{"recipe": "redis", "domain": "redi-abc123.ci.commoninternet.net"},
|
|
]
|
|
deps.write_run_state(state)
|
|
loaded = deps.load_run_state()
|
|
assert loaded == state
|
|
|
|
|
|
def test_load_run_state_missing_env_returns_empty(monkeypatch):
|
|
"""No $CCCI_DEPS_FILE -> empty list."""
|
|
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
|
|
assert deps.load_run_state() == []
|
|
|
|
|
|
def test_write_run_state_no_env_is_noop(monkeypatch):
|
|
"""write_run_state silently no-ops without $CCCI_DEPS_FILE (so standalone helper use doesn't
|
|
require setting up the env)."""
|
|
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
|
|
deps.write_run_state([{"recipe": "x", "domain": "y"}]) # must not raise
|