Skip to content

Commit 79fd4d3

Browse files
authored
strip ANSI escape codes from the output (#57)
* hook up a ansi removing function * use a regular expression to remove ansi escapes * prioritize CSI over C1 * check that `strip_ansi` works properly * check that pre-formatted report strips ansi escapes
1 parent fd5c7fc commit 79fd4d3

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

parse_logs.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@
1212
from pytest import CollectReport, TestReport
1313

1414
test_collection_stage = "test collection session"
15+
fe_bytes = "[\x40-\x5f]"
16+
parameter_bytes = "[\x30-\x3f]"
17+
intermediate_bytes = "[\x20-\x2f]"
18+
final_bytes = "[\x40-\x7e]"
19+
ansi_fe_escape_re = re.compile(
20+
rf"""
21+
\x1B # ESC
22+
(?:
23+
\[ # CSI
24+
{parameter_bytes}*
25+
{intermediate_bytes}*
26+
{final_bytes}
27+
| {fe_bytes} # single-byte Fe
28+
)
29+
""",
30+
re.VERBOSE,
31+
)
32+
33+
34+
def strip_ansi(msg):
35+
"""strip all ansi escape sequences"""
36+
return ansi_fe_escape_re.sub("", msg)
1537

1638

1739
@dataclass
@@ -45,6 +67,9 @@ class PreformattedReport:
4567
variant: str | None
4668
message: str
4769

70+
def __post_init__(self):
71+
self.message = strip_ansi(self.message)
72+
4873

4974
@dataclass
5075
class CollectionError:

test_parse_log.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@
1919
messages = st.text()
2020

2121

22+
def ansi_csi_escapes():
23+
parameter_bytes = st.lists(st.characters(min_codepoint=0x30, max_codepoint=0x3F))
24+
intermediate_bytes = st.lists(st.characters(min_codepoint=0x20, max_codepoint=0x2F))
25+
final_bytes = st.characters(min_codepoint=0x40, max_codepoint=0x7E)
26+
27+
return st.builds(
28+
lambda *args: "".join(["\x1b[", *args]),
29+
parameter_bytes.map("".join),
30+
intermediate_bytes.map("".join),
31+
final_bytes,
32+
)
33+
34+
35+
def ansi_c1_escapes():
36+
byte_ = st.characters(
37+
codec="ascii", min_codepoint=0x40, max_codepoint=0x5F, exclude_characters=["["]
38+
)
39+
return st.builds(lambda b: f"\x1b{b}", byte_)
40+
41+
42+
def ansi_fe_escapes():
43+
return ansi_csi_escapes() | ansi_c1_escapes()
44+
45+
2246
def preformatted_reports():
2347
return st.tuples(filepaths, names, variants | st.none(), messages).map(
2448
lambda x: parse_logs.PreformattedReport(*x)
@@ -47,3 +71,23 @@ def test_truncate(reports, max_chars):
4771
formatted = parse_logs.truncate(reports, max_chars=max_chars, py_version=py_version)
4872

4973
assert formatted is None or len(formatted) <= max_chars
74+
75+
76+
@given(st.lists(ansi_fe_escapes()).map("".join))
77+
def test_strip_ansi_multiple(escapes):
78+
assert parse_logs.strip_ansi(escapes) == ""
79+
80+
81+
@given(ansi_fe_escapes())
82+
def test_strip_ansi(escape):
83+
message = f"some {escape}text"
84+
85+
assert parse_logs.strip_ansi(message) == "some text"
86+
87+
88+
@given(ansi_fe_escapes())
89+
def test_preformatted_report_ansi(escape):
90+
actual = parse_logs.PreformattedReport(
91+
filepath="a", name="b", variant=None, message=f"{escape}text"
92+
)
93+
assert actual.message == "text"

0 commit comments

Comments
 (0)