Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ExceptionGroup and fix magic-exception handling #130

Merged
merged 3 commits into from Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions 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 <https://github.com/pytest-dev/pytest/issues/9680>`__
for design discussion. This feature is particularly useful if you've enabled
`the new strict_exception_groups=True option
<https://trio.readthedocs.io/en/stable/reference-core.html#strict-versus-loose-exceptiongroup-semantics>`__.
4 changes: 4 additions & 0 deletions newsfragments/128.misc.rst
@@ -0,0 +1,4 @@
Trio 0.22.0 deprecated ``MultiError`` in favor of the standard-library
(or `backported <https://pypi.org/project/exceptiongroup/>`__) ``ExceptionGroup``
type; ``pytest-trio`` now uses ``ExceptionGroup`` and therefore requires
Trio 0.22.0 or later.
5 changes: 5 additions & 0 deletions 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
4 changes: 3 additions & 1 deletion pytest_trio/_tests/test_async_yield_fixture.py
Expand Up @@ -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"]
)
49 changes: 49 additions & 0 deletions pytest_trio/_tests/test_basic.py
Expand Up @@ -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)
4 changes: 3 additions & 1 deletion pytest_trio/_tests/test_sync_fixture.py
Expand Up @@ -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"]
)
41 changes: 30 additions & 11 deletions 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
Expand All @@ -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
Expand Down Expand Up @@ -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
################################################################
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down