- test_scm_configured.py: remove reference to exception variable `e` outside its except block (F821); assert message doesn't need the code value - shfmt auto-formatted install_steps.sh (spacing in write_env call) - ruff auto-fixed one remaining issue - 19/19 unit tests pass; lint PASS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
5.7 KiB
Python
132 lines
5.7 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ADV-drone-02 fallback: load_run_state provides teardown data when deps_state={}
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_load_run_state_provides_fallback_for_enrichment_failure(tmp_path, monkeypatch):
|
|
"""ADV-drone-02: deploy_deps writes legacy-list to $CCCI_DEPS_FILE; if _enrich_deps_with_sso
|
|
raises, deps_state = {} in main(). The fallback in the finally block reads load_run_state()
|
|
to recover the deployed-but-unenriched entries for teardown.
|
|
|
|
This test verifies that load_run_state() returns the entries written by deploy_deps (legacy
|
|
list shape) so the fallback teardown receives the correct cold entries."""
|
|
state_path = tmp_path / "deps.json"
|
|
monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path))
|
|
# deploy_deps writes the legacy list shape (before enrichment converts to dict)
|
|
deployed_by_deploy_deps = [
|
|
{"recipe": "gitea", "domain": "gite-aabbcc.ci.commoninternet.net"},
|
|
]
|
|
deps.write_run_state(deployed_by_deploy_deps)
|
|
|
|
raw = deps.load_run_state()
|
|
assert isinstance(raw, list), "load_run_state must return the legacy list shape"
|
|
assert len(raw) == 1
|
|
cold_raw = [e for e in raw if isinstance(e, dict) and not e.get("warm")]
|
|
assert len(cold_raw) == 1
|
|
assert cold_raw[0]["recipe"] == "gitea"
|
|
assert cold_raw[0]["domain"] == "gite-aabbcc.ci.commoninternet.net"
|
|
|
|
|
|
def test_fallback_skips_warm_entries(tmp_path, monkeypatch):
|
|
"""ADV-drone-02 fallback filtering: warm entries (keycloak live-warm provider) must NOT be
|
|
undeployed — only cold entries should be in the teardown list."""
|
|
state_path = tmp_path / "deps.json"
|
|
monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path))
|
|
mixed = [
|
|
{"recipe": "keycloak", "domain": "keyc-live.ci.commoninternet.net", "warm": True},
|
|
{"recipe": "gitea", "domain": "gite-aabbcc.ci.commoninternet.net"},
|
|
]
|
|
deps.write_run_state(mixed)
|
|
|
|
raw = deps.load_run_state()
|
|
cold_raw = [
|
|
e
|
|
for e in (raw if isinstance(raw, list) else list(raw.values()))
|
|
if isinstance(e, dict) and not e.get("warm")
|
|
]
|
|
assert len(cold_raw) == 1
|
|
assert cold_raw[0]["recipe"] == "gitea"
|