Skip to content

Commit 2ba99e6

Browse files
authored
Merge pull request #130 from Zac-HD/use-exceptiongroup
Use `ExceptionGroup` and fix magic-exception handling
2 parents cbd6197 + 6fbe63e commit 2ba99e6

File tree

7 files changed

+104
-13
lines changed

7 files changed

+104
-13
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>`__.

newsfragments/128.misc.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Trio 0.22.0 deprecated ``MultiError`` in favor of the standard-library
2+
(or `backported <https://pypi.org/project/exceptiongroup/>`__) ``ExceptionGroup``
3+
type; ``pytest-trio`` now uses ``ExceptionGroup`` and therefore requires
4+
Trio 0.22.0 or later.

pytest.ini

+5
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
[pytest]
22
addopts = -ra -v --pyargs pytest_trio --verbose --cov
3+
filterwarnings =
4+
error
5+
default::pytest.PytestAssertRewriteWarning
6+
default::pytest.PytestDeprecationWarning
7+
default::pytest.PytestUnraisableExceptionWarning

pytest_trio/_tests/test_async_yield_fixture.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,6 @@ def test_after():
300300
result = testdir.runpytest()
301301

302302
result.assert_outcomes(failed=1, passed=2)
303-
result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"])
303+
result.stdout.re_match_lines(
304+
[r"(E\W+| +\| )RuntimeError: Crash during fixture teardown"]
305+
)

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/_tests/test_sync_fixture.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,6 @@ def test_after():
139139
result = testdir.runpytest()
140140

141141
result.assert_outcomes(failed=1, passed=2)
142-
result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"])
142+
result.stdout.re_match_lines(
143+
[r"(E\W+| +\| )RuntimeError: Crash during fixture teardown"]
144+
)

pytest_trio/plugin.py

+30-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""pytest-trio implementation."""
2+
import sys
23
from functools import wraps, partial
3-
from traceback import format_exception
44
from collections.abc import Coroutine, Generator
55
from contextlib import asynccontextmanager
66
from inspect import isasyncgen, isasyncgenfunction, iscoroutinefunction
@@ -10,6 +10,10 @@
1010
import trio
1111
from trio.abc import Clock, Instrument
1212
from trio.testing import MockClock
13+
from _pytest.outcomes import Skipped, XFailed
14+
15+
if sys.version_info[:2] < (3, 11):
16+
from exceptiongroup import BaseExceptionGroup
1317

1418
################################################################
1519
# Basic setup
@@ -52,13 +56,6 @@ def pytest_configure(config):
5256
)
5357

5458

55-
@pytest.hookimpl(tryfirst=True)
56-
def pytest_exception_interact(node, call, report):
57-
if issubclass(call.excinfo.type, trio.MultiError):
58-
# TODO: not really elegant (pytest cannot output color with this hack)
59-
report.longrepr = "".join(format_exception(*call.excinfo._excinfo))
60-
61-
6259
################################################################
6360
# Core support for trio fixtures and trio tests
6461
################################################################
@@ -347,7 +344,25 @@ def wrapper(**kwargs):
347344
f"Expected at most one Clock in kwargs, got {clocks!r}"
348345
)
349346
instruments = [i for i in kwargs.values() if isinstance(i, Instrument)]
350-
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
351366

352367
return wrapper
353368

@@ -407,8 +422,12 @@ async def _bootstrap_fixtures_and_run_test(**kwargs):
407422
)
408423
)
409424

410-
if test_ctx.error_list:
411-
raise trio.MultiError(test_ctx.error_list)
425+
if len(test_ctx.error_list) == 1:
426+
raise test_ctx.error_list[0]
427+
elif test_ctx.error_list:
428+
raise BaseExceptionGroup(
429+
"errors in async test and trio fixtures", test_ctx.error_list
430+
)
412431

413432
_bootstrap_fixtures_and_run_test._trio_test_runner_wrapped = True
414433
return _bootstrap_fixtures_and_run_test

0 commit comments

Comments
 (0)