(root)/
Python-3.12.0/
Lib/
idlelib/
idle_test/
test_squeezer.py
       1  "Test squeezer, coverage 95%"
       2  
       3  from textwrap import dedent
       4  from tkinter import Text, Tk
       5  import unittest
       6  from unittest.mock import Mock, NonCallableMagicMock, patch, sentinel, ANY
       7  from test.support import requires
       8  
       9  from idlelib.config import idleConf
      10  from idlelib.percolator import Percolator
      11  from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \
      12      Squeezer
      13  from idlelib import macosx
      14  from idlelib.textview import view_text
      15  from idlelib.tooltip import Hovertip
      16  
      17  SENTINEL_VALUE = sentinel.SENTINEL_VALUE
      18  
      19  
      20  def get_test_tk_root(test_instance):
      21      """Helper for tests: Create a root Tk object."""
      22      requires('gui')
      23      root = Tk()
      24      root.withdraw()
      25  
      26      def cleanup_root():
      27          root.update_idletasks()
      28          root.destroy()
      29      test_instance.addCleanup(cleanup_root)
      30  
      31      return root
      32  
      33  
      34  class ESC[4;38;5;81mCountLinesTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      35      """Tests for the count_lines_with_wrapping function."""
      36      def check(self, expected, text, linewidth):
      37          return self.assertEqual(
      38              expected,
      39              count_lines_with_wrapping(text, linewidth),
      40          )
      41  
      42      def test_count_empty(self):
      43          """Test with an empty string."""
      44          self.assertEqual(count_lines_with_wrapping(""), 0)
      45  
      46      def test_count_begins_with_empty_line(self):
      47          """Test with a string which begins with a newline."""
      48          self.assertEqual(count_lines_with_wrapping("\ntext"), 2)
      49  
      50      def test_count_ends_with_empty_line(self):
      51          """Test with a string which ends with a newline."""
      52          self.assertEqual(count_lines_with_wrapping("text\n"), 1)
      53  
      54      def test_count_several_lines(self):
      55          """Test with several lines of text."""
      56          self.assertEqual(count_lines_with_wrapping("1\n2\n3\n"), 3)
      57  
      58      def test_empty_lines(self):
      59          self.check(expected=1, text='\n', linewidth=80)
      60          self.check(expected=2, text='\n\n', linewidth=80)
      61          self.check(expected=10, text='\n' * 10, linewidth=80)
      62  
      63      def test_long_line(self):
      64          self.check(expected=3, text='a' * 200, linewidth=80)
      65          self.check(expected=3, text='a' * 200 + '\n', linewidth=80)
      66  
      67      def test_several_lines_different_lengths(self):
      68          text = dedent("""\
      69              13 characters
      70              43 is the number of characters on this line
      71  
      72              7 chars
      73              13 characters""")
      74          self.check(expected=5, text=text, linewidth=80)
      75          self.check(expected=5, text=text + '\n', linewidth=80)
      76          self.check(expected=6, text=text, linewidth=40)
      77          self.check(expected=7, text=text, linewidth=20)
      78          self.check(expected=11, text=text, linewidth=10)
      79  
      80  
      81  class ESC[4;38;5;81mSqueezerTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      82      """Tests for the Squeezer class."""
      83      def make_mock_editor_window(self, with_text_widget=False):
      84          """Create a mock EditorWindow instance."""
      85          editwin = NonCallableMagicMock()
      86          editwin.width = 80
      87  
      88          if with_text_widget:
      89              editwin.root = get_test_tk_root(self)
      90              text_widget = self.make_text_widget(root=editwin.root)
      91              editwin.text = editwin.per.bottom = text_widget
      92  
      93          return editwin
      94  
      95      def make_squeezer_instance(self, editor_window=None):
      96          """Create an actual Squeezer instance with a mock EditorWindow."""
      97          if editor_window is None:
      98              editor_window = self.make_mock_editor_window()
      99          squeezer = Squeezer(editor_window)
     100          return squeezer
     101  
     102      def make_text_widget(self, root=None):
     103          if root is None:
     104              root = get_test_tk_root(self)
     105          text_widget = Text(root)
     106          text_widget["font"] = ('Courier', 10)
     107          text_widget.mark_set("iomark", "1.0")
     108          return text_widget
     109  
     110      def set_idleconf_option_with_cleanup(self, configType, section, option, value):
     111          prev_val = idleConf.GetOption(configType, section, option)
     112          idleConf.SetOption(configType, section, option, value)
     113          self.addCleanup(idleConf.SetOption,
     114                          configType, section, option, prev_val)
     115  
     116      def test_count_lines(self):
     117          """Test Squeezer.count_lines() with various inputs."""
     118          editwin = self.make_mock_editor_window()
     119          squeezer = self.make_squeezer_instance(editwin)
     120  
     121          for text_code, line_width, expected in [
     122              (r"'\n'", 80, 1),
     123              (r"'\n' * 3", 80, 3),
     124              (r"'a' * 40 + '\n'", 80, 1),
     125              (r"'a' * 80 + '\n'", 80, 1),
     126              (r"'a' * 200 + '\n'", 80, 3),
     127              (r"'aa\t' * 20", 80, 2),
     128              (r"'aa\t' * 21", 80, 3),
     129              (r"'aa\t' * 20", 40, 4),
     130          ]:
     131              with self.subTest(text_code=text_code,
     132                                line_width=line_width,
     133                                expected=expected):
     134                  text = eval(text_code)
     135                  with patch.object(editwin, 'width', line_width):
     136                      self.assertEqual(squeezer.count_lines(text), expected)
     137  
     138      def test_init(self):
     139          """Test the creation of Squeezer instances."""
     140          editwin = self.make_mock_editor_window()
     141          squeezer = self.make_squeezer_instance(editwin)
     142          self.assertIs(squeezer.editwin, editwin)
     143          self.assertEqual(squeezer.expandingbuttons, [])
     144  
     145      def test_write_no_tags(self):
     146          """Test Squeezer's overriding of the EditorWindow's write() method."""
     147          editwin = self.make_mock_editor_window()
     148          for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
     149              editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
     150              squeezer = self.make_squeezer_instance(editwin)
     151  
     152              self.assertEqual(squeezer.editwin.write(text, ()), SENTINEL_VALUE)
     153              self.assertEqual(orig_write.call_count, 1)
     154              orig_write.assert_called_with(text, ())
     155              self.assertEqual(len(squeezer.expandingbuttons), 0)
     156  
     157      def test_write_not_stdout(self):
     158          """Test Squeezer's overriding of the EditorWindow's write() method."""
     159          for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
     160              editwin = self.make_mock_editor_window()
     161              editwin.write.return_value = SENTINEL_VALUE
     162              orig_write = editwin.write
     163              squeezer = self.make_squeezer_instance(editwin)
     164  
     165              self.assertEqual(squeezer.editwin.write(text, "stderr"),
     166                                SENTINEL_VALUE)
     167              self.assertEqual(orig_write.call_count, 1)
     168              orig_write.assert_called_with(text, "stderr")
     169              self.assertEqual(len(squeezer.expandingbuttons), 0)
     170  
     171      def test_write_stdout(self):
     172          """Test Squeezer's overriding of the EditorWindow's write() method."""
     173          editwin = self.make_mock_editor_window()
     174  
     175          for text in ['', 'TEXT']:
     176              editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
     177              squeezer = self.make_squeezer_instance(editwin)
     178              squeezer.auto_squeeze_min_lines = 50
     179  
     180              self.assertEqual(squeezer.editwin.write(text, "stdout"),
     181                               SENTINEL_VALUE)
     182              self.assertEqual(orig_write.call_count, 1)
     183              orig_write.assert_called_with(text, "stdout")
     184              self.assertEqual(len(squeezer.expandingbuttons), 0)
     185  
     186          for text in ['LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
     187              editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
     188              squeezer = self.make_squeezer_instance(editwin)
     189              squeezer.auto_squeeze_min_lines = 50
     190  
     191              self.assertEqual(squeezer.editwin.write(text, "stdout"), None)
     192              self.assertEqual(orig_write.call_count, 0)
     193              self.assertEqual(len(squeezer.expandingbuttons), 1)
     194  
     195      def test_auto_squeeze(self):
     196          """Test that the auto-squeezing creates an ExpandingButton properly."""
     197          editwin = self.make_mock_editor_window(with_text_widget=True)
     198          text_widget = editwin.text
     199          squeezer = self.make_squeezer_instance(editwin)
     200          squeezer.auto_squeeze_min_lines = 5
     201          squeezer.count_lines = Mock(return_value=6)
     202  
     203          editwin.write('TEXT\n'*6, "stdout")
     204          self.assertEqual(text_widget.get('1.0', 'end'), '\n')
     205          self.assertEqual(len(squeezer.expandingbuttons), 1)
     206  
     207      def test_squeeze_current_text(self):
     208          """Test the squeeze_current_text method."""
     209          # Squeezing text should work for both stdout and stderr.
     210          for tag_name in ["stdout", "stderr"]:
     211              editwin = self.make_mock_editor_window(with_text_widget=True)
     212              text_widget = editwin.text
     213              squeezer = self.make_squeezer_instance(editwin)
     214              squeezer.count_lines = Mock(return_value=6)
     215  
     216              # Prepare some text in the Text widget.
     217              text_widget.insert("1.0", "SOME\nTEXT\n", tag_name)
     218              text_widget.mark_set("insert", "1.0")
     219              self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
     220  
     221              self.assertEqual(len(squeezer.expandingbuttons), 0)
     222  
     223              # Test squeezing the current text.
     224              retval = squeezer.squeeze_current_text()
     225              self.assertEqual(retval, "break")
     226              self.assertEqual(text_widget.get('1.0', 'end'), '\n\n')
     227              self.assertEqual(len(squeezer.expandingbuttons), 1)
     228              self.assertEqual(squeezer.expandingbuttons[0].s, 'SOME\nTEXT')
     229  
     230              # Test that expanding the squeezed text works and afterwards
     231              # the Text widget contains the original text.
     232              squeezer.expandingbuttons[0].expand()
     233              self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
     234              self.assertEqual(len(squeezer.expandingbuttons), 0)
     235  
     236      def test_squeeze_current_text_no_allowed_tags(self):
     237          """Test that the event doesn't squeeze text without a relevant tag."""
     238          editwin = self.make_mock_editor_window(with_text_widget=True)
     239          text_widget = editwin.text
     240          squeezer = self.make_squeezer_instance(editwin)
     241          squeezer.count_lines = Mock(return_value=6)
     242  
     243          # Prepare some text in the Text widget.
     244          text_widget.insert("1.0", "SOME\nTEXT\n", "TAG")
     245          text_widget.mark_set("insert", "1.0")
     246          self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
     247  
     248          self.assertEqual(len(squeezer.expandingbuttons), 0)
     249  
     250          # Test squeezing the current text.
     251          retval = squeezer.squeeze_current_text()
     252          self.assertEqual(retval, "break")
     253          self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
     254          self.assertEqual(len(squeezer.expandingbuttons), 0)
     255  
     256      def test_squeeze_text_before_existing_squeezed_text(self):
     257          """Test squeezing text before existing squeezed text."""
     258          editwin = self.make_mock_editor_window(with_text_widget=True)
     259          text_widget = editwin.text
     260          squeezer = self.make_squeezer_instance(editwin)
     261          squeezer.count_lines = Mock(return_value=6)
     262  
     263          # Prepare some text in the Text widget and squeeze it.
     264          text_widget.insert("1.0", "SOME\nTEXT\n", "stdout")
     265          text_widget.mark_set("insert", "1.0")
     266          squeezer.squeeze_current_text()
     267          self.assertEqual(len(squeezer.expandingbuttons), 1)
     268  
     269          # Test squeezing the current text.
     270          text_widget.insert("1.0", "MORE\nSTUFF\n", "stdout")
     271          text_widget.mark_set("insert", "1.0")
     272          retval = squeezer.squeeze_current_text()
     273          self.assertEqual(retval, "break")
     274          self.assertEqual(text_widget.get('1.0', 'end'), '\n\n\n')
     275          self.assertEqual(len(squeezer.expandingbuttons), 2)
     276          self.assertTrue(text_widget.compare(
     277              squeezer.expandingbuttons[0],
     278              '<',
     279              squeezer.expandingbuttons[1],
     280          ))
     281  
     282      def test_reload(self):
     283          """Test the reload() class-method."""
     284          editwin = self.make_mock_editor_window(with_text_widget=True)
     285          squeezer = self.make_squeezer_instance(editwin)
     286  
     287          orig_auto_squeeze_min_lines = squeezer.auto_squeeze_min_lines
     288  
     289          # Increase auto-squeeze-min-lines.
     290          new_auto_squeeze_min_lines = orig_auto_squeeze_min_lines + 10
     291          self.set_idleconf_option_with_cleanup(
     292              'main', 'PyShell', 'auto-squeeze-min-lines',
     293              str(new_auto_squeeze_min_lines))
     294  
     295          Squeezer.reload()
     296          self.assertEqual(squeezer.auto_squeeze_min_lines,
     297                           new_auto_squeeze_min_lines)
     298  
     299      def test_reload_no_squeezer_instances(self):
     300          """Test that Squeezer.reload() runs without any instances existing."""
     301          Squeezer.reload()
     302  
     303  
     304  class ESC[4;38;5;81mExpandingButtonTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     305      """Tests for the ExpandingButton class."""
     306      # In these tests the squeezer instance is a mock, but actual tkinter
     307      # Text and Button instances are created.
     308      def make_mock_squeezer(self):
     309          """Helper for tests: Create a mock Squeezer object."""
     310          root = get_test_tk_root(self)
     311          squeezer = Mock()
     312          squeezer.editwin.text = Text(root)
     313          squeezer.editwin.per = Percolator(squeezer.editwin.text)
     314          self.addCleanup(squeezer.editwin.per.close)
     315  
     316          # Set default values for the configuration settings.
     317          squeezer.auto_squeeze_min_lines = 50
     318          return squeezer
     319  
     320      @patch('idlelib.squeezer.Hovertip', autospec=Hovertip)
     321      def test_init(self, MockHovertip):
     322          """Test the simplest creation of an ExpandingButton."""
     323          squeezer = self.make_mock_squeezer()
     324          text_widget = squeezer.editwin.text
     325  
     326          expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
     327          self.assertEqual(expandingbutton.s, 'TEXT')
     328  
     329          # Check that the underlying tkinter.Button is properly configured.
     330          self.assertEqual(expandingbutton.master, text_widget)
     331          self.assertTrue('50 lines' in expandingbutton.cget('text'))
     332  
     333          # Check that the text widget still contains no text.
     334          self.assertEqual(text_widget.get('1.0', 'end'), '\n')
     335  
     336          # Check that the mouse events are bound.
     337          self.assertIn('<Double-Button-1>', expandingbutton.bind())
     338          right_button_code = '<Button-%s>' % ('2' if macosx.isAquaTk() else '3')
     339          self.assertIn(right_button_code, expandingbutton.bind())
     340  
     341          # Check that ToolTip was called once, with appropriate values.
     342          self.assertEqual(MockHovertip.call_count, 1)
     343          MockHovertip.assert_called_with(expandingbutton, ANY, hover_delay=ANY)
     344  
     345          # Check that 'right-click' appears in the tooltip text.
     346          tooltip_text = MockHovertip.call_args[0][1]
     347          self.assertIn('right-click', tooltip_text.lower())
     348  
     349      def test_expand(self):
     350          """Test the expand event."""
     351          squeezer = self.make_mock_squeezer()
     352          expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
     353  
     354          # Insert the button into the text widget
     355          # (this is normally done by the Squeezer class).
     356          text_widget = squeezer.editwin.text
     357          text_widget.window_create("1.0", window=expandingbutton)
     358  
     359          # trigger the expand event
     360          retval = expandingbutton.expand(event=Mock())
     361          self.assertEqual(retval, None)
     362  
     363          # Check that the text was inserted into the text widget.
     364          self.assertEqual(text_widget.get('1.0', 'end'), 'TEXT\n')
     365  
     366          # Check that the 'TAGS' tag was set on the inserted text.
     367          text_end_index = text_widget.index('end-1c')
     368          self.assertEqual(text_widget.get('1.0', text_end_index), 'TEXT')
     369          self.assertEqual(text_widget.tag_nextrange('TAGS', '1.0'),
     370                            ('1.0', text_end_index))
     371  
     372          # Check that the button removed itself from squeezer.expandingbuttons.
     373          self.assertEqual(squeezer.expandingbuttons.remove.call_count, 1)
     374          squeezer.expandingbuttons.remove.assert_called_with(expandingbutton)
     375  
     376      def test_expand_dangerous_oupput(self):
     377          """Test that expanding very long output asks user for confirmation."""
     378          squeezer = self.make_mock_squeezer()
     379          text = 'a' * 10**5
     380          expandingbutton = ExpandingButton(text, 'TAGS', 50, squeezer)
     381          expandingbutton.set_is_dangerous()
     382          self.assertTrue(expandingbutton.is_dangerous)
     383  
     384          # Insert the button into the text widget
     385          # (this is normally done by the Squeezer class).
     386          text_widget = expandingbutton.text
     387          text_widget.window_create("1.0", window=expandingbutton)
     388  
     389          # Patch the message box module to always return False.
     390          with patch('idlelib.squeezer.messagebox') as mock_msgbox:
     391              mock_msgbox.askokcancel.return_value = False
     392              mock_msgbox.askyesno.return_value = False
     393              # Trigger the expand event.
     394              retval = expandingbutton.expand(event=Mock())
     395  
     396          # Check that the event chain was broken and no text was inserted.
     397          self.assertEqual(retval, 'break')
     398          self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), '')
     399  
     400          # Patch the message box module to always return True.
     401          with patch('idlelib.squeezer.messagebox') as mock_msgbox:
     402              mock_msgbox.askokcancel.return_value = True
     403              mock_msgbox.askyesno.return_value = True
     404              # Trigger the expand event.
     405              retval = expandingbutton.expand(event=Mock())
     406  
     407          # Check that the event chain wasn't broken and the text was inserted.
     408          self.assertEqual(retval, None)
     409          self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), text)
     410  
     411      def test_copy(self):
     412          """Test the copy event."""
     413          # Testing with the actual clipboard proved problematic, so this
     414          # test replaces the clipboard manipulation functions with mocks
     415          # and checks that they are called appropriately.
     416          squeezer = self.make_mock_squeezer()
     417          expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
     418          expandingbutton.clipboard_clear = Mock()
     419          expandingbutton.clipboard_append = Mock()
     420  
     421          # Trigger the copy event.
     422          retval = expandingbutton.copy(event=Mock())
     423          self.assertEqual(retval, None)
     424  
     425          # Vheck that the expanding button called clipboard_clear() and
     426          # clipboard_append('TEXT') once each.
     427          self.assertEqual(expandingbutton.clipboard_clear.call_count, 1)
     428          self.assertEqual(expandingbutton.clipboard_append.call_count, 1)
     429          expandingbutton.clipboard_append.assert_called_with('TEXT')
     430  
     431      def test_view(self):
     432          """Test the view event."""
     433          squeezer = self.make_mock_squeezer()
     434          expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
     435          expandingbutton.selection_own = Mock()
     436  
     437          with patch('idlelib.squeezer.view_text', autospec=view_text)\
     438                  as mock_view_text:
     439              # Trigger the view event.
     440              expandingbutton.view(event=Mock())
     441  
     442              # Check that the expanding button called view_text.
     443              self.assertEqual(mock_view_text.call_count, 1)
     444  
     445              # Check that the proper text was passed.
     446              self.assertEqual(mock_view_text.call_args[0][2], 'TEXT')
     447  
     448      def test_rmenu(self):
     449          """Test the context menu."""
     450          squeezer = self.make_mock_squeezer()
     451          expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
     452          with patch('tkinter.Menu') as mock_Menu:
     453              mock_menu = Mock()
     454              mock_Menu.return_value = mock_menu
     455              mock_event = Mock()
     456              mock_event.x = 10
     457              mock_event.y = 10
     458              expandingbutton.context_menu_event(event=mock_event)
     459              self.assertEqual(mock_menu.add_command.call_count,
     460                               len(expandingbutton.rmenu_specs))
     461              for label, *data in expandingbutton.rmenu_specs:
     462                  mock_menu.add_command.assert_any_call(label=label, command=ANY)
     463  
     464  
     465  if __name__ == '__main__':
     466      unittest.main(verbosity=2)