# Copyright 2021 The Pigweed Authors # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of # the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. """Tests for pw_console.console_app""" import logging import unittest from unittest.mock import MagicMock from prompt_toolkit.application import create_app_session from prompt_toolkit.output import ColorDepth # inclusive-language: ignore from prompt_toolkit.output import DummyOutput as FakeOutput from pw_console.console_app import ConsoleApp from pw_console.console_prefs import ConsolePrefs from pw_console.window_manager import _WINDOW_SPLIT_ADJUST from pw_console.window_list import _WINDOW_HEIGHT_ADJUST, DisplayMode def _create_console_app(logger_count=2): prefs = ConsolePrefs( project_file=False, project_user_file=False, user_file=False ) prefs.set_code_theme('default') console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT, prefs=prefs) console_app.focus_on_container = MagicMock() loggers = {} for i in range(logger_count): loggers['Log{}'.format(i)] = [logging.getLogger('test_log{}'.format(i))] for window_title, logger_instances in loggers.items(): console_app.add_log_handler(window_title, logger_instances) return console_app _WINDOW_MANAGER_WIDTH = 80 _WINDOW_MANAGER_HEIGHT = 30 _DEFAULT_WINDOW_WIDTH = 10 _DEFAULT_WINDOW_HEIGHT = 10 def _window_list_widths(window_manager): window_manager.update_window_manager_size( _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT ) return [ window_list.width.preferred for window_list in window_manager.window_lists ] def _window_list_heights(window_manager): window_manager.update_window_manager_size( _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT ) return [ window_list.height.preferred for window_list in window_manager.window_lists ] def _window_pane_widths(window_manager, window_list_index=0): window_manager.update_window_manager_size( _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT ) return [ pane.width.preferred for pane in window_manager.window_lists[window_list_index].active_panes ] def _window_pane_heights(window_manager, window_list_index=0): window_manager.update_window_manager_size( _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT ) return [ pane.height.preferred for pane in window_manager.window_lists[window_list_index].active_panes ] def _window_pane_counts(window_manager): return [ len(window_list.active_panes) for window_list in window_manager.window_lists ] def window_pane_titles(window_manager): return [ [ pane.pane_title() + ' - ' + pane.pane_subtitle() for pane in window_list.active_panes ] for window_list in window_manager.window_lists ] def target_list_and_pane(window_manager, list_index, pane_index): # pylint: disable=protected-access # Bypass prompt_toolkit has_focus() pane = window_manager.window_lists[list_index].active_panes[pane_index] # If the pane is in focus it will be visible. pane.show_pane = True window_manager._get_active_window_list_and_pane = MagicMock( # type: ignore return_value=( window_manager.window_lists[list_index], window_manager.window_lists[list_index].active_panes[pane_index], ) ) class TestWindowManager(unittest.TestCase): # pylint: disable=protected-access """Tests for window management functions.""" maxDiff = None def test_find_window_list_and_pane(self) -> None: """Test getting the window list for a given pane.""" with create_app_session(output=FakeOutput()): console_app = _create_console_app(logger_count=3) window_manager = console_app.window_manager self.assertEqual([4], _window_pane_counts(window_manager)) # Move 2 windows to the right into their own splits target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() target_list_and_pane(window_manager, 1, 0) window_manager.move_pane_right() # 3 splits, first split has 2 windows self.assertEqual([2, 1, 1], _window_pane_counts(window_manager)) # Move the first window in the first split left target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_left() # 4 splits, each with their own window self.assertEqual([1, 1, 1, 1], _window_pane_counts(window_manager)) # Move the first window to the right target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() # 3 splits, first split has 2 windows self.assertEqual([2, 1, 1], _window_pane_counts(window_manager)) target_pane = window_manager.window_lists[2].active_panes[0] ( result_window_list, result_pane_index, ) = window_manager.find_window_list_and_pane_index(target_pane) self.assertEqual( (result_window_list, result_pane_index), (window_manager.window_lists[2], 0), ) window_manager.remove_pane(target_pane) self.assertEqual([2, 1], _window_pane_counts(window_manager)) def test_window_list_moving_and_resizing(self) -> None: """Test window split movement resizing.""" with create_app_session(output=FakeOutput()): console_app = _create_console_app(logger_count=3) window_manager = console_app.window_manager target_list_and_pane(window_manager, 0, 0) # Should have one window list split of size 50. self.assertEqual( _window_list_widths(window_manager), [_WINDOW_MANAGER_WIDTH], ) # Move one pane to the right, creating a new window_list split. window_manager.move_pane_right() self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 2), int(_WINDOW_MANAGER_WIDTH / 2), ], ) # Move another pane to the right twice, creating a third # window_list split. target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() # Above window pane is at a new location target_list_and_pane(window_manager, 1, 0) window_manager.move_pane_right() # Should have 3 splits now self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 3), int(_WINDOW_MANAGER_WIDTH / 3), int(_WINDOW_MANAGER_WIDTH / 3), ], ) # Total of 4 active panes self.assertEqual(len(list(window_manager.active_panes())), 4) # Target the middle split target_list_and_pane(window_manager, 1, 0) # Shrink the middle split twice window_manager.shrink_split() window_manager.shrink_split() self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 3), int(_WINDOW_MANAGER_WIDTH / 3) - (2 * _WINDOW_SPLIT_ADJUST), int(_WINDOW_MANAGER_WIDTH / 3) + (2 * _WINDOW_SPLIT_ADJUST), ], ) # Target the first split target_list_and_pane(window_manager, 0, 0) window_manager.reset_split_sizes() # Shrink the first split twice window_manager.shrink_split() self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 3) - (1 * _WINDOW_SPLIT_ADJUST), int(_WINDOW_MANAGER_WIDTH / 3) + (1 * _WINDOW_SPLIT_ADJUST), int(_WINDOW_MANAGER_WIDTH / 3), ], ) # Target the third (last) split target_list_and_pane(window_manager, 2, 0) window_manager.reset_split_sizes() # Shrink the third split once window_manager.shrink_split() self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 3), int(_WINDOW_MANAGER_WIDTH / 3) + (1 * _WINDOW_SPLIT_ADJUST), int(_WINDOW_MANAGER_WIDTH / 3) - (1 * _WINDOW_SPLIT_ADJUST), ], ) window_manager.reset_split_sizes() # Enlarge the third split a few times. window_manager.enlarge_split() window_manager.enlarge_split() window_manager.enlarge_split() self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 3), int(_WINDOW_MANAGER_WIDTH / 3) - (3 * _WINDOW_SPLIT_ADJUST), int(_WINDOW_MANAGER_WIDTH / 3) + (3 * _WINDOW_SPLIT_ADJUST), ], ) # Target the middle split target_list_and_pane(window_manager, 1, 0) # Move the middle window pane left window_manager.move_pane_left() # This is called on the next render pass window_manager.rebalance_window_list_sizes() # Middle split should be removed self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 2) - (3 * _WINDOW_SPLIT_ADJUST), # This split is removed int(_WINDOW_MANAGER_WIDTH / 2) + (2 * _WINDOW_SPLIT_ADJUST), ], ) # Revert sizes to default window_manager.reset_split_sizes() self.assertEqual( _window_list_widths(window_manager), [ int(_WINDOW_MANAGER_WIDTH / 2), int(_WINDOW_MANAGER_WIDTH / 2), ], ) def test_get_pane_titles(self) -> None: """Test window resizing.""" with create_app_session(output=FakeOutput()): console_app = _create_console_app(logger_count=3) window_manager = console_app.window_manager list_pane_titles = [ # Remove mouse click handler partials in tup[2] [(tup[0], tup[1]) for tup in window_list.get_pane_titles()] for window_list in window_manager.window_lists ] self.assertEqual( list_pane_titles[0], [ ('', ' '), ('class:window-tab-inactive', ' Log2 test_log2 '), ('', ' '), ('class:window-tab-inactive', ' Log1 test_log1 '), ('', ' '), ('class:window-tab-inactive', ' Log0 test_log0 '), ('', ' '), ('class:window-tab-inactive', ' Python Repl '), ('', ' '), ], ) def test_window_pane_movement_resizing(self) -> None: """Test window resizing.""" with create_app_session(output=FakeOutput()): console_app = _create_console_app(logger_count=3) window_manager = console_app.window_manager # 4 panes, 3 for the loggers and 1 for the repl. self.assertEqual( len(window_manager.first_window_list().active_panes), 4 ) def target_window_pane(index: int): # Bypass prompt_toolkit has_focus() window_manager._get_active_window_list_and_pane = ( MagicMock( # type: ignore return_value=( window_manager.window_lists[0], window_manager.window_lists[0].active_panes[index], ) ) ) window_list = console_app.window_manager.first_window_list() window_list.get_current_active_pane = MagicMock( # type: ignore return_value=window_list.active_panes[index] ) # Target the first window pane target_window_pane(0) # Shrink the first pane window_manager.shrink_pane() self.assertEqual( _window_pane_heights(window_manager), [ _DEFAULT_WINDOW_HEIGHT - (1 * _WINDOW_HEIGHT_ADJUST), _DEFAULT_WINDOW_HEIGHT + (1 * _WINDOW_HEIGHT_ADJUST), _DEFAULT_WINDOW_HEIGHT, _DEFAULT_WINDOW_HEIGHT, ], ) # Reset pane sizes window_manager.window_lists[0].current_window_list_height = ( 4 * _DEFAULT_WINDOW_HEIGHT ) window_manager.reset_pane_sizes() self.assertEqual( _window_pane_heights(window_manager), [ _DEFAULT_WINDOW_HEIGHT, _DEFAULT_WINDOW_HEIGHT, _DEFAULT_WINDOW_HEIGHT, _DEFAULT_WINDOW_HEIGHT, ], ) # Shrink last pane target_window_pane(3) window_manager.shrink_pane() self.assertEqual( _window_pane_heights(window_manager), [ _DEFAULT_WINDOW_HEIGHT, _DEFAULT_WINDOW_HEIGHT, _DEFAULT_WINDOW_HEIGHT + (1 * _WINDOW_HEIGHT_ADJUST), _DEFAULT_WINDOW_HEIGHT - (1 * _WINDOW_HEIGHT_ADJUST), ], ) # Enlarge second pane target_window_pane(1) window_manager.reset_pane_sizes() window_manager.enlarge_pane() window_manager.enlarge_pane() self.assertEqual( _window_pane_heights(window_manager), [ _DEFAULT_WINDOW_HEIGHT, _DEFAULT_WINDOW_HEIGHT + (2 * _WINDOW_HEIGHT_ADJUST), _DEFAULT_WINDOW_HEIGHT - (2 * _WINDOW_HEIGHT_ADJUST), _DEFAULT_WINDOW_HEIGHT, ], ) # Check window pane ordering self.assertEqual( window_pane_titles(window_manager), [ [ 'Log2 - test_log2', 'Log1 - test_log1', 'Log0 - test_log0', 'Python Repl - ', ], ], ) target_window_pane(0) window_manager.move_pane_down() self.assertEqual( window_pane_titles(window_manager), [ [ 'Log1 - test_log1', 'Log2 - test_log2', 'Log0 - test_log0', 'Python Repl - ', ], ], ) target_window_pane(2) window_manager.move_pane_up() target_window_pane(1) window_manager.move_pane_up() self.assertEqual( window_pane_titles(window_manager), [ [ 'Log0 - test_log0', 'Log1 - test_log1', 'Log2 - test_log2', 'Python Repl - ', ], ], ) target_window_pane(0) window_manager.move_pane_up() self.assertEqual( window_pane_titles(window_manager), [ [ 'Log0 - test_log0', 'Log1 - test_log1', 'Log2 - test_log2', 'Python Repl - ', ], ], ) def test_focus_next_and_previous_pane(self) -> None: """Test switching focus to next and previous window panes.""" with create_app_session(output=FakeOutput()): console_app = _create_console_app(logger_count=4) window_manager = console_app.window_manager window_manager.window_lists[0].set_display_mode(DisplayMode.STACK) self.assertEqual( window_pane_titles(window_manager), [ [ 'Log3 - test_log3', 'Log2 - test_log2', 'Log1 - test_log1', 'Log0 - test_log0', 'Python Repl - ', ], ], ) # Scenario: Move between panes with a single stacked window list. # Set the first pane in focus. target_list_and_pane(window_manager, 0, 0) # Switch focus to the next pane window_manager.focus_next_pane() # Pane index 1 should now be focused. console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[1] ) console_app.focus_on_container.reset_mock() # Set the first pane in focus. target_list_and_pane(window_manager, 0, 0) # Switch focus to the previous pane window_manager.focus_previous_pane() # Previous pane should wrap around to the last pane in the first # window_list. console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[-1] ) console_app.focus_on_container.reset_mock() # Set the last pane in focus. target_list_and_pane(window_manager, 0, 4) # Switch focus to the next pane window_manager.focus_next_pane() # Next pane should wrap around to the first pane in the first # window_list. console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[0] ) console_app.focus_on_container.reset_mock() # Scenario: Move between panes with a single tabbed window list. # Switch to Tabbed view mode window_manager.window_lists[0].set_display_mode(DisplayMode.TABBED) # The set_display_mode call above will call focus_on_container once. console_app.focus_on_container.reset_mock() # Setup the switch_to_tab mock window_manager.window_lists[0].switch_to_tab = MagicMock( wraps=window_manager.window_lists[0].switch_to_tab ) # Set the first pane/tab in focus. target_list_and_pane(window_manager, 0, 0) # Switch focus to the next pane/tab window_manager.focus_next_pane() # Check switch_to_tab is called window_manager.window_lists[ 0 ].switch_to_tab.assert_called_once_with(1) # And that focus_on_container is called only once console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[1] ) console_app.focus_on_container.reset_mock() window_manager.window_lists[0].switch_to_tab.reset_mock() # Set the last pane/tab in focus. target_list_and_pane(window_manager, 0, 4) # Switch focus to the next pane/tab window_manager.focus_next_pane() # Check switch_to_tab is called window_manager.window_lists[ 0 ].switch_to_tab.assert_called_once_with(0) # And that focus_on_container is called only once console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[0] ) console_app.focus_on_container.reset_mock() window_manager.window_lists[0].switch_to_tab.reset_mock() # Set the first pane/tab in focus. target_list_and_pane(window_manager, 0, 0) # Switch focus to the prev pane/tab window_manager.focus_previous_pane() # Check switch_to_tab is called window_manager.window_lists[ 0 ].switch_to_tab.assert_called_once_with(4) # And that focus_on_container is called only once console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[4] ) console_app.focus_on_container.reset_mock() window_manager.window_lists[0].switch_to_tab.reset_mock() # Scenario: Move between multiple window lists with mixed stacked # and tabbed view modes. # Setup: Move two panes to the right into their own stacked # window_list. target_list_and_pane(window_manager, 0, 4) window_manager.move_pane_right() target_list_and_pane(window_manager, 0, 3) window_manager.move_pane_right() self.assertEqual( window_pane_titles(window_manager), [ [ 'Log3 - test_log3', 'Log2 - test_log2', 'Log1 - test_log1', ], [ 'Log0 - test_log0', 'Python Repl - ', ], ], ) # Setup the switch_to_tab mock on the second window_list window_manager.window_lists[1].switch_to_tab = MagicMock( wraps=window_manager.window_lists[1].switch_to_tab ) # Set Log1 in focus target_list_and_pane(window_manager, 0, 2) window_manager.focus_next_pane() # Log0 should now have focus console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[1].active_panes[0] ) console_app.focus_on_container.reset_mock() # Set Log0 in focus target_list_and_pane(window_manager, 1, 0) window_manager.focus_previous_pane() # Log1 should now have focus console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[2] ) # The first window list is in tabbed mode so switch_to_tab should be # called once. window_manager.window_lists[ 0 ].switch_to_tab.assert_called_once_with(2) # Reset window_manager.window_lists[0].switch_to_tab.reset_mock() console_app.focus_on_container.reset_mock() # Set Python Repl in focus target_list_and_pane(window_manager, 1, 1) window_manager.focus_next_pane() # Log3 should now have focus console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[0] ) window_manager.window_lists[ 0 ].switch_to_tab.assert_called_once_with(0) # Reset window_manager.window_lists[0].switch_to_tab.reset_mock() console_app.focus_on_container.reset_mock() # Set Log3 in focus target_list_and_pane(window_manager, 0, 0) window_manager.focus_next_pane() # Log2 should now have focus console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[0].active_panes[1] ) window_manager.window_lists[ 0 ].switch_to_tab.assert_called_once_with(1) # Reset window_manager.window_lists[0].switch_to_tab.reset_mock() console_app.focus_on_container.reset_mock() # Set Python Repl in focus target_list_and_pane(window_manager, 1, 1) window_manager.focus_previous_pane() # Log0 should now have focus console_app.focus_on_container.assert_called_once_with( window_manager.window_lists[1].active_panes[0] ) # The second window list is in stacked mode so switch_to_tab should # not be called. window_manager.window_lists[1].switch_to_tab.assert_not_called() # Reset window_manager.window_lists[1].switch_to_tab.reset_mock() console_app.focus_on_container.reset_mock() def test_resize_vertical_splits(self) -> None: """Test resizing window splits.""" with create_app_session(output=FakeOutput()): console_app = _create_console_app(logger_count=4) window_manager = console_app.window_manager # Required before moving windows window_manager.update_window_manager_size( _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT ) window_manager.create_root_container() # Vertical split by default self.assertTrue(window_manager.vertical_window_list_spliting()) # Move windows to create 3 splits target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() target_list_and_pane(window_manager, 1, 1) window_manager.move_pane_right() # Check windows are where expected self.assertEqual( window_pane_titles(window_manager), [ [ 'Log1 - test_log1', 'Log0 - test_log0', 'Python Repl - ', ], [ 'Log2 - test_log2', ], [ 'Log3 - test_log3', ], ], ) # Check initial split widths widths = [ int(_WINDOW_MANAGER_WIDTH / 3), int(_WINDOW_MANAGER_WIDTH / 3), int(_WINDOW_MANAGER_WIDTH / 3), ] self.assertEqual(_window_list_widths(window_manager), widths) # Decrease size of first split window_manager.adjust_split_size(window_manager.window_lists[0], -4) widths = [ widths[0] - (4 * _WINDOW_SPLIT_ADJUST), widths[1] + (4 * _WINDOW_SPLIT_ADJUST), widths[2], ] self.assertEqual(_window_list_widths(window_manager), widths) # Increase size of last split widths = [ widths[0], widths[1] - (4 * _WINDOW_SPLIT_ADJUST), widths[2] + (4 * _WINDOW_SPLIT_ADJUST), ] window_manager.adjust_split_size(window_manager.window_lists[2], 4) self.assertEqual(_window_list_widths(window_manager), widths) # Check heights are all the same window_manager.rebalance_window_list_sizes() heights = [ int(_WINDOW_MANAGER_HEIGHT), int(_WINDOW_MANAGER_HEIGHT), int(_WINDOW_MANAGER_HEIGHT), ] self.assertEqual(_window_list_heights(window_manager), heights) def test_resize_horizontal_splits(self) -> None: """Test resizing window splits.""" with create_app_session(output=FakeOutput()): console_app = _create_console_app(logger_count=4) window_manager = console_app.window_manager # We want horizontal window splits window_manager.vertical_window_list_spliting = MagicMock( return_value=False ) self.assertFalse(window_manager.vertical_window_list_spliting()) # Required before moving windows window_manager.update_window_manager_size( _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT ) window_manager.create_root_container() # Move windows to create 3 splits target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() target_list_and_pane(window_manager, 0, 0) window_manager.move_pane_right() target_list_and_pane(window_manager, 1, 1) window_manager.move_pane_right() # Check windows are where expected self.assertEqual( window_pane_titles(window_manager), [ [ 'Log1 - test_log1', 'Log0 - test_log0', 'Python Repl - ', ], [ 'Log2 - test_log2', ], [ 'Log3 - test_log3', ], ], ) # Check initial split widths heights = [ int(_WINDOW_MANAGER_HEIGHT / 3), int(_WINDOW_MANAGER_HEIGHT / 3), int(_WINDOW_MANAGER_HEIGHT / 3), ] self.assertEqual(_window_list_heights(window_manager), heights) # Decrease size of first split window_manager.adjust_split_size(window_manager.window_lists[0], -4) heights = [ heights[0] - (4 * _WINDOW_SPLIT_ADJUST), heights[1] + (4 * _WINDOW_SPLIT_ADJUST), heights[2], ] self.assertEqual(_window_list_heights(window_manager), heights) # Increase size of last split heights = [ heights[0], heights[1] - (4 * _WINDOW_SPLIT_ADJUST), heights[2] + (4 * _WINDOW_SPLIT_ADJUST), ] window_manager.adjust_split_size(window_manager.window_lists[2], 4) self.assertEqual(_window_list_heights(window_manager), heights) # Check widths are all the same window_manager.rebalance_window_list_sizes() widths = [ int(_WINDOW_MANAGER_WIDTH), int(_WINDOW_MANAGER_WIDTH), int(_WINDOW_MANAGER_WIDTH), ] self.assertEqual(_window_list_widths(window_manager), widths) if __name__ == '__main__': unittest.main()