diff --git a/newsfragments/104.feature.rst b/newsfragments/104.feature.rst new file mode 100644 index 0000000..19e9f2f --- /dev/null +++ b/newsfragments/104.feature.rst @@ -0,0 +1,10 @@ +If a test raises an ``ExceptionGroup`` (or nested ``ExceptionGroup``\ s) with only +a single 'leaf' exception from ``pytest.xfail()`` or ``pytest.skip()``\ , we now +unwrap it to have the desired effect on Pytest. ``ExceptionGroup``\ s with two or +more leaf exceptions, even of the same type, are not changed and will be treated +as ordinary test failures. + +See `pytest-dev/pytest#9680 `__ +for design discussion. This feature is particularly useful if you've enabled +`the new strict_exception_groups=True option +`__. diff --git a/newsfragments/128.misc.rst b/newsfragments/128.misc.rst new file mode 100644 index 0000000..dacb961 --- /dev/null +++ b/newsfragments/128.misc.rst @@ -0,0 +1,4 @@ +Trio 0.22.0 deprecated ``MultiError`` in favor of the standard-library +(or `backported `__) ``ExceptionGroup`` +type; ``pytest-trio`` now uses ``ExceptionGroup`` and therefore requires +Trio 0.22.0 or later. diff --git a/pytest.ini b/pytest.ini index 92c555f..67ed440 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,7 @@ [pytest] addopts = -ra -v --pyargs pytest_trio --verbose --cov +filterwarnings = + error + default::pytest.PytestAssertRewriteWarning + default::pytest.PytestDeprecationWarning + default::pytest.PytestUnraisableExceptionWarning diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 58d1a55..ca99159 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -300,4 +300,6 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) + result.stdout.re_match_lines( + [r"(E\W+| +\| )RuntimeError: Crash during fixture teardown"] + ) diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index 65dd911..9702498 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -74,3 +74,52 @@ def test_invalid(): result = testdir.runpytest() result.assert_outcomes(errors=1) + + +def test_skip_and_xfail(testdir): + + testdir.makepyfile( + """ + import functools + import pytest + import trio + + trio.run = functools.partial(trio.run, strict_exception_groups=True) + + @pytest.mark.trio + async def test_xfail(): + pytest.xfail() + + @pytest.mark.trio + async def test_skip(): + pytest.skip() + + async def callback(fn): + fn() + + async def fail(): + raise RuntimeError + + @pytest.mark.trio + async def test_xfail_and_fail(): + async with trio.open_nursery() as nursery: + nursery.start_soon(callback, pytest.xfail) + nursery.start_soon(fail) + + @pytest.mark.trio + async def test_skip_and_fail(): + async with trio.open_nursery() as nursery: + nursery.start_soon(callback, pytest.skip) + nursery.start_soon(fail) + + @pytest.mark.trio + async def test_xfail_and_skip(): + async with trio.open_nursery() as nursery: + nursery.start_soon(callback, pytest.skip) + nursery.start_soon(callback, pytest.xfail) + """ + ) + + result = testdir.runpytest("-s") + + result.assert_outcomes(skipped=1, xfailed=1, failed=3) diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 508b2f0..738ffbc 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -139,4 +139,6 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) + result.stdout.re_match_lines( + [r"(E\W+| +\| )RuntimeError: Crash during fixture teardown"] + ) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 34e64a8..6638d67 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,6 +1,6 @@ """pytest-trio implementation.""" +import sys from functools import wraps, partial -from traceback import format_exception from collections.abc import Coroutine, Generator from contextlib import asynccontextmanager from inspect import isasyncgen, isasyncgenfunction, iscoroutinefunction @@ -10,6 +10,10 @@ import trio from trio.abc import Clock, Instrument from trio.testing import MockClock +from _pytest.outcomes import Skipped, XFailed + +if sys.version_info[:2] < (3, 11): + from exceptiongroup import BaseExceptionGroup ################################################################ # Basic setup @@ -52,13 +56,6 @@ def pytest_configure(config): ) -@pytest.hookimpl(tryfirst=True) -def pytest_exception_interact(node, call, report): - if issubclass(call.excinfo.type, trio.MultiError): - # TODO: not really elegant (pytest cannot output color with this hack) - report.longrepr = "".join(format_exception(*call.excinfo._excinfo)) - - ################################################################ # Core support for trio fixtures and trio tests ################################################################ @@ -347,7 +344,25 @@ def wrapper(**kwargs): f"Expected at most one Clock in kwargs, got {clocks!r}" ) instruments = [i for i in kwargs.values() if isinstance(i, Instrument)] - return run(partial(fn, **kwargs), clock=clock, instruments=instruments) + try: + return run(partial(fn, **kwargs), clock=clock, instruments=instruments) + except BaseExceptionGroup as eg: + queue = [eg] + leaves = [] + while queue: + ex = queue.pop() + if isinstance(ex, BaseExceptionGroup): + queue.extend(ex.exceptions) + else: + leaves.append(ex) + if len(leaves) == 1: + if isinstance(leaves[0], XFailed): + pytest.xfail() + if isinstance(leaves[0], Skipped): + pytest.skip() + # Since our leaf exceptions don't consist of exactly one 'magic' + # skipped or xfailed exception, re-raise the whole group. + raise return wrapper @@ -407,8 +422,12 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): ) ) - if test_ctx.error_list: - raise trio.MultiError(test_ctx.error_list) + if len(test_ctx.error_list) == 1: + raise test_ctx.error_list[0] + elif test_ctx.error_list: + raise BaseExceptionGroup( + "errors in async test and trio fixtures", test_ctx.error_list + ) _bootstrap_fixtures_and_run_test._trio_test_runner_wrapped = True return _bootstrap_fixtures_and_run_test