1 "Test codecontext, coverage 100%"
2
3 from idlelib import codecontext
4 import unittest
5 import unittest.mock
6 from test.support import requires
7 from tkinter import NSEW, Tk, Frame, Text, TclError
8
9 from unittest import mock
10 import re
11 from idlelib import config
12
13
14 usercfg = codecontext.idleConf.userCfg
15 testcfg = {
16 'main': config.IdleUserConfParser(''),
17 'highlight': config.IdleUserConfParser(''),
18 'keys': config.IdleUserConfParser(''),
19 'extensions': config.IdleUserConfParser(''),
20 }
21 code_sample = """\
22
23 class C1:
24 # Class comment.
25 def __init__(self, a, b):
26 self.a = a
27 self.b = b
28 def compare(self):
29 if a > b:
30 return a
31 elif a < b:
32 return b
33 else:
34 return None
35 """
36
37
38 class ESC[4;38;5;81mDummyEditwin:
39 def __init__(self, root, frame, text):
40 self.root = root
41 self.top = root
42 self.text_frame = frame
43 self.text = text
44 self.label = ''
45
46 def getlineno(self, index):
47 return int(float(self.text.index(index)))
48
49 def update_menu_label(self, **kwargs):
50 self.label = kwargs['label']
51
52
53 class ESC[4;38;5;81mCodeContextTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
54
55 @classmethod
56 def setUpClass(cls):
57 requires('gui')
58 root = cls.root = Tk()
59 root.withdraw()
60 frame = cls.frame = Frame(root)
61 text = cls.text = Text(frame)
62 text.insert('1.0', code_sample)
63 # Need to pack for creation of code context text widget.
64 frame.pack(side='left', fill='both', expand=1)
65 text.grid(row=1, column=1, sticky=NSEW)
66 cls.editor = DummyEditwin(root, frame, text)
67 codecontext.idleConf.userCfg = testcfg
68
69 @classmethod
70 def tearDownClass(cls):
71 codecontext.idleConf.userCfg = usercfg
72 cls.editor.text.delete('1.0', 'end')
73 del cls.editor, cls.frame, cls.text
74 cls.root.update_idletasks()
75 cls.root.destroy()
76 del cls.root
77
78 def setUp(self):
79 self.text.yview(0)
80 self.text['font'] = 'TkFixedFont'
81 self.cc = codecontext.CodeContext(self.editor)
82
83 self.highlight_cfg = {"background": '#abcdef',
84 "foreground": '#123456'}
85 orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight
86 def mock_idleconf_GetHighlight(theme, element):
87 if element == 'context':
88 return self.highlight_cfg
89 return orig_idleConf_GetHighlight(theme, element)
90 GetHighlight_patcher = unittest.mock.patch.object(
91 codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
92 GetHighlight_patcher.start()
93 self.addCleanup(GetHighlight_patcher.stop)
94
95 self.font_override = 'TkFixedFont'
96 def mock_idleconf_GetFont(root, configType, section):
97 return self.font_override
98 GetFont_patcher = unittest.mock.patch.object(
99 codecontext.idleConf, 'GetFont', mock_idleconf_GetFont)
100 GetFont_patcher.start()
101 self.addCleanup(GetFont_patcher.stop)
102
103 def tearDown(self):
104 if self.cc.context:
105 self.cc.context.destroy()
106 # Explicitly call __del__ to remove scheduled scripts.
107 self.cc.__del__()
108 del self.cc.context, self.cc
109
110 def test_init(self):
111 eq = self.assertEqual
112 ed = self.editor
113 cc = self.cc
114
115 eq(cc.editwin, ed)
116 eq(cc.text, ed.text)
117 eq(cc.text['font'], ed.text['font'])
118 self.assertIsNone(cc.context)
119 eq(cc.info, [(0, -1, '', False)])
120 eq(cc.topvisible, 1)
121 self.assertIsNone(self.cc.t1)
122
123 def test_del(self):
124 self.cc.__del__()
125
126 def test_del_with_timer(self):
127 timer = self.cc.t1 = self.text.after(10000, lambda: None)
128 self.cc.__del__()
129 with self.assertRaises(TclError) as cm:
130 self.root.tk.call('after', 'info', timer)
131 self.assertIn("doesn't exist", str(cm.exception))
132
133 def test_reload(self):
134 codecontext.CodeContext.reload()
135 self.assertEqual(self.cc.context_depth, 15)
136
137 def test_toggle_code_context_event(self):
138 eq = self.assertEqual
139 cc = self.cc
140 toggle = cc.toggle_code_context_event
141
142 # Make sure code context is off.
143 if cc.context:
144 toggle()
145
146 # Toggle on.
147 toggle()
148 self.assertIsNotNone(cc.context)
149 eq(cc.context['font'], self.text['font'])
150 eq(cc.context['fg'], self.highlight_cfg['foreground'])
151 eq(cc.context['bg'], self.highlight_cfg['background'])
152 eq(cc.context.get('1.0', 'end-1c'), '')
153 eq(cc.editwin.label, 'Hide Code Context')
154 eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
155
156 # Toggle off.
157 toggle()
158 self.assertIsNone(cc.context)
159 eq(cc.editwin.label, 'Show Code Context')
160 self.assertIsNone(self.cc.t1)
161
162 # Scroll down and toggle back on.
163 line11_context = '\n'.join(x[2] for x in cc.get_context(11)[0])
164 cc.text.yview(11)
165 toggle()
166 eq(cc.context.get('1.0', 'end-1c'), line11_context)
167
168 # Toggle off and on again.
169 toggle()
170 toggle()
171 eq(cc.context.get('1.0', 'end-1c'), line11_context)
172
173 def test_get_context(self):
174 eq = self.assertEqual
175 gc = self.cc.get_context
176
177 # stopline must be greater than 0.
178 with self.assertRaises(AssertionError):
179 gc(1, stopline=0)
180
181 eq(gc(3), ([(2, 0, 'class C1:', 'class')], 0))
182
183 # Don't return comment.
184 eq(gc(4), ([(2, 0, 'class C1:', 'class')], 0))
185
186 # Two indentation levels and no comment.
187 eq(gc(5), ([(2, 0, 'class C1:', 'class'),
188 (4, 4, ' def __init__(self, a, b):', 'def')], 0))
189
190 # Only one 'def' is returned, not both at the same indent level.
191 eq(gc(10), ([(2, 0, 'class C1:', 'class'),
192 (7, 4, ' def compare(self):', 'def'),
193 (8, 8, ' if a > b:', 'if')], 0))
194
195 # With 'elif', also show the 'if' even though it's at the same level.
196 eq(gc(11), ([(2, 0, 'class C1:', 'class'),
197 (7, 4, ' def compare(self):', 'def'),
198 (8, 8, ' if a > b:', 'if'),
199 (10, 8, ' elif a < b:', 'elif')], 0))
200
201 # Set stop_line to not go back to first line in source code.
202 # Return includes stop_line.
203 eq(gc(11, stopline=2), ([(2, 0, 'class C1:', 'class'),
204 (7, 4, ' def compare(self):', 'def'),
205 (8, 8, ' if a > b:', 'if'),
206 (10, 8, ' elif a < b:', 'elif')], 0))
207 eq(gc(11, stopline=3), ([(7, 4, ' def compare(self):', 'def'),
208 (8, 8, ' if a > b:', 'if'),
209 (10, 8, ' elif a < b:', 'elif')], 4))
210 eq(gc(11, stopline=8), ([(8, 8, ' if a > b:', 'if'),
211 (10, 8, ' elif a < b:', 'elif')], 8))
212
213 # Set stop_indent to test indent level to stop at.
214 eq(gc(11, stopindent=4), ([(7, 4, ' def compare(self):', 'def'),
215 (8, 8, ' if a > b:', 'if'),
216 (10, 8, ' elif a < b:', 'elif')], 4))
217 # Check that the 'if' is included.
218 eq(gc(11, stopindent=8), ([(8, 8, ' if a > b:', 'if'),
219 (10, 8, ' elif a < b:', 'elif')], 8))
220
221 def test_update_code_context(self):
222 eq = self.assertEqual
223 cc = self.cc
224 # Ensure code context is active.
225 if not cc.context:
226 cc.toggle_code_context_event()
227
228 # Invoke update_code_context without scrolling - nothing happens.
229 self.assertIsNone(cc.update_code_context())
230 eq(cc.info, [(0, -1, '', False)])
231 eq(cc.topvisible, 1)
232
233 # Scroll down to line 1.
234 cc.text.yview(1)
235 cc.update_code_context()
236 eq(cc.info, [(0, -1, '', False)])
237 eq(cc.topvisible, 2)
238 eq(cc.context.get('1.0', 'end-1c'), '')
239
240 # Scroll down to line 2.
241 cc.text.yview(2)
242 cc.update_code_context()
243 eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')])
244 eq(cc.topvisible, 3)
245 eq(cc.context.get('1.0', 'end-1c'), 'class C1:')
246
247 # Scroll down to line 3. Since it's a comment, nothing changes.
248 cc.text.yview(3)
249 cc.update_code_context()
250 eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1:', 'class')])
251 eq(cc.topvisible, 4)
252 eq(cc.context.get('1.0', 'end-1c'), 'class C1:')
253
254 # Scroll down to line 4.
255 cc.text.yview(4)
256 cc.update_code_context()
257 eq(cc.info, [(0, -1, '', False),
258 (2, 0, 'class C1:', 'class'),
259 (4, 4, ' def __init__(self, a, b):', 'def')])
260 eq(cc.topvisible, 5)
261 eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
262 ' def __init__(self, a, b):')
263
264 # Scroll down to line 11. Last 'def' is removed.
265 cc.text.yview(11)
266 cc.update_code_context()
267 eq(cc.info, [(0, -1, '', False),
268 (2, 0, 'class C1:', 'class'),
269 (7, 4, ' def compare(self):', 'def'),
270 (8, 8, ' if a > b:', 'if'),
271 (10, 8, ' elif a < b:', 'elif')])
272 eq(cc.topvisible, 12)
273 eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
274 ' def compare(self):\n'
275 ' if a > b:\n'
276 ' elif a < b:')
277
278 # No scroll. No update, even though context_depth changed.
279 cc.update_code_context()
280 cc.context_depth = 1
281 eq(cc.info, [(0, -1, '', False),
282 (2, 0, 'class C1:', 'class'),
283 (7, 4, ' def compare(self):', 'def'),
284 (8, 8, ' if a > b:', 'if'),
285 (10, 8, ' elif a < b:', 'elif')])
286 eq(cc.topvisible, 12)
287 eq(cc.context.get('1.0', 'end-1c'), 'class C1:\n'
288 ' def compare(self):\n'
289 ' if a > b:\n'
290 ' elif a < b:')
291
292 # Scroll up.
293 cc.text.yview(5)
294 cc.update_code_context()
295 eq(cc.info, [(0, -1, '', False),
296 (2, 0, 'class C1:', 'class'),
297 (4, 4, ' def __init__(self, a, b):', 'def')])
298 eq(cc.topvisible, 6)
299 # context_depth is 1.
300 eq(cc.context.get('1.0', 'end-1c'), ' def __init__(self, a, b):')
301
302 def test_jumptoline(self):
303 eq = self.assertEqual
304 cc = self.cc
305 jump = cc.jumptoline
306
307 if not cc.context:
308 cc.toggle_code_context_event()
309
310 # Empty context.
311 cc.text.yview('2.0')
312 cc.update_code_context()
313 eq(cc.topvisible, 2)
314 cc.context.mark_set('insert', '1.5')
315 jump()
316 eq(cc.topvisible, 1)
317
318 # 4 lines of context showing.
319 cc.text.yview('12.0')
320 cc.update_code_context()
321 eq(cc.topvisible, 12)
322 cc.context.mark_set('insert', '3.0')
323 jump()
324 eq(cc.topvisible, 8)
325
326 # More context lines than limit.
327 cc.context_depth = 2
328 cc.text.yview('12.0')
329 cc.update_code_context()
330 eq(cc.topvisible, 12)
331 cc.context.mark_set('insert', '1.0')
332 jump()
333 eq(cc.topvisible, 8)
334
335 # Context selection stops jump.
336 cc.text.yview('5.0')
337 cc.update_code_context()
338 cc.context.tag_add('sel', '1.0', '2.0')
339 cc.context.mark_set('insert', '1.0')
340 jump() # Without selection, to line 2.
341 eq(cc.topvisible, 5)
342
343 @mock.patch.object(codecontext.CodeContext, 'update_code_context')
344 def test_timer_event(self, mock_update):
345 # Ensure code context is not active.
346 if self.cc.context:
347 self.cc.toggle_code_context_event()
348 self.cc.timer_event()
349 mock_update.assert_not_called()
350
351 # Activate code context.
352 self.cc.toggle_code_context_event()
353 self.cc.timer_event()
354 mock_update.assert_called()
355
356 def test_font(self):
357 eq = self.assertEqual
358 cc = self.cc
359
360 orig_font = cc.text['font']
361 test_font = 'TkTextFont'
362 self.assertNotEqual(orig_font, test_font)
363
364 # Ensure code context is not active.
365 if cc.context is not None:
366 cc.toggle_code_context_event()
367
368 self.font_override = test_font
369 # Nothing breaks or changes with inactive code context.
370 cc.update_font()
371
372 # Activate code context, previous font change is immediately effective.
373 cc.toggle_code_context_event()
374 eq(cc.context['font'], test_font)
375
376 # Call the font update, change is picked up.
377 self.font_override = orig_font
378 cc.update_font()
379 eq(cc.context['font'], orig_font)
380
381 def test_highlight_colors(self):
382 eq = self.assertEqual
383 cc = self.cc
384
385 orig_colors = dict(self.highlight_cfg)
386 test_colors = {'background': '#222222', 'foreground': '#ffff00'}
387
388 def assert_colors_are_equal(colors):
389 eq(cc.context['background'], colors['background'])
390 eq(cc.context['foreground'], colors['foreground'])
391
392 # Ensure code context is not active.
393 if cc.context:
394 cc.toggle_code_context_event()
395
396 self.highlight_cfg = test_colors
397 # Nothing breaks with inactive code context.
398 cc.update_highlight_colors()
399
400 # Activate code context, previous colors change is immediately effective.
401 cc.toggle_code_context_event()
402 assert_colors_are_equal(test_colors)
403
404 # Call colors update with no change to the configured colors.
405 cc.update_highlight_colors()
406 assert_colors_are_equal(test_colors)
407
408 # Call the colors update with code context active, change is picked up.
409 self.highlight_cfg = orig_colors
410 cc.update_highlight_colors()
411 assert_colors_are_equal(orig_colors)
412
413
414 class ESC[4;38;5;81mHelperFunctionText(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
415
416 def test_get_spaces_firstword(self):
417 get = codecontext.get_spaces_firstword
418 test_lines = (
419 (' first word', (' ', 'first')),
420 ('\tfirst word', ('\t', 'first')),
421 (' \u19D4\u19D2: ', (' ', '\u19D4\u19D2')),
422 ('no spaces', ('', 'no')),
423 ('', ('', '')),
424 ('# TEST COMMENT', ('', '')),
425 (' (continuation)', (' ', ''))
426 )
427 for line, expected_output in test_lines:
428 self.assertEqual(get(line), expected_output)
429
430 # Send the pattern in the call.
431 self.assertEqual(get(' (continuation)',
432 c=re.compile(r'^(\s*)([^\s]*)')),
433 (' ', '(continuation)'))
434
435 def test_get_line_info(self):
436 eq = self.assertEqual
437 gli = codecontext.get_line_info
438 lines = code_sample.splitlines()
439
440 # Line 1 is not a BLOCKOPENER.
441 eq(gli(lines[0]), (codecontext.INFINITY, '', False))
442 # Line 2 is a BLOCKOPENER without an indent.
443 eq(gli(lines[1]), (0, 'class C1:', 'class'))
444 # Line 3 is not a BLOCKOPENER and does not return the indent level.
445 eq(gli(lines[2]), (codecontext.INFINITY, ' # Class comment.', False))
446 # Line 4 is a BLOCKOPENER and is indented.
447 eq(gli(lines[3]), (4, ' def __init__(self, a, b):', 'def'))
448 # Line 8 is a different BLOCKOPENER and is indented.
449 eq(gli(lines[7]), (8, ' if a > b:', 'if'))
450 # Test tab.
451 eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if'))
452
453
454 if __name__ == '__main__':
455 unittest.main(verbosity=2)