import os from dataclasses import dataclass from pathlib import Path from categorizer import CategoryRecord, FixtureClass, Speed @dataclass(frozen=True) class Batch: tier: str label: str files: list[Path] pytest_args: list[str] estimated_seconds: float skip_reason: str | None = None _TIER_ORDER: tuple[str, ...] = ("0", "1", "2", "3", "H", "P") _SPEED_SECONDS: dict[str, float] = { "fast": 0.5, "medium": 3.0, "slow": 15.0, "very_slow": 60.0, } def _est(r: CategoryRecord) -> float: return _SPEED_SECONDS.get(r.speed.value, 3.0) def _env_set(name: str) -> bool: return bool(os.environ.get(name)) def _batches_for_unit(records: list[CategoryRecord], xdist: bool) -> list[Batch]: by_group: dict[str, list[CategoryRecord]] = {} for r in records: by_group.setdefault(r.batch_group or "core", []).append(r) batches: list[Batch] = [] for group in sorted(by_group): files = [Path("tests") / r.filename for r in by_group[group]] args: list[str] = ["--maxfail=10"] if xdist: args = ["-n", "auto"] + args batches.append(Batch( tier="1", label=f"tier-1-unit-{group}", files=files, pytest_args=args, estimated_seconds=sum(_est(r) for r in by_group[group]), )) return batches def _batches_for_mock_app(records: list[CategoryRecord]) -> list[Batch]: by_group: dict[str, list[CategoryRecord]] = {} for r in records: by_group.setdefault(r.batch_group or "core", []).append(r) batches: list[Batch] = [] for group in sorted(by_group): files = [Path("tests") / r.filename for r in by_group[group]] batches.append(Batch( tier="2", label=f"tier-2-mock_app-{group}", files=files, pytest_args=["--maxfail=5"], estimated_seconds=sum(_est(r) for r in by_group[group]), )) return batches def _batches_for_live_gui(records: list[CategoryRecord]) -> list[Batch]: if not records: return [] files = [Path("tests") / r.filename for r in records] return [Batch( tier="3", label="tier-3-live_gui", files=files, pytest_args=["--maxfail=1"], estimated_seconds=sum(_est(r) for r in records), )] def _batches_for_headless(records: list[CategoryRecord]) -> list[Batch]: if not records: return [] files = [Path("tests") / r.filename for r in records] return [Batch( tier="H", label="tier-H-headless", files=files, pytest_args=["--maxfail=5"], estimated_seconds=sum(_est(r) for r in records), )] def _batches_for_performance(records: list[CategoryRecord]) -> list[Batch]: if not records: return [] files = [Path("tests") / r.filename for r in records] return [Batch( tier="P", label="tier-P-performance", files=files, pytest_args=["--maxfail=1"], estimated_seconds=sum(_est(r) for r in records), )] def _batches_for_opt_in(records: list[CategoryRecord], include_opt_in: bool) -> list[Batch]: batches: list[Batch] = [] for r in records: files = [Path("tests") / r.filename] skip_reason: str | None = None if not include_opt_in: skip_reason = "--include-opt-in not set" elif r.filename.startswith("test_clean_install") and not _env_set("RUN_CLEAN_INSTALL_TEST"): skip_reason = "RUN_CLEAN_INSTALL_TEST not set" elif r.filename.startswith("test_docker_build") and not _env_set("RUN_DOCKER_TEST"): skip_reason = "RUN_DOCKER_TEST not set" batches.append(Batch( tier="0", label=f"tier-0-opt_in-{r.filename.removeprefix('test_').removesuffix('.py')}", files=files, pytest_args=["--maxfail=1"], estimated_seconds=_est(r), skip_reason=skip_reason, )) return batches def plan( records: list[CategoryRecord], *, tiers: set[str] = set(_TIER_ORDER), include_opt_in: bool = False, xdist: bool = True, ) -> list[Batch]: by_fc: dict[FixtureClass, list[CategoryRecord]] = {fc: [] for fc in FixtureClass} for r in records: by_fc[r.fixture_class].append(r) out: list[Batch] = [] if "0" in tiers: out.extend(_batches_for_opt_in(by_fc[FixtureClass.OPT_IN], include_opt_in)) if "1" in tiers: out.extend(_batches_for_unit(by_fc[FixtureClass.UNIT], xdist)) if "2" in tiers: out.extend(_batches_for_mock_app(by_fc[FixtureClass.MOCK_APP])) if "3" in tiers: out.extend(_batches_for_live_gui(by_fc[FixtureClass.LIVE_GUI])) if "H" in tiers: out.extend(_batches_for_headless(by_fc[FixtureClass.HEADLESS])) if "P" in tiers: out.extend(_batches_for_performance(by_fc[FixtureClass.PERFORMANCE])) out.sort(key=lambda b: (_TIER_ORDER.index(b.tier), b.label)) return out