Files
meta-openembedded/meta-python/recipes-devtools/python/python3-pytest-forked/0001-Fix-report-serialization-with-pytest-9.2.patch
Khem Raj 85304ba807 python3-pytest-forked: Upgrade 1.6.0 -> 1.7.5
1.7.5 replaced py.process.ForkedFunc with multiprocessing, so swap the
python3-py runtime dependency for python3-multiprocessing. The sdist is
now named pytest_forked, the pypi class default, so drop
PYPI_PACKAGE_SDIST.

Backport the pending upstream fix for pytest 9.2, where TestReport
carries a NodeId object in _id that marshal cannot serialize; every
forked test crashed with "ValueError: unmarshallable object". This made
python3-starlette ptests report no results at all; all of them pass
now on qemux86-64.

AI-Generated: Uses Claude Code
Signed-off-by: Khem Raj <khem.raj@oss.qualcomm.com>
2026-11-06 00:12:32 -08:00

143 lines
4.6 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Fish Wang <fishw@asu.edu>
Date: Wed, 28 Oct 2026 01:55:59 +0000
Subject: [PATCH] Fix report serialization with pytest 9.2.
pytest 9.2 turned TestReport.nodeid into a property backed by a NodeId
object. It is not marshalable, so pytest-forked breaks.
- Now serialize_report emits a plain nodeid string.
- Crash reports use the call phase; pytest 9.2 prints the phase in the
short summary, which exposed the ??? placeholder.
- The forked child writes its traceback to the captured stderr file
before exiting with "status 3," so the next similar failure is easier
to spot.
Upstream-Status: Submitted [https://github.com/pytest-dev/pytest-forked/pull/112]
Signed-off-by: Khem Raj <raj.khem@gmail.com>
---
CHANGELOG.rst | 9 ++++++++
src/pytest_forked/__init__.py | 19 +++++++++++-----
testing/test_boxed.py | 43 +++++++++++++++++++++++++++++++++++
3 files changed, 65 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index fe6e527..20d76c0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,12 @@
+Unreleased
+==========
+
+* Fix compatibility with pytest 9.2: reports now carry a ``NodeId`` object that
+ ``marshal`` cannot serialize, which made every forked test exit with status 3.
+* Report crashes as the ``call`` phase instead of ``???``.
+* Show the traceback when the forked child fails outside the test itself.
+
+
v1.7.5
======
diff --git a/src/pytest_forked/__init__.py b/src/pytest_forked/__init__.py
index 6d135cb..2f53266 100644
--- a/src/pytest_forked/__init__.py
+++ b/src/pytest_forked/__init__.py
@@ -13,6 +13,10 @@ from _pytest import runner
# copied from xdist remote
def serialize_report(rep):
d = rep.__dict__.copy()
+ # pytest >= 9.2 stores a NodeId object in ``_id`` and exposes ``nodeid``
+ # as a property. convert it to a plain string.
+ if "_id" in d:
+ d["nodeid"] = str(d.pop("_id"))
if hasattr(rep.longrepr, "toterminal"):
d["longrepr"] = str(rep.longrepr)
else:
@@ -97,6 +101,11 @@ def _worker(runforked_fn, stdout_path, stderr_path, retval_path):
code = e.code if e.code is not None else 0
os._exit(int(code))
except Exception:
+ import traceback
+
+ # pytest's capture has restored fd 2 by now; write to the file directly
+ with open(stderr_path, "a") as f:
+ traceback.print_exc(file=f)
os._exit(EXITSTATUS_EXCEPTION)
finally:
try:
@@ -221,12 +230,10 @@ def report_process_crash(item, result):
from _pytest import runner
- # pytest >= 4.1
- has_from_call = getattr(runner.CallInfo, "from_call", None) is not None
- if has_from_call:
- call = runner.CallInfo.from_call(lambda: 0 / 0, "???")
- else:
- call = runner.CallInfo(lambda: 0 / 0, "???")
+ # The child died while running the test body, so report it as the
+ # "call" phase.
+ # pytest >= 9.2 prints the phase in the short summary; handle it here.
+ call = runner.CallInfo.from_call(lambda: 0 / 0, "call")
call.excinfo = info
rep = runner.pytest_runtest_makereport(item, call)
if result.out:
diff --git a/testing/test_boxed.py b/testing/test_boxed.py
index 3d67a9c..269c2a4 100644
--- a/testing/test_boxed.py
+++ b/testing/test_boxed.py
@@ -1,6 +1,10 @@
+import marshal
import os
import pytest
+from _pytest.reports import TestReport
+
+from pytest_forked import serialize_report
needsfork = pytest.mark.skipif(not hasattr(os, "fork"), reason="os.fork required")
@@ -119,3 +123,42 @@ def test_forked_followed_by_other_test_tears_down(testdir):
def test_is_not_boxed_by_default(testdir):
config = testdir.parseconfig(testdir.tmpdir)
assert not config.option.forked
+
+
+def test_serialize_report_roundtrips_through_marshal():
+ rep = TestReport(
+ "test_x.py::test_y",
+ ("test_x.py", 1, "test_y"),
+ {},
+ "passed",
+ None,
+ "call",
+ [],
+ 0.1,
+ 0.0,
+ 0.1,
+ [],
+ )
+ d = marshal.loads(marshal.dumps(serialize_report(rep)))
+ assert "_id" not in d
+ rebuilt = TestReport(**d)
+ assert rebuilt.nodeid == rep.nodeid
+ assert rebuilt.outcome == "passed"
+
+
+@needsfork
+def test_child_exception_traceback_is_reported(testdir):
+ p1 = testdir.makepyfile(
+ """
+ def test_function(record_property):
+ record_property("obj", object())
+ """
+ )
+ result = testdir.runpytest(p1, "--forked")
+ result.stdout.fnmatch_lines(
+ [
+ "*EXITED with status 3*",
+ "*ValueError: unmarshallable object*",
+ "*1 failed*",
+ ]
+ )