ouroboros/tests/test_run_tests_script.py
Anton Razzhigaev 915f93604d Add scripts/run_tests.py, the one fast local test battery
The documented local command (`make test`, CONTRIBUTING §4, README) was
`pytest tests/ -q --tb=short`: the whole default suite (23k tests) in a
single process, behind a stale "smoke tests" comment. An Ouroboros task
followed it, hit its 1200 s timeout and withheld its PR. The correct
recipe — a parallel `not serial` xdist pass, then the serial pass — lived
only inside the hermetic commit gate.

scripts/run_tests.py is the single entry point for every actor. A bare
call always runs the FULL battery: the node lane (a missing or too-old
node is a red NOT_RUN, never a silent skip), then every default-lane
test in ONE xdist run under `--dist loadgroup --serial-shards=N`.
tests/conftest.py gains that option: each serial FILE is pinned to one of
N file-sharded groups (one per four workers, at most four; four cores
keep a single group), so serial tests overlap the parallel ones while a
serial file still never splits across workers or runs beside another
file of its shard; other tests keep the scope loadscope uses. The option
defaults to 0 and is inert for CI and the commit gate, which keep the
two-pass split and stay the authority. `--sequential` keeps the gate's
marker split and xdist flags in two passes; extra arguments forward a
focused run. Marker exclusions and xdist flags are imported from
ouroboros.preflight_runner, never restated.

Measured on an 18-core Mac shared with other agents: the two-pass
battery took 649 s (parallel 203 s + serial 446 s); the one-run battery
took 217–287 s with identical verdicts (22 950 passed).

Also: `make test` now calls the script; CONTRIBUTING, README, the Build
& CI chapter, the architecture map and one sentence of prompts/SYSTEM.md
point at it and say which opt-in lanes it leaves out and what to record
when a full run could not finish; tests/test_plan_author_presentation.py
resolves node through ouroboros.preflight_node like the rest of the
suite instead of a bare PATH `node`; tests/test_run_tests_script.py pins
the recipe parity, the bounded shard count, bare call = full battery,
node NOT_RUN = red, and the grouping semantics.

Not changed: the commit gate and .github/workflows/ci.yml; the three
gate self-tests that wait out real 30/20 s timeouts (their slack exists
for nested-xdist startup on slow runners); no version carrier.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-19 02:13:01 +03:00

141 lines
6.6 KiB
Python

"""The local battery entry point keeps the gate's recipe and a bounded serial overlap."""
import importlib.util
import pathlib
import types
import pytest
from ouroboros.preflight_runner import LANE_EXCLUSION_EXPR, PARALLEL_PASS_FLAGS
from tests import conftest
REPO = pathlib.Path(__file__).resolve().parents[1]
@pytest.fixture(scope="module")
def run_tests():
spec = importlib.util.spec_from_file_location("run_tests_script", REPO / "scripts" / "run_tests.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _item(nodeid, *, serial):
markers = [types.SimpleNamespace(name="serial")] if serial else []
item = types.SimpleNamespace(nodeid=nodeid, added=[])
item.get_closest_marker = lambda name: next((m for m in markers if m.name == name), None)
item.add_marker = item.added.append
return item
def _group(item):
(mark,) = item.added
assert mark.name == "xdist_group"
return mark.args[0]
@pytest.mark.parametrize("workers, shards", [(2, 1), (4, 1), (7, 1), (8, 2), (12, 3), (16, 4), (64, 4)])
def test_serial_overlap_is_bounded_by_the_worker_count(run_tests, workers, shards):
assert run_tests.serial_shards(workers) == shards
def test_sequential_mode_keeps_the_gate_marker_split_and_xdist_flags(run_tests, monkeypatch):
monkeypatch.setenv("OUROBOROS_PREFLIGHT_TEST_WORKERS", "6")
(parallel_label, parallel), (serial_label, serial) = run_tests.battery_commands(sequential=True)
assert (parallel_label, serial_label) == ("parallel", "serial")
assert parallel[3:] == ["tests/", "-m", f"not serial and {LANE_EXCLUSION_EXPR}", *PARALLEL_PASS_FLAGS]
assert serial[3:] == ["tests/", "-m", f"serial and {LANE_EXCLUSION_EXPR}"]
def test_default_mode_is_one_run_over_both_lanes(run_tests, monkeypatch):
monkeypatch.setenv("OUROBOROS_PREFLIGHT_TEST_WORKERS", "8")
((label, command),) = run_tests.battery_commands(sequential=False)
assert label == "overlapped"
assert command[:3] == [run_tests.sys.executable, "-m", "pytest"]
command = command[3:]
assert command[command.index("-m") + 1] == LANE_EXCLUSION_EXPR
assert command[command.index("--dist") + 1] == "loadgroup"
assert command[command.index("-n") + 1] == "auto"
assert command[-1] == "--serial-shards=2"
untouched = [flag for flag in PARALLEL_PASS_FLAGS if flag.startswith("--") and flag != "--dist"]
assert all(flag in command for flag in untouched)
def test_the_worker_override_steers_xdist_like_the_gate(run_tests, monkeypatch):
seen = {}
monkeypatch.setenv("OUROBOROS_PREFLIGHT_TEST_WORKERS", "3")
monkeypatch.setattr(run_tests.subprocess, "call", lambda argv, cwd, env: seen.update(env=env) or 0)
assert run_tests._run("x", [run_tests.sys.executable, "-m", "pytest"], run_tests.REPO) == 0
assert seen["env"]["PYTEST_XDIST_AUTO_NUM_WORKERS"] == "3"
def test_sequential_with_pytest_arguments_is_refused(run_tests, monkeypatch):
monkeypatch.setattr(run_tests, "_run", lambda label, argv, cwd: pytest.fail("nothing must run"))
with pytest.raises(SystemExit) as exit_info:
run_tests.main(["--sequential", "tests/test_x.py"])
assert exit_info.value.code == 2
def test_a_bare_call_never_means_a_subset(run_tests, monkeypatch):
calls = []
monkeypatch.setattr(run_tests, "_node_lane", lambda: 0)
monkeypatch.setattr(run_tests, "_run", lambda label, argv, cwd: calls.append((label, argv)) or 0)
assert run_tests.main([]) == 0
assert [label for label, _ in calls] == ["overlapped"]
calls.clear()
assert run_tests.main(["tests/test_x.py", "-k", "one"]) == 0
assert calls == [("focused", [run_tests.sys.executable, "-m", "pytest", "tests/test_x.py", "-k", "one"])]
def test_a_missing_node_is_a_red_not_run_never_a_silent_skip(run_tests, monkeypatch):
monkeypatch.setattr(run_tests, "resolve_node", lambda: None, raising=False)
monkeypatch.setattr(run_tests, "_run", lambda label, argv, cwd: pytest.fail("pytest must not start"))
from ouroboros import preflight_node
monkeypatch.setattr(preflight_node, "resolve_node", lambda: None)
monkeypatch.setattr(preflight_node, "candidate_node_tests", lambda repo: ["tests/x.test.js"])
assert run_tests.main([]) == 1
def test_a_red_lane_stops_the_battery_and_an_empty_battery_is_red(run_tests, monkeypatch):
monkeypatch.setattr(run_tests, "_node_lane", lambda: 0)
codes = iter([1])
seen = []
monkeypatch.setattr(run_tests, "_run", lambda label, argv, cwd: seen.append(label) or next(codes))
assert run_tests.main(["--sequential"]) == 1
assert seen == ["parallel"]
monkeypatch.setattr(run_tests, "_run", lambda label, argv, cwd: 5)
assert run_tests.main(["--sequential"]) == 1
def test_lane_groups_pin_serial_files_and_keep_other_files_whole():
items = [
_item("tests/test_a.py::test_one", serial=False),
_item("tests/test_s.py::test_one", serial=True),
_item("tests/test_a.py::test_two", serial=False),
_item("tests/test_s.py::test_two[x]", serial=True),
_item("tests/test_t.py::test_one", serial=True),
_item("tests/test_c.py::TestBox::test_one", serial=False),
_item("tests/test_s.py::TestSer::test_three", serial=True),
]
conftest._pin_lane_groups(items, 2)
assert [item.nodeid.split("::")[0] for item in items[:4]] == ["tests/test_s.py", "tests/test_s.py", "tests/test_t.py", "tests/test_s.py"]
groups = {item.nodeid: _group(item) for item in items}
assert groups["tests/test_a.py::test_one"] == groups["tests/test_a.py::test_two"] == "tests/test_a.py"
assert groups["tests/test_c.py::TestBox::test_one"] == "tests/test_c.py::TestBox"
assert groups["tests/test_s.py::TestSer::test_three"] == groups["tests/test_s.py::test_one"]
assert groups["tests/test_s.py::test_one"] == groups["tests/test_s.py::test_two[x]"]
assert {groups["tests/test_s.py::test_one"], groups["tests/test_t.py::test_one"]} <= {"serial0", "serial1"}
def test_lane_groups_are_inert_without_the_option():
items = [_item("tests/test_s.py::test_one", serial=True), _item("tests/test_a.py::test_one", serial=False)]
conftest._pin_lane_groups(items, 0)
assert [item.added for item in items] == [[], []]
assert [item.nodeid for item in items] == ["tests/test_s.py::test_one", "tests/test_a.py::test_one"]
def test_the_documented_command_is_the_script():
makefile = (REPO / "Makefile").read_text(encoding="utf-8")
assert "test:\n\tuv run --locked python scripts/run_tests.py\n" in makefile
for doc in ("CONTRIBUTING.md", "README.md", "docs/development/14-build-and-ci.md", "prompts/SYSTEM.md"):
assert "scripts/run_tests.py" in (REPO / doc).read_text(encoding="utf-8"), doc