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