diff --git a/meta-python/recipes-devtools/python/python3-pytest-forked/0001-Fix-report-serialization-with-pytest-9.2.patch b/meta-python/recipes-devtools/python/python3-pytest-forked/0001-Fix-report-serialization-with-pytest-9.2.patch new file mode 100644 index 0000000000..55c7e8feee --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-pytest-forked/0001-Fix-report-serialization-with-pytest-9.2.patch @@ -0,0 +1,142 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Fish Wang +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 +--- + 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*", ++ ] ++ ) diff --git a/meta-python/recipes-devtools/python/python3-pytest-forked_1.6.0.bb b/meta-python/recipes-devtools/python/python3-pytest-forked_1.7.5.bb similarity index 66% rename from meta-python/recipes-devtools/python/python3-pytest-forked_1.6.0.bb rename to meta-python/recipes-devtools/python/python3-pytest-forked_1.7.5.bb index 825141faa2..e75443a431 100644 --- a/meta-python/recipes-devtools/python/python3-pytest-forked_1.6.0.bb +++ b/meta-python/recipes-devtools/python/python3-pytest-forked_1.7.5.bb @@ -3,17 +3,17 @@ HOMEPAGE = "https://github.com/pytest-dev/pytest-forked" LICENSE = "MIT" LIC_FILES_CHKSUM = "file://LICENSE;md5=772fcdaca14b378878d05c7d857e6c3e" -SRC_URI[sha256sum] = "4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f" +SRC_URI += "file://0001-Fix-report-serialization-with-pytest-9.2.patch" + +SRC_URI[sha256sum] = "00f2bee51612f29b8e6b81eed2c3b2975e824c2693394f5bdaf7a1369078ba5f" inherit pypi python_setuptools_build_meta -PYPI_PACKAGE_SDIST = "pytest-forked" - PEP517_BUILD_OPTS = "--skip-dependency-check" DEPENDS += "python3-setuptools-scm-native" RDEPENDS:${PN} += " \ - python3-py \ + python3-multiprocessing \ python3-pytest \ "