tests/functional2: add terminal code eater

I want this for being able to write reasonable expect-test style tests
for oneliners. We will still probably want something like insta for more
complicated test cases where you actually *want* the output in a
different file, but for now this will do.

cc: #595
Change-Id: I6ddc42963cc49177762cfca206fe9a9efe1ae65d
This commit is contained in:
jade 2024-12-05 14:13:54 -08:00
parent 93d5221b9b
commit c0808bd855
3 changed files with 100 additions and 0 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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 <assert.h>
@ -39,6 +40,8 @@ void TerminalCodeEater::feed(char c, std::function<void(char)> 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;