import typing as typ
import pytest
from coverage_lanes import CoverageJob, coverage_jobs_in, load_workflow_documents
from nextest_budgets import (
bounds_a_single_test,
global_timeout,
largest_test_allowance,
termination_allowance,
)
from timeout_budgets import (
CEILING_MARGIN_SECONDS,
COLD_BUILD_ALLOWANCE_SECONDS,
COVERAGE_ACTION,
DECLARED_WATCHDOG_SECONDS,
NEXTEST_CONFIG,
OUTSIDE_WATCHDOG_ALLOWANCE_SECONDS,
REQUIRED_GLOBAL_TIMEOUT_SECONDS,
REQUIRED_JOB_CEILING_SECONDS,
WATCHDOG_VARIABLE,
required_ceiling,
)
REQUIRED_CONDITIONS: typ.Final[dict[tuple[str, str], tuple[object, object]]] = {
("ci.yml", "build-test"): (
"${{ github.event_name == 'pull_request' && matrix.privilege == 'unprivileged' }}",
None,
),
("coverage-main.yml", "coverage-upload"): (None, None),
}
@pytest.fixture(scope="module")
def nextest_config() -> str:
return NEXTEST_CONFIG.read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def coverage_jobs() -> tuple[CoverageJob, ...]:
return coverage_jobs_in(load_workflow_documents())
def test_the_coverage_action_is_invoked_somewhere(
coverage_jobs: tuple[CoverageJob, ...],
) -> None:
assert coverage_jobs, (
f"no workflow job uses {COVERAGE_ACTION}; either coverage moved or "
f"this contract stopped recognizing it"
)
def test_every_coverage_step_runs_under_an_explicit_watchdog(
coverage_jobs: tuple[CoverageJob, ...],
) -> None:
missing = [
f"{job}: step {index + 1} of {job.steps}"
for job in coverage_jobs
for index, watchdog in enumerate(job.watchdogs)
if watchdog is None
]
assert not missing, (
f"these coverage steps do not set {WATCHDOG_VARIABLE} and so inherit "
f"the shared action's undocumented default: {missing}"
)
def test_every_coverage_step_declares_the_pinned_watchdog(
coverage_jobs: tuple[CoverageJob, ...],
) -> None:
wrong = [
f"{job}: step {index + 1} of {job.steps} declares {watchdog}s"
for job in coverage_jobs
for index, watchdog in enumerate(job.watchdogs)
if watchdog != DECLARED_WATCHDOG_SECONDS
]
assert not wrong, (
f"every coverage step must declare {WATCHDOG_VARIABLE} = "
f"{DECLARED_WATCHDOG_SECONDS:.0f}s; these do not: {wrong}"
)
assert coverage_jobs, "no coverage step was read, so nothing was pinned"
def test_the_job_ceiling_contains_every_watchdog_and_the_work_around_them(
coverage_jobs: tuple[CoverageJob, ...],
) -> None:
assert CEILING_MARGIN_SECONDS > 0, (
"the ceiling requirement must carry a stated margin as a term; "
"strictness alone would accept a ceiling one second above the "
"watchdogs and the work around them"
)
for job in coverage_jobs:
budgets = [watchdog for watchdog in job.watchdogs if watchdog is not None]
assert len(budgets) == job.steps, str(job)
allowance = OUTSIDE_WATCHDOG_ALLOWANCE_SECONDS
required = required_ceiling(budgets, allowance)
assert job.job_timeout is not None, (
f"{job} runs {job.steps} watchdog-bounded cargo invocation(s) in a "
f"job with no timeout-minutes; the outermost tier is missing and "
f"GitHub's six-hour default applies"
)
assert job.job_timeout == REQUIRED_JOB_CEILING_SECONDS, (
f"{job} has a ceiling of {job.job_timeout:.0f}s, not the "
f"{REQUIRED_JOB_CEILING_SECONDS:.0f}s the developers' guide "
f"states; the derivation below accepts a range, so only this "
f"pin keeps the guide and the workflows one statement"
)
assert job.job_timeout > required, (
f"{job} has a ceiling of {job.job_timeout:.0f}s, which does not "
f"clear the {required:.0f}s needed to contain {job.steps} watchdog(s) "
f"totalling {sum(budgets):.0f}s, {allowance:.0f}s of measured "
f"work outside them, and a {CEILING_MARGIN_SECONDS:.0f}s margin "
f"above that sum; an overrun would be cancelled rather than "
f"reported"
)
def test_the_whole_run_budget_is_the_one_the_guide_states(
nextest_config: str,
) -> None:
whole_run = global_timeout(nextest_config)
assert whole_run is not None, (
"`.config/nextest.toml` must set a global-timeout; without it "
"nothing bounds the whole run and the watchdog reports the cargo "
"invocation rather than the suite"
)
assert whole_run == REQUIRED_GLOBAL_TIMEOUT_SECONDS, (
f"the global-timeout is {whole_run:.0f}s, not the "
f"{REQUIRED_GLOBAL_TIMEOUT_SECONDS:.0f}s the developers' guide "
f"states; change the guide with it or change it back"
)
def test_the_whole_run_budget_sits_inside_each_watchdog(
coverage_jobs: tuple[CoverageJob, ...], nextest_config: str
) -> None:
whole_run = global_timeout(nextest_config)
assert whole_run is not None, "no global-timeout is set"
largest = largest_test_allowance(nextest_config)
assert whole_run > largest, (
f"the {whole_run:.0f}s global-timeout is not above the {largest:.0f}s "
f"largest per-test allowance; the run would end before that test "
f"could use its budget"
)
required = (
whole_run + termination_allowance(nextest_config) + COLD_BUILD_ALLOWANCE_SECONDS
)
for job in coverage_jobs:
for index, watchdog in enumerate(job.watchdogs):
assert watchdog is not None, str(job)
assert watchdog >= required, (
f"{job} step {index + 1} sets a {watchdog:.0f}s watchdog, "
f"below the {required:.0f}s needed to cover the "
f"{whole_run:.0f}s whole-run budget, nextest's termination "
f"procedure, and a cold build"
)
def test_each_coverage_lane_carries_the_condition_it_is_meant_to(
coverage_jobs: tuple[CoverageJob, ...],
) -> None:
found = {(job.workflow, job.job): job.conditions for job in coverage_jobs}
assert set(found) == set(REQUIRED_CONDITIONS), (
f"the coverage lanes are not the ones this contract pins: "
f"unlisted {sorted(set(found) - set(REQUIRED_CONDITIONS))}, missing "
f"{sorted(set(REQUIRED_CONDITIONS) - set(found))}; a lane with no "
f"entry here is a lane whose condition nobody has judged"
)
wrong = {
coordinate: (expected, found[coordinate])
for coordinate, expected in REQUIRED_CONDITIONS.items()
if set(found[coordinate]) != {expected}
}
assert not wrong, (
f"these coverage lanes do not carry the conditions the developers' "
f"guide records, as expected versus found: {wrong}; a lane that is "
f"skipped runs no cargo, so its watchdog never arms"
)
def test_the_default_profile_bounds_a_test_no_override_matches(
nextest_config: str,
) -> None:
assert bounds_a_single_test(nextest_config), (
"[profile.default] itself must set slow-timeout with terminate-after; "
"an override satisfies the file as a whole while leaving every test it "
"does not match unbounded"
)