"""Shared fixtures and helpers for E2E canary regression tests. The regression tests call the real cc-ci harness (run_recipe_ci.py) as a subprocess and assert on its outputs (exit code, results.json). They run ON the cc-ci server, not the orchestrator — abra, Docker, and Swarm must be present. Invoke: cc-ci-run python -m pytest tests/regression/ -m canary -v """ from __future__ import annotations import json import os import subprocess import sys import time ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) def pytest_configure(config): config.addinivalue_line( "markers", "canary: slow E2E canary test — drives the full cold CI lifecycle; run on-demand only.", ) config.addinivalue_line( "markers", "canary_fast: fast per-tier RED canary (still tagged canary); subset for quick pre-merge checks.", ) def run_recipe_ci( recipe: str, src: str, ref: str, pr: str = "0", stages: str = "install,upgrade,backup,restore,custom", runs_dir: str | None = None, run_id_prefix: str = "regression", timeout: int = 3600, ) -> tuple[int, dict | None, str]: """Invoke run_recipe_ci.py with the given canary params. Returns (rc, results_dict_or_None, run_artifact_dir). Stdout/stderr stream live so a human can follow progress. """ ts = int(time.time()) run_id = f"{run_id_prefix}-{recipe}-{ref[:12]}-{ts}" if runs_dir is None: runs_dir = "/var/lib/cc-ci-runs" env = dict(os.environ) env.update( { "RECIPE": recipe, "REF": ref, "SRC": src, "PR": pr, "STAGES": stages, "CCCI_RUN_ID": run_id, "CCCI_RUNS_DIR": runs_dir, "HOME": "/root", } ) # Keep PLAYWRIGHT env from the outer cc-ci-run wrapper (already in os.environ if running under it) script = os.path.join(ROOT, "runner", "run_recipe_ci.py") result = subprocess.run( [sys.executable, script], env=env, timeout=timeout, ) rc = result.returncode artifact_dir = os.path.join(runs_dir, run_id) results_path = os.path.join(artifact_dir, "results.json") results_data: dict | None = None if os.path.exists(results_path): with open(results_path) as f: results_data = json.load(f) return rc, results_data, artifact_dir def find_stage_tests(results: dict, stage_name: str) -> list[dict]: """Return the per-test list for a named stage from results.json, or [].""" for stage in results.get("stages", []): if stage.get("name") == stage_name: return stage.get("tests", []) return [] def stage_has_passing_test(results: dict, stage_name: str, test_name_substr: str) -> bool: """True if the named stage contains a passing test whose name includes test_name_substr.""" for t in find_stage_tests(results, stage_name): if test_name_substr in t.get("name", "") and t.get("status") == "pass": return True return False def stage_has_failing_test(results: dict, stage_name: str, test_name_substr: str) -> bool: """True if the named stage contains a failing test whose name includes test_name_substr.""" for t in find_stage_tests(results, stage_name): if test_name_substr in t.get("name", "") and t.get("status") in ("fail", "error"): return True return False