• 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
26# inclusive-language: ignore
27from prompt_toolkit.output import DummyOutput as FakeOutput
28
29from pw_console.console_app import ConsoleApp
30from pw_console.console_prefs import ConsolePrefs
31from pw_console.text_formatting import (
32    flatten_formatted_text_tuples,
33    join_adjacent_style_tuples,
34)
35
36
37def _create_console_app(log_pane_count=2):
38    prefs = ConsolePrefs(
39        project_file=False, project_user_file=False, user_file=False
40    )
41    prefs.set_code_theme('default')
42    console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT, prefs=prefs)
43
44    console_app.prefs.reset_config()
45
46    # Setup log panes
47    loggers = {}
48    for i in range(log_pane_count):
49        loggers['LogPane-{}'.format(i)] = [
50            logging.getLogger('test_log{}'.format(i))
51        ]
52    for window_title, logger_instances in loggers.items():
53        console_app.add_log_handler(window_title, logger_instances)
54
55    return console_app
56
57
58def window_pane_titles(window_manager):
59    return [
60        [
61            pane.pane_title() + ' - ' + pane.pane_subtitle()
62            for pane in window_list.active_panes
63        ]
64        for window_list in window_manager.window_lists
65    ]
66
67
68def target_list_and_pane(window_manager, list_index, pane_index):
69    # pylint: disable=protected-access
70    # Bypass prompt_toolkit has_focus()
71    pane = window_manager.window_lists[list_index].active_panes[pane_index]
72    # If the pane is in focus it will be visible.
73    pane.show_pane = True
74    window_manager._get_active_window_list_and_pane = MagicMock(  # type: ignore
75        return_value=(
76            window_manager.window_lists[list_index],
77            window_manager.window_lists[list_index].active_panes[pane_index],
78        )
79    )
80
81
82class TestCommandRunner(unittest.TestCase):
83    """Tests for CommandRunner."""
84
85    def setUp(self):
86        self.maxDiff = None  # pylint: disable=invalid-name
87
88    def test_flatten_menu_items(self) -> None:
89        with create_app_session(output=FakeOutput()):
90            console_app = _create_console_app(log_pane_count=2)
91            flattened_menu_items = [
92                text
93                # pylint: disable=line-too-long
94                for text, handler in console_app.command_runner.load_menu_items()
95                # pylint: enable=line-too-long
96            ]
97
98            # Check some common menu items exist.
99            self.assertIn('[File] > Open Logger', flattened_menu_items)
100            self.assertIn(
101                '[File] > Themes > UI Themes > High Contrast',
102                flattened_menu_items,
103            )
104            self.assertIn('[Help] > User Guide', flattened_menu_items)
105            self.assertIn('[Help] > Keyboard Shortcuts', flattened_menu_items)
106            # Check for log windows
107            self.assertRegex(
108                '\n'.join(flattened_menu_items),
109                re.compile(
110                    r'^\[Windows\] > .* LogPane-[0-9]+ > .*$', re.MULTILINE
111                ),
112            )
113
114    def test_filter_and_highlight_matches(self) -> None:
115        """Check filtering matches and highlighting works correctly."""
116        with create_app_session(output=FakeOutput()):
117            console_app = _create_console_app(log_pane_count=2)
118            command_runner = console_app.command_runner
119
120            command_runner.filter_completions = MagicMock(
121                wraps=command_runner.filter_completions
122            )
123            command_runner.width = 20
124
125            # Define custom completion items
126            def empty_handler() -> None:
127                return None
128
129            def get_completions() -> List[Tuple[str, Callable]]:
130                return [
131                    ('[File] > Open Logger', empty_handler),
132                    ('[Windows] > 1: Host Logs > Show/Hide', empty_handler),
133                    ('[Windows] > 2: Device Logs > Show/Hide', empty_handler),
134                    ('[Help] > User Guide', empty_handler),
135                ]
136
137            command_runner.filter_completions.assert_not_called()
138            command_runner.set_completions(
139                window_title='Test Completions',
140                load_completions=get_completions,
141            )
142            command_runner.filter_completions.assert_called_once()
143            command_runner.filter_completions.reset_mock()
144
145            # Input field should be empty
146            self.assertEqual(command_runner.input_field.buffer.text, '')
147            # Flatten resulting formatted text
148            result_items = join_adjacent_style_tuples(
149                flatten_formatted_text_tuples(
150                    command_runner.completion_fragments
151                )
152            )
153
154            # index 0: the selected line
155            # index 1: the rest of the completions with line breaks
156            self.assertEqual(len(result_items), 2)
157            first_item_style = result_items[0][0]
158            first_item_text = result_items[0][1]
159            second_item_text = result_items[1][1]
160            # Check expected number of lines are present
161            self.assertEqual(len(first_item_text.splitlines()), 1)
162            self.assertEqual(len(second_item_text.splitlines()), 3)
163            # First line is highlighted as a selected item
164            self.assertEqual(
165                first_item_style, 'class:command-runner-selected-item'
166            )
167            self.assertIn('[File] > Open Logger', first_item_text)
168
169            # Type: file open
170            command_runner.input_field.buffer.text = 'file open'
171            self.assertEqual(
172                command_runner.input_field.buffer.text, 'file open'
173            )
174            # Run the filter
175            command_runner.filter_completions()
176            # Flatten resulting formatted text
177            result_items = join_adjacent_style_tuples(
178                flatten_formatted_text_tuples(
179                    command_runner.completion_fragments
180                )
181            )
182            # Check file and open are highlighted
183            self.assertEqual(
184                result_items[:4],
185                [
186                    ('class:command-runner-selected-item', '['),
187                    (
188                        'class:command-runner-selected-item '
189                        'class:command-runner-fuzzy-highlight-0 ',
190                        'File',
191                    ),
192                    ('class:command-runner-selected-item', '] > '),
193                    (
194                        'class:command-runner-selected-item '
195                        'class:command-runner-fuzzy-highlight-1 ',
196                        'Open',
197                    ),
198                ],
199            )
200
201            # Type: open file
202            command_runner.input_field.buffer.text = 'open file'
203            # Run the filter
204            command_runner.filter_completions()
205            result_items = join_adjacent_style_tuples(
206                flatten_formatted_text_tuples(
207                    command_runner.completion_fragments
208                )
209            )
210            # Check file and open are highlighted, the fuzzy-highlight class
211            # should be swapped.
212            self.assertEqual(
213                result_items[:4],
214                [
215                    ('class:command-runner-selected-item', '['),
216                    (
217                        'class:command-runner-selected-item '
218                        'class:command-runner-fuzzy-highlight-1 ',
219                        'File',
220                    ),
221                    ('class:command-runner-selected-item', '] > '),
222                    (
223                        'class:command-runner-selected-item '
224                        'class:command-runner-fuzzy-highlight-0 ',
225                        'Open',
226                    ),
227                ],
228            )
229
230            # Clear input
231            command_runner._reset_selected_item()  # pylint: disable=protected-access
232            command_runner.filter_completions()
233            result_items = join_adjacent_style_tuples(
234                flatten_formatted_text_tuples(
235                    command_runner.completion_fragments
236                )
237            )
238            self.assertEqual(len(first_item_text.splitlines()), 1)
239            self.assertEqual(len(second_item_text.splitlines()), 3)
240
241            # Press down (select the next item)
242            command_runner._next_item()  # pylint: disable=protected-access
243            # Filter and check results
244            command_runner.filter_completions()
245            result_items = join_adjacent_style_tuples(
246                flatten_formatted_text_tuples(
247                    command_runner.completion_fragments
248                )
249            )
250            self.assertEqual(len(result_items), 3)
251            # First line - not selected
252            self.assertEqual(result_items[0], ('', '[File] > Open Logger\n'))
253            # Second line - is selected
254            self.assertEqual(
255                result_items[1],
256                (
257                    'class:command-runner-selected-item',
258                    '[Windows] > 1: Host Logs > Show/Hide\n',
259                ),
260            )
261            # Third and fourth lines separated by \n - not selected
262            self.assertEqual(
263                result_items[2],
264                (
265                    '',
266                    '[Windows] > 2: Device Logs > Show/Hide\n'
267                    '[Help] > User Guide',
268                ),
269            )
270
271    def test_run_action(self) -> None:
272        """Check running an action works correctly."""
273        with create_app_session(output=FakeOutput()):
274            console_app = _create_console_app(log_pane_count=2)
275            command_runner = console_app.command_runner
276            self.assertEqual(
277                window_pane_titles(console_app.window_manager),
278                [
279                    # Split 1
280                    [
281                        'LogPane-1 - test_log1',
282                        'LogPane-0 - test_log0',
283                        'Python Repl - ',
284                    ],
285                ],
286            )
287            command_runner.open_dialog()
288            # Set LogPane-1 as the focused window pane
289            target_list_and_pane(console_app.window_manager, 0, 0)
290
291            command_runner.input_field.buffer.text = 'move right'
292
293            # pylint: disable=protected-access
294            command_runner._make_regexes = MagicMock(
295                wraps=command_runner._make_regexes
296            )
297            # pylint: enable=protected-access
298            command_runner.filter_completions()
299            # Filter should only be re-run if input text changed
300            command_runner.filter_completions()
301            command_runner._make_regexes.assert_called_once()  # pylint: disable=protected-access
302
303            self.assertIn(
304                '[View] > Move Window Right', command_runner.selected_item_text
305            )
306            # Run the Move Window Right action
307            command_runner._run_selected_item()  # pylint: disable=protected-access
308            # Dialog should be closed
309            self.assertFalse(command_runner.show_dialog)
310            # LogPane-1 should be moved to the right in it's own split
311            self.assertEqual(
312                window_pane_titles(console_app.window_manager),
313                [
314                    # Split 1
315                    [
316                        'LogPane-0 - test_log0',
317                        'Python Repl - ',
318                    ],
319                    # Split 2
320                    [
321                        'LogPane-1 - test_log1',
322                    ],
323                ],
324            )
325
326
327if __name__ == '__main__':
328    unittest.main()
329