1"Test codecontext, coverage 100%" 2 3from idlelib import codecontext 4import unittest 5import unittest.mock 6from test.support import requires 7from tkinter import NSEW, Tk, Frame, Text, TclError 8 9from unittest import mock 10import re 11from idlelib import config 12 13 14usercfg = codecontext.idleConf.userCfg 15testcfg = { 16 'main': config.IdleUserConfParser(''), 17 'highlight': config.IdleUserConfParser(''), 18 'keys': config.IdleUserConfParser(''), 19 'extensions': config.IdleUserConfParser(''), 20} 21code_sample = """\ 22 23class 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 38class DummyEditwin: 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 53class CodeContextTest(unittest.TestCase): 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 414class HelperFunctionText(unittest.TestCase): 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 454if __name__ == '__main__': 455 unittest.main(verbosity=2) 456