diff --git a/changelog/11907.bugfix.rst b/changelog/11907.bugfix.rst new file mode 100644 index 00000000000..10aa39aeb0c --- /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 aa58d43b405..93cf0737b04 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -20,6 +20,7 @@ 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 @@ def __exit__( __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 e2b2eb8a2d5..51da8035086 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -4,9 +4,10 @@ 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: @@ -477,3 +478,68 @@ def test_catch_warning_within_raise(self) -> None: with pytest.raises(ValueError, match="some exception"): 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()