[Bugfix][CI] Report subprocess test skips as skips, not passes (#58701)

Signed-off-by: Stefan Koncarevic <Stefan.Koncarevic@amd.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com>

stefankoncarevic committed Sep 25, 2026 at 21:44 UTC 2d7dd0e5c1f9800a3b8c8f38fd832ee335fa7fba
2 files changed +25 -8
tests/utils.py
+12 -8
@@ -1928,7 +1928,7 @@ def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None]
1928
1929 The child inherits the parent's stdout/stderr so its output (engine
1930 cores, NCCL, CUDA, ...) reaches the test runner live; the Python-level
1931 - traceback is serialized to ``tb_file`` for structured re-raising. A
1931 + traceback or skip reason is serialized to ``tb_file`` for propagation. A
1932 native crash leaves ``tb_file`` empty — the diagnostic is then only in
1933 the inherited subprocess output.
1934 """
@@ -1953,7 +1953,7 @@ def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None]
1953 )
1954
1955 child_script = (
1956 - "import sys, importlib, cloudpickle, traceback\n"
1956 + "import sys, importlib, cloudpickle, traceback, json\n"
1957 "try:\n"
1958 " from _pytest.outcomes import Skipped\n"
1959 "except ImportError:\n"
@@ -1965,7 +1965,9 @@ def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None]
1965 " target = getattr(target, name)\n"
1966 "try:\n"
1967 " target(*data['args'], **data['kwargs'])\n"
1968 - "except Skipped:\n"
1968 + "except Skipped as exc:\n"
1969 + " with open(data['tb_file'], 'w') as fp:\n"
1970 + " json.dump(str(exc), fp)\n"
1971 " sys.exit(0)\n"
1972 "except BaseException:\n"
1973 " 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]
1986 env=env,
1987 )
1988
1989 + try:
1990 + with open(tb_file) as fp:
1991 + tb = fp.read()
1992 + except OSError:
1993 + tb = ""
1994 if result.returncode != 0:
1988 - try:
1989 - with open(tb_file) as fp:
1990 - tb = fp.read()
1991 - except OSError:
1992 - tb = ""
1995 if not tb:
1996 tb = "<no Python traceback; see subprocess output above>"
1997 raise RuntimeError(
1998 f"Test subprocess '{f.__name__}' failed "
1999 f"({_format_subprocess_exit(result.returncode)}):\n{tb}"
2000 )
2001 + if tb:
2002 + pytest.skip(json.loads(tb))
2003 finally:
2004 with contextlib.suppress(OSError):
2005 os.remove(tb_file)
tests/utils_/test_spawn_decorator.py
+13
@@ -2,10 +2,14 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for spawn_new_process_for_each_test decorator."""
4
5 +from pathlib import Path
6 +
7 import pytest
8
9 from tests.utils import spawn_new_process_for_each_test
10
11 +pytest_plugins = ["pytester"]
12 +
13
14 @spawn_new_process_for_each_test
15 def test_spawn_decorator_passing():
@@ -26,6 +30,15 @@ def test_spawn_decorator_skip():
30 pytest.skip("intentional skip")
31
32
33 +def test_spawn_decorator_reports_skip(pytester):
34 + """A child skip must remain a skip in the parent pytest report."""
35 + result = pytester.runpytest_subprocess(
36 + f"{Path(__file__).resolve()}::test_spawn_decorator_skip", "-rs"
37 + )
38 + result.assert_outcomes(skipped=1)
39 + result.stdout.fnmatch_lines(["SKIPPED *intentional skip"])
40 +
41 +
42 @spawn_new_process_for_each_test
43 @pytest.mark.parametrize("x,y,expected", [(1, 2, 3), (0, 0, 0)])
44 def test_spawn_decorator_parametrized(x, y, expected):