• 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
19
20from unittest.mock import MagicMock
21
22from prompt_toolkit.application import create_app_session
23from prompt_toolkit.output import ColorDepth
24
25# inclusive-language: ignore
26from prompt_toolkit.output import DummyOutput as FakeOutput
27
28from pw_console.command_runner import CommandRunnerItem
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    maxDiff = None
86
87    def test_flatten_menu_items(self) -> None:
88        with create_app_session(output=FakeOutput()):
89            console_app = _create_console_app(log_pane_count=2)
90            flattened_menu_items = [
91                item.title
92                for item in console_app.command_runner.load_menu_items()
93            ]
94
95            # Check some common menu items exist.
96            self.assertIn('[File] > Open Logger', flattened_menu_items)
97            self.assertIn(
98                '[File] > Themes > UI Themes > High Contrast',
99                flattened_menu_items,
100            )
101            self.assertIn('[Help] > User Guide', flattened_menu_items)
102            self.assertIn('[Help] > Keyboard Shortcuts', flattened_menu_items)
103            # Check for log windows
104            self.assertRegex(
105                '\n'.join(flattened_menu_items),
106                re.compile(
107                    r'^\[Windows\] > .* LogPane-[0-9]+ > .*$', re.MULTILINE
108                ),
109            )
110
111    def test_filter_and_highlight_matches(self) -> None:
112        """Check filtering matches and highlighting works correctly."""
113        with create_app_session(output=FakeOutput()):
114            console_app = _create_console_app(log_pane_count=2)
115            command_runner = console_app.command_runner
116
117            command_runner.filter_completions = MagicMock(
118                wraps=command_runner.filter_completions
119            )
120            command_runner.width = 20
121
122            # Define custom completion items
123            def empty_handler() -> None:
124                return None
125
126            def get_completions() -> list[CommandRunnerItem]:
127                return [
128                    CommandRunnerItem('[File] > Open Logger', empty_handler),
129                    CommandRunnerItem(
130                        '[Windows] > 1: Host Logs > Show/Hide', empty_handler
131                    ),
132                    CommandRunnerItem(
133                        '[Windows] > 2: Device Logs > Show/Hide', empty_handler
134                    ),
135                    CommandRunnerItem('[Help] > User Guide', empty_handler),
136                ]
137
138            command_runner.filter_completions.assert_not_called()
139            command_runner.set_completions(
140                window_title='Test Completions',
141                load_completions=get_completions,
142            )
143            command_runner.filter_completions.assert_called_once()
144            command_runner.filter_completions.reset_mock()
145
146            # Input field should be empty
147            self.assertEqual(command_runner.input_field.buffer.text, '')
148            # Flatten resulting formatted text
149            result_items = join_adjacent_style_tuples(
150                flatten_formatted_text_tuples(
151                    command_runner.completion_fragments
152                )
153            )
154
155            # index 0: the selected line
156            # index 1: the rest of the completions with line breaks
157            self.assertEqual(len(result_items), 2)
158            first_item_style = result_items[0][0]
159            first_item_text = result_items[0][1]
160            second_item_text = result_items[1][1]
161            # Check expected number of lines are present
162            self.assertEqual(len(first_item_text.splitlines()), 1)
163            self.assertEqual(len(second_item_text.splitlines()), 3)
164            # First line is highlighted as a selected item
165            self.assertEqual(
166                first_item_style, 'class:command-runner-selected-item'
167            )
168            self.assertIn('[File] > Open Logger', first_item_text)
169
170            # Type: file open
171            command_runner.input_field.buffer.text = 'file open'
172            self.assertEqual(
173                command_runner.input_field.buffer.text, 'file open'
174            )
175            # Run the filter
176            command_runner.filter_completions()
177            # Flatten resulting formatted text
178            result_items = join_adjacent_style_tuples(
179                flatten_formatted_text_tuples(
180                    command_runner.completion_fragments
181                )
182            )
183            # Check file and open are highlighted
184            self.assertEqual(
185                result_items[:4],
186                [
187                    ('class:command-runner-selected-item', '['),
188                    (
189                        'class:command-runner-selected-item '
190                        'class:command-runner-fuzzy-highlight-0 ',
191                        'File',
192                    ),
193                    ('class:command-runner-selected-item', '] > '),
194                    (
195                        'class:command-runner-selected-item '
196                        'class:command-runner-fuzzy-highlight-1 ',
197                        'Open',
198                    ),
199                ],
200            )
201
202            # Type: open file
203            command_runner.input_field.buffer.text = 'open file'
204            # Run the filter
205            command_runner.filter_completions()
206            result_items = join_adjacent_style_tuples(
207                flatten_formatted_text_tuples(
208                    command_runner.completion_fragments
209                )
210            )
211            # Check file and open are highlighted, the fuzzy-highlight class
212            # should be swapped.
213            self.assertEqual(
214                result_items[:4],
215                [
216                    ('class:command-runner-selected-item', '['),
217                    (
218                        'class:command-runner-selected-item '
219                        'class:command-runner-fuzzy-highlight-1 ',
220                        'File',
221                    ),
222                    ('class:command-runner-selected-item', '] > '),
223                    (
224                        'class:command-runner-selected-item '
225                        'class:command-runner-fuzzy-highlight-0 ',
226                        'Open',
227                    ),
228                ],
229            )
230
231            # Clear input
232            command_runner._reset_selected_item()  # pylint: disable=protected-access
233            command_runner.filter_completions()
234            result_items = join_adjacent_style_tuples(
235                flatten_formatted_text_tuples(
236                    command_runner.completion_fragments
237                )
238            )
239            self.assertEqual(len(first_item_text.splitlines()), 1)
240            self.assertEqual(len(second_item_text.splitlines()), 3)
241
242            # Press down (select the next item)
243            command_runner._next_item()  # pylint: disable=protected-access
244            # Filter and check results
245            command_runner.filter_completions()
246            result_items = join_adjacent_style_tuples(
247                flatten_formatted_text_tuples(
248                    command_runner.completion_fragments
249                )
250            )
251            self.assertEqual(len(result_items), 3)
252            # First line - not selected
253            self.assertEqual(result_items[0], ('', '[File] > Open Logger\n'))
254            # Second line - is selected
255            self.assertEqual(
256                result_items[1],
257                (
258                    'class:command-runner-selected-item',
259                    '[Windows] > 1: Host Logs > Show/Hide\n',
260                ),
261            )
262            # Third and fourth lines separated by \n - not selected
263            self.assertEqual(
264                result_items[2],
265                (
266                    '',
267                    '[Windows] > 2: Device Logs > Show/Hide\n'
268                    '[Help] > User Guide',
269                ),
270            )
271
272    def test_run_action(self) -> None:
273        """Check running an action works correctly."""
274        with create_app_session(output=FakeOutput()):
275            console_app = _create_console_app(log_pane_count=2)
276            command_runner = console_app.command_runner
277            self.assertEqual(
278                window_pane_titles(console_app.window_manager),
279                [
280                    # Split 1
281                    [
282                        'LogPane-1 - test_log1',
283                        'LogPane-0 - test_log0',
284                        'Python Repl - ',
285                    ],
286                ],
287            )
288            command_runner.open_dialog()
289            # Set LogPane-1 as the focused window pane
290            target_list_and_pane(console_app.window_manager, 0, 0)
291
292            command_runner.input_field.buffer.text = 'move right'
293
294            # pylint: disable=protected-access
295            command_runner._make_regexes = MagicMock(
296                wraps=command_runner._make_regexes
297            )
298            # pylint: enable=protected-access
299            command_runner.filter_completions()
300            # Filter should only be re-run if input text changed
301            command_runner.filter_completions()
302            command_runner._make_regexes.assert_called_once()  # pylint: disable=protected-access
303
304            self.assertIn(
305                '[View] > Move Window Right', command_runner.selected_item_title
306            )
307            # Run the Move Window Right action
308            command_runner._run_selected_item()  # pylint: disable=protected-access
309            # Dialog should be closed
310            self.assertFalse(command_runner.show_dialog)
311            # LogPane-1 should be moved to the right in it's own split
312            self.assertEqual(
313                window_pane_titles(console_app.window_manager),
314                [
315                    # Split 1
316                    [
317                        'LogPane-0 - test_log0',
318                        'Python Repl - ',
319                    ],
320                    # Split 2
321                    [
322                        'LogPane-1 - test_log1',
323                    ],
324                ],
325            )
326
327
328if __name__ == '__main__':
329    unittest.main()
330