diff --git a/.github/scripts/run_serial_tests.py b/.github/scripts/run_serial_tests.py index 9cbcaea2fe..f09f9c3302 100644 --- a/.github/scripts/run_serial_tests.py +++ b/.github/scripts/run_serial_tests.py @@ -3,6 +3,7 @@ import argparse import fnmatch import os +import subprocess import sys from pathlib import Path @@ -46,7 +47,14 @@ def main() -> None: marker_expression = ( "serial and not review_optional" if args.exclude_review_optional else "serial" ) - os.execv(sys.executable, _serial_args(marker_expression=marker_expression)) + command = _serial_args(marker_expression=marker_expression) + if sys.platform == "win32": + # Windows has no process replacement: os.execv spawns a child and + # terminates this process with 0, so the caller sees success no matter + # how pytest exits. Mirror run_integration_tests.py and wait instead. + completed = subprocess.run(command, check=False) + raise SystemExit(completed.returncode) + os.execv(sys.executable, command) if __name__ == "__main__": diff --git a/tests/test_run_serial_tests.py b/tests/test_run_serial_tests.py index 81b1c1241c..36b3c51ff6 100644 --- a/tests/test_run_serial_tests.py +++ b/tests/test_run_serial_tests.py @@ -1,9 +1,10 @@ from __future__ import annotations import importlib.util +import subprocess import sys from pathlib import Path -from types import ModuleType +from types import ModuleType, SimpleNamespace import pytest @@ -96,3 +97,55 @@ def test_serial_command_targets_only_discovered_files( "-m", "serial", ] + + +def test_windows_runner_uses_subprocess_and_propagates_exit_code( + serial_test_runner: ModuleType, + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: list[tuple[list[str], bool]] = [] + + def capture_run(command: list[str], *, check: bool) -> SimpleNamespace: + captured.append((command, check)) + return SimpleNamespace(returncode=23) + + monkeypatch.setattr(serial_test_runner.sys, "platform", "win32") + monkeypatch.setattr(subprocess, "run", capture_run) + monkeypatch.setattr(serial_test_runner.os, "chdir", lambda _path: None) + monkeypatch.setattr( + serial_test_runner.os, + "execv", + lambda *_args: pytest.fail("Windows serial runner must not call os.execv"), + ) + monkeypatch.setattr(sys, "argv", ["run_serial_tests.py"]) + + with pytest.raises(SystemExit) as exc_info: + serial_test_runner.main() + + assert exc_info.value.code == 23 + assert len(captured) == 1 + assert captured[0][1] is False + + +def test_non_windows_runner_still_execs( + serial_test_runner: ModuleType, + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: list[list[str]] = [] + + monkeypatch.setattr(serial_test_runner.sys, "platform", "linux") + monkeypatch.setattr(serial_test_runner.os, "chdir", lambda _path: None) + monkeypatch.setattr( + subprocess, + "run", + lambda *_args, **_kwargs: pytest.fail("POSIX serial runner must not call subprocess.run"), + ) + monkeypatch.setattr( + serial_test_runner.os, "execv", lambda _executable, command: captured.append(command) + ) + monkeypatch.setattr(sys, "argv", ["run_serial_tests.py"]) + + serial_test_runner.main() + + assert len(captured) == 1 + assert "serial" in captured[0]