From 7585f4500ab8081454151563743e1983e4f51f8e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Nov 2025 19:27:26 -0300 Subject: [PATCH] Ensure subtest's context kwargs are JSON serializable Convert all the values of `SubtestContext.kwargs` to strings using `saferepr`. This complies with the requirement that the returned dict from `pytest_report_to_serializable` is serializable to JSON, at the cost of losing type information for objects that are natively supported by JSON. Fixes pytest-dev/pytest-xdist#1273 --- changelog/13963.bugfix.rst | 3 +++ src/_pytest/subtests.py | 5 ++++- testing/test_subtests.py | 38 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 changelog/13963.bugfix.rst diff --git a/changelog/13963.bugfix.rst b/changelog/13963.bugfix.rst new file mode 100644 index 00000000000..90cb5ae6315 --- /dev/null +++ b/changelog/13963.bugfix.rst @@ -0,0 +1,3 @@ +Fixed subtests running with `pytest-xdist `__ when their contexts contain non-standard objects. + +Fixes `pytest-dev/pytest-xdist#1273 `__. diff --git a/src/_pytest/subtests.py b/src/_pytest/subtests.py index e0ceb27f4b1..d00346e076d 100644 --- a/src/_pytest/subtests.py +++ b/src/_pytest/subtests.py @@ -62,7 +62,10 @@ class SubtestContext: kwargs: Mapping[str, Any] def _to_json(self) -> dict[str, Any]: - return dataclasses.asdict(self) + result = dataclasses.asdict(self) + # Brute-force the returned kwargs dict to be JSON serializable (pytest-dev/pytest-xdist#1273). + result["kwargs"] = {k: saferepr(v) for (k, v) in result["kwargs"].items()} + return result @classmethod def _from_json(cls, d: dict[str, Any]) -> Self: diff --git a/testing/test_subtests.py b/testing/test_subtests.py index 6849df53622..8af48d00d02 100644 --- a/testing/test_subtests.py +++ b/testing/test_subtests.py @@ -1,8 +1,10 @@ from __future__ import annotations +from enum import Enum import sys from typing import Literal +from _pytest._io.saferepr import saferepr from _pytest.subtests import SubtestContext from _pytest.subtests import SubtestReport import pytest @@ -958,9 +960,13 @@ def test(subtests): def test_serialization() -> None: + """Ensure subtest's kwargs are serialized using `saferepr` (pytest-dev/pytest-xdist#1273).""" from _pytest.subtests import pytest_report_from_serializable from _pytest.subtests import pytest_report_to_serializable + class MyEnum(Enum): + A = "A" + report = SubtestReport( "test_foo::test_foo", ("test_foo.py", 12, ""), @@ -968,10 +974,38 @@ def test_serialization() -> None: outcome="passed", when="call", longrepr=None, - context=SubtestContext(msg="custom message", kwargs=dict(i=10)), + context=SubtestContext(msg="custom message", kwargs=dict(i=10, a=MyEnum.A)), ) data = pytest_report_to_serializable(report) assert data is not None new_report = pytest_report_from_serializable(data) assert new_report is not None - assert new_report.context == SubtestContext(msg="custom message", kwargs=dict(i=10)) + assert new_report.context == SubtestContext( + msg="custom message", kwargs=dict(i=saferepr(10), a=saferepr(MyEnum.A)) + ) + + +def test_serialization_xdist(pytester: pytest.Pytester) -> None: + """Regression test for pytest-dev/pytest-xdist#1273.""" + pytest.importorskip("xdist") + pytester.makepyfile( + """ + from enum import Enum + import unittest + + class MyEnum(Enum): + A = "A" + + def test(subtests): + with subtests.test(a=MyEnum.A): + pass + + class T(unittest.TestCase): + + def test(self): + with self.subTest(a=MyEnum.A): + pass + """ + ) + result = pytester.runpytest("-n1", "-pxdist.plugin") + result.assert_outcomes(passed=2)