rich/tests/test_ansi.py
dosisod f021979c79 Strip problematic private escape sequences:
["Private escape sequences"](https://en.wikipedia.org/wiki/ANSI_escape_code#Fp_Escape_sequences)
are escape sequences that are reserved for private use, though `\x1b7` and
`\x1b8` are commonly used for storing and restoring the current cursor
position. When those escape codes are not stripped the cursor jumps around and
causes Rich to write garbage output. An example of a program that uses this
cursor store/restore functionality is the APK package manager in Alpine Linux:

48d91f482e/src/print.c (L232-240)

This commit updates the ANSI parser to ignore the `\x1b0`-`\x1b?` escape
sequences, thus preventing them from being printed and causing havoc.
2024-02-12 16:49:00 -08:00

84 lines
3.1 KiB
Python

import pytest
from rich.ansi import AnsiDecoder
from rich.console import Console
from rich.style import Style
from rich.text import Span, Text
def test_decode():
console = Console(
force_terminal=True, legacy_windows=False, color_system="truecolor"
)
console.begin_capture()
console.print("Hello")
console.print("[b]foo[/b]")
console.print("[link http://example.org]bar")
console.print("[#ff0000 on color(200)]red")
console.print("[color(200) on #ff0000]red")
terminal_codes = console.end_capture()
decoder = AnsiDecoder()
lines = list(decoder.decode(terminal_codes))
expected = [
Text("Hello"),
Text("foo", spans=[Span(0, 3, Style.parse("bold"))]),
Text("bar", spans=[Span(0, 3, Style.parse("link http://example.org"))]),
Text("red", spans=[Span(0, 3, Style.parse("#ff0000 on color(200)"))]),
Text("red", spans=[Span(0, 3, Style.parse("color(200) on #ff0000"))]),
]
assert lines == expected
def test_decode_example():
ansi_bytes = b"\x1b[01m\x1b[KC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:\x1b[m\x1b[K In function '\x1b[01m\x1b[Kmain\x1b[m\x1b[K':\n\x1b[01m\x1b[KC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:3:5:\x1b[m\x1b[K \x1b[01;35m\x1b[Kwarning: \x1b[m\x1b[Kunused variable '\x1b[01m\x1b[Ka\x1b[m\x1b[K' [\x1b[01;35m\x1b[K-Wunused-variable\x1b[m\x1b[K]\n 3 | int \x1b[01;35m\x1b[Ka\x1b[m\x1b[K=1;\n | \x1b[01;35m\x1b[K^\x1b[m\x1b[K\n"
ansi_text = ansi_bytes.decode("utf-8")
text = Text.from_ansi(ansi_text)
console = Console(
force_terminal=True, legacy_windows=False, color_system="truecolor"
)
with console.capture() as capture:
console.print(text)
result = capture.get()
print(repr(result))
expected = "\x1b[1mC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:\x1b[0m In function '\x1b[1mmain\x1b[0m':\n\x1b[1mC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:3:5:\x1b[0m \x1b[1;35mwarning: \x1b[0munused variable '\x1b[1ma\x1b[0m' \n[\x1b[1;35m-Wunused-variable\x1b[0m]\n 3 | int \x1b[1;35ma\x1b[0m=1;\n | \x1b[1;35m^\x1b[0m\n"
assert result == expected
@pytest.mark.parametrize(
"ansi_bytes, expected_text",
[
# https://github.com/Textualize/rich/issues/2688
(
b"\x1b[31mFound 4 errors in 2 files (checked 18 source files)\x1b(B\x1b[m\n",
"Found 4 errors in 2 files (checked 18 source files)",
),
# https://mail.python.org/pipermail/python-list/2007-December/424756.html
(b"Hallo", "Hallo"),
(b"\x1b(BHallo", "Hallo"),
(b"\x1b(JHallo", "Hallo"),
(b"\x1b(BHal\x1b(Jlo", "Hallo"),
],
)
def test_decode_issue_2688(ansi_bytes, expected_text):
text = Text.from_ansi(ansi_bytes.decode())
assert str(text) == expected_text
@pytest.mark.parametrize("code", [*"0123456789:;<=>?"])
def test_strip_private_escape_sequences(code):
text = Text.from_ansi(f"\x1b{code}x")
console = Console(force_terminal=True)
with console.capture() as capture:
console.print(text)
expected = "x\n"
assert capture.get() == expected