mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 12:18:39 +00:00
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>
141 lines
6.6 KiB
Python
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
|