"""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 — declared_deps reading from `tests//recipe_meta.py`, 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 def test_declared_deps_returns_empty_for_no_meta(monkeypatch, tmp_path): """A recipe with no recipe_meta.py returns [].""" fake_recipe = "ccci-no-meta" # No file at tests//recipe_meta.py -> declared_deps reads nothing -> [] monkeypatch.chdir(tmp_path) assert deps.declared_deps(fake_recipe) == [] def test_declared_deps_reads_DEPS_list(tmp_path, monkeypatch): """A recipe_meta.py with `DEPS = [...]` returns the list.""" fake_recipe = "ccci-with-deps" # Build a fake repo layout under tmp_path recipe_dir = tmp_path / "tests" / fake_recipe recipe_dir.mkdir(parents=True) (recipe_dir / "recipe_meta.py").write_text('HEALTH_PATH = "/"\nDEPS = ["keycloak", "redis"]\n') # Patch the deps module's idea of "where the repo is" by monkey-patching __file__ for the # function indirectly: declared_deps uses `os.path.dirname(__file__), "..", "..", "tests"` — # which resolves to the real repo's `tests/`. So instead, override that with a symlink/dir # under tmp_path: deps.__file__ points at the runner module. We can't easily relocate that. # Instead, mock the path by writing the fake recipe under the REAL tests/ dir. real_tests = os.path.join(os.path.dirname(deps.__file__), "..", "..", "tests") target_dir = os.path.join(real_tests, fake_recipe) os.makedirs(target_dir, exist_ok=True) target_meta = os.path.join(target_dir, "recipe_meta.py") try: with open(target_meta, "w") as f: f.write('DEPS = ["keycloak", "redis"]\n') result = deps.declared_deps(fake_recipe) assert result == ["keycloak", "redis"] finally: if os.path.exists(target_meta): os.remove(target_meta) if os.path.isdir(target_dir): os.rmdir(target_dir) 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: -<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