Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/13963.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed subtests running with `pytest-xdist <https://github.com/pytest-dev/pytest>`__ when their contexts contain non-standard objects.

Fixes `pytest-dev/pytest-xdist#1273 <https://github.com/pytest-dev/pytest-xdist/issues/1273>`__.
5 changes: 4 additions & 1 deletion src/_pytest/subtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should strive to have the property that from_json(to_json(report)) == report. In this case it means that we should have self.kwargs itself should be serializable, not just in to_json.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saferepr itself might be non-optimal, e.g. it would make strings "foo" render as 'foo' while we probably want plain foo. The _idval_from_value function does some reasonable conversions I think.

return result

@classmethod
def _from_json(cls, d: dict[str, Any]) -> Self:
Expand Down
38 changes: 36 additions & 2 deletions testing/test_subtests.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -958,20 +960,52 @@ 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, ""),
keywords={},
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)