From: Christian Heller Date: Fri, 12 Sep 2025 09:54:57 +0000 (+0200) Subject: Add first stab at testing infrastructure. X-Git-Url: https://plomlompom.com/repos/reset_cookie?a=commitdiff_plain;h=HEAD;p=ircplom Add first stab at testing infrastructure. --- diff --git a/ircplom.py b/ircplom.py index eb70cda..0522008 100755 --- a/ircplom.py +++ b/ircplom.py @@ -1,19 +1,22 @@ #!/usr/bin/env python3 'Attempt at an IRC client.' from queue import SimpleQueue +from sys import argv from ircplom.events import ExceptionEvent, QuitEvent from ircplom.client import ClientsDb, ClientEvent, NewClientEvent -from ircplom.tui_base import Terminal, TuiEvent +from ircplom.tui_base import (BaseTui, Terminal, TerminalInterface, + TestTerminal, TestTui, TuiEvent) from ircplom.client_tui import ClientTui -def main_loop() -> None: +def main_loop(cls_term: type[TerminalInterface], cls_tui: type[BaseTui] + ) -> None: 'Main execution code / loop.' q_events: SimpleQueue = SimpleQueue() clients_db: ClientsDb = {} try: - with Terminal(_q_out=q_events).setup() as term: - tui = ClientTui(_q_out=q_events, term=term) + with cls_term(_q_out=q_events).setup() as term: + tui = cls_tui(_q_out=q_events, term=term) while True: event = q_events.get() if isinstance(event, QuitEvent): @@ -32,4 +35,8 @@ def main_loop() -> None: if __name__ == '__main__': - main_loop() + if len(argv) > 1 and argv[1] == 'test': + main_loop(TestTerminal, TestTui) + print('test finished') + else: + main_loop(Terminal, ClientTui) diff --git a/ircplom/tui_base.py b/ircplom/tui_base.py index 7542ebb..91ca6e6 100644 --- a/ircplom/tui_base.py +++ b/ircplom/tui_base.py @@ -5,6 +5,7 @@ from base64 import b64decode from contextlib import contextmanager from datetime import datetime from inspect import _empty as inspect_empty, signature, stack +from queue import SimpleQueue, Empty as QueueEmpty from signal import SIGWINCH, signal from typing import (Callable, Generator, Iterator, NamedTuple, Optional, Sequence) @@ -386,10 +387,48 @@ class TuiEvent(AffectiveEvent): 'To affect TUI.' +class TerminalInterface(ABC): + 'What BaseTui expects from a Terminal.' + size: _YX + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + @abstractmethod + @contextmanager + def setup(self) -> Generator: + 'Combine multiple contexts into one and run keypress loop.' + + @abstractmethod + def calc_geometry(self) -> None: + '(Re-)calculate .size..' + + @abstractmethod + def flush(self) -> None: + 'Flush terminal.' + + @abstractmethod + def wrap(self, line: str) -> list[str]: + 'Wrap line to list of lines fitting into terminal width.' + + @abstractmethod + def write(self, + msg: str = '', + start_y: Optional[int] = None, + attribute: Optional[str] = None, + padding: bool = True + ) -> None: + 'Print to terminal, with position, padding to line end, attributes.' + + @abstractmethod + def _get_keypresses(self) -> Iterator[Optional[TuiEvent]]: + pass + + class BaseTui(QueueMixin): 'Base for graphical user interface elements.' - def __init__(self, term: 'Terminal', **kwargs) -> None: + def __init__(self, term: TerminalInterface, **kwargs) -> None: super().__init__(**kwargs) self._term = term self._window_idx = 0 @@ -576,9 +615,8 @@ class BaseTui(QueueMixin): return None -class Terminal(QueueMixin): +class Terminal(QueueMixin, TerminalInterface): 'Abstraction of terminal interface.' - size: _YX _cursor_yx_: _YX def __init__(self, **kwargs) -> None: @@ -588,7 +626,6 @@ class Terminal(QueueMixin): @contextmanager def setup(self) -> Generator: - 'Combine multiple contexts into one and run keypress loop.' print(self._blessed.clear, end='') with (self._blessed.raw(), self._blessed.fullscreen(), @@ -606,15 +643,12 @@ class Terminal(QueueMixin): self._cursor_yx_ = yx def calc_geometry(self) -> None: - '(Re-)calculate .size..' self.size = _YX(self._blessed.height, self._blessed.width) def flush(self) -> None: - 'Flush terminal.' print('', end='', flush=True) def wrap(self, line: str) -> list[str]: - 'Wrap line to list of lines fitting into terminal width.' return self._blessed.wrap(line, width=self.size.x, subsequent_indent=' '*4) @@ -624,7 +658,6 @@ class Terminal(QueueMixin): attribute: Optional[str] = None, padding: bool = True ) -> None: - 'Print to terminal, with position, padding to line end, attributes.' if start_y is not None: self._cursor_yx = _YX(start_y, 0) # ._blessed.length can slow down things notably: only use where needed! @@ -684,3 +717,92 @@ class Terminal(QueueMixin): yield (TuiEvent.affector('handle_keyboard_event' ).kw(typed_in=to_yield) if to_yield else None) + + +PLAYBOOK: tuple[str, ...] = ( + '<', + '>/help', + '/list', + '/quit', + '<' +) + + +class TestTerminal(QueueMixin, TerminalInterface): + 'Collects keypresses from string queue, otherwise mostly dummy.' + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._q_keypresses: SimpleQueue = SimpleQueue() + + @contextmanager + def setup(self) -> Generator: + with Loop(iterator=self._get_keypresses(), _q_out=self._q_out): + yield self + + def flush(self) -> None: + pass + + def calc_geometry(self) -> None: + self.size = _YX(0, 0) + + def wrap(self, line: str) -> list[str]: + return [] + + def write(self, + msg: str = '', + start_y: Optional[int] = None, + attribute: Optional[str] = None, + padding: bool = True + ) -> None: + pass + + def _get_keypresses(self) -> Iterator[Optional[TuiEvent]]: + while True: + try: + to_yield = self._q_keypresses.get(timeout=0.1) + except QueueEmpty: + break + yield TuiEvent.affector('handle_keyboard_event' + ).kw(typed_in=to_yield) + + +class TestTui(BaseTui): + 'Collects keypresses via TestTerminal from PLAYBOOK, compares log results.' + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._test_idx = 0 + assert isinstance(self._term, TestTerminal) + self._q_keypresses = self._term._q_keypresses + self._log('') + + def _log(self, msg: str, **kwargs) -> None: + entry = PLAYBOOK[self._test_idx] + direction, expected = entry[0], entry[1:] + assert direction == '<' + assert expected == msg, (self._test_idx, expected, msg) + super()._log(msg, **kwargs) + while True: + self._test_idx += 1 + entry = PLAYBOOK[self._test_idx] + direction, msg = entry[0], entry[1:] + if direction == '>': + for c in msg: + self._q_keypresses.put(c) + self._q_keypresses.put('KEY_ENTER') + else: + break