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
 #!/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.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
 
 
 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:
     '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):
             while True:
                 event = q_events.get()
                 if isinstance(event, QuitEvent):
@@ -32,4 +35,8 @@ def main_loop() -> None:
 
 
 if __name__ == '__main__':
 
 
 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 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)
 from signal import SIGWINCH, signal
 from typing import (Callable, Generator, Iterator, NamedTuple, Optional,
                     Sequence)
@@ -386,10 +387,48 @@ class TuiEvent(AffectiveEvent):
     'To affect TUI.'
 
 
     '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.'
 
 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
         super().__init__(**kwargs)
         self._term = term
         self._window_idx = 0
@@ -576,9 +615,8 @@ class BaseTui(QueueMixin):
         return None
 
 
         return None
 
 
-class Terminal(QueueMixin):
+class Terminal(QueueMixin, TerminalInterface):
     'Abstraction of terminal interface.'
     'Abstraction of terminal interface.'
-    size: _YX
     _cursor_yx_: _YX
 
     def __init__(self, **kwargs) -> None:
     _cursor_yx_: _YX
 
     def __init__(self, **kwargs) -> None:
@@ -588,7 +626,6 @@ class Terminal(QueueMixin):
 
     @contextmanager
     def setup(self) -> Generator:
 
     @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(),
         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:
         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:
         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]:
         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)
 
         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:
               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!
         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)
             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