home · contact · privacy
Add first stab at testing infrastructure. master
authorChristian Heller <c.heller@plomlompom.de>
Fri, 12 Sep 2025 09:54:57 +0000 (11:54 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Fri, 12 Sep 2025 09:54:57 +0000 (11:54 +0200)
ircplom.py
ircplom/tui_base.py

index eb70cda5601cf736a5f1e3e0b3a2c001e8f270a4..0522008d14a83519989e0e6b761518de38f58401 100755 (executable)
@@ -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)
index 7542ebb591cf97ad36e7dd5e901d4702a5381702..91ca6e63c245d215c170df979920cb8d43a81efd 100644 (file)
@@ -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',
+    '<commands available in this window:',
+    '<  /help ',
+    '<  /list ',
+    '<  /prompt_enter ',
+    '<  /quit ',
+    '<  /window TOWARDS',
+    '<  /window.history.scroll DIRECTION',
+    '<  /window.paste ',
+    '<  /window.prompt.backspace ',
+    '<  /window.prompt.move_cursor DIRECTION',
+    '<  /window.prompt.scroll DIRECTION',
+    '>/list',
+    '<windows available via /window:',
+    '<  0) :start',
+    '>/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