Skip to content

Commit

Permalink
Introduce no_fnmatch_line/no_re_match_line in pytester
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nicoddemus committed Oct 5, 2019
1 parent c1361b4 commit 4163556
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 4 deletions.
19 changes: 19 additions & 0 deletions changelog/5914.feature.rst
@@ -0,0 +1,19 @@
``pytester`` learned two new functions, `no_fnmatch_line <https://docs.pytest.org/en/latest/reference.html#_pytest.pytester.LineMatcher.no_fnmatch_line>`_ and
`no_re_match_line <https://docs.pytest.org/en/latest/reference.html#_pytest.pytester.LineMatcher.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.
43 changes: 39 additions & 4 deletions src/_pytest/pytester.py
Expand Up @@ -1319,8 +1319,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")
Expand All @@ -1331,8 +1330,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")
Expand Down Expand Up @@ -1375,3 +1373,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 = []
46 changes: 46 additions & 0 deletions testing/test_pytester.py
Expand Up @@ -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")

Expand Down

0 comments on commit 4163556

Please sign in to comment.