From 31512197851e556f5ed8bb964d69eef6398294e4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 13 Jun 2020 10:24:44 -0300 Subject: [PATCH] assertoutcomes() only accepts plural forms Fix #6505 --- changelog/6505.breaking.rst | 20 ++++++++++++++++++++ src/_pytest/pytester.py | 37 ++++++++++++++++++++++++++++--------- testing/python/fixtures.py | 2 +- testing/test_pytester.py | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 changelog/6505.breaking.rst diff --git a/changelog/6505.breaking.rst b/changelog/6505.breaking.rst new file mode 100644 index 000000000..164b69485 --- /dev/null +++ b/changelog/6505.breaking.rst @@ -0,0 +1,20 @@ +``Testdir.run().parseoutcomes()`` now always returns the parsed nouns in plural form. + +Originally ``parseoutcomes()`` would always returns the nouns in plural form, but a change +meant to improve the terminal summary by using singular form single items (``1 warning`` or ``1 error``) +caused an unintended regression by changing the keys returned by ``parseoutcomes()``. + +Now the API guarantees to always return the plural form, so calls like this: + +.. code-block:: python + + result = testdir.runpytest() + result.assert_outcomes(error=1) + +Need to be changed to: + + +.. code-block:: python + + result = testdir.runpytest() + result.assert_outcomes(errors=1) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2913c6065..cf3dbd201 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -452,28 +452,47 @@ class RunResult: ) def parseoutcomes(self) -> Dict[str, int]: - """Return a dictionary of outcomestring->num from parsing the terminal + """Return a dictionary of outcome noun -> count from parsing the terminal output that the test process produced. + The returned nouns will always be in plural form:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}`` """ - for line in reversed(self.outlines): + return self.parse_summary_nouns(self.outlines) + + @classmethod + def parse_summary_nouns(cls, lines) -> Dict[str, int]: + """Extracts the nouns from a pytest terminal summary line. + + It always returns the plural noun for consistency:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}`` + """ + for line in reversed(lines): if rex_session_duration.search(line): outcomes = rex_outcome.findall(line) ret = {noun: int(count) for (count, noun) in outcomes} break else: raise ValueError("Pytest terminal summary report not found") - if "errors" in ret: - assert "error" not in ret - ret["error"] = ret.pop("errors") - return ret + + to_plural = { + "warning": "warnings", + "error": "errors", + } + return {to_plural.get(k, k): v for k, v in ret.items()} def assert_outcomes( self, passed: int = 0, skipped: int = 0, failed: int = 0, - error: int = 0, + errors: int = 0, xpassed: int = 0, xfailed: int = 0, ) -> None: @@ -487,7 +506,7 @@ class RunResult: "passed": d.get("passed", 0), "skipped": d.get("skipped", 0), "failed": d.get("failed", 0), - "error": d.get("error", 0), + "errors": d.get("errors", 0), "xpassed": d.get("xpassed", 0), "xfailed": d.get("xfailed", 0), } @@ -495,7 +514,7 @@ class RunResult: "passed": passed, "skipped": skipped, "failed": failed, - "error": error, + "errors": errors, "xpassed": xpassed, "xfailed": xfailed, } diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 353ce46cd..e4351a816 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4342,6 +4342,6 @@ def test_yield_fixture_with_no_value(testdir): ) expected = "E ValueError: custom did not yield a value" result = testdir.runpytest() - result.assert_outcomes(error=1) + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines([expected]) assert result.ret == ExitCode.TESTS_FAILED diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 1d3321455..d0afb40b0 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -763,9 +763,38 @@ def test_testdir_outcomes_with_multiple_errors(testdir): """ ) result = testdir.runpytest(str(p1)) - result.assert_outcomes(error=2) + result.assert_outcomes(errors=2) - assert result.parseoutcomes() == {"error": 2} + assert result.parseoutcomes() == {"errors": 2} + + +def test_parse_summary_line_always_plural(): + """Parsing summaries always returns plural nouns (#6505)""" + lines = [ + "some output 1", + "some output 2", + "======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====", + "done.", + ] + assert pytester.RunResult.parse_summary_nouns(lines) == { + "errors": 1, + "failed": 1, + "passed": 1, + "warnings": 1, + } + + lines = [ + "some output 1", + "some output 2", + "======= 1 failed, 1 passed, 2 warnings, 2 errors in 0.13s ====", + "done.", + ] + assert pytester.RunResult.parse_summary_nouns(lines) == { + "errors": 2, + "failed": 1, + "passed": 1, + "warnings": 2, + } def test_makefile_joins_absolute_path(testdir: Testdir) -> None: