From fef1f09475c544c8bd26f0d417ced29d1d620d2b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 07:58:15 +0000 Subject: [PATCH 01/18] apply warnings filter as soon as possible, and remove it as late as possible --- src/_pytest/warnings.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 64ea3ff222d..07f07e86558 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Generator -from contextlib import contextmanager +from contextlib import contextmanager, ExitStack import sys from typing import Literal import warnings @@ -131,3 +131,12 @@ def pytest_load_initial_conftests( config=early_config, ihook=early_config.hook, when="config", item=None ): return (yield) + +def pytest_configure(config: Config) -> None: + with ExitStack() as stack: + stack.enter_context( + catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ) + ) + config.add_cleanup(stack.pop_all().close) From 229a64b0c208849d274177805c9f9843315906ee Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 07:59:22 +0000 Subject: [PATCH 02/18] apply unraisablehook and threadexception before the warning plugin, so gc.collect() is called with the warning filters in place --- src/_pytest/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6160f780b1b..3d4befcba49 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -264,11 +264,11 @@ def directory_arg(path: str, optname: str) -> str: "setuponly", "setupplan", "stepwise", + "unraisableexception", + "threadexception", "warnings", "logging", "reports", - "unraisableexception", - "threadexception", "faulthandler", ) From 10eb342d9e68adb87692d3848b4ad788f5cf4dec Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 07:59:51 +0000 Subject: [PATCH 03/18] apply the warning filter during pytest_configure --- src/_pytest/config/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3d4befcba49..0161f5952b8 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1112,9 +1112,7 @@ def add_cleanup(self, func: Callable[[], None]) -> None: def _do_configure(self) -> None: assert not self._configured self._configured = True - with warnings.catch_warnings(): - warnings.simplefilter("default") - self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) def _ensure_unconfigure(self) -> None: try: From 11714ddb8494069f2ec7c7fdd3e2351bd0910d56 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:04:58 +0000 Subject: [PATCH 04/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/warnings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 07f07e86558..c39ff5870df 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -2,7 +2,8 @@ from __future__ import annotations from collections.abc import Generator -from contextlib import contextmanager, ExitStack +from contextlib import contextmanager +from contextlib import ExitStack import sys from typing import Literal import warnings @@ -132,6 +133,7 @@ def pytest_load_initial_conftests( ): return (yield) + def pytest_configure(config: Config) -> None: with ExitStack() as stack: stack.enter_context( From cc5b55591f5cba7f00b8a3ed2d295001670c2707 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 08:08:00 +0000 Subject: [PATCH 05/18] restore the filterwarnings marker config --- src/_pytest/warnings.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index c39ff5870df..5b5d5ba36d2 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -18,14 +18,6 @@ import pytest -def pytest_configure(config: Config) -> None: - config.addinivalue_line( - "markers", - "filterwarnings(warning): add a warning filter to the given test. " - "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ", - ) - - @contextmanager def catch_warnings_for_item( config: Config, @@ -141,4 +133,9 @@ def pytest_configure(config: Config) -> None: config=config, ihook=config.hook, when="config", item=None ) ) + config.addinivalue_line( + "markers", + "filterwarnings(warning): add a warning filter to the given test. " + "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ", + ) config.add_cleanup(stack.pop_all().close) From a3881db34ec2d327623a5408adbfc243ea70ef07 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 08:21:34 +0000 Subject: [PATCH 06/18] move lsof warning to something ignorable and ignore it --- pyproject.toml | 3 +++ src/_pytest/pytester.py | 4 ++-- src/_pytest/warning_types.py | 5 +++++ src/pytest/__init__.py | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dce6a0870e1..246c04d5990 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -403,6 +403,9 @@ filterwarnings = [ "ignore:VendorImporter\\.find_spec\\(\\) not found; falling back to find_module\\(\\):ImportWarning", # https://github.com/pytest-dev/execnet/pull/127 "ignore:isSet\\(\\) is deprecated, use is_set\\(\\) instead:DeprecationWarning", + # https://github.com/pytest-dev/pytest/issues/2366 + # https://github.com/pytest-dev/pytest/pull/13057 + "default:.*:pytest.PytestFDWarning", ] pytester_example_dir = "testing/example_scripts" markers = [ diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 412d850d2da..59839562031 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -65,7 +65,7 @@ from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import PytestFDWarning if TYPE_CHECKING: @@ -188,7 +188,7 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object] "*** function {}:{}: {} ".format(*item.location), "See issue #2366", ] - item.warn(PytestWarning("\n".join(error))) + item.warn(PytestFDWarning("\n".join(error))) # used at least by pytest-xdist plugin diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index b8e9998cd2e..119fa490e66 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -123,6 +123,11 @@ def format(self, **kwargs: Any) -> _W: return self.category(self.template.format(**kwargs)) +@final +class PytestFDWarning(PytestWarning): + pass + + def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None: """ Issue the warning :param:`message` for the definition of the given :param:`method` diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index f0c3516f4cc..70096d6593e 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -77,6 +77,7 @@ from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning +from _pytest.warning_types import PytestFDWarning from _pytest.warning_types import PytestRemovedIn9Warning from _pytest.warning_types import PytestUnhandledThreadExceptionWarning from _pytest.warning_types import PytestUnknownMarkWarning @@ -124,6 +125,7 @@ "PytestConfigWarning", "PytestDeprecationWarning", "PytestExperimentalApiWarning", + "PytestFDWarning", "PytestPluginManager", "PytestRemovedIn9Warning", "PytestUnhandledThreadExceptionWarning", From f37d07ac9135a765d41a451cc6a296f91e741ef1 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 08:25:24 +0000 Subject: [PATCH 07/18] add newsfragment --- changelog/10404.bugfix.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/10404.bugfix.rst diff --git a/changelog/10404.bugfix.rst b/changelog/10404.bugfix.rst new file mode 100644 index 00000000000..2d24a658e2f --- /dev/null +++ b/changelog/10404.bugfix.rst @@ -0,0 +1,5 @@ +Apply filterwarnings from config/cli as soon as possible, and revert them as late as possible +so that warnings as errors are collected throughout the pytest run and before the +unraisable and threadexcept hooks are removed. + +This allows very late warnings and unraisable/threadexcept exceptions to fail the test suite. From 9a953f486adc6aa5ef148f33d51a7b080631f95c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 08:30:07 +0000 Subject: [PATCH 08/18] add test --- testing/test_unraisableexception.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 5aa6aa773d9..65d77c7b634 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -235,6 +235,38 @@ def test_it(): result.assert_outcomes(passed=1) result.stderr.fnmatch_lines("ValueError: del is broken") +@pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") +def test_create_task_unraisable_warning_filter(pytester: Pytester) -> None: + # see: https://github.com/pytest-dev/pytest/issues/10404 + pytester.makepyfile( + test_it=""" + import pytest + + class BrokenDel: + def __init__(self): + self.self = self # make a reference cycle + + def __del__(self): + raise ValueError("del is broken") + + def test_it(): + BrokenDel() + """ + ) + + was_enabled = gc.isenabled() + gc.disable() + try: + result = pytester.runpytest("-Werror") + finally: + if was_enabled: + gc.enable() + + # TODO: should be a test failure or error + assert result.ret == pytest.ExitCode.INTERNAL_ERROR + + result.assert_outcomes(passed=1) + result.stderr.fnmatch_lines("ValueError: del is broken") @pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") def test_possibly_none_excinfo(pytester: Pytester) -> None: From 758113788fd72b8b3be7a7541a9fb8146c55000f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:30:35 +0000 Subject: [PATCH 09/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_unraisableexception.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 65d77c7b634..1d12cc54975 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -235,6 +235,7 @@ def test_it(): result.assert_outcomes(passed=1) result.stderr.fnmatch_lines("ValueError: del is broken") + @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_create_task_unraisable_warning_filter(pytester: Pytester) -> None: # see: https://github.com/pytest-dev/pytest/issues/10404 @@ -268,6 +269,7 @@ def test_it(): result.assert_outcomes(passed=1) result.stderr.fnmatch_lines("ValueError: del is broken") + @pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") def test_possibly_none_excinfo(pytester: Pytester) -> None: pytester.makepyfile( From b32316cb73d0100383b6d15a0635efee4ff46f2c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 08:31:57 +0000 Subject: [PATCH 10/18] simplify PytestFDWarning filter --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 246c04d5990..d75ada222cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -405,7 +405,7 @@ filterwarnings = [ "ignore:isSet\\(\\) is deprecated, use is_set\\(\\) instead:DeprecationWarning", # https://github.com/pytest-dev/pytest/issues/2366 # https://github.com/pytest-dev/pytest/pull/13057 - "default:.*:pytest.PytestFDWarning", + "default::pytest.PytestFDWarning", ] pytester_example_dir = "testing/example_scripts" markers = [ From 72475f4e32f54b5c7b1bfe77fd579dfe7623debf Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 08:57:44 +0000 Subject: [PATCH 11/18] fix PytestFDWarning module --- src/_pytest/warning_types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 119fa490e66..8c9ff2d9a36 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -125,7 +125,9 @@ def format(self, **kwargs: Any) -> _W: @final class PytestFDWarning(PytestWarning): - pass + """When the lsof plugin finds leaked fds.""" + + __module__ = "pytest" def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None: From e2e81b23b4338fce266460d34a940aab27f01ee3 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 13 Dec 2024 10:04:30 +0000 Subject: [PATCH 12/18] get more patch coverage --- testing/test_unraisableexception.py | 34 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 1d12cc54975..f84c20662c7 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -1,5 +1,7 @@ from __future__ import annotations +from collections.abc import Generator +import contextlib import gc import sys from unittest import mock @@ -203,6 +205,24 @@ class MyError(BaseException): ) +def _set_gc_state(enabled: bool) -> bool: + was_enabled = gc.isenabled() + if enabled: + gc.enable() + else: + gc.disable() + return was_enabled + + +@contextlib.contextmanager +def _disable_gc() -> Generator[None]: + was_enabled = _set_gc_state(enabled=False) + try: + yield + finally: + _set_gc_state(enabled=was_enabled) + + def test_create_task_unraisable(pytester: Pytester) -> None: # see: https://github.com/pytest-dev/pytest/issues/10404 pytester.makepyfile( @@ -221,13 +241,8 @@ def test_it(): """ ) - was_enabled = gc.isenabled() - gc.disable() - try: + with _disable_gc(): result = pytester.runpytest() - finally: - if was_enabled: - gc.enable() # TODO: should be a test failure or error assert result.ret == pytest.ExitCode.INTERNAL_ERROR @@ -255,13 +270,8 @@ def test_it(): """ ) - was_enabled = gc.isenabled() - gc.disable() - try: + with _disable_gc(): result = pytester.runpytest("-Werror") - finally: - if was_enabled: - gc.enable() # TODO: should be a test failure or error assert result.ret == pytest.ExitCode.INTERNAL_ERROR From b5e00fb3b256bf0910977c1e9d9dbb8e39bfe822 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 17 Dec 2024 09:47:02 +0000 Subject: [PATCH 13/18] Update changelog/10404.bugfix.rst --- changelog/10404.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog/10404.bugfix.rst b/changelog/10404.bugfix.rst index 2d24a658e2f..4c98ea03d64 100644 --- a/changelog/10404.bugfix.rst +++ b/changelog/10404.bugfix.rst @@ -3,3 +3,5 @@ so that warnings as errors are collected throughout the pytest run and before th unraisable and threadexcept hooks are removed. This allows very late warnings and unraisable/threadexcept exceptions to fail the test suite. + +This also changes the warning that the lsof plugin issues from PytestWarning to the new warning PytestFDWarning so it can be more easily filtered. From a00679a23715ca126b84b785089bebb85322b20b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 17 Dec 2024 10:26:17 +0000 Subject: [PATCH 14/18] add more realistic create_task/unawaited coro warning test --- testing/test_unraisableexception.py | 31 +++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index f84c20662c7..b36748a22de 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -223,7 +223,7 @@ def _disable_gc() -> Generator[None]: _set_gc_state(enabled=was_enabled) -def test_create_task_unraisable(pytester: Pytester) -> None: +def test_refcycle_unraisable(pytester: Pytester) -> None: # see: https://github.com/pytest-dev/pytest/issues/10404 pytester.makepyfile( test_it=""" @@ -252,7 +252,7 @@ def test_it(): @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") -def test_create_task_unraisable_warning_filter(pytester: Pytester) -> None: +def test_refcycle_unraisable_warning_filter(pytester: Pytester) -> None: # see: https://github.com/pytest-dev/pytest/issues/10404 pytester.makepyfile( test_it=""" @@ -280,6 +280,33 @@ def test_it(): result.stderr.fnmatch_lines("ValueError: del is broken") +@pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") +def test_create_task_raises_unraisable_warning_filter(pytester: Pytester) -> None: + # see: https://github.com/pytest-dev/pytest/issues/10404 + pytester.makepyfile( + test_it=""" + import asyncio + import pytest + + async def my_task(): + pass + + def test_scheduler_must_be_created_within_running_loop() -> None: + with pytest.raises(RuntimeError) as _: + asyncio.create_task(my_task()) + """ + ) + + with _disable_gc(): + result = pytester.runpytest("-Werror") + + # TODO: should be a test failure or error + assert result.ret == pytest.ExitCode.INTERNAL_ERROR + + result.assert_outcomes(passed=1) + result.stderr.fnmatch_lines("RuntimeWarning: coroutine 'my_task' was never awaited") + + @pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") def test_possibly_none_excinfo(pytester: Pytester) -> None: pytester.makepyfile( From e828fa941d5a1fd85cc4545bfe717b89210a7ad0 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 17 Dec 2024 10:28:33 +0000 Subject: [PATCH 15/18] update comments --- testing/test_unraisableexception.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index b36748a22de..85eccc5bad0 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -253,6 +253,8 @@ def test_it(): @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_refcycle_unraisable_warning_filter(pytester: Pytester) -> None: + # note that the host pytest warning filter is disabled and the pytester + # warning filter applies during config teardown of unraisablehook. # see: https://github.com/pytest-dev/pytest/issues/10404 pytester.makepyfile( test_it=""" @@ -282,7 +284,11 @@ def test_it(): @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_create_task_raises_unraisable_warning_filter(pytester: Pytester) -> None: + # note that the host pytest warning filter is disabled and the pytester + # warning filter applies during config teardown of unraisablehook. # see: https://github.com/pytest-dev/pytest/issues/10404 + # This is a dupe of the above test, but using the exact reproducer from + # the issue pytester.makepyfile( test_it=""" import asyncio From 003a51faaa5eb41afedb48e3ca3cc6fa5bc4b41b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 17 Dec 2024 10:31:26 +0000 Subject: [PATCH 16/18] enable coverage on lsof tests --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4058716b47d..5814c81fb30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,6 +123,7 @@ jobs: python: "3.9" os: ubuntu-latest tox_env: "py39-lsof-numpy-pexpect" + use_coverage: true - name: "ubuntu-py39-pluggy" python: "3.9" From 808ad3de45080a8ebe066d497051385c18b2b783 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Wed, 18 Dec 2024 06:46:45 +0000 Subject: [PATCH 17/18] ensure global warning filter application does not swallow default warnings --- src/_pytest/warnings.py | 37 +++++++++++++++++++---------- testing/test_unraisableexception.py | 31 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 5b5d5ba36d2..806681a5020 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -24,6 +24,8 @@ def catch_warnings_for_item( ihook, when: Literal["config", "collect", "runtest"], item: Item | None, + *, + record: bool = True, ) -> Generator[None]: """Context manager that catches warnings generated in the contained execution block. @@ -33,10 +35,7 @@ def catch_warnings_for_item( """ config_filters = config.getini("filterwarnings") cmdline_filters = config.known_args_namespace.pythonwarnings or [] - with warnings.catch_warnings(record=True) as log: - # mypy can't infer that record=True means log is not None; help it. - assert log is not None - + with warnings.catch_warnings(record=record) as log: if not sys.warnoptions: # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908). warnings.filterwarnings("always", category=DeprecationWarning) @@ -57,15 +56,19 @@ def catch_warnings_for_item( try: yield finally: - for warning_message in log: - ihook.pytest_warning_recorded.call_historic( - kwargs=dict( - warning_message=warning_message, - nodeid=nodeid, - when=when, - location=None, + if record: + # mypy can't infer that record=True means log is not None; help it. + assert log is not None + + for warning_message in log: + ihook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=warning_message, + nodeid=nodeid, + when=when, + location=None, + ) ) - ) def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: @@ -130,7 +133,15 @@ def pytest_configure(config: Config) -> None: with ExitStack() as stack: stack.enter_context( catch_warnings_for_item( - config=config, ihook=config.hook, when="config", item=None + config=config, + ihook=config.hook, + when="config", + item=None, + # this disables recording because the terminalreporter has + # finished by the time it comes to reporting logged warnings + # from the end of config cleanup. So for now, this is only + # useful for setting a warning filter with an 'error' action. + record=False, ) ) config.addinivalue_line( diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 85eccc5bad0..ae8cb8782dc 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -313,6 +313,37 @@ def test_scheduler_must_be_created_within_running_loop() -> None: result.stderr.fnmatch_lines("RuntimeWarning: coroutine 'my_task' was never awaited") +def test_refcycle_unraisable_warning_filter_default(pytester: Pytester) -> None: + # note this time we use a default warning filter for pytester + # and run it in a subprocess, because the warning can only go to the + # sys.stdout rather than the terminal reporter, which has already + # finished. + # see: https://github.com/pytest-dev/pytest/pull/13057#discussion_r1888396126 + pytester.makepyfile( + test_it=""" + import pytest + + class BrokenDel: + def __init__(self): + self.self = self # make a reference cycle + + def __del__(self): + raise ValueError("del is broken") + + def test_it(): + BrokenDel() + """ + ) + + with _disable_gc(): + result = pytester.runpytest_subprocess("-Wdefault") + + assert result.ret == pytest.ExitCode.OK + + result.assert_outcomes(passed=1) + result.stderr.fnmatch_lines("ValueError: del is broken") + + @pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") def test_possibly_none_excinfo(pytester: Pytester) -> None: pytester.makepyfile( From c5fb1580601467738b4fc17899d29d174eb5c300 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Wed, 18 Dec 2024 09:42:54 +0000 Subject: [PATCH 18/18] Update testing/test_unraisableexception.py --- testing/test_unraisableexception.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index ae8cb8782dc..3f191073e2b 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -340,6 +340,8 @@ def test_it(): assert result.ret == pytest.ExitCode.OK + # TODO: should be warnings=1, but the outcome has already come out + # by the time the warning triggers result.assert_outcomes(passed=1) result.stderr.fnmatch_lines("ValueError: del is broken")