Skip to content

Commit 0c18e24

Browse files
committed
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.
1 parent b847d57 commit 0c18e24

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed

changelog/5914.feature.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
``pytester`` learned two new functions, `no_fnmatch_line <https://docs.pytest.org/en/latest/reference.html#_pytest.pytester.LineMatcher.no_fnmatch_line>`_ and
2+
`no_re_match_line <https://docs.pytest.org/en/latest/reference.html#_pytest.pytester.LineMatcher.no_re_match_line>`_.
3+
4+
The functions are used to ensure the captured text *does not* match the given
5+
pattern.
6+
7+
The previous idiom was to use ``re.match``:
8+
9+
.. code-block:: python
10+
11+
assert re.match(pat, result.stdout.str()) is None
12+
13+
Or the ``in`` operator:
14+
15+
.. code-block:: python
16+
17+
assert text in result.stdout.str()
18+
19+
But the new functions produce best output on failure.

src/_pytest/pytester.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,8 +1318,7 @@ def fnmatch_lines(self, lines2):
13181318
13191319
The argument is a list of lines which have to match and can use glob
13201320
wildcards. If they do not match a pytest.fail() is called. The
1321-
matches and non-matches are also printed on stdout.
1322-
1321+
matches and non-matches are also shown as part of the error message.
13231322
"""
13241323
__tracebackhide__ = True
13251324
self._match_lines(lines2, fnmatch, "fnmatch")
@@ -1330,8 +1329,7 @@ def re_match_lines(self, lines2):
13301329
The argument is a list of lines which have to match using ``re.match``.
13311330
If they do not match a pytest.fail() is called.
13321331
1333-
The matches and non-matches are also printed on stdout.
1334-
1332+
The matches and non-matches are also shown as part of the error message.
13351333
"""
13361334
__tracebackhide__ = True
13371335
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):
13741372
else:
13751373
self._log("remains unmatched: {!r}".format(line))
13761374
pytest.fail(self._log_text)
1375+
1376+
def no_fnmatch_line(self, pat):
1377+
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
1378+
1379+
:param str pat: the pattern to match lines.
1380+
"""
1381+
__tracebackhide__ = True
1382+
self._no_match_line(pat, fnmatch, "fnmatch")
1383+
1384+
def no_re_match_line(self, pat):
1385+
"""Ensure captured lines do not match the given pattern, using ``re.match``.
1386+
1387+
:param str pat: the regular expression to match lines.
1388+
"""
1389+
__tracebackhide__ = True
1390+
self._no_match_line(pat, lambda name, pat: re.match(pat, name), "re.match")
1391+
1392+
def _no_match_line(self, pat, match_func, match_nickname):
1393+
"""Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
1394+
1395+
:param str pat: the pattern to match lines
1396+
"""
1397+
__tracebackhide__ = True
1398+
nomatch_printed = False
1399+
try:
1400+
for line in self.lines:
1401+
if match_func(line, pat):
1402+
self._log("%s:" % match_nickname, repr(pat))
1403+
self._log(" with:", repr(line))
1404+
pytest.fail(self._log_text)
1405+
else:
1406+
if not nomatch_printed:
1407+
self._log("nomatch:", repr(pat))
1408+
nomatch_printed = True
1409+
self._log(" and:", repr(line))
1410+
finally:
1411+
self._log_output = []

testing/test_pytester.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,52 @@ def test_linematcher_with_nonlist():
457457
assert lm._getlines(set()) == set()
458458

459459

460+
@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"])
461+
def test_no_matching(function):
462+
""""""
463+
if function == "no_fnmatch_line":
464+
match_func_name = "fnmatch"
465+
good_pattern = "*.py OK*"
466+
bad_pattern = "*X.py OK*"
467+
else:
468+
assert function == "no_re_match_line"
469+
match_func_name = "re.match"
470+
good_pattern = r".*py OK"
471+
bad_pattern = r".*Xpy OK"
472+
473+
lm = LineMatcher(
474+
[
475+
"cachedir: .pytest_cache",
476+
"collecting ... collected 1 item",
477+
"",
478+
"show_fixtures_per_test.py OK",
479+
"=== elapsed 1s ===",
480+
]
481+
)
482+
483+
def check_failure_lines(lines):
484+
expected = [
485+
"nomatch: '{}'".format(good_pattern),
486+
" and: 'cachedir: .pytest_cache'",
487+
" and: 'collecting ... collected 1 item'",
488+
" and: ''",
489+
"{}: '{}'".format(match_func_name, good_pattern),
490+
" with: 'show_fixtures_per_test.py OK'",
491+
]
492+
assert lines == expected
493+
494+
# check the function twice to ensure we don't accumulate the internal buffer
495+
for i in range(2):
496+
with pytest.raises(pytest.fail.Exception) as e:
497+
func = getattr(lm, function)
498+
func(good_pattern)
499+
obtained = str(e.value).splitlines()
500+
check_failure_lines(obtained)
501+
502+
func = getattr(lm, function)
503+
func(bad_pattern) # bad pattern does not match any line: passes
504+
505+
460506
def test_pytester_addopts(request, monkeypatch):
461507
monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused")
462508

0 commit comments

Comments
 (0)