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")