"""Evidence bundler pytest plugin. For each test, collects supporting artifacts (`.tlog`, FDR archive snapshots, screenshots, profiler traces, tegrastats / jtop CSVs) into a per-run bundle at ``--evidence-out`` (default ``/e2e-results//evidence/``) and records the resulting paths in the CSV reporter's ``evidence_paths`` column. The bundler is INERT by default: tests opt in by calling the ``attach_evidence`` fixture with a file path. The runner conftest registers this plugin via `pytest_plugins`. """ from __future__ import annotations import shutil from collections.abc import Callable from pathlib import Path import pytest from .csv_reporter import reporter_for def _safe_relpath(target: Path, base: Path) -> str: try: return str(target.relative_to(base)) except ValueError: # If the target isn't under base, we still record its absolute path # — the bundle copy below makes the absolute fallback robust to # arbitrary source locations (e.g. /tlogs/.tlog). return str(target) @pytest.fixture def attach_evidence( request: pytest.FixtureRequest, evidence_dir: Path, ) -> Callable[[str | Path], str]: """Copy a file into the run evidence bundle and record its CSV path. Returns a callable ``attach(path) -> str`` — the test invokes it after capturing an artifact (e.g., the .tlog file or an FDR snapshot). The returned string is the path that will appear in the CSV ``evidence_paths`` column. The implementation copies the file (rather than moving it) so the same artifact can be referenced by multiple tests if needed. """ nodeid = request.node.nodeid config = request.config reporter = reporter_for(config) bundle_root = evidence_dir / _slug(nodeid) bundle_root.mkdir(parents=True, exist_ok=True) def _attach(path: str | Path) -> str: src = Path(path) if not src.exists(): raise FileNotFoundError(f"attach_evidence: {src} not found") dst = bundle_root / src.name # If a test attaches the same name twice in one run, disambiguate. if dst.exists(): stem, suffix = src.stem, src.suffix counter = 1 while dst.exists(): dst = bundle_root / f"{stem}__{counter}{suffix}" counter += 1 shutil.copy2(src, dst) rel = _safe_relpath(dst, evidence_dir.parent) if reporter is not None: reporter.attach_evidence(nodeid, rel) return rel return _attach def _slug(nodeid: str) -> str: """Filesystem-safe slug for the nodeid (preserves uniqueness, no path chars).""" return ( nodeid.replace("/", "_") .replace("::", "__") .replace("[", "_") .replace("]", "") .replace(" ", "") )