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.percolator import Percolator 11from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \ 12 Squeezer 13from idlelib import macosx 14from idlelib.textview import view_text 15from idlelib.tooltip import Hovertip 16 17SENTINEL_VALUE = sentinel.SENTINEL_VALUE 18 19 20def 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 34class CountLinesTest(unittest.TestCase): 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 81class SqueezerTest(unittest.TestCase): 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 304class ExpandingButtonTest(unittest.TestCase): 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 465if __name__ == '__main__': 466 unittest.main(verbosity=2) 467