Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/6820.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for asserting complete output with :class:`~pytest.LineMatcher`'s :func:`~pytest.LineMatcher.fnmatch_lines` and :func:`~pytest.LineMatcher.re_match_lines` via a new ``complete`` option.
28 changes: 25 additions & 3 deletions src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1653,7 +1653,11 @@ def _log_text(self) -> str:
return "\n".join(self._log_output)

def fnmatch_lines(
self, lines2: Sequence[str], *, consecutive: bool = False
self,
lines2: Sequence[str],
*,
consecutive: bool = False,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the original issue noted that this maybe better served as option

as part of the matchers experiments i have been considering that rather than adding bools to this method

we should have a matcher to specify the behaviour

this should be shelved for a design decission

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that makes sense. Is there a sketch of the matcher API I could target? Since this is default-off, a later matcher API could subsume it without breaking existing callers. Acceptable as an interim step, or shelve until the design decision lands?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now shelfe - i may need a bit longer to get back to making a POC of matchers - currently its a internal design document only

complete: bool = False,
) -> None:
"""Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).

Expand All @@ -1663,12 +1667,19 @@ def fnmatch_lines(

:param lines2: String patterns to match.
:param consecutive: Match lines consecutively?
:param complete: Fail if the output contains lines which were not asserted?
"""
__tracebackhide__ = True
self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
self._match_lines(
lines2, fnmatch, "fnmatch", consecutive=consecutive, complete=complete
)

def re_match_lines(
self, lines2: Sequence[str], *, consecutive: bool = False
self,
lines2: Sequence[str],
*,
consecutive: bool = False,
complete: bool = False,
) -> None:
"""Check lines exist in the output (using :func:`python:re.match`).

Expand All @@ -1679,13 +1690,15 @@ def re_match_lines(

:param lines2: string patterns to match.
:param consecutive: match lines consecutively?
:param complete: Fail if the output contains lines which were not asserted?
"""
__tracebackhide__ = True
self._match_lines(
lines2,
lambda name, pat: bool(re.match(pat, name)),
"re.match",
consecutive=consecutive,
complete=complete,
)

def _match_lines(
Expand All @@ -1695,6 +1708,7 @@ def _match_lines(
match_nickname: str,
*,
consecutive: bool = False,
complete: bool = False,
) -> None:
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.

Expand All @@ -1710,6 +1724,8 @@ def _match_lines(
when a match occurs.
:param consecutive:
Match lines consecutively?
:param complete:
Fail if the output contains lines which were not asserted?
"""
if not isinstance(lines2, collections.abc.Sequence):
raise TypeError(f"invalid type for lines2: {type(lines2).__name__}")
Expand Down Expand Up @@ -1753,6 +1769,12 @@ def _match_lines(
msg = f"remains unmatched: {line!r}"
self._log(msg)
self._fail(msg)
if complete:
unasserted = extralines + lines1
if unasserted:
msg = f"output has unasserted lines: {unasserted!r}"
self._log(msg)
self._fail(msg)
self._log_output = []

def no_fnmatch_line(self, pat: str) -> None:
Expand Down
55 changes: 55 additions & 0 deletions testing/test_pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,61 @@ def test_linematcher_consecutive() -> None:
]


def test_linematcher_complete() -> None:
lm = LineMatcher(["1", "2", "3"])
lm.fnmatch_lines(["1", "2", "3"], complete=True)
lm.re_match_lines(["1", "2", "3"], complete=True)

lm = LineMatcher(["unexpected", "1", "2", "3"])
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.fnmatch_lines(["1", "2", "3"], complete=True)
assert str(excinfo.value).splitlines() == [
"nomatch: '1'",
" and: 'unexpected'",
"exact match: '1'",
"exact match: '2'",
"exact match: '3'",
"output has unasserted lines: ['unexpected']",
]

lm = LineMatcher(["1", "2", "3", "trailing"])
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.re_match_lines(["1", "2", "3"], complete=True)
assert str(excinfo.value).splitlines() == [
"exact match: '1'",
"exact match: '2'",
"exact match: '3'",
"output has unasserted lines: ['trailing']",
]

lm = LineMatcher(["0", "1", "2", "3"])
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.fnmatch_lines(["1", "2", "3"], consecutive=True, complete=True)
assert str(excinfo.value).splitlines() == [
"nomatch: '1'",
" and: '0'",
"exact match: '1'",
"exact match: '2'",
"exact match: '3'",
"output has unasserted lines: ['0']",
]

lm = LineMatcher(["x"])
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.fnmatch_lines([], complete=True)
assert str(excinfo.value).splitlines() == [
"output has unasserted lines: ['x']",
]

lm = LineMatcher(["1", "1"])
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.fnmatch_lines(["1"], complete=True)
assert str(excinfo.value).splitlines() == [
"exact match: '1'",
"output has unasserted lines: ['1']",
]


@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"])
def test_linematcher_no_matching(function: str) -> None:
if function == "no_fnmatch_line":
Expand Down
Loading