From 2d7dd0e5c1f9800a3b8c8f38fd832ee335fa7fba Mon Sep 17 00:00:00 2001 From: stefankoncarevic Date: Fri, 25 Sep 2026 21:44:49 +0200 Subject: [PATCH] [Bugfix][CI] Report subprocess test skips as skips, not passes (#58701) Signed-off-by: Stefan Koncarevic Co-authored-by: Andreas Karatzas --- tests/utils.py | 20 ++++++++++++-------- tests/utils_/test_spawn_decorator.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 399a741df0..d90690be96 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1928,7 +1928,7 @@ def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None] The child inherits the parent's stdout/stderr so its output (engine cores, NCCL, CUDA, ...) reaches the test runner live; the Python-level - traceback is serialized to ``tb_file`` for structured re-raising. A + traceback or skip reason is serialized to ``tb_file`` for propagation. A native crash leaves ``tb_file`` empty — the diagnostic is then only in the inherited subprocess output. """ @@ -1953,7 +1953,7 @@ def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None] ) child_script = ( - "import sys, importlib, cloudpickle, traceback\n" + "import sys, importlib, cloudpickle, traceback, json\n" "try:\n" " from _pytest.outcomes import Skipped\n" "except ImportError:\n" @@ -1965,7 +1965,9 @@ def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None] " target = getattr(target, name)\n" "try:\n" " target(*data['args'], **data['kwargs'])\n" - "except Skipped:\n" + "except Skipped as exc:\n" + " with open(data['tb_file'], 'w') as fp:\n" + " json.dump(str(exc), fp)\n" " sys.exit(0)\n" "except BaseException:\n" " with open(data['tb_file'], 'w') as fp:\n" @@ -1984,18 +1986,20 @@ def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None] env=env, ) + try: + with open(tb_file) as fp: + tb = fp.read() + except OSError: + tb = "" if result.returncode != 0: - try: - with open(tb_file) as fp: - tb = fp.read() - except OSError: - tb = "" if not tb: tb = "" raise RuntimeError( f"Test subprocess '{f.__name__}' failed " f"({_format_subprocess_exit(result.returncode)}):\n{tb}" ) + if tb: + pytest.skip(json.loads(tb)) finally: with contextlib.suppress(OSError): os.remove(tb_file) diff --git a/tests/utils_/test_spawn_decorator.py b/tests/utils_/test_spawn_decorator.py index 1740ea30de..d92d589ff1 100644 --- a/tests/utils_/test_spawn_decorator.py +++ b/tests/utils_/test_spawn_decorator.py @@ -2,10 +2,14 @@ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Tests for spawn_new_process_for_each_test decorator.""" +from pathlib import Path + import pytest from tests.utils import spawn_new_process_for_each_test +pytest_plugins = ["pytester"] + @spawn_new_process_for_each_test def test_spawn_decorator_passing(): @@ -26,6 +30,15 @@ def test_spawn_decorator_skip(): pytest.skip("intentional skip") +def test_spawn_decorator_reports_skip(pytester): + """A child skip must remain a skip in the parent pytest report.""" + result = pytester.runpytest_subprocess( + f"{Path(__file__).resolve()}::test_spawn_decorator_skip", "-rs" + ) + result.assert_outcomes(skipped=1) + result.stdout.fnmatch_lines(["SKIPPED *intentional skip"]) + + @spawn_new_process_for_each_test @pytest.mark.parametrize("x,y,expected", [(1, 2, 3), (0, 0, 0)]) def test_spawn_decorator_parametrized(x, y, expected):