diff --git a/tests/functional2/testlib/fixtures.py b/tests/functional2/testlib/fixtures.py index 1f136f449..fa00e3e58 100644 --- a/tests/functional2/testlib/fixtures.py +++ b/tests/functional2/testlib/fixtures.py @@ -4,6 +4,7 @@ import subprocess from typing import Any from pathlib import Path from functools import partial, partialmethod +from functional2.testlib.terminal_code_eater import eat_terminal_codes import dataclasses @@ -33,6 +34,26 @@ class CommandResult: output=self.stdout) return self + @property + def stdout_s(self) -> str: + """Command stdout as str""" + return self.stdout.decode('utf-8', errors='replace') + + @property + def stderr_s(self) -> str: + """Command stderr as str""" + return self.stderr.decode('utf-8', errors='replace') + + @property + def stdout_plain(self) -> str: + """Command stderr as str with terminal escape sequences eaten and whitespace stripped""" + return eat_terminal_codes(self.stdout).decode('utf-8', errors='replace').strip() + + @property + def stderr_plain(self) -> str: + """Command stderr as str with terminal escape sequences eaten and whitespace stripped""" + return eat_terminal_codes(self.stderr).decode('utf-8', errors='replace').strip() + def json(self) -> Any: self.ok() return json.loads(self.stdout) diff --git a/tests/functional2/testlib/terminal_code_eater.py b/tests/functional2/testlib/terminal_code_eater.py new file mode 100644 index 000000000..675d34d3c --- /dev/null +++ b/tests/functional2/testlib/terminal_code_eater.py @@ -0,0 +1,76 @@ +# copy pasta from libutil-support's terminal-code-eater.cc + +import enum +import dataclasses + + +class State(enum.Enum): + ExpectESC = 1 + ExpectESCSeq = 2 + InCSIParams = 3 + InCSIIntermediates = 4 + + +@dataclasses.dataclass +class TerminalCodeEater: + state: State = State.ExpectESC + + def feed(self, data: bytes) -> bytes: + is_param_char = lambda c: c >= 0x30 and c <= 0x3f + is_intermediate_char = lambda c: c >= 0x20 and c <= 0x2f + is_final_char = lambda c: c >= 0x40 and c <= 0x7e + + ret = bytearray() + for c in data: + match self.state: + case State.ExpectESC: + match c: + case 0x1b: # \e + self._transition(State.ExpectESCSeq) + continue + case 0xd: # \r + continue + ret.append(c) + case State.ExpectESCSeq: + match c: + # CSI ('[') + case 0x5b: + self._transition(State.InCSIParams) + continue + # FIXME(jade): whatever this was, we do not know how to + # delimit it, so we just eat the next character and + # keep going. Should we actually eat it? + case _: + self._transition(State.ExpectESC) + continue + # https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences + # A CSI sequence is: CSI [\x30-\x3f]* [\x20-\x2f]* [\x40-\x7e] + # ^ params ^ intermediates ^ final byte + case State.InCSIParams: + if is_final_char(c): + self._transition(State.ExpectESC) + continue + elif is_intermediate_char(c): + self._transition(State.InCSIIntermediates) + continue + elif is_param_char(c): + continue + else: + raise ValueError(f'Corrupt escape sequence, at {c:x}') + case State.InCSIIntermediates: + if is_final_char(c): + self._transition(State.ExpectESC) + continue + elif is_intermediate_char(c): + continue + else: + raise ValueError(f'Corrupt escape sequence in intermediates, at {c:x}') + + return bytes(ret) + + def _transition(self, new_state: State): + self.state = new_state + + +def eat_terminal_codes(s: bytes) -> bytes: + return TerminalCodeEater().feed(s) diff --git a/tests/unit/libutil-support/tests/terminal-code-eater.cc b/tests/unit/libutil-support/tests/terminal-code-eater.cc index ac4fbe980..83be1a136 100644 --- a/tests/unit/libutil-support/tests/terminal-code-eater.cc +++ b/tests/unit/libutil-support/tests/terminal-code-eater.cc @@ -1,3 +1,4 @@ +// this file has a hissing snake twin in functional2/testlib/terminal_code_eater.py #include "terminal-code-eater.hh" #include "lix/libutil/escape-char.hh" #include @@ -39,6 +40,8 @@ void TerminalCodeEater::feed(char c, std::function on_char) case '[': transition(State::InCSIParams); return; + // FIXME(jade): whatever this was, we do not know how to delimit it, so + // we just eat the next character and keep going default: transition(State::ExpectESC); return;