Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
30a7a40
Add RaisesGroup, a helper for catching ExceptionGroups in tests
jakkdl Dec 6, 2023
0d08e19
fix some CI test errors, now only some grumpy export tests left
jakkdl Dec 6, 2023
8f1e221
clarify mixed loose test
jakkdl Dec 6, 2023
f7ec079
rename variable to fit new class name
jakkdl Dec 6, 2023
c7c6264
ignore RaisesGroup in test_exports for now
jakkdl Dec 9, 2023
49af03d
fix test_export fail
jakkdl Dec 9, 2023
3f63030
fix pyright --verifytypes errors
jakkdl Dec 9, 2023
3adc49a
un-unittest the tests
jakkdl Dec 9, 2023
cd9d3a5
add test for _ExceptionInfo (and fix it)
jakkdl Dec 9, 2023
e9688be
rewrite not to use any in assert, since codecov doesn't like it
jakkdl Dec 9, 2023
97fb79b
* Split out type tests
jakkdl Dec 10, 2023
5f0298b
Merge remote-tracking branch 'origin/master' into raisesgroup
jakkdl Dec 16, 2023
faaebf5
rewrite another test to use RaisesGroup
jakkdl Dec 16, 2023
22d8b5a
add new classes to docs
jakkdl Dec 22, 2023
32d079a
Merge remote-tracking branch 'origin/master' into raisesgroup
jakkdl Dec 22, 2023
43a51b6
Fix ruff issues
CoolCat467 Dec 22, 2023
3cc15e6
add fix for TracebackType
jakkdl Dec 22, 2023
efbccbc
cover __str__ of Matcher with no type specified
jakkdl Dec 22, 2023
1261dc1
properly "fix" sphinx+TracebackType
jakkdl Dec 22, 2023
ccdb79d
add newsfragment
jakkdl Dec 22, 2023
7956848
fix url in newsfragment
jakkdl Dec 23, 2023
d6d7595
Merge branch 'master' into raisesgroup
CoolCat467 Dec 28, 2023
4990a7d
Add overloads to Matcher() to precisely check init parameters
TeamSpen210 Dec 29, 2023
86287a5
Pre-compile strings passed to Matcher(), but unwrap in __str__
TeamSpen210 Dec 29, 2023
663c12b
update comment, add pyright: ignore
jakkdl Dec 30, 2023
148df67
fix formatting, move matcher_tostring test to test_testing_raisesgroup
jakkdl Dec 30, 2023
c8f7983
add docstrings
jakkdl Jan 3, 2024
9b66187
Apply suggestions from code review
jakkdl Jan 5, 2024
d020655
add comments
jakkdl Jan 5, 2024
957b4fd
switch to tell type checkers that we always use _ExceptionInfo. Add n…
jakkdl Jan 5, 2024
614f85a
fix broken test
jakkdl Jan 5, 2024
80b7031
fix newsfragment quoting of pytest
jakkdl Jan 5, 2024
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
110 changes: 41 additions & 69 deletions src/trio/_core/_tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@
import sniffio

from ... import _core
from ..._core._multierror import MultiError, NonBaseMultiError
from ..._threads import to_thread_run_sync
from ..._timeouts import fail_after, sleep
from ...testing import Sequencer, assert_checkpoints, wait_all_tasks_blocked
from ...testing import (
Matcher,
RaisesGroup,
Sequencer,
assert_checkpoints,
wait_all_tasks_blocked,
)
from .._run import DEADLINE_HEAP_MIN_PRUNE_THRESHOLD
from .tutil import (
buggy_pypy_asyncgens,
Expand Down Expand Up @@ -195,13 +200,8 @@ async def main() -> NoReturn:
nursery.start_soon(crasher)
raise KeyError

with pytest.raises(MultiError) as excinfo:
with RaisesGroup(ValueError, KeyError):
_core.run(main)
print(excinfo.value)
assert {type(exc) for exc in excinfo.value.exceptions} == {
ValueError,
KeyError,
}


def test_two_child_crashes() -> None:
Expand All @@ -213,12 +213,8 @@ async def main() -> None:
nursery.start_soon(crasher, KeyError)
nursery.start_soon(crasher, ValueError)

with pytest.raises(MultiError) as excinfo:
with RaisesGroup(ValueError, KeyError):
_core.run(main)
assert {type(exc) for exc in excinfo.value.exceptions} == {
ValueError,
KeyError,
}


async def test_child_crash_wakes_parent() -> None:
Expand Down Expand Up @@ -437,7 +433,13 @@ async def crasher() -> NoReturn:
# KeyError from crasher()
with pytest.raises(KeyError):
with _core.CancelScope() as outer:
try:
# Since the outer scope became cancelled before the
# nursery block exited, all cancellations inside the
# nursery block continue propagating to reach the
# outer scope.
with RaisesGroup(
_core.Cancelled, _core.Cancelled, _core.Cancelled, KeyError
) as excinfo:
async with _core.open_nursery() as nursery:
# Two children that get cancelled by the nursery scope
nursery.start_soon(sleep_forever) # t1
Expand All @@ -451,20 +453,7 @@ async def crasher() -> NoReturn:
# And one that raises a different error
nursery.start_soon(crasher) # t4
# and then our __aexit__ also receives an outer Cancelled
except MultiError as multi_exc:
# Since the outer scope became cancelled before the
# nursery block exited, all cancellations inside the
# nursery block continue propagating to reach the
# outer scope.
assert len(multi_exc.exceptions) == 4
summary: dict[type, int] = {}
for exc in multi_exc.exceptions:
summary.setdefault(type(exc), 0)
summary[type(exc)] += 1
assert summary == {_core.Cancelled: 3, KeyError: 1}
raise
else:
raise AssertionError("No ExceptionGroup")
raise excinfo.value


async def test_precancelled_task() -> None:
Expand Down Expand Up @@ -784,14 +773,22 @@ async def task2() -> None:
RuntimeError, match="which had already been exited"
) as exc_info:
await nursery_mgr.__aexit__(*sys.exc_info())
assert type(exc_info.value.__context__) is NonBaseMultiError
assert len(exc_info.value.__context__.exceptions) == 3
cancelled_in_context = False
for exc in exc_info.value.__context__.exceptions:
assert isinstance(exc, RuntimeError)
assert "closed before the task exited" in str(exc)
cancelled_in_context |= isinstance(exc.__context__, _core.Cancelled)
assert cancelled_in_context # for the sleep_forever

def no_context(exc: RuntimeError) -> bool:
return exc.__context__ is None

msg = "closed before the task exited"
subexceptions = (
Matcher(RuntimeError, match=msg, check=no_context),
Matcher(RuntimeError, match=msg, check=no_context),
# sleep_forever
Matcher(
RuntimeError,
match=msg,
check=lambda x: isinstance(x.__context__, _core.Cancelled),
),
)
assert RaisesGroup(*subexceptions).matches(exc_info.value.__context__)

# Trying to exit a cancel scope from an unrelated task raises an error
# without affecting any state
Expand Down Expand Up @@ -945,11 +942,7 @@ async def main() -> None:
with pytest.raises(_core.TrioInternalError) as excinfo:
_core.run(main)

me = excinfo.value.__cause__
assert isinstance(me, MultiError)
assert len(me.exceptions) == 2
for exc in me.exceptions:
assert isinstance(exc, (KeyError, ValueError))
assert RaisesGroup(KeyError, ValueError).matches(excinfo.value.__cause__)


def test_system_task_crash_plus_Cancelled() -> None:
Expand Down Expand Up @@ -1202,11 +1195,11 @@ async def test_nursery_exception_chaining_doesnt_make_context_loops() -> None:
async def crasher() -> NoReturn:
raise KeyError

with pytest.raises(MultiError) as excinfo:
with RaisesGroup(ValueError, KeyError) as excinfo:
async with _core.open_nursery() as nursery:
nursery.start_soon(crasher)
raise ValueError
# the MultiError should not have the KeyError or ValueError as context
# the ExceptionGroup should not have the KeyError or ValueError as context
assert excinfo.value.__context__ is None


Expand Down Expand Up @@ -1963,11 +1956,10 @@ async def test_nursery_stop_iteration() -> None:
async def fail() -> NoReturn:
raise ValueError

with pytest.raises(ExceptionGroup) as excinfo:
with RaisesGroup(StopIteration, ValueError):
async with _core.open_nursery() as nursery:
nursery.start_soon(fail)
raise StopIteration
assert tuple(map(type, excinfo.value.exceptions)) == (StopIteration, ValueError)


async def test_nursery_stop_async_iteration() -> None:
Expand Down Expand Up @@ -2507,13 +2499,9 @@ async def main() -> NoReturn:
async with _core.open_nursery():
raise Exception("foo")

with pytest.raises(MultiError) as exc:
with RaisesGroup(Matcher(Exception, match="^foo$")):
_core.run(main, strict_exception_groups=True)

assert len(exc.value.exceptions) == 1
assert type(exc.value.exceptions[0]) is Exception
assert exc.value.exceptions[0].args == ("foo",)


def test_run_strict_exception_groups_nursery_override() -> None:
"""
Expand All @@ -2531,14 +2519,10 @@ async def main() -> NoReturn:

async def test_nursery_strict_exception_groups() -> None:
"""Test that strict exception groups can be enabled on a per-nursery basis."""
with pytest.raises(MultiError) as exc:
with RaisesGroup(Matcher(Exception, match="^foo$")):
async with _core.open_nursery(strict_exception_groups=True):
raise Exception("foo")

assert len(exc.value.exceptions) == 1
assert type(exc.value.exceptions[0]) is Exception
assert exc.value.exceptions[0].args == ("foo",)


async def test_nursery_collapse_strict() -> None:
"""
Expand All @@ -2549,7 +2533,7 @@ async def test_nursery_collapse_strict() -> None:
async def raise_error() -> NoReturn:
raise RuntimeError("test error")

with pytest.raises(MultiError) as exc:
with RaisesGroup(RuntimeError, RaisesGroup(RuntimeError)):
async with _core.open_nursery() as nursery:
nursery.start_soon(sleep_forever)
nursery.start_soon(raise_error)
Expand All @@ -2558,13 +2542,6 @@ async def raise_error() -> NoReturn:
nursery2.start_soon(raise_error)
nursery.cancel_scope.cancel()

exceptions = exc.value.exceptions
assert len(exceptions) == 2
assert isinstance(exceptions[0], RuntimeError)
assert isinstance(exceptions[1], MultiError)
assert len(exceptions[1].exceptions) == 1
assert isinstance(exceptions[1].exceptions[0], RuntimeError)


async def test_nursery_collapse_loose() -> None:
"""
Expand All @@ -2575,7 +2552,7 @@ async def test_nursery_collapse_loose() -> None:
async def raise_error() -> NoReturn:
raise RuntimeError("test error")

with pytest.raises(MultiError) as exc:
with RaisesGroup(RuntimeError, RuntimeError):
async with _core.open_nursery() as nursery:
nursery.start_soon(sleep_forever)
nursery.start_soon(raise_error)
Expand All @@ -2584,11 +2561,6 @@ async def raise_error() -> NoReturn:
nursery2.start_soon(raise_error)
nursery.cancel_scope.cancel()

exceptions = exc.value.exceptions
assert len(exceptions) == 2
assert isinstance(exceptions[0], RuntimeError)
assert isinstance(exceptions[1], RuntimeError)


async def test_cancel_scope_no_cancellederror() -> None:
"""
Expand Down
13 changes: 12 additions & 1 deletion src/trio/_tests/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@ def lookup_symbol(symbol: str) -> dict[str, str]:
if class_name.startswith("_"): # pragma: no cover
continue

# ignore class that does dirty tricks
if class_ is trio.testing.RaisesGroup:
continue

# dir() and inspect.getmembers doesn't display properties from the metaclass
# also ignore some dunder methods that tend to differ but are of no consequence
ignore_names = set(dir(type(class_))) | {
Expand Down Expand Up @@ -431,7 +435,9 @@ def lookup_symbol(symbol: str) -> dict[str, str]:
if tool == "mypy" and class_ == trio.Nursery:
extra.remove("cancel_scope")

# TODO: I'm not so sure about these, but should still be looked at.
# These are (mostly? solely?) *runtime* attributes, often set in
# __init__, which doesn't show up with dir() or inspect.getmembers,
# but we get them in the way we query mypy & jedi
EXTRAS = {
trio.DTLSChannel: {"peer_address", "endpoint"},
trio.DTLSEndpoint: {"socket", "incoming_packets_buffer"},
Expand All @@ -446,6 +452,11 @@ def lookup_symbol(symbol: str) -> dict[str, str]:
"send_all_hook",
"wait_send_all_might_not_block_hook",
},
trio.testing.Matcher: {
"exception_type",
"match",
"check",
},
}
if tool == "mypy" and class_ in EXTRAS:
before = len(extra)
Expand Down
1 change: 1 addition & 0 deletions src/trio/_tests/test_highlevel_open_tcp_listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ async def test_open_tcp_listeners_some_address_families_unavailable(
await open_tcp_listeners(80, host="example.org")

assert "This system doesn't support" in str(exc_info.value)
# TODO: remove the `else` with strict_exception_groups=True
if isinstance(exc_info.value.__cause__, BaseExceptionGroup):
for subexc in exc_info.value.__cause__.exceptions:
assert "nope" in str(subexc)
Expand Down
9 changes: 7 additions & 2 deletions src/trio/_tests/test_highlevel_open_tcp_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
reorder_for_rfc_6555_section_5_4,
)
from trio.socket import AF_INET, AF_INET6, IPPROTO_TCP, SOCK_STREAM, SocketType
from trio.testing import Matcher, RaisesGroup

if TYPE_CHECKING:
from trio.testing import MockClock
Expand Down Expand Up @@ -506,8 +507,12 @@ async def test_all_fail(autojump_clock: MockClock) -> None:
expect_error=OSError,
)
assert isinstance(exc, OSError)
assert isinstance(exc.__cause__, BaseExceptionGroup)
assert len(exc.__cause__.exceptions) == 4

subexceptions = (Matcher(OSError, match="^sorry$"),) * 4
assert RaisesGroup(
*subexceptions, match="all attempts to connect to test.example.com:80 failed"
).matches(exc.__cause__)

assert trio.current_time() == (0.1 + 0.2 + 10)
assert scenario.connect_times == {
"1.1.1.1": 0,
Expand Down
Loading