diff --git a/src/_pytest/raises.py b/src/_pytest/raises.py index 7c246fde280..75eea7d8cc9 100644 --- a/src/_pytest/raises.py +++ b/src/_pytest/raises.py @@ -363,14 +363,14 @@ def _check_raw_type( def is_fully_escaped(s: str) -> bool: # we know we won't compile with re.VERBOSE, so whitespace doesn't need to be escaped - metacharacters = "{}()+.*?^$[]" + metacharacters = "{}()+.*?^$[]|" return not any( c in metacharacters and (i == 0 or s[i - 1] != "\\") for (i, c) in enumerate(s) ) def unescape(s: str) -> str: - return re.sub(r"\\([{}()+-.*?^$\[\]\s\\])", r"\1", s) + return re.sub(r"\\([{}()+-.*?^$\[\]\s\\|])", r"\1", s) # These classes conceptually differ from ExceptionInfo in that ExceptionInfo is tied, and diff --git a/testing/python/raises.py b/testing/python/raises.py index 6b2a765e7fb..374a9d4882c 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -430,3 +430,22 @@ def test_raises_match_compiled_regex(self) -> None: pattern_with_flags = re.compile(r"INVALID LITERAL", re.IGNORECASE) with pytest.raises(ValueError, match=pattern_with_flags): int("asdf") + + def test_pipe_metacharacter_not_treated_as_literal(self) -> None: + """Regression test: | (pipe) must be recognized as a regex metacharacter. + + When match='^foo|bar$' is used, is_fully_escaped should recognize this + as a real regex (not a fully-escaped literal), so that pytest does not + attempt an exact-string diff against it. + """ + from _pytest.raises import is_fully_escaped + from _pytest.raises import unescape + + # Pipe is a metacharacter and should not be treated as escaped + assert not is_fully_escaped("foo|bar") + + # Escaped pipe should be treated as a literal + assert is_fully_escaped(r"foo\|bar") + + # unescape should remove the backslash from an escaped pipe + assert unescape(r"foo\|bar") == "foo|bar"