Skip to content

Commit 648f9e9

Browse files
committed
Unwrap magic exceptions from single-leaf groups
1 parent ed732d3 commit 648f9e9

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

newsfragments/104.feature.rst

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
If a test raises an ``ExceptionGroup`` (or nested ``ExceptionGroup``\ s) with only
2+
a single 'leaf' exception from ``pytest.xfail()`` or ``pytest.skip()``\ , we now
3+
unwrap it to have the desired effect on Pytest. ``ExceptionGroup``\ s with two or
4+
more leaf exceptions, even of the same type, are not changed and will be treated
5+
as ordinary test failures.
6+
7+
See `pytest-dev/pytest#9680 <https://github.com/pytest-dev/pytest/issues/9680>`__
8+
for design discussion. This feature is particularly useful if you've enabled
9+
`the new strict_exception_groups=True option
10+
<https://trio.readthedocs.io/en/stable/reference-core.html#strict-versus-loose-exceptiongroup-semantics>`__.

pytest_trio/_tests/test_basic.py

+49
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,52 @@ def test_invalid():
7474
result = testdir.runpytest()
7575

7676
result.assert_outcomes(errors=1)
77+
78+
79+
def test_skip_and_xfail(testdir):
80+
81+
testdir.makepyfile(
82+
"""
83+
import functools
84+
import pytest
85+
import trio
86+
87+
trio.run = functools.partial(trio.run, strict_exception_groups=True)
88+
89+
@pytest.mark.trio
90+
async def test_xfail():
91+
pytest.xfail()
92+
93+
@pytest.mark.trio
94+
async def test_skip():
95+
pytest.skip()
96+
97+
async def callback(fn):
98+
fn()
99+
100+
async def fail():
101+
raise RuntimeError
102+
103+
@pytest.mark.trio
104+
async def test_xfail_and_fail():
105+
async with trio.open_nursery() as nursery:
106+
nursery.start_soon(callback, pytest.xfail)
107+
nursery.start_soon(fail)
108+
109+
@pytest.mark.trio
110+
async def test_skip_and_fail():
111+
async with trio.open_nursery() as nursery:
112+
nursery.start_soon(callback, pytest.skip)
113+
nursery.start_soon(fail)
114+
115+
@pytest.mark.trio
116+
async def test_xfail_and_skip():
117+
async with trio.open_nursery() as nursery:
118+
nursery.start_soon(callback, pytest.skip)
119+
nursery.start_soon(callback, pytest.xfail)
120+
"""
121+
)
122+
123+
result = testdir.runpytest("-s")
124+
125+
result.assert_outcomes(skipped=1, xfailed=1, failed=3)

pytest_trio/plugin.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import trio
1111
from trio.abc import Clock, Instrument
1212
from trio.testing import MockClock
13+
from _pytest.outcomes import Skipped, XFailed
1314

1415
if sys.version_info[:2] < (3, 11):
1516
from exceptiongroup import BaseExceptionGroup
@@ -343,7 +344,25 @@ def wrapper(**kwargs):
343344
f"Expected at most one Clock in kwargs, got {clocks!r}"
344345
)
345346
instruments = [i for i in kwargs.values() if isinstance(i, Instrument)]
346-
return run(partial(fn, **kwargs), clock=clock, instruments=instruments)
347+
try:
348+
return run(partial(fn, **kwargs), clock=clock, instruments=instruments)
349+
except BaseExceptionGroup as eg:
350+
queue = [eg]
351+
leaves = []
352+
while queue:
353+
ex = queue.pop()
354+
if isinstance(ex, BaseExceptionGroup):
355+
queue.extend(ex.exceptions)
356+
else:
357+
leaves.append(ex)
358+
if len(leaves) == 1:
359+
if isinstance(leaves[0], XFailed):
360+
pytest.xfail()
361+
if isinstance(leaves[0], Skipped):
362+
pytest.skip()
363+
# Since our leaf exceptions don't consist of exactly one 'magic'
364+
# skipped or xfailed exception, re-raise the whole group.
365+
raise
347366

348367
return wrapper
349368

0 commit comments

Comments
 (0)