From 5256542ea41583e1c02783b2650b7c8a23749088 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 23:33:24 +0100 Subject: [PATCH] pytester.LineMatcher: add support for matching lines consecutively --- changelog/6653.improvement.rst | 1 + src/_pytest/pytester.py | 30 ++++++++++++++++++++++++++---- testing/test_pytester.py | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 changelog/6653.improvement.rst diff --git a/changelog/6653.improvement.rst b/changelog/6653.improvement.rst new file mode 100644 index 000000000..4c081e673 --- /dev/null +++ b/changelog/6653.improvement.rst @@ -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`. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 11f81b76e..f80a62e6f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1380,7 +1380,9 @@ class LineMatcher: def _log_text(self) -> str: 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`). 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. :param lines2: string patterns to match. + :param consecutive: match lines consecutive? """ __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`). 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. :param lines2: string patterns to match. + :param consecutive: match lines consecutively? """ __tracebackhide__ = True 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( @@ -1412,6 +1421,8 @@ class LineMatcher: lines2: Sequence[str], match_func: Callable[[str, str], bool], match_nickname: str, + *, + consecutive: bool = False ) -> None: """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. @@ -1422,6 +1433,7 @@ class LineMatcher: pattern :param str match_nickname: the nickname for the match function that will be logged to stdout when a match occurs + :param consecutive: match lines consecutively? """ if not isinstance(lines2, collections.abc.Sequence): raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__)) @@ -1431,20 +1443,30 @@ class LineMatcher: extralines = [] __tracebackhide__ = True wnick = len(match_nickname) + 1 + started = False for line in lines2: nomatchprinted = False while lines1: nextline = lines1.pop(0) if line == nextline: self._log("exact match:", repr(line)) + started = True break elif match_func(nextline, line): self._log("%s:" % match_nickname, repr(line)) self._log( "{:>{width}}".format("with:", width=wnick), repr(nextline) ) + started = True break 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: self._log( "{:>{width}}".format("nomatch:", width=wnick), repr(line) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index a6901e967..bc0d9d0c3 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -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"]) def test_linematcher_no_matching(function) -> None: if function == "no_fnmatch_line":