Skip to content
1 change: 1 addition & 0 deletions changelog/13859.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify the error message for `pytest.raises()` when a regex `match` fails.
6 changes: 5 additions & 1 deletion src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,11 @@ def match(self, regexp: str | re.Pattern[str]) -> Literal[True]:
"""
__tracebackhide__ = True
value = stringify_exception(self.value)
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
msg = (
f"Regex pattern did not match.\n"
f" Expected regex: {regexp!r}\n"
f" Actual message: {value!r}"
)
if regexp == value:
msg += "\n Did you mean to `re.escape()` the regex?"
assert re.search(regexp, value), msg
Expand Down
6 changes: 2 additions & 4 deletions src/_pytest/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,12 +519,10 @@ def _check_match(self, e: BaseException) -> bool:
self._fail_reason = ("\n" if diff[0][0] == "-" else "") + "\n".join(diff)
return False

# I don't love "Regex"+"Input" vs something like "expected regex"+"exception message"
# when they're similar it's not always obvious which is which
self._fail_reason = (
f"Regex pattern did not match{maybe_specify_type}.\n"
f" Regex: {_match_pattern(self.match)!r}\n"
f" Input: {stringified_exception!r}"
f" Expected regex: {_match_pattern(self.match)!r}\n"
f" Actual message: {stringified_exception!r}"
)
if _match_pattern(self.match) == stringified_exception:
self._fail_reason += "\n Did you mean to `re.escape()` the regex?"
Expand Down
6 changes: 3 additions & 3 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,9 +442,9 @@ def test_division_zero():
assert result.ret != 0

match = [
r"E .* AssertionError: Regex pattern did not match.",
r"E .* Regex: '\[123\]\+'",
r"E .* Input: 'division by zero'",
r"E\s+AssertionError: Regex pattern did not match.",
r"E\s+Expected regex: '\[123\]\+'",
r"E\s+Actual message: 'division by zero'",
]
result.stdout.re_match_lines(match)
result.stdout.no_fnmatch_line("*__tracebackhide__ = True*")
Expand Down
13 changes: 8 additions & 5 deletions testing/python/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ def test_raises_match(self) -> None:
msg = "with base 16"
expr = (
"Regex pattern did not match.\n"
f" Regex: {msg!r}\n"
" Input: \"invalid literal for int() with base 10: 'asdf'\""
f" Expected regex: {msg!r}\n"
f" Actual message: \"invalid literal for int() with base 10: 'asdf'\""
)
with pytest.raises(AssertionError, match="^" + re.escape(expr) + "$"):
with pytest.raises(ValueError, match=msg):
Expand Down Expand Up @@ -289,7 +289,10 @@ def test_match_failure_string_quoting(self):
with pytest.raises(AssertionError, match="'foo"):
raise AssertionError("'bar")
(msg,) = excinfo.value.args
assert msg == '''Regex pattern did not match.\n Regex: "'foo"\n Input: "'bar"'''
assert (
msg
== '''Regex pattern did not match.\n Expected regex: "'foo"\n Actual message: "'bar"'''
)

def test_match_failure_exact_string_message(self):
message = "Oh here is a message with (42) numbers in parameters"
Expand All @@ -299,8 +302,8 @@ def test_match_failure_exact_string_message(self):
(msg,) = excinfo.value.args
assert msg == (
"Regex pattern did not match.\n"
" Regex: 'Oh here is a message with (42) numbers in parameters'\n"
" Input: 'Oh here is a message with (42) numbers in parameters'\n"
" Expected regex: 'Oh here is a message with (42) numbers in parameters'\n"
" Actual message: 'Oh here is a message with (42) numbers in parameters'\n"
" Did you mean to `re.escape()` the regex?"
)

Expand Down
88 changes: 45 additions & 43 deletions testing/python/raises_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,8 @@ def test_match() -> None:
with (
fails_raises_group(
"Regex pattern did not match the `ExceptionGroup()`.\n"
" Regex: 'foo'\n"
" Input: 'bar'"
" Expected regex: 'foo'\n"
" Actual message: 'bar'"
),
RaisesGroup(ValueError, match="foo"),
):
Expand All @@ -396,8 +396,8 @@ def test_match() -> None:
with (
fails_raises_group(
"Regex pattern did not match the `ExceptionGroup()`.\n"
" Regex: 'foo'\n"
" Input: 'bar'\n"
" Expected regex: 'foo'\n"
" Actual message: 'bar'\n"
" but matched the expected `ValueError`.\n"
" You might want `RaisesGroup(RaisesExc(ValueError, match='foo'))`"
),
Expand Down Expand Up @@ -570,8 +570,8 @@ def test_assert_message() -> None:
" ExceptionGroup('', [RuntimeError()]):\n"
" RaisesGroup(ValueError): `RuntimeError()` is not an instance of `ValueError`\n"
" RaisesGroup(ValueError, match='a'): Regex pattern did not match the `ExceptionGroup()`.\n"
" Regex: 'a'\n"
" Input: ''\n"
" Expected regex: 'a'\n"
" Actual message: ''\n"
" RuntimeError():\n"
" RaisesGroup(ValueError): `RuntimeError()` is not an exception group\n"
" RaisesGroup(ValueError, match='a'): `RuntimeError()` is not an exception group",
Expand Down Expand Up @@ -634,8 +634,8 @@ def test_assert_message() -> None:
fails_raises_group(
# TODO: did not match Exceptiongroup('h(ell)o', ...) ?
"Raised exception group did not match: Regex pattern did not match the `ExceptionGroup()`.\n"
" Regex: 'h(ell)o'\n"
" Input: 'h(ell)o'\n"
" Expected regex: 'h(ell)o'\n"
" Actual message: 'h(ell)o'\n"
" Did you mean to `re.escape()` the regex?",
add_prefix=False, # to see the full structure
),
Expand All @@ -645,8 +645,8 @@ def test_assert_message() -> None:
with (
fails_raises_group(
"RaisesExc(match='h(ell)o'): Regex pattern did not match.\n"
" Regex: 'h(ell)o'\n"
" Input: 'h(ell)o'\n"
" Expected regex: 'h(ell)o'\n"
" Actual message: 'h(ell)o'\n"
" Did you mean to `re.escape()` the regex?",
),
RaisesGroup(RaisesExc(match="h(ell)o")),
Expand Down Expand Up @@ -799,8 +799,8 @@ def test_suggestion_on_nested_and_brief_error() -> None:
"The following raised exceptions did not find a match\n"
" ExceptionGroup('^hello', [Exception()]):\n"
" RaisesGroup(Exception, match='^hello'): Regex pattern did not match the `ExceptionGroup()`.\n"
" Regex: '^hello'\n"
" Input: '^hello'\n"
" Expected regex: '^hello'\n"
" Actual message: '^hello'\n"
" Did you mean to `re.escape()` the regex?\n"
" Unexpected nested `ExceptionGroup()`, expected `ValueError`"
),
Expand Down Expand Up @@ -830,8 +830,8 @@ def test_assert_message_nested() -> None:
" RaisesGroup(ValueError): `TypeError()` is not an instance of `ValueError`\n"
" RaisesGroup(RaisesGroup(ValueError)): RaisesGroup(ValueError): `TypeError()` is not an exception group\n"
" RaisesGroup(RaisesExc(TypeError, match='foo')): RaisesExc(TypeError, match='foo'): Regex pattern did not match.\n"
" Regex: 'foo'\n"
" Input: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'\n"
" Expected regex: 'foo'\n"
" Actual message: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'\n"
" RaisesGroup(TypeError, ValueError): 1 matched exception. Too few exceptions raised, found no match for: [ValueError]\n"
" ExceptionGroup('Exceptions from Trio nursery', [TypeError('cccccccccccccccccccccccccccccc'), TypeError('dddddddddddddddddddddddddddddd')]):\n"
" RaisesGroup(ValueError): \n"
Expand All @@ -856,12 +856,12 @@ def test_assert_message_nested() -> None:
" The following raised exceptions did not find a match\n"
" TypeError('cccccccccccccccccccccccccccccc'):\n"
" RaisesExc(TypeError, match='foo'): Regex pattern did not match.\n"
" Regex: 'foo'\n"
" Input: 'cccccccccccccccccccccccccccccc'\n"
" Expected regex: 'foo'\n"
" Actual message: 'cccccccccccccccccccccccccccccc'\n"
" TypeError('dddddddddddddddddddddddddddddd'):\n"
" RaisesExc(TypeError, match='foo'): Regex pattern did not match.\n"
" Regex: 'foo'\n"
" Input: 'dddddddddddddddddddddddddddddd'\n"
" Expected regex: 'foo'\n"
" Actual message: 'dddddddddddddddddddddddddddddd'\n"
" RaisesGroup(TypeError, ValueError): \n"
" 1 matched exception. \n"
" The following expected exceptions did not find a match:\n"
Expand Down Expand Up @@ -945,8 +945,8 @@ def test_misordering_example() -> None:
" It matches `ValueError` which was paired with `ValueError('foo')`\n"
" It matches `ValueError` which was paired with `ValueError('foo')`\n"
" RaisesExc(ValueError, match='foo'): Regex pattern did not match.\n"
" Regex: 'foo'\n"
" Input: 'bar'\n"
" Expected regex: 'foo'\n"
" Actual message: 'bar'\n"
"There exist a possible match when attempting an exhaustive check, but RaisesGroup uses a greedy algorithm. Please make your expected exceptions more stringent with `RaisesExc` etc so the greedy algorithm can function."
),
RaisesGroup(
Expand Down Expand Up @@ -1036,34 +1036,34 @@ def test_identity_oopsies() -> None:
"The following raised exceptions did not find a match\n"
" ValueError('foo'):\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" ValueError('foo'):\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" ValueError('foo'):\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'\n"
" Expected regex: 'bar'\n"
" Actual message: 'foo'\n"
" RaisesExc(match='bar'): Regex pattern did not match.\n"
" Regex: 'bar'\n"
" Input: 'foo'"
" Expected regex: 'bar'\n"
" Actual message: 'foo'"
),
RaisesGroup(m, m, m),
):
Expand Down Expand Up @@ -1120,7 +1120,9 @@ def test_raisesexc() -> None:
# currently RaisesGroup says "Raised exception did not match" but RaisesExc doesn't...
with pytest.raises(
AssertionError,
match=wrap_escape("Regex pattern did not match.\n Regex: 'foo'\n Input: 'bar'"),
match=wrap_escape(
"Regex pattern did not match.\n Expected regex: 'foo'\n Actual message: 'bar'"
),
):
with RaisesExc(TypeError, match="foo"):
raise TypeError("bar")
Expand All @@ -1132,8 +1134,8 @@ def test_raisesexc_match() -> None:
with (
fails_raises_group(
"RaisesExc(ValueError, match='foo'): Regex pattern did not match.\n"
" Regex: 'foo'\n"
" Input: 'bar'"
" Expected regex: 'foo'\n"
" Actual message: 'bar'"
),
RaisesGroup(RaisesExc(ValueError, match="foo")),
):
Expand All @@ -1145,8 +1147,8 @@ def test_raisesexc_match() -> None:
with (
fails_raises_group(
"RaisesExc(match='foo'): Regex pattern did not match.\n"
" Regex: 'foo'\n"
" Input: 'bar'"
" Expected regex: 'foo'\n"
" Actual message: 'bar'"
),
RaisesGroup(RaisesExc(match="foo")),
):
Expand Down