pytester.LineMatcher: add support for matching lines consecutively

This commit is contained in:
Daniel Hahler 2020-02-01 23:33:24 +01:00
parent 50f81db817
commit 5256542ea4
3 changed files with 47 additions and 4 deletions

View File

@ -0,0 +1 @@
Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`.

View File

@ -1380,7 +1380,9 @@ class LineMatcher:
def _log_text(self) -> str: def _log_text(self) -> str:
return "\n".join(self._log_output) return "\n".join(self._log_output)
def fnmatch_lines(self, lines2: Sequence[str]) -> None: def fnmatch_lines(
self, lines2: Sequence[str], *, consecutive: bool = False
) -> None:
"""Check lines exist in the output (using :func:`python:fnmatch.fnmatch`). """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).
The argument is a list of lines which have to match and can use glob The argument is a list of lines which have to match and can use glob
@ -1388,11 +1390,14 @@ class LineMatcher:
matches and non-matches are also shown as part of the error message. matches and non-matches are also shown as part of the error message.
:param lines2: string patterns to match. :param lines2: string patterns to match.
:param consecutive: match lines consecutive?
""" """
__tracebackhide__ = True __tracebackhide__ = True
self._match_lines(lines2, fnmatch, "fnmatch") self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
def re_match_lines(self, lines2: Sequence[str]) -> None: def re_match_lines(
self, lines2: Sequence[str], *, consecutive: bool = False
) -> None:
"""Check lines exist in the output (using :func:`python:re.match`). """Check lines exist in the output (using :func:`python:re.match`).
The argument is a list of lines which have to match using ``re.match``. The argument is a list of lines which have to match using ``re.match``.
@ -1401,10 +1406,14 @@ class LineMatcher:
The matches and non-matches are also shown as part of the error message. The matches and non-matches are also shown as part of the error message.
:param lines2: string patterns to match. :param lines2: string patterns to match.
:param consecutive: match lines consecutively?
""" """
__tracebackhide__ = True __tracebackhide__ = True
self._match_lines( self._match_lines(
lines2, lambda name, pat: bool(re.match(pat, name)), "re.match" lines2,
lambda name, pat: bool(re.match(pat, name)),
"re.match",
consecutive=consecutive,
) )
def _match_lines( def _match_lines(
@ -1412,6 +1421,8 @@ class LineMatcher:
lines2: Sequence[str], lines2: Sequence[str],
match_func: Callable[[str, str], bool], match_func: Callable[[str, str], bool],
match_nickname: str, match_nickname: str,
*,
consecutive: bool = False
) -> None: ) -> None:
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
@ -1422,6 +1433,7 @@ class LineMatcher:
pattern pattern
:param str match_nickname: the nickname for the match function that :param str match_nickname: the nickname for the match function that
will be logged to stdout when a match occurs will be logged to stdout when a match occurs
:param consecutive: match lines consecutively?
""" """
if not isinstance(lines2, collections.abc.Sequence): if not isinstance(lines2, collections.abc.Sequence):
raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__)) raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__))
@ -1431,20 +1443,30 @@ class LineMatcher:
extralines = [] extralines = []
__tracebackhide__ = True __tracebackhide__ = True
wnick = len(match_nickname) + 1 wnick = len(match_nickname) + 1
started = False
for line in lines2: for line in lines2:
nomatchprinted = False nomatchprinted = False
while lines1: while lines1:
nextline = lines1.pop(0) nextline = lines1.pop(0)
if line == nextline: if line == nextline:
self._log("exact match:", repr(line)) self._log("exact match:", repr(line))
started = True
break break
elif match_func(nextline, line): elif match_func(nextline, line):
self._log("%s:" % match_nickname, repr(line)) self._log("%s:" % match_nickname, repr(line))
self._log( self._log(
"{:>{width}}".format("with:", width=wnick), repr(nextline) "{:>{width}}".format("with:", width=wnick), repr(nextline)
) )
started = True
break break
else: else:
if consecutive and started:
msg = "no consecutive match: {!r}".format(line)
self._log(msg)
self._log(
"{:>{width}}".format("with:", width=wnick), repr(nextline)
)
self._fail(msg)
if not nomatchprinted: if not nomatchprinted:
self._log( self._log(
"{:>{width}}".format("nomatch:", width=wnick), repr(line) "{:>{width}}".format("nomatch:", width=wnick), repr(line)

View File

@ -508,6 +508,26 @@ def test_linematcher_match_failure() -> None:
] ]
def test_linematcher_consecutive():
lm = LineMatcher(["1", "", "2"])
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.fnmatch_lines(["1", "2"], consecutive=True)
assert str(excinfo.value).splitlines() == [
"exact match: '1'",
"no consecutive match: '2'",
" with: ''",
]
lm.re_match_lines(["1", r"\d?", "2"], consecutive=True)
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.re_match_lines(["1", r"\d", "2"], consecutive=True)
assert str(excinfo.value).splitlines() == [
"exact match: '1'",
r"no consecutive match: '\\d'",
" with: ''",
]
@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"]) @pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"])
def test_linematcher_no_matching(function) -> None: def test_linematcher_no_matching(function) -> None:
if function == "no_fnmatch_line": if function == "no_fnmatch_line":