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.
This commit is contained in:
Bruno Oliveira 2019-10-05 12:17:20 -03:00
parent b847d5712b
commit 0c18e24433
3 changed files with 104 additions and 4 deletions

View File

@ -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.

View File

@ -1318,8 +1318,7 @@ class LineMatcher:
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 @@ class LineMatcher:
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 @@ class LineMatcher:
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 = []

View File

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