From 0c18e244334d1c04fa2a7942fe5e2eb179ba6915 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 5 Oct 2019 12:17:20 -0300 Subject: [PATCH] Introduce no_fnmatch_line/no_re_match_line in pytester The current idiom is to use: assert re.match(pat, result.stdout.str()) Or assert line in result.stdout.str() But this does not really give good results when it fails. Those new functions produce similar output to ther other match lines functions. --- changelog/5914.feature.rst | 19 ++++++++++++++++ src/_pytest/pytester.py | 43 +++++++++++++++++++++++++++++++---- testing/test_pytester.py | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 changelog/5914.feature.rst diff --git a/changelog/5914.feature.rst b/changelog/5914.feature.rst new file mode 100644 index 00000000000..68cd66f9902 --- /dev/null +++ b/changelog/5914.feature.rst @@ -0,0 +1,19 @@ +``pytester`` learned two new functions, `no_fnmatch_line `_ and +`no_re_match_line `_. + +The functions are used to ensure the captured text *does not* match the given +pattern. + +The previous idiom was to use ``re.match``: + +.. code-block:: python + + assert re.match(pat, result.stdout.str()) is None + +Or the ``in`` operator: + +.. code-block:: python + + assert text in result.stdout.str() + +But the new functions produce best output on failure. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0f346074184..a050dad09e5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1318,8 +1318,7 @@ def fnmatch_lines(self, lines2): The argument is a list of lines which have to match and can use glob wildcards. If they do not match a pytest.fail() is called. The - matches and non-matches are also printed on stdout. - + matches and non-matches are also shown as part of the error message. """ __tracebackhide__ = True self._match_lines(lines2, fnmatch, "fnmatch") @@ -1330,8 +1329,7 @@ def re_match_lines(self, lines2): The argument is a list of lines which have to match using ``re.match``. If they do not match a pytest.fail() is called. - The matches and non-matches are also printed on stdout. - + The matches and non-matches are also shown as part of the error message. """ __tracebackhide__ = True self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match") @@ -1374,3 +1372,40 @@ def _match_lines(self, lines2, match_func, match_nickname): else: self._log("remains unmatched: {!r}".format(line)) pytest.fail(self._log_text) + + def no_fnmatch_line(self, pat): + """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. + + :param str pat: the pattern to match lines. + """ + __tracebackhide__ = True + self._no_match_line(pat, fnmatch, "fnmatch") + + def no_re_match_line(self, pat): + """Ensure captured lines do not match the given pattern, using ``re.match``. + + :param str pat: the regular expression to match lines. + """ + __tracebackhide__ = True + self._no_match_line(pat, lambda name, pat: re.match(pat, name), "re.match") + + def _no_match_line(self, pat, match_func, match_nickname): + """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch`` + + :param str pat: the pattern to match lines + """ + __tracebackhide__ = True + nomatch_printed = False + try: + for line in self.lines: + if match_func(line, pat): + self._log("%s:" % match_nickname, repr(pat)) + self._log(" with:", repr(line)) + pytest.fail(self._log_text) + else: + if not nomatch_printed: + self._log("nomatch:", repr(pat)) + nomatch_printed = True + self._log(" and:", repr(line)) + finally: + self._log_output = [] diff --git a/testing/test_pytester.py b/testing/test_pytester.py index d330ff2532b..f8b0896c5fd 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -457,6 +457,52 @@ def test_linematcher_with_nonlist(): assert lm._getlines(set()) == set() +@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"]) +def test_no_matching(function): + """""" + if function == "no_fnmatch_line": + match_func_name = "fnmatch" + good_pattern = "*.py OK*" + bad_pattern = "*X.py OK*" + else: + assert function == "no_re_match_line" + match_func_name = "re.match" + good_pattern = r".*py OK" + bad_pattern = r".*Xpy OK" + + lm = LineMatcher( + [ + "cachedir: .pytest_cache", + "collecting ... collected 1 item", + "", + "show_fixtures_per_test.py OK", + "=== elapsed 1s ===", + ] + ) + + def check_failure_lines(lines): + expected = [ + "nomatch: '{}'".format(good_pattern), + " and: 'cachedir: .pytest_cache'", + " and: 'collecting ... collected 1 item'", + " and: ''", + "{}: '{}'".format(match_func_name, good_pattern), + " with: 'show_fixtures_per_test.py OK'", + ] + assert lines == expected + + # check the function twice to ensure we don't accumulate the internal buffer + for i in range(2): + with pytest.raises(pytest.fail.Exception) as e: + func = getattr(lm, function) + func(good_pattern) + obtained = str(e.value).splitlines() + check_failure_lines(obtained) + + func = getattr(lm, function) + func(bad_pattern) # bad pattern does not match any line: passes + + def test_pytester_addopts(request, monkeypatch): monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused")