• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Tests for pw_console.command_runner dialog."""
15
16import logging
17import re
18import unittest
19from typing import Callable, List, Tuple
20
21from unittest.mock import MagicMock
22
23from prompt_toolkit.application import create_app_session
24from prompt_toolkit.output import ColorDepth
25# inclusive-language: ignore
26from prompt_toolkit.output import DummyOutput as FakeOutput
27
28from pw_console.console_app import ConsoleApp
29from pw_console.console_prefs import ConsolePrefs
30from pw_console.text_formatting import (
31    flatten_formatted_text_tuples,
32    join_adjacent_style_tuples,
33)
34from window_manager_test import target_list_and_pane, window_pane_titles
35
36
37def _create_console_app(log_pane_count=2):
38    console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT,
39                             prefs=ConsolePrefs(project_file=False,
40                                                project_user_file=False,
41                                                user_file=False))
42    console_app.prefs.reset_config()
43
44    # Setup log panes
45    loggers = {}
46    for i in range(log_pane_count):
47        loggers['LogPane-{}'.format(i)] = [
48            logging.getLogger('test_log{}'.format(i))
49        ]
50    for window_title, logger_instances in loggers.items():
51        console_app.add_log_handler(window_title, logger_instances)
52
53    return console_app
54
55
56class TestCommandRunner(unittest.TestCase):
57    """Tests for CommandRunner."""
58    def setUp(self):
59        self.maxDiff = None  # pylint: disable=invalid-name
60
61    def test_flatten_menu_items(self) -> None:
62        with create_app_session(output=FakeOutput()):
63            console_app = _create_console_app(log_pane_count=2)
64            flattened_menu_items = [
65                text for text, handler in
66                console_app.command_runner.load_menu_items()
67            ]
68
69            # Check some common menu items exist.
70            self.assertIn('[File] > Open Logger', flattened_menu_items)
71            self.assertIn('[File] > Themes > UI Themes > High Contrast',
72                          flattened_menu_items)
73            self.assertIn('[Help] > User Guide', flattened_menu_items)
74            self.assertIn('[Help] > Keyboard Shortcuts', flattened_menu_items)
75            # Check for log windows
76            self.assertRegex(
77                '\n'.join(flattened_menu_items),
78                re.compile(r'^\[Windows\] > .* LogPane-[0-9]+ > .*$',
79                           re.MULTILINE),
80            )
81
82    def test_filter_and_highlight_matches(self) -> None:
83        """Check filtering matches and highlighting works correctly."""
84        with create_app_session(output=FakeOutput()):
85            console_app = _create_console_app(log_pane_count=2)
86            command_runner = console_app.command_runner
87
88            command_runner.filter_completions = MagicMock(
89                wraps=command_runner.filter_completions)
90            command_runner.width = 20
91
92            # Define custom completion items
93            def empty_handler() -> None:
94                return None
95
96            def get_completions() -> List[Tuple[str, Callable]]:
97                return [
98                    ('[File] > Open Logger', empty_handler),
99                    ('[Windows] > 1: Host Logs > Show/Hide', empty_handler),
100                    ('[Windows] > 2: Device Logs > Show/Hide', empty_handler),
101                    ('[Help] > User Guide', empty_handler),
102                ]
103
104            command_runner.filter_completions.assert_not_called()
105            command_runner.set_completions(window_title='Test Completions',
106                                           load_completions=get_completions)
107            command_runner.filter_completions.assert_called_once()
108            command_runner.filter_completions.reset_mock()
109
110            # Input field should be empty
111            self.assertEqual(command_runner.input_field.buffer.text, '')
112            # Flatten resulting formatted text
113            result_items = join_adjacent_style_tuples(
114                flatten_formatted_text_tuples(
115                    command_runner.completion_fragments))
116
117            # index 0: the selected line
118            # index 1: the rest of the completions with line breaks
119            self.assertEqual(len(result_items), 2)
120            first_item_style = result_items[0][0]
121            first_item_text = result_items[0][1]
122            second_item_text = result_items[1][1]
123            # Check expected number of lines are present
124            self.assertEqual(len(first_item_text.splitlines()), 1)
125            self.assertEqual(len(second_item_text.splitlines()), 3)
126            # First line is highlighted as a selected item
127            self.assertEqual(first_item_style,
128                             'class:command-runner-selected-item')
129            self.assertIn('[File] > Open Logger', first_item_text)
130
131            # Type: file open
132            command_runner.input_field.buffer.text = 'file open'
133            self.assertEqual(command_runner.input_field.buffer.text,
134                             'file open')
135            # Run the filter
136            command_runner.filter_completions()
137            # Flatten resulting formatted text
138            result_items = join_adjacent_style_tuples(
139                flatten_formatted_text_tuples(
140                    command_runner.completion_fragments))
141            # Check file and open are highlighted
142            self.assertEqual(
143                result_items[:4],
144                [
145                    ('class:command-runner-selected-item', '['),
146                    ('class:command-runner-selected-item '
147                     'class:command-runner-fuzzy-highlight-0 ', 'File'),
148                    ('class:command-runner-selected-item', '] > '),
149                    ('class:command-runner-selected-item '
150                     'class:command-runner-fuzzy-highlight-1 ', 'Open'),
151                ],
152            )
153
154            # Type: open file
155            command_runner.input_field.buffer.text = 'open file'
156            # Run the filter
157            command_runner.filter_completions()
158            result_items = join_adjacent_style_tuples(
159                flatten_formatted_text_tuples(
160                    command_runner.completion_fragments))
161            # Check file and open are highlighted, the fuzzy-highlight class
162            # should be swapped.
163            self.assertEqual(
164                result_items[:4],
165                [
166                    ('class:command-runner-selected-item', '['),
167                    ('class:command-runner-selected-item '
168                     'class:command-runner-fuzzy-highlight-1 ', 'File'),
169                    ('class:command-runner-selected-item', '] > '),
170                    ('class:command-runner-selected-item '
171                     'class:command-runner-fuzzy-highlight-0 ', 'Open'),
172                ],
173            )
174
175            # Clear input
176            command_runner._reset_selected_item()  # pylint: disable=protected-access
177            command_runner.filter_completions()
178            result_items = join_adjacent_style_tuples(
179                flatten_formatted_text_tuples(
180                    command_runner.completion_fragments))
181            self.assertEqual(len(first_item_text.splitlines()), 1)
182            self.assertEqual(len(second_item_text.splitlines()), 3)
183
184            # Press down (select the next item)
185            command_runner._next_item()  # pylint: disable=protected-access
186            # Filter and check results
187            command_runner.filter_completions()
188            result_items = join_adjacent_style_tuples(
189                flatten_formatted_text_tuples(
190                    command_runner.completion_fragments))
191            self.assertEqual(len(result_items), 3)
192            # First line - not selected
193            self.assertEqual(result_items[0], ('', '[File] > Open Logger\n'))
194            # Second line - is selected
195            self.assertEqual(result_items[1],
196                             ('class:command-runner-selected-item',
197                              '[Windows] > 1: Host Logs > Show/Hide\n'))
198            # Third and fourth lines separated by \n - not selected
199            self.assertEqual(result_items[2],
200                             ('', '[Windows] > 2: Device Logs > Show/Hide\n'
201                              '[Help] > User Guide'))
202
203    def test_run_action(self) -> None:
204        """Check running an action works correctly."""
205        with create_app_session(output=FakeOutput()):
206            console_app = _create_console_app(log_pane_count=2)
207            command_runner = console_app.command_runner
208            self.assertEqual(
209                window_pane_titles(console_app.window_manager),
210                [
211                    # Split 1
212                    [
213                        'LogPane-1 - test_log1',
214                        'LogPane-0 - test_log0',
215                        'Python Repl - ',
216                    ],
217                ],
218            )
219            command_runner.open_dialog()
220            # Set LogPane-1 as the focused window pane
221            target_list_and_pane(console_app.window_manager, 0, 0)
222
223            command_runner.input_field.buffer.text = 'move right'
224
225            # pylint: disable=protected-access
226            command_runner._make_regexes = MagicMock(
227                wraps=command_runner._make_regexes)
228            # pylint: enable=protected-access
229            command_runner.filter_completions()
230            # Filter should only be re-run if input text changed
231            command_runner.filter_completions()
232            command_runner._make_regexes.assert_called_once()  # pylint: disable=protected-access
233
234            self.assertIn('[View] > Move Window Right',
235                          command_runner.selected_item_text)
236            # Run the Move Window Right action
237            command_runner._run_selected_item()  # pylint: disable=protected-access
238            # Dialog should be closed
239            self.assertFalse(command_runner.show_dialog)
240            # LogPane-1 should be moved to the right in it's own split
241            self.assertEqual(
242                window_pane_titles(console_app.window_manager),
243                [
244                    # Split 1
245                    [
246                        'LogPane-0 - test_log0',
247                        'Python Repl - ',
248                    ],
249                    # Split 2
250                    [
251                        'LogPane-1 - test_log1',
252                    ],
253                ],
254            )
255
256
257if __name__ == '__main__':
258    unittest.main()
259