Example #1
0
    def test_select(self, patch_curses: Any, caplog: pytest.LogCaptureFixture,
                    selection: Set[str]) -> None:
        """Test `cobib.tui.tui.TUI.select`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
            selection: the set of selected labels.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        tui = TUI(stdscr, debug=True)
        tui.selection = copy.deepcopy(selection)
        caplog.clear()

        tui.select()
        assert tui.selection == (set() if selection else {"knuthwebsite"})
        expected_log = [
            ("cobib.tui.tui", 10, "Select command triggered."),
            ("cobib.tui.frame", 10, 'Obtaining current label "under" cursor.'),
            ("cobib.tui.frame", 10, 'Current label at "0" is "knuthwebsite".'),
        ]
        if selection:
            expected_log.append(
                ("cobib.tui.tui", 20,
                 "Removing 'knuthwebsite' from the selection."))
        else:
            expected_log.append(("cobib.tui.tui", 20,
                                 "Adding 'knuthwebsite' to the selection."))
        assert [
            record for record in caplog.record_tuples
            if record[0] in ("cobib.tui.frame", "cobib.tui.tui")
        ] == expected_log
Example #2
0
    def test_loop(
        self,
        patch_curses: Any,
        caplog: pytest.LogCaptureFixture,
        keys: Union[str, List[List[int]]],
    ) -> None:
        """Test `cobib.tui.tui.TUI.loop`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
            keys: the keys to send to the TUI.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        stdscr.returned_chars = keys  # type: ignore
        config.tui.prompt_before_quit = False
        tui = TUI(stdscr, debug=True)
        # we expect normal execution
        tui.loop(debug=True)
        # minimal assertions
        for key in keys:
            assert ("cobib.tui.tui", 10,
                    f"Key press registered: {key}") in caplog.record_tuples
        assert caplog.record_tuples[-2] == ("cobib.tui.tui", 10,
                                            "Quitting from lowest level.")
        assert caplog.record_tuples[-1] == ("cobib.tui.tui", 10,
                                            "Stopping key event loop.")
Example #3
0
    def test_prompt_print(self, patch_curses: Any,
                          caplog: pytest.LogCaptureFixture,
                          text: List[str]) -> None:
        """Test `cobib.tui.tui.TUI.prompt_print`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
            text: the text to print to the prompt.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        tui = TUI(stdscr, debug=True)
        caplog.clear()

        tui.prompt_print("\n".join(text))
        assert tui.prompt.lines == [text[0]]  # type: ignore
        if len(text) > 1:
            # assert popup on multi-line text messages
            assert (
                "cobib.tui.buffer",
                10,
                "Appending string to text buffer: " + "\n".join(text),
            ) in caplog.record_tuples
            assert ("cobib.tui.buffer", 10,
                    "Create popup window.") in caplog.record_tuples
Example #4
0
    def test_config_rgb_color(
        self,
        patch_curses: Any,
        caplog: pytest.LogCaptureFixture,
        monkeypatch: pytest.MonkeyPatch,
        can_change_color: bool,
    ) -> None:
        """Test overwriting the RGB color value.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
            monkeypatch: the built-in pytest fixture.
            can_change_color: whether `curses.can_change_color` is enabled.
        """
        monkeypatch.setattr("curses.can_change_color",
                            lambda: can_change_color)
        config.tui.colors.white = "#AA0000"
        TUI.colors()
        if not can_change_color:
            assert (
                "cobib.tui.tui",
                30,
                "Curses cannot change the default colors. Skipping color setup.",
            ) in caplog.record_tuples
        else:
            assert ("TUITest", 10,
                    "init_color: (7, 666, 0, 0)") in caplog.record_tuples
Example #5
0
    def test_loop_inactive_commands(self, patch_curses: Any,
                                    caplog: pytest.LogCaptureFixture) -> None:
        """Test that inactive commands are not triggered during the TUI loop.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        stdscr.returned_chars = [ord("q"), ord("\n")]
        config.tui.prompt_before_quit = False
        tui = TUI(stdscr, debug=True)
        tui.STATE.inactive_commands = ["Show"]
        # we expect normal execution
        tui.loop(debug=True)
        # minimal assertions
        assert (
            "cobib.commands.show",
            10,
            "Show command triggered from TUI.",
        ) not in caplog.record_tuples
        for key in stdscr.returned_chars:
            assert ("cobib.tui.tui", 10,
                    f"Key press registered: {key}") in caplog.record_tuples
        assert caplog.record_tuples[-2] == ("cobib.tui.tui", 10,
                                            "Quitting from lowest level.")
        assert caplog.record_tuples[-1] == ("cobib.tui.tui", 10,
                                            "Stopping key event loop.")
Example #6
0
    def test_colors(self, patch_curses: Any,
                    caplog: pytest.LogCaptureFixture) -> None:
        """Test `cobib.tui.tui.TUI.colors`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
        """
        TUI.colors()
        assert TUI.ANSI_MAP == {
            "\x1b[30;43m": 2,
            "\x1b[34;40m": 3,
            "\x1b[31;40m": 4,
            "\x1b[37;46m": 5,
            "\x1b[37;42m": 6,
            "\x1b[37;44m": 7,
            "\x1b[37;41m": 8,
            "\x1b[37;45m": 9,
        }
        expected_log = [
            ("TUITest", 10, "start_color"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 1 for top_statusbar"),
            ("TUITest", 10, "init_pair: (1, 0, 3)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for top_statusbar"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 2 for bottom_statusbar"),
            ("TUITest", 10, "init_pair: (2, 0, 3)"),
            ("cobib.tui.tui", 10,
             "Adding ANSI color code for bottom_statusbar"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 3 for search_label"),
            ("TUITest", 10, "init_pair: (3, 4, 0)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for search_label"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 4 for search_query"),
            ("TUITest", 10, "init_pair: (4, 1, 0)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for search_query"),
            ("cobib.tui.tui", 10, "Initiliazing color pair 5 for cursor_line"),
            ("TUITest", 10, "init_pair: (5, 7, 6)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for cursor_line"),
            ("cobib.tui.tui", 10, "Initiliazing color pair 6 for popup_help"),
            ("TUITest", 10, "init_pair: (6, 7, 2)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for popup_help"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 7 for popup_stdout"),
            ("TUITest", 10, "init_pair: (7, 7, 4)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for popup_stdout"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 8 for popup_stderr"),
            ("TUITest", 10, "init_pair: (8, 7, 1)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for popup_stderr"),
            ("cobib.tui.tui", 10, "Initiliazing color pair 9 for selection"),
            ("TUITest", 10, "init_pair: (9, 7, 5)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for selection"),
        ]
        assert caplog.record_tuples == expected_log
Example #7
0
    def test_help(self, patch_curses: Any,
                  caplog: pytest.LogCaptureFixture) -> None:
        # pylint: disable=consider-using-f-string
        """Test `cobib.tui.tui.TUI.help`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        tui = TUI(stdscr, debug=True)
        caplog.clear()

        tui.help()
        expected_log = [
            ("cobib.tui.tui", 10, "Help command triggered."),
            ("cobib.tui.tui", 10, "Generating help text."),
            ("MockCursesPad", 10, "erase"),
            ("MockCursesPad", 10, "refresh: 0 0 0 0 22 80"),
            ("MockCursesPad", 10, "resize: 22 80"),
            ("MockCursesPad", 10,
             "addstr: 1 1                              coBib TUI Help"),
            ("MockCursesPad", 10, "addstr: 2 1   Key    Command  Description"),
            ("MockCursesPad", 10, "bkgd:   (6,)"),
            ("MockCursesPad", 10, "box"),
            ("MockCursesPad", 10, "refresh: 0 0 0 0 22 80"),
            ("MockCursesPad", 10, "getch"),
            ("MockCursesPad", 10, "clear"),
            ("cobib.tui.tui", 10, "Handling resize event."),
        ]
        inv_keys = {}
        for key, cmd in TUI.KEYDICT.items():
            if cmd in TUI.HELP_DICT:
                inv_keys[cmd] = "ENTER" if key in (10, 13) else chr(key)
        for idx, (cmd, desc) in enumerate(TUI.HELP_DICT.items()):
            expected_log.insert(
                -6,
                (
                    "MockCursesPad",
                    10,
                    f"addstr: {3+idx} 1 " + "{:^8} {:<8} {}".format(
                        "[" + config.tui.key_bindings[cmd.lower()] + "]",
                        cmd + ":", desc),
                ),
            )
        for log, truth in zip(
                expected_log,
            [
                record for record in caplog.record_tuples
                if record[0] in ("MockCursesPad", "cobib.tui.tui")
            ],
        ):
            assert log == truth
Example #8
0
    def test_config_bind_key(self, caplog: pytest.LogCaptureFixture) -> None:
        """Test `cobib.tui.tui.TUI.bind_keys` when binding a non-default key.

        Args:
            caplog: the built-in pytest fixture.
        """
        config.tui.key_bindings.prompt = "p"
        TUI.bind_keys()
        assert ord(":") not in TUI.KEYDICT
        assert ord("p") in TUI.KEYDICT and TUI.KEYDICT[ord("p")] == "Prompt"
        assert ("cobib.tui.tui", 20,
                "Binding key p to the Prompt command.") in caplog.record_tuples
Example #9
0
    def test_config_color(self, patch_curses: Any,
                          caplog: pytest.LogCaptureFixture) -> None:
        """Test `cobib.tui.tui.TUI.colors` when setting a non-default color value.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
        """
        config.tui.colors.selection_fg = "red"
        TUI.colors()
        assert ("TUITest", 10, "init_pair: (9, 1, 5)") in caplog.record_tuples
        assert ("cobib.tui.tui", 10,
                "Adding ANSI color code for selection") in caplog.record_tuples
Example #10
0
    def test_config_unknown_command(self,
                                    caplog: pytest.LogCaptureFixture) -> None:
        """Test that binding an unknown command logs a warning.

        Args:
            caplog: the built-in pytest fixture.
        """
        config.tui.key_bindings.dummy = "p"
        TUI.bind_keys()
        assert (
            "cobib.tui.tui",
            30,
            'Unknown command "Dummy". Ignoring key binding.',
        ) in caplog.record_tuples
Example #11
0
    def test_config_unknown_color(self, patch_curses: Any,
                                  caplog: pytest.LogCaptureFixture) -> None:
        """Test that setting an unknown color logs a warning.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
        """
        config.tui.colors.dummy_fg = "white"
        TUI.colors()
        assert (
            "cobib.tui.tui",
            30,
            "Detected unknown TUI color name specification: dummy",
        ) in caplog.record_tuples
Example #12
0
 def test_infoline(self) -> None:
     """Test `cobib.tui.tui.TUI.infoline`."""
     infoline = TUI.infoline()
     assert (
         infoline ==
         "a:Add d:Delete e:Edit x:Export f:Filter ?:Help i:Import m:Modify o:Open ::Prompt "
         "q:Quit r:Redo /:Search v:Select ENTER:Show s:Sort u:Undo w:Wrap")
Example #13
0
    def test_prompt_handler(self, patch_curses: Any, keys: List[Union[int,
                                                                      str]],
                            expected: str) -> None:
        """Test `cobib.tui.tui.TUI.prompt_handler`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            keys: the keys to send to the prompt handler.
            expected: the expected string to be returned from the prompt handler.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        config.tui.prompt_before_quit = False
        tui = TUI(stdscr, debug=True)
        tui.prompt.returned_chars = [  # type: ignore
            ord(k) if isinstance(k, str) else k for k in reversed(keys)
        ]
        command = tui.prompt_handler("")
        assert command == expected
Example #14
0
    def test_statusbar(self, attr: int,
                       caplog: pytest.LogCaptureFixture) -> None:
        """Test `cobib.tui.tui.TUI.statusbar`.

        Args:
            attr: the attribute number to use for the printed text.
            caplog: the built-in pytest fixture.
        """
        pad = MockCursesPad()
        text = "Test statusbar text"
        TUI.statusbar(pad, text, attr)
        assert pad.lines == [text]
        expected_log = [
            ("MockCursesPad", 10, "erase"),
            ("MockCursesPad", 10, "getmaxyx"),
            ("MockCursesPad", 10,
             f"addnstr: 0 0 Test statusbar text -1 {attr}"),
            ("MockCursesPad", 10, "refresh: None None None None None None"),
        ]
        assert caplog.record_tuples == expected_log
Example #15
0
    def test_quit(
        self,
        patch_curses: Any,
        caplog: pytest.LogCaptureFixture,
        prompt_quit: bool,
        returned_char: int,
        mode: str,
    ) -> None:
        """Test `cobib.tui.tui.TUI.quit`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
            prompt_quit: whether to prompt before actually quitting.
            returned_char: the value for `tests.tui.mock_curses.MockCursesPad.returned_chars`.
            mode: the `cobib.tui.state.Mode` value.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        config.tui.prompt_before_quit = prompt_quit
        tui = TUI(stdscr, debug=True)
        STATE.mode = mode
        caplog.clear()

        tui.prompt.returned_chars = [returned_char]  # type: ignore

        expected_log = []
        if mode == Mode.LIST.value:
            expected_log.append(
                ("cobib.tui.tui", 10, "Quitting from lowest level."))
        else:
            expected_log.append(
                ("cobib.tui.tui", 10,
                 "Quitting higher menu level. Falling back to list view."))
        if prompt_quit:
            expected_log.append(("TUITest", 10, "curs_set: (1,)"))

        if returned_char == ord("n"):
            expected_log.append(
                ("cobib.tui.tui", 20, "User aborted quitting."))
            expected_log.append(("TUITest", 10, "curs_set: (0,)"))

        if mode == Mode.LIST.value and returned_char != ord("n"):
            with pytest.raises(StopIteration):
                tui.quit()
        else:
            tui.quit()
        assert [
            record for record in caplog.record_tuples
            if record[0] in ("cobib.tui.tui", "TUITest")
        ] == expected_log
Example #16
0
def assert_list_view(screen, current, expected):
    """Asserts the list view of the TUI."""
    term_width = len(screen.buffer[0])
    # assert default colors
    assert [c.bg for c in screen.buffer[0].values()] == ['brown'] * term_width
    assert [c.bg for c in screen.buffer[len(screen.buffer)-2].values()] == ['brown'] * term_width
    # the top statusline contains the version info and number of entries
    assert f"CoBib v{version} - {len(expected)} Entries" in screen.display[0]
    # check current line
    if current >= 0:
        assert [c.fg for c in screen.buffer[current].values()] == ['white'] * term_width
        assert [c.bg for c in screen.buffer[current].values()] == ['cyan'] * term_width
    # the entries per line
    for idx, label in enumerate(expected):
        # offset of 1 due to top statusline
        assert label in screen.display[idx+1]
    # the bottom statusline should contain at least parts of the information string
    assert screen.display[-2].strip() in TUI.infoline()
    # the prompt line should be empty
    assert screen.display[-1].strip() == ""
Example #17
0
def test_tui_resize(setup):
    """Test TUI resize handling."""
    # create pseudo-terminal
    pid, f_d = os.forkpty()
    if pid == 0:
        # child process spawns TUI
        curses.wrapper(TUI)
    else:
        # resize pseudo terminal
        fcntl.ioctl(f_d, termios.TIOCSWINSZ, array('h', [10, 120, 1200, 220]))
        # parent process sets up virtual screen of identical size
        screen = pyte.Screen(120, 10)
        stream = pyte.ByteStream(screen)
        # scrape pseudo-terminal's screen
        while True:
            try:
                [f_d], _, _ = select.select([f_d], [], [], 1)
            except (KeyboardInterrupt, ValueError):
                # either test was interrupted or file descriptor of child process provides nothing
                # to be read
                break
            else:
                try:
                    # scrape screen of child process
                    data = os.read(f_d, 1024)
                    stream.feed(data)
                except OSError:
                    # reading empty
                    break
        for line in screen.display:
            print(line)
        assert_list_view(screen, 1, [
            'dummy_entry_for_scroll_testing', 'knuthwebsite', 'latexcompanion', 'einstein'
        ])
        # the terminal should be wide enough to contain the full information text
        assert screen.display[-2].strip() == TUI.infoline()
Example #18
0
    def test_bind_keys(self, caplog: pytest.LogCaptureFixture) -> None:
        """Test `cobib.tui.tui.TUI.bind_keys`.

        Args:
            caplog: the built-in pytest fixture.
        """
        TUI.bind_keys()
        assert TUI.KEYDICT == {
            258: ("y", 1),
            259: ("y", -1),
            338: ("y", 20),
            339: ("y", -20),
            106: ("y", 1),
            107: ("y", -1),
            103: ("y", "g"),
            71: ("y", "G"),
            2: ("y", -20),
            4: ("y", 10),
            6: ("y", 20),
            21: ("y", -10),
            260: ("x", -1),
            261: ("x", 1),
            104: ("x", -1),
            108: ("x", 1),
            48: ("x", 0),
            36: ("x", "$"),
            58: "Prompt",
            47: "Search",
            63: "Help",
            97: "Add",
            100: "Delete",
            101: "Edit",
            102: "Filter",
            105: "Import",
            109: "Modify",
            111: "Open",
            113: "Quit",
            114: "Redo",
            115: "Sort",
            117: "Undo",
            118: "Select",
            119: "Wrap",
            120: "Export",
            10: "Show",
            13: "Show",
        }
        expected_log = [
            ("cobib.tui.tui", 20, "Binding key : to the Prompt command."),
            ("cobib.tui.tui", 20, "Binding key / to the Search command."),
            ("cobib.tui.tui", 20, "Binding key ? to the Help command."),
            ("cobib.tui.tui", 20, "Binding key a to the Add command."),
            ("cobib.tui.tui", 20, "Binding key d to the Delete command."),
            ("cobib.tui.tui", 20, "Binding key e to the Edit command."),
            ("cobib.tui.tui", 20, "Binding key f to the Filter command."),
            ("cobib.tui.tui", 20, "Binding key i to the Import command."),
            ("cobib.tui.tui", 20, "Binding key m to the Modify command."),
            ("cobib.tui.tui", 20, "Binding key o to the Open command."),
            ("cobib.tui.tui", 20, "Binding key q to the Quit command."),
            ("cobib.tui.tui", 20, "Binding key r to the Redo command."),
            ("cobib.tui.tui", 20, "Binding key s to the Sort command."),
            ("cobib.tui.tui", 20, "Binding key u to the Undo command."),
            ("cobib.tui.tui", 20, "Binding key v to the Select command."),
            ("cobib.tui.tui", 20, "Binding key w to the Wrap command."),
            ("cobib.tui.tui", 20, "Binding key x to the Export command."),
            ("cobib.tui.tui", 20, "Binding key ENTER to the Show command."),
        ]
        assert caplog.record_tuples == expected_log
Example #19
0
    def test_resize(self, patch_curses: Any,
                    caplog: pytest.LogCaptureFixture) -> None:
        """Test `cobib.tui.tui.TUI.resize_handler`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        tui = TUI(stdscr, debug=True)
        caplog.clear()

        tui.height, tui.width = (12, 70)
        tui.resize_handler(None, None)
        assert tui.width == 70
        assert tui.height == 12
        assert tui.topbar.size[1] == 70  # type: ignore
        assert tui.botbar.size[1] == 70  # type: ignore
        assert tui.prompt.size[1] == 70  # type: ignore
        expected_log = [
            ("TUITest", 10, "resize_term"),
            ("MockCursesPad", 10, "keypad: True"),
            ("MockCursesPad", 10, "clear"),
            ("MockCursesPad", 10, "refresh: None None None None None None"),
            ("MockCursesPad", 10, "resize: 1 70"),
            ("MockCursesPad", 10, "erase"),
            ("MockCursesPad", 10, "getmaxyx"),
            ("MockCursesPad", 10,
             "addnstr: 0 0 coBib VERSION - 3 Entries 69 0"),  # will be skipped
            ("MockCursesPad", 10, "refresh: None None None None None None"),
            ("MockCursesPad", 10, "refresh: None None None None None None"),
            ("MockCursesPad", 10, "resize: 1 70"),
            ("MockCursesPad", 10, "mvwin: 10 0"),
            ("MockCursesPad", 10, "erase"),
            ("MockCursesPad", 10, "getmaxyx"),
            (
                "MockCursesPad",
                10,
                "addnstr: 0 0 a:Add d:Delete e:Edit x:Export f:Filter ?:Help i:Import m:Modify "
                "o:Open ::Prompt q:Quit r:Redo /:Search v:Select ENTER:Show s:Sort u:Undo w:Wrap "
                "69 0",
            ),
            ("MockCursesPad", 10, "refresh: None None None None None None"),
            ("MockCursesPad", 10, "refresh: None None None None None None"),
            ("MockCursesPad", 10, "resize: 1 70"),
            ("MockCursesPad", 10, "refresh: 0 0 11 0 12 69"),
            ("MockCursesPad", 10, "refresh: 0 0 1 0 9 69"),
        ]
        for log, truth in zip(
                expected_log,
            [
                record for record in caplog.record_tuples
                if record[0] in ("MockCursesPad", "TUITest")
            ],
        ):
            assert log[0] == truth[0]
            assert log[1] == truth[1]
            if truth[2].startswith("addnstr: 0 0 coBib v"):
                # skip version-containing log
                continue
            assert log[2] == truth[2]
Example #20
0
    def run_tui(
        keys: Union[str, List[Union[str, signal.Signals]]],  # pylint: disable=no-member
        assertion: Callable,  # type: ignore
        assertion_kwargs: Dict,  # type: ignore
    ) -> None:
        """Spawns the coBib TUI in a forked pseudo-terminal.

        This method attaches a pyte object to the forked terminal to allow screen scraping. It also
        allows passing characters to the TUI by writing to the forked processes file descriptor.
        Furthermore, it also takes care of gathering the log, stdout/stderr and coverage information
        produced by the subprocess.

        For more information check out this
        [blog post](https://mrossinek.gitlab.io/programming/testing-tui-applications-in-python/).

        Args:
            keys: a string of characters passed to the TUI process ad verbatim.
            assertion: a callable method to assert the TUI state. This callable must take two
                       arguments: a pyte screen object and the caplog.record_tuples.
            assertion_kwargs: additional keyword arguments propagated to the assertion call.
        """
        # "hack" the fallback terminal size
        os.environ["COLUMNS"] = "80"
        os.environ["LINES"] = "24"

        # create pseudo-terminal
        pid, f_d = os.forkpty()

        if pid == 0:
            # setup subprocess coverage collection
            cov = TUITest.init_subprocess_coverage()
            signal.signal(signal.SIGTERM, partial(TUITest.end_subprocess_coverage, cov=cov))
            # redirect logging
            file_handler = get_file_handler(logging.DEBUG, TMP_LOGFILE)
            logging.getLogger().addHandler(file_handler)
            # child process initializes curses and spawns the TUI
            try:
                stdscr = curses.initscr()
                stdscr.resize(24, 80)
                if curses.has_colors():
                    curses.start_color()
                curses.cbreak()
                curses.noecho()
                stdscr.keypad(True)
                TUI(stdscr)
            finally:
                stdscr.keypad(False)
                curses.nocbreak()
                curses.echo()
        else:
            # parent process sets up virtual screen of identical size
            screen = pyte.Screen(80, 24)
            stream = pyte.ByteStream(screen)
            # send keys char-wise to TUI
            for key in keys:
                if key == signal.SIGWINCH:
                    sleep(0.25)
                    # resize pseudo terminal
                    buf = struct.pack("HHHH", 10, 45, 0, 0)
                    fcntl.ioctl(f_d, termios.TIOCSWINSZ, buf)
                    # overwrite screen
                    screen = pyte.Screen(45, 10)
                    stream = pyte.ByteStream(screen)
                else:
                    os.write(f_d, str.encode(str(key)))
            # scrape pseudo-terminal's screen
            while True:
                try:
                    [f_d], _, _ = select.select([f_d], [], [], 1)
                except (KeyboardInterrupt, ValueError):
                    # either test was interrupted or file descriptor of child process provides
                    # nothing to be read
                    break
                else:
                    try:
                        # scrape screen of child process
                        data = os.read(f_d, 1024)
                        stream.feed(data)
                    except OSError:
                        # reading empty
                        break
            # send SIGTERM to child process (which is necessary in order for pytest to not show
            # "failed" test cases produced by the forked child processes)
            os.kill(pid, signal.SIGTERM)
            # read the child process log file
            logs: List[Tuple[str, int, str]] = []
            print("### Captured logs from child process ###")
            with open(TMP_LOGFILE, "r", encoding="utf-8") as logfile:
                for line in logfile.readlines():
                    print(line.strip("\n"))
                    if not line.startswith(str(date.today())):
                        logs[-1] = (logs[-1][0], logs[-1][1], logs[-1][2] + line)
                        continue
                    split_line = line.split()
                    logs.append(
                        (
                            split_line[3],  # source
                            getattr(logging, split_line[2].strip("[]")),  # log level
                            " ".join(split_line[5:]),  # message
                        )
                    )
            os.remove(TMP_LOGFILE)
            # dump screen contents for easier debugging
            print("### Captured screen contents from child process ###")
            for line in screen.display:
                print(line)

            # It is necessary to read the database here, since we are in another subprocess
            Database().read()
            assertion(screen, logs, **assertion_kwargs)
Example #21
0
    def test_init(self, patch_curses: Any,
                  caplog: pytest.LogCaptureFixture) -> None:
        """Test `cobib.tui.tui.TUI.__init__`.

        Args:
            patch_curses: the `tests.tui.tui_test.TUITest.patch_curses` fixture.
            caplog: the built-in pytest fixture.
        """
        stdscr = MockCursesPad()
        stdscr.size = (24, 80)
        tui = TUI(stdscr, debug=True)
        assert tui.stdscr == stdscr
        assert tui.width == 80
        assert tui.height == 24
        assert tui.STATE == STATE
        assert tui.prompt_before_quit is True
        assert tui.selection == set()
        assert isinstance(tui.topbar, MockCursesPad)
        assert tui.topbar.lines[0].startswith("coBib")
        assert tui.topbar.lines[0].endswith("3 Entries")
        assert isinstance(tui.botbar, MockCursesPad)
        assert (
            tui.botbar.lines[0] ==
            "a:Add d:Delete e:Edit x:Export f:Filter ?:Help i:Import m:Modify o:Open ::Prompt "
            "q:Quit r:Redo /:Search v:Select ENTER:Show s:Sort u:Undo w:Wrap")
        assert isinstance(tui.prompt, MockCursesPad)
        assert isinstance(tui.viewport, Frame)
        assert tui.viewport.width == 80
        assert tui.viewport.height == 21
        expected_lines = [
            "knuthwebsite    Knuth: Computers and Typesetting",
            "latexcompanion  The \\LaTeX\\ Companion",
            'einstein        Zur Elektrodynamik bewegter K{\\"o}rper',
        ]
        for line, truth in zip_longest(expected_lines,
                                       tui.viewport.buffer.lines):
            assert line == truth.strip()
        expected_log = [
            ("cobib.tui.tui", 20, "Initializing TUI."),
            ("TUITest", 10, "curs_set: (0,)"),
            ("cobib.tui.tui", 10, "stdscr size determined to be 80x24"),
            ("cobib.tui.tui", 10, "Initializing colors."),
            ("TUITest", 10, "use_default_colors"),
            ("TUITest", 10, "start_color"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 1 for top_statusbar"),
            ("TUITest", 10, "init_pair: (1, 0, 3)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for top_statusbar"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 2 for bottom_statusbar"),
            ("TUITest", 10, "init_pair: (2, 0, 3)"),
            ("cobib.tui.tui", 10,
             "Adding ANSI color code for bottom_statusbar"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 3 for search_label"),
            ("TUITest", 10, "init_pair: (3, 4, 0)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for search_label"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 4 for search_query"),
            ("TUITest", 10, "init_pair: (4, 1, 0)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for search_query"),
            ("cobib.tui.tui", 10, "Initiliazing color pair 5 for cursor_line"),
            ("TUITest", 10, "init_pair: (5, 7, 6)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for cursor_line"),
            ("cobib.tui.tui", 10, "Initiliazing color pair 6 for popup_help"),
            ("TUITest", 10, "init_pair: (6, 7, 2)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for popup_help"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 7 for popup_stdout"),
            ("TUITest", 10, "init_pair: (7, 7, 4)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for popup_stdout"),
            ("cobib.tui.tui", 10,
             "Initiliazing color pair 8 for popup_stderr"),
            ("TUITest", 10, "init_pair: (8, 7, 1)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for popup_stderr"),
            ("cobib.tui.tui", 10, "Initiliazing color pair 9 for selection"),
            ("TUITest", 10, "init_pair: (9, 7, 5)"),
            ("cobib.tui.tui", 10, "Adding ANSI color code for selection"),
            ("cobib.tui.tui", 10, "Initializing key bindings."),
            ("cobib.tui.tui", 20, "Binding key : to the Prompt command."),
            ("cobib.tui.tui", 20, "Binding key / to the Search command."),
            ("cobib.tui.tui", 20, "Binding key ? to the Help command."),
            ("cobib.tui.tui", 20, "Binding key a to the Add command."),
            ("cobib.tui.tui", 20, "Binding key d to the Delete command."),
            ("cobib.tui.tui", 20, "Binding key e to the Edit command."),
            ("cobib.tui.tui", 20, "Binding key f to the Filter command."),
            ("cobib.tui.tui", 20, "Binding key i to the Import command."),
            ("cobib.tui.tui", 20, "Binding key m to the Modify command."),
            ("cobib.tui.tui", 20, "Binding key o to the Open command."),
            ("cobib.tui.tui", 20, "Binding key q to the Quit command."),
            ("cobib.tui.tui", 20, "Binding key r to the Redo command."),
            ("cobib.tui.tui", 20, "Binding key s to the Sort command."),
            ("cobib.tui.tui", 20, "Binding key u to the Undo command."),
            ("cobib.tui.tui", 20, "Binding key v to the Select command."),
            ("cobib.tui.tui", 20, "Binding key w to the Wrap command."),
            ("cobib.tui.tui", 20, "Binding key x to the Export command."),
            ("cobib.tui.tui", 20, "Binding key ENTER to the Show command."),
            ("cobib.tui.tui", 10, "Initializing global State"),
            ("cobib.tui.tui", 10, "Populating top status bar."),
            ("cobib.tui.tui", 10, "Populating bottom status bar."),
            ("cobib.tui.tui", 10, "Initializing viewport with Frame"),
            ("cobib.tui.tui", 10, "Populating viewport buffer."),
        ]
        assert [
            record for record in caplog.record_tuples
            if record[0] in ("cobib.tui.tui", "TUITest")
        ] == expected_log