From 9ed1adc5c8bff83516a0beac55a448b9d4372cf7 Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Fri, 28 Aug 2026 03:36:52 +0400 Subject: [PATCH] fix: propagate the serial test runner's exit code on Windows run_serial_tests.py ends in os.execv. Windows has no process replacement, so the CRT spawns a child and terminates the caller with 0. make tests-serial returns in about 100 ms reporting success while pytest keeps running orphaned, and its output arrives afterwards, interleaved into whatever the shell runs next. run_integration_tests.py already handles this at lines 103-106, added in 21a1f9b4. Apply the same win32-guarded shape here: subprocess.run with check=False, then raise SystemExit on the child's return code. POSIX is unchanged. os.execv still runs there, so pytest keeps owning the terminal and its own signal handling. Adds two tests mirroring the precedent's test_windows_bootstrap_uses_subprocess_and_propagates_exit_code: the win32 case asserts the code propagates, that check is False, and that os.execv is never called; the POSIX case asserts execv is still used and subprocess.run is not. --- .github/scripts/run_serial_tests.py | 10 +++++- tests/test_run_serial_tests.py | 55 ++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) 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]