diff --git a/changelog/11907.bugfix.rst b/changelog/11907.bugfix.rst new file mode 100644 index 000000000..10aa39aeb --- /dev/null +++ b/changelog/11907.bugfix.rst @@ -0,0 +1 @@ +Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 62df274bd..ddb240eb2 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -20,6 +20,7 @@ import warnings from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture +from _pytest.outcomes import Exit from _pytest.outcomes import fail @@ -302,6 +303,17 @@ class WarningsChecker(WarningsRecorder): __tracebackhide__ = True + # BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within + # pytest.warns should *not* trigger "DID NOT WARN" and get suppressed + # when the warning doesn't happen. Control-flow exceptions should always + # propagate. + if exc_val is not None and ( + not isinstance(exc_val, Exception) + # Exit is an Exception, not a BaseException, for some reason. + or isinstance(exc_val, Exit) + ): + return + def found_str(): return pformat([record.message for record in self], indent=2) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index e269bd7dd..1feb3e654 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -5,9 +5,10 @@ from typing import Optional from typing import Type import warnings -from _pytest.pytester import Pytester -from _pytest.recwarn import WarningsRecorder import pytest +from pytest import ExitCode +from pytest import Pytester +from pytest import WarningsRecorder def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None: @@ -479,6 +480,71 @@ class TestWarns: warnings.warn("some warning", category=FutureWarning) raise ValueError("some exception") + def test_skip_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + pytest.skip("this is OK") + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.OK + result.assert_outcomes(skipped=1) + + def test_fail_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + pytest.fail("BOOM") + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.TESTS_FAILED + result.assert_outcomes(failed=1) + assert "DID NOT WARN" not in str(result.stdout) + + def test_exit_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + pytest.exit() + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.INTERRUPTED + result.assert_outcomes() + + def test_keyboard_interrupt_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + raise KeyboardInterrupt() + """, + ) + + result = pytester.runpytest_subprocess() + assert result.ret == ExitCode.INTERRUPTED + result.assert_outcomes() + def test_raise_type_error_on_non_string_warning() -> None: """Check pytest.warns validates warning messages are strings (#10865)."""