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)