diff --git a/ci.sh b/ci.sh index 2601b1dcdd..499868ba45 100755 --- a/ci.sh +++ b/ci.sh @@ -143,7 +143,7 @@ else cd empty INSTALLDIR=$(python -c "import os, trio; print(os.path.dirname(trio.__file__))") - cp ../setup.cfg $INSTALLDIR + cp ../pyproject.toml $INSTALLDIR # We have to copy .coveragerc into this directory, rather than passing # --cov-config=../.coveragerc to pytest, because codecov.sh will run # 'coverage xml' to generate the report that it uses, and that will only diff --git a/pyproject.toml b/pyproject.toml index 8597a4f545..1378e5df7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,3 +50,10 @@ directory = "misc" name = "Miscellaneous internal changes" showcontent = true +[tool.pytest.ini_options] +addopts = ["--strict-markers", "--strict-config"] +xfail_strict = true +faulthandler_timeout = 60 +markers = ["redistributors_should_skip: tests that should be skipped by downstream redistributors"] +junit_family = "xunit2" +filterwarnings = ["error"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6f79c6eab9..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[tool:pytest] -xfail_strict = true -faulthandler_timeout=60 -markers = - redistributors_should_skip: tests that should be skipped by downstream redistributors -junit_family = xunit2 -addopts = - -p no:unraisableexception - -p no:threadexception diff --git a/trio/_core/tests/test_asyncgen.py b/trio/_core/tests/test_asyncgen.py index 1f886e11ab..6ce0af366f 100644 --- a/trio/_core/tests/test_asyncgen.py +++ b/trio/_core/tests/test_asyncgen.py @@ -5,7 +5,7 @@ from functools import partial from async_generator import aclosing from ... import _core -from .tutil import gc_collect_harder, buggy_pypy_asyncgens +from .tutil import gc_collect_harder, buggy_pypy_asyncgens, restore_unraisablehook def test_asyncgen_basics(): @@ -94,8 +94,9 @@ async def agen(): record.append("crashing") raise ValueError("oops") - await agen().asend(None) - gc_collect_harder() + with restore_unraisablehook(): + await agen().asend(None) + gc_collect_harder() await _core.wait_all_tasks_blocked() assert record == ["crashing"] exc_type, exc_value, exc_traceback = caplog.records[0].exc_info @@ -170,6 +171,7 @@ async def async_main(): assert record == ["innermost"] + list(range(100)) +@restore_unraisablehook() def test_last_minute_gc_edge_case(): saved = [] record = [] @@ -267,17 +269,18 @@ async def awaits_after_yield(): yield 42 await _core.cancel_shielded_checkpoint() - await step_outside_async_context(well_behaved()) - gc_collect_harder() - assert capsys.readouterr().err == "" + with restore_unraisablehook(): + await step_outside_async_context(well_behaved()) + gc_collect_harder() + assert capsys.readouterr().err == "" - await step_outside_async_context(yields_after_yield()) - gc_collect_harder() - assert "ignored GeneratorExit" in capsys.readouterr().err + await step_outside_async_context(yields_after_yield()) + gc_collect_harder() + assert "ignored GeneratorExit" in capsys.readouterr().err - await step_outside_async_context(awaits_after_yield()) - gc_collect_harder() - assert "awaited something during finalization" in capsys.readouterr().err + await step_outside_async_context(awaits_after_yield()) + gc_collect_harder() + assert "awaited something during finalization" in capsys.readouterr().err @pytest.mark.skipif(buggy_pypy_asyncgens, reason="pypy 7.2.0 is buggy") @@ -307,10 +310,11 @@ async def async_main(): await _core.wait_all_tasks_blocked() assert record == ["trio collected ours"] - old_hooks = sys.get_asyncgen_hooks() - sys.set_asyncgen_hooks(my_firstiter, my_finalizer) - try: - _core.run(async_main) - finally: - assert sys.get_asyncgen_hooks() == (my_firstiter, my_finalizer) - sys.set_asyncgen_hooks(*old_hooks) + with restore_unraisablehook(): + old_hooks = sys.get_asyncgen_hooks() + sys.set_asyncgen_hooks(my_firstiter, my_finalizer) + try: + _core.run(async_main) + finally: + assert sys.get_asyncgen_hooks() == (my_firstiter, my_finalizer) + sys.set_asyncgen_hooks(*old_hooks) diff --git a/trio/_core/tests/test_guest_mode.py b/trio/_core/tests/test_guest_mode.py index c9701e7cdd..0184ff3103 100644 --- a/trio/_core/tests/test_guest_mode.py +++ b/trio/_core/tests/test_guest_mode.py @@ -13,7 +13,7 @@ import trio import trio.testing -from .tutil import gc_collect_harder, buggy_pypy_asyncgens +from .tutil import gc_collect_harder, buggy_pypy_asyncgens, restore_unraisablehook from ..._util import signal_raise # The simplest possible "host" loop. @@ -277,6 +277,7 @@ def after_io_wait(self, timeout): assert trivial_guest_run(trio_main) == "ok" +@restore_unraisablehook() def test_guest_warns_if_abandoned(): # This warning is emitted from the garbage collector. So we have to make # sure that our abandoned run is garbage. The easiest way to do this is to @@ -506,6 +507,7 @@ async def trio_main(in_host): sys.implementation.name == "pypy" and sys.version_info >= (3, 7), reason="async generator issue under investigation", ) +@restore_unraisablehook() def test_guest_mode_asyncgens(): import sniffio diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index d2bcdfd740..8d92e6cec4 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -23,6 +23,7 @@ gc_collect_harder, ignore_coroutine_never_awaited_warnings, buggy_pypy_asyncgens, + restore_unraisablehook, ) from ... import _core @@ -844,6 +845,7 @@ async def stubborn_sleeper(): assert record == ["sleep", "woke", "cancelled"] +@restore_unraisablehook() def test_broken_abort(): async def main(): # These yields are here to work around an annoying warning -- we're @@ -870,6 +872,7 @@ async def main(): gc_collect_harder() +@restore_unraisablehook() def test_error_in_run_loop(): # Blow stuff up real good to check we at least get a TrioInternalError async def main(): @@ -2154,6 +2157,7 @@ def abort_fn(_): assert abort_fn_called +@restore_unraisablehook() def test_async_function_implemented_in_C(): # These used to crash because we'd try to mutate the coroutine object's # cr_frame, but C functions don't have Python frames. diff --git a/trio/_core/tests/test_thread_cache.py b/trio/_core/tests/test_thread_cache.py index 0f6e0a0715..d60288b3c1 100644 --- a/trio/_core/tests/test_thread_cache.py +++ b/trio/_core/tests/test_thread_cache.py @@ -3,8 +3,9 @@ from queue import Queue import time import sys +from contextlib import contextmanager -from .tutil import slow, gc_collect_harder +from .tutil import slow, gc_collect_harder, disable_threading_excepthook from .. import _thread_cache from .._thread_cache import start_thread_soon, ThreadCache @@ -99,6 +100,17 @@ def test_idle_threads_exit(monkeypatch): assert not seen_thread.is_alive() +@contextmanager +def _join_started_threads(): + before = frozenset(threading.enumerate()) + try: + yield + finally: + for thread in threading.enumerate(): + if thread not in before: + thread.join() + + def test_race_between_idle_exit_and_job_assignment(monkeypatch): # This is a lock where the first few times you try to acquire it with a # timeout, it waits until the lock is available and then pretends to time @@ -138,13 +150,15 @@ def release(self): monkeypatch.setattr(_thread_cache, "Lock", JankyLock) - tc = ThreadCache() - done = threading.Event() - tc.start_thread_soon(lambda: None, lambda _: done.set()) - done.wait() - # Let's kill the thread we started, so it doesn't hang around until the - # test suite finishes. Doesn't really do any harm, but it can be confusing - # to see it in debug output. This is hacky, and leaves our ThreadCache - # object in an inconsistent state... but it doesn't matter, because we're - # not going to use it again anyway. - tc.start_thread_soon(lambda: None, lambda _: sys.exit()) + with disable_threading_excepthook(), _join_started_threads(): + tc = ThreadCache() + done = threading.Event() + tc.start_thread_soon(lambda: None, lambda _: done.set()) + done.wait() + # Let's kill the thread we started, so it doesn't hang around until the + # test suite finishes. Doesn't really do any harm, but it can be confusing + # to see it in debug output. This is hacky, and leaves our ThreadCache + # object in an inconsistent state... but it doesn't matter, because we're + # not going to use it again anyway. + + tc.start_thread_soon(lambda: None, lambda _: sys.exit()) diff --git a/trio/_core/tests/test_windows.py b/trio/_core/tests/test_windows.py index e6bab82204..bd81ef0f33 100644 --- a/trio/_core/tests/test_windows.py +++ b/trio/_core/tests/test_windows.py @@ -8,7 +8,7 @@ # Mark all the tests in this file as being windows-only pytestmark = pytest.mark.skipif(not on_windows, reason="windows only") -from .tutil import slow, gc_collect_harder +from .tutil import slow, gc_collect_harder, restore_unraisablehook from ... import _core, sleep, move_on_after from ...testing import wait_all_tasks_blocked @@ -111,6 +111,7 @@ def pipe_with_overlapped_read(): kernel32.CloseHandle(ffi.cast("HANDLE", write_handle)) +@restore_unraisablehook() def test_forgot_to_register_with_iocp(): with pipe_with_overlapped_read() as (write_fp, read_handle): with write_fp: diff --git a/trio/_core/tests/tutil.py b/trio/_core/tests/tutil.py index 00669e883e..7f51750869 100644 --- a/trio/_core/tests/tutil.py +++ b/trio/_core/tests/tutil.py @@ -1,5 +1,6 @@ # Utilities for testing import socket as stdlib_socket +import threading import os import sys from typing import TYPE_CHECKING @@ -80,6 +81,40 @@ def ignore_coroutine_never_awaited_warnings(): gc_collect_harder() +def _noop(*args, **kwargs): + pass + + +if sys.version_info >= (3, 8): + + @contextmanager + def restore_unraisablehook(): + sys.unraisablehook, prev = sys.__unraisablehook__, sys.unraisablehook + try: + yield + finally: + sys.unraisablehook = prev + + @contextmanager + def disable_threading_excepthook(): + threading.excepthook, prev = _noop, threading.excepthook + try: + yield + finally: + threading.excepthook = prev + + +else: + + @contextmanager + def restore_unraisablehook(): # pragma: no cover + yield + + @contextmanager + def disable_threading_excepthook(): # pragma: no cover + yield + + # template is like: # [1, {2.1, 2.2}, 3] -> matches [1, 2.1, 2.2, 3] or [1, 2.2, 2.1, 3] def check_sequence_matches(seq, template):