Files
tegwick d061c777d1 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-03-27:
  - update .custodian-brief.md for the-custodian
2026-03-27 00:52:18 +01:00

132 lines
3.6 KiB
Python

"""
Full e2e lifecycle: up → health-wait → test → down → result.
"""
from __future__ import annotations
import time
from dataclasses import dataclass
from pathlib import Path
from .sandbox import Sandbox
from .schema import E2EConfig
@dataclass
class RunResult:
sandbox_id: str
repo: str
passed: bool
exit_code: int
duration_s: float
output: str
error: str = ""
def run_e2e(
repo_path: Path,
host: str,
ssh_user: str = "root",
ssh_key: str | None = None,
keep: bool = False,
) -> RunResult:
config = E2EConfig.load(repo_path)
sandbox = Sandbox(
host=host,
repo_path=repo_path,
ssh_user=ssh_user,
ssh_key=ssh_key,
)
project_name = f"e2e-{config.name}-{sandbox.sandbox_id}"
compose_path = f"{sandbox.remote_dir}/{config.compose_file}"
started = time.time()
output_lines: list[str] = []
def log(msg: str) -> None:
print(msg)
output_lines.append(msg)
log(f"\n{'='*60}")
log(f"E2E run: {config.name} sandbox={sandbox.sandbox_id}")
log(f"Host: {host} project: {project_name}")
log(f"{'='*60}\n")
try:
# 1. Provision
sandbox.provision()
# 2. docker compose up
env_flags = " ".join(f"-e {k}={v}" for k, v in config.env.items())
up_cmd = (
f"cd {sandbox.remote_dir} && "
f"docker compose -p {project_name} -f {compose_path} up -d"
)
log(f"[runner] docker compose up ({project_name})")
rc, out = sandbox.run(up_cmd, timeout=180, stream=False)
output_lines.append(out)
if rc != 0:
raise RuntimeError(f"docker compose up failed (exit {rc}):\n{out}")
# 3. Health checks
for hc in config.health_checks:
ok = sandbox.wait_for_url(hc.url, timeout=hc.timeout)
if not ok:
raise RuntimeError(f"Health check failed: {hc.name} ({hc.url})")
# 4. Run tests
log(f"\n[runner] running: {config.test_command}")
test_cmd = f"cd {sandbox.remote_dir} && {config.test_command}"
rc, test_out = sandbox.run(test_cmd, timeout=config.timeout, stream=False)
output_lines.append(test_out)
print(test_out)
passed = rc == 0
duration = time.time() - started
log(f"\n[runner] {'PASSED' if passed else 'FAILED'} (exit={rc}, {duration:.1f}s)")
return RunResult(
sandbox_id=sandbox.sandbox_id,
repo=config.name,
passed=passed,
exit_code=rc,
duration_s=duration,
output="\n".join(output_lines),
)
except Exception as exc:
duration = time.time() - started
log(f"\n[runner] ERROR: {exc}")
return RunResult(
sandbox_id=sandbox.sandbox_id,
repo=config.name,
passed=False,
exit_code=-1,
duration_s=duration,
output="\n".join(output_lines),
error=str(exc),
)
finally:
_compose_down(sandbox, project_name, compose_path, config, keep)
def _compose_down(
sandbox: Sandbox,
project_name: str,
compose_path: str,
config: E2EConfig,
keep: bool,
) -> None:
if keep or config.cleanup == "never":
print(f"[runner] skipping cleanup (keep={keep}, cleanup={config.cleanup})")
return
print(f"[runner] docker compose down ({project_name})")
down_cmd = (
f"cd {sandbox.remote_dir} && "
f"docker compose -p {project_name} -f {compose_path} down -v --remove-orphans 2>&1 || true"
)
sandbox.run(down_cmd, timeout=60)
if not keep:
sandbox.teardown()