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