diff --git a/parse_logs.py b/parse_logs.py index 86d2221..756aa19 100644 --- a/parse_logs.py +++ b/parse_logs.py @@ -12,6 +12,28 @@ from pytest import CollectReport, TestReport test_collection_stage = "test collection session" +fe_bytes = "[\x40-\x5f]" +parameter_bytes = "[\x30-\x3f]" +intermediate_bytes = "[\x20-\x2f]" +final_bytes = "[\x40-\x7e]" +ansi_fe_escape_re = re.compile( + rf""" + \x1B # ESC + (?: + \[ # CSI + {parameter_bytes}* + {intermediate_bytes}* + {final_bytes} + | {fe_bytes} # single-byte Fe + ) + """, + re.VERBOSE, +) + + +def strip_ansi(msg): + """strip all ansi escape sequences""" + return ansi_fe_escape_re.sub("", msg) @dataclass @@ -45,6 +67,9 @@ class PreformattedReport: variant: str | None message: str + def __post_init__(self): + self.message = strip_ansi(self.message) + @dataclass class CollectionError: diff --git a/test_parse_log.py b/test_parse_log.py index 0c419b9..9407b3f 100644 --- a/test_parse_log.py +++ b/test_parse_log.py @@ -19,6 +19,30 @@ messages = st.text() +def ansi_csi_escapes(): + parameter_bytes = st.lists(st.characters(min_codepoint=0x30, max_codepoint=0x3F)) + intermediate_bytes = st.lists(st.characters(min_codepoint=0x20, max_codepoint=0x2F)) + final_bytes = st.characters(min_codepoint=0x40, max_codepoint=0x7E) + + return st.builds( + lambda *args: "".join(["\x1b[", *args]), + parameter_bytes.map("".join), + intermediate_bytes.map("".join), + final_bytes, + ) + + +def ansi_c1_escapes(): + byte_ = st.characters( + codec="ascii", min_codepoint=0x40, max_codepoint=0x5F, exclude_characters=["["] + ) + return st.builds(lambda b: f"\x1b{b}", byte_) + + +def ansi_fe_escapes(): + return ansi_csi_escapes() | ansi_c1_escapes() + + def preformatted_reports(): return st.tuples(filepaths, names, variants | st.none(), messages).map( lambda x: parse_logs.PreformattedReport(*x) @@ -47,3 +71,23 @@ def test_truncate(reports, max_chars): formatted = parse_logs.truncate(reports, max_chars=max_chars, py_version=py_version) assert formatted is None or len(formatted) <= max_chars + + +@given(st.lists(ansi_fe_escapes()).map("".join)) +def test_strip_ansi_multiple(escapes): + assert parse_logs.strip_ansi(escapes) == "" + + +@given(ansi_fe_escapes()) +def test_strip_ansi(escape): + message = f"some {escape}text" + + assert parse_logs.strip_ansi(message) == "some text" + + +@given(ansi_fe_escapes()) +def test_preformatted_report_ansi(escape): + actual = parse_logs.PreformattedReport( + filepath="a", name="b", variant=None, message=f"{escape}text" + ) + assert actual.message == "text"