diff --git a/AUTHORS b/AUTHORS index ba5672d4c51..093d5fd472a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -87,6 +87,7 @@ croc100 Cal Leeming Carl Friedrich Bolz Carlos Jenkins +Cen Fangyu (Dmao233) Ceridwen Charles Cloud Charles Machalow diff --git a/changelog/14953.bugfix.rst b/changelog/14953.bugfix.rst new file mode 100644 index 00000000000..bacb105878e --- /dev/null +++ b/changelog/14953.bugfix.rst @@ -0,0 +1 @@ +The :confval:`max_warnings` configuration option now accepts integer values in TOML configuration files, while still accepting strings for backward compatibility. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 362d5c917e2..62165abfb30 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1705,7 +1705,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: max_warnings - :type: ``int`` + :type: ``int | str`` .. versionadded:: 9.1 @@ -3671,7 +3671,7 @@ All the command-line flags can also be obtained by running ``pytest --help``:: Each line specifies a pattern for warnings.filterwarnings. Processed after -W/--pythonwarnings. - max_warnings (string): + max_warnings (int | string): Exit with error if all tests pass but the number of warnings exceeds this threshold norecursedirs (args): Directory patterns to avoid for recursion diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1b337e20c7e..6f2e34ed56e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -141,9 +141,13 @@ def pytest_addoption(parser: Parser) -> None: "warnings.filterwarnings. " "Processed after -W/--pythonwarnings.", ) + # ``int | str`` (not plain ``int``) for backward compatibility: INI files + # and ``-o`` overrides provide the value as a string. parser.addini( "max_warnings", help="Exit with error if all tests pass but the number of warnings exceeds this threshold", + type=int | str, + default=None, ) group = parser.getgroup("collect", "collection") diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 825435225b3..26eb1d341cf 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1093,9 +1093,9 @@ def _get_max_warnings(self) -> int | None: if value is not None: return int(value) ini_value = self.config.getini("max_warnings") - if ini_value: - return int(ini_value) - return None + if ini_value is None: + return None + return int(ini_value) # # Summaries for sessionfinish. diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 017781c2355..402333bb298 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1005,6 +1005,12 @@ def test_two(): warnings.warn(UserWarning("warning two")) """ + ONE_WARNING_PYFILE = """ + import warnings + def test_warning(): + warnings.warn(UserWarning("example warning")) + """ + @pytest.mark.filterwarnings("default::UserWarning") def test_max_warnings_not_set(self, pytester: Pytester) -> None: """Without --max-warnings, warnings don't affect exit code.""" @@ -1145,6 +1151,96 @@ def test_one(): result.assert_outcomes(passed=1, warnings=1) assert result.ret == ExitCode.OK + @pytest.mark.filterwarnings("default::UserWarning") + @pytest.mark.parametrize( + ("writer", "source"), + [ + pytest.param( + "toml", + """ + [pytest] + max_warnings = 0 + """, + id="pytest-toml-int", + ), + pytest.param( + "toml", + """ + [pytest] + max_warnings = "0" + """, + id="pytest-toml-string", + ), + pytest.param( + "pyproject", + """ + [tool.pytest] + max_warnings = 0 + """, + id="pyproject-native-int", + ), + pytest.param( + "pyproject", + """ + [tool.pytest] + max_warnings = "0" + """, + id="pyproject-native-string", + ), + pytest.param( + "ini", + """ + [pytest] + max_warnings = 0 + """, + id="pytest-ini", + ), + ], + ) + def test_max_warnings_config_accepts_int_and_string( + self, pytester: Pytester, writer: str, source: str + ) -> None: + """Native TOML integers and quoted/INI strings are accepted (#14953). + + An explicit zero must be distinguished from the unset default. + """ + if writer == "toml": + pytester.maketoml(source) + elif writer == "pyproject": + pytester.makepyprojecttoml(source) + else: + assert writer == "ini" + pytester.makeini(source) + pytester.makepyfile(self.ONE_WARNING_PYFILE) + result = pytester.runpytest() + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + result.stdout.fnmatch_lines( + ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] + ) + + @pytest.mark.filterwarnings("default::UserWarning") + def test_max_warnings_override_ini(self, pytester: Pytester) -> None: + """-o max_warnings=0 is accepted and treated as an explicit zero (#14953).""" + pytester.makepyfile(self.ONE_WARNING_PYFILE) + result = pytester.runpytest("-o", "max_warnings=0") + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + result.stdout.fnmatch_lines( + ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] + ) + + @pytest.mark.filterwarnings("default::UserWarning") + def test_max_warnings_cli_overrides_ini(self, pytester: Pytester) -> None: + """--max-warnings takes precedence over the configuration value.""" + pytester.makeini( + """ + [pytest] + max_warnings = 10 + """ + ) + pytester.makepyfile(self.PYFILE) + result = pytester.runpytest("--max-warnings", "0") + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + def test_pythonwarnings_not_duplicated(pytester: Pytester) -> None: """Regression test for #13484: -W values should not be duplicated in