1"Test colorizer, coverage 93%." 2 3from idlelib import colorizer 4from test.support import requires 5import unittest 6from unittest import mock 7 8from functools import partial 9from tkinter import Tk, Text 10from idlelib import config 11from idlelib.percolator import Percolator 12 13 14usercfg = colorizer.idleConf.userCfg 15testcfg = { 16 'main': config.IdleUserConfParser(''), 17 'highlight': config.IdleUserConfParser(''), 18 'keys': config.IdleUserConfParser(''), 19 'extensions': config.IdleUserConfParser(''), 20} 21 22source = ( 23 "if True: int ('1') # keyword, builtin, string, comment\n" 24 "elif False: print(0) # 'string' in comment\n" 25 "else: float(None) # if in comment\n" 26 "if iF + If + IF: 'keyword matching must respect case'\n" 27 "if'': x or'' # valid string-keyword no-space combinations\n" 28 "async def f(): await g()\n" 29 "'x', '''x''', \"x\", \"\"\"x\"\"\"\n" 30 ) 31 32 33def setUpModule(): 34 colorizer.idleConf.userCfg = testcfg 35 36 37def tearDownModule(): 38 colorizer.idleConf.userCfg = usercfg 39 40 41class FunctionTest(unittest.TestCase): 42 43 def test_any(self): 44 self.assertEqual(colorizer.any('test', ('a', 'b', 'cd')), 45 '(?P<test>a|b|cd)') 46 47 def test_make_pat(self): 48 # Tested in more detail by testing prog. 49 self.assertTrue(colorizer.make_pat()) 50 51 def test_prog(self): 52 prog = colorizer.prog 53 eq = self.assertEqual 54 line = 'def f():\n print("hello")\n' 55 m = prog.search(line) 56 eq(m.groupdict()['KEYWORD'], 'def') 57 m = prog.search(line, m.end()) 58 eq(m.groupdict()['SYNC'], '\n') 59 m = prog.search(line, m.end()) 60 eq(m.groupdict()['BUILTIN'], 'print') 61 m = prog.search(line, m.end()) 62 eq(m.groupdict()['STRING'], '"hello"') 63 m = prog.search(line, m.end()) 64 eq(m.groupdict()['SYNC'], '\n') 65 66 def test_idprog(self): 67 idprog = colorizer.idprog 68 m = idprog.match('nospace') 69 self.assertIsNone(m) 70 m = idprog.match(' space') 71 self.assertEqual(m.group(0), ' space') 72 73 74class ColorConfigTest(unittest.TestCase): 75 76 @classmethod 77 def setUpClass(cls): 78 requires('gui') 79 root = cls.root = Tk() 80 root.withdraw() 81 cls.text = Text(root) 82 83 @classmethod 84 def tearDownClass(cls): 85 del cls.text 86 cls.root.update_idletasks() 87 cls.root.destroy() 88 del cls.root 89 90 def test_color_config(self): 91 text = self.text 92 eq = self.assertEqual 93 colorizer.color_config(text) 94 # Uses IDLE Classic theme as default. 95 eq(text['background'], '#ffffff') 96 eq(text['foreground'], '#000000') 97 eq(text['selectbackground'], 'gray') 98 eq(text['selectforeground'], '#000000') 99 eq(text['insertbackground'], 'black') 100 eq(text['inactiveselectbackground'], 'gray') 101 102 103class ColorDelegatorInstantiationTest(unittest.TestCase): 104 105 @classmethod 106 def setUpClass(cls): 107 requires('gui') 108 root = cls.root = Tk() 109 root.withdraw() 110 text = cls.text = Text(root) 111 112 @classmethod 113 def tearDownClass(cls): 114 del cls.text 115 cls.root.update_idletasks() 116 cls.root.destroy() 117 del cls.root 118 119 def setUp(self): 120 self.color = colorizer.ColorDelegator() 121 122 def tearDown(self): 123 self.color.close() 124 self.text.delete('1.0', 'end') 125 self.color.resetcache() 126 del self.color 127 128 def test_init(self): 129 color = self.color 130 self.assertIsInstance(color, colorizer.ColorDelegator) 131 132 def test_init_state(self): 133 # init_state() is called during the instantiation of 134 # ColorDelegator in setUp(). 135 color = self.color 136 self.assertIsNone(color.after_id) 137 self.assertTrue(color.allow_colorizing) 138 self.assertFalse(color.colorizing) 139 self.assertFalse(color.stop_colorizing) 140 141 142class ColorDelegatorTest(unittest.TestCase): 143 144 @classmethod 145 def setUpClass(cls): 146 requires('gui') 147 root = cls.root = Tk() 148 root.withdraw() 149 text = cls.text = Text(root) 150 cls.percolator = Percolator(text) 151 # Delegator stack = [Delegator(text)] 152 153 @classmethod 154 def tearDownClass(cls): 155 cls.percolator.redir.close() 156 del cls.percolator, cls.text 157 cls.root.update_idletasks() 158 cls.root.destroy() 159 del cls.root 160 161 def setUp(self): 162 self.color = colorizer.ColorDelegator() 163 self.percolator.insertfilter(self.color) 164 # Calls color.setdelegate(Delegator(text)). 165 166 def tearDown(self): 167 self.color.close() 168 self.percolator.removefilter(self.color) 169 self.text.delete('1.0', 'end') 170 self.color.resetcache() 171 del self.color 172 173 def test_setdelegate(self): 174 # Called in setUp when filter is attached to percolator. 175 color = self.color 176 self.assertIsInstance(color.delegate, colorizer.Delegator) 177 # It is too late to mock notify_range, so test side effect. 178 self.assertEqual(self.root.tk.call( 179 'after', 'info', color.after_id)[1], 'timer') 180 181 def test_LoadTagDefs(self): 182 highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic') 183 for tag, colors in self.color.tagdefs.items(): 184 with self.subTest(tag=tag): 185 self.assertIn('background', colors) 186 self.assertIn('foreground', colors) 187 if tag not in ('SYNC', 'TODO'): 188 self.assertEqual(colors, highlight(element=tag.lower())) 189 190 def test_config_colors(self): 191 text = self.text 192 highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic') 193 for tag in self.color.tagdefs: 194 for plane in ('background', 'foreground'): 195 with self.subTest(tag=tag, plane=plane): 196 if tag in ('SYNC', 'TODO'): 197 self.assertEqual(text.tag_cget(tag, plane), '') 198 else: 199 self.assertEqual(text.tag_cget(tag, plane), 200 highlight(element=tag.lower())[plane]) 201 # 'sel' is marked as the highest priority. 202 self.assertEqual(text.tag_names()[-1], 'sel') 203 204 @mock.patch.object(colorizer.ColorDelegator, 'notify_range') 205 def test_insert(self, mock_notify): 206 text = self.text 207 # Initial text. 208 text.insert('insert', 'foo') 209 self.assertEqual(text.get('1.0', 'end'), 'foo\n') 210 mock_notify.assert_called_with('1.0', '1.0+3c') 211 # Additional text. 212 text.insert('insert', 'barbaz') 213 self.assertEqual(text.get('1.0', 'end'), 'foobarbaz\n') 214 mock_notify.assert_called_with('1.3', '1.3+6c') 215 216 @mock.patch.object(colorizer.ColorDelegator, 'notify_range') 217 def test_delete(self, mock_notify): 218 text = self.text 219 # Initialize text. 220 text.insert('insert', 'abcdefghi') 221 self.assertEqual(text.get('1.0', 'end'), 'abcdefghi\n') 222 # Delete single character. 223 text.delete('1.7') 224 self.assertEqual(text.get('1.0', 'end'), 'abcdefgi\n') 225 mock_notify.assert_called_with('1.7') 226 # Delete multiple characters. 227 text.delete('1.3', '1.6') 228 self.assertEqual(text.get('1.0', 'end'), 'abcgi\n') 229 mock_notify.assert_called_with('1.3') 230 231 def test_notify_range(self): 232 text = self.text 233 color = self.color 234 eq = self.assertEqual 235 236 # Colorizing already scheduled. 237 save_id = color.after_id 238 eq(self.root.tk.call('after', 'info', save_id)[1], 'timer') 239 self.assertFalse(color.colorizing) 240 self.assertFalse(color.stop_colorizing) 241 self.assertTrue(color.allow_colorizing) 242 243 # Coloring scheduled and colorizing in progress. 244 color.colorizing = True 245 color.notify_range('1.0', 'end') 246 self.assertFalse(color.stop_colorizing) 247 eq(color.after_id, save_id) 248 249 # No colorizing scheduled and colorizing in progress. 250 text.after_cancel(save_id) 251 color.after_id = None 252 color.notify_range('1.0', '1.0+3c') 253 self.assertTrue(color.stop_colorizing) 254 self.assertIsNotNone(color.after_id) 255 eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') 256 # New event scheduled. 257 self.assertNotEqual(color.after_id, save_id) 258 259 # No colorizing scheduled and colorizing off. 260 text.after_cancel(color.after_id) 261 color.after_id = None 262 color.allow_colorizing = False 263 color.notify_range('1.4', '1.4+10c') 264 # Nothing scheduled when colorizing is off. 265 self.assertIsNone(color.after_id) 266 267 def test_toggle_colorize_event(self): 268 color = self.color 269 eq = self.assertEqual 270 271 # Starts with colorizing allowed and scheduled. 272 self.assertFalse(color.colorizing) 273 self.assertFalse(color.stop_colorizing) 274 self.assertTrue(color.allow_colorizing) 275 eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') 276 277 # Toggle colorizing off. 278 color.toggle_colorize_event() 279 self.assertIsNone(color.after_id) 280 self.assertFalse(color.colorizing) 281 self.assertFalse(color.stop_colorizing) 282 self.assertFalse(color.allow_colorizing) 283 284 # Toggle on while colorizing in progress (doesn't add timer). 285 color.colorizing = True 286 color.toggle_colorize_event() 287 self.assertIsNone(color.after_id) 288 self.assertTrue(color.colorizing) 289 self.assertFalse(color.stop_colorizing) 290 self.assertTrue(color.allow_colorizing) 291 292 # Toggle off while colorizing in progress. 293 color.toggle_colorize_event() 294 self.assertIsNone(color.after_id) 295 self.assertTrue(color.colorizing) 296 self.assertTrue(color.stop_colorizing) 297 self.assertFalse(color.allow_colorizing) 298 299 # Toggle on while colorizing not in progress. 300 color.colorizing = False 301 color.toggle_colorize_event() 302 eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') 303 self.assertFalse(color.colorizing) 304 self.assertTrue(color.stop_colorizing) 305 self.assertTrue(color.allow_colorizing) 306 307 @mock.patch.object(colorizer.ColorDelegator, 'recolorize_main') 308 def test_recolorize(self, mock_recmain): 309 text = self.text 310 color = self.color 311 eq = self.assertEqual 312 # Call recolorize manually and not scheduled. 313 text.after_cancel(color.after_id) 314 315 # No delegate. 316 save_delegate = color.delegate 317 color.delegate = None 318 color.recolorize() 319 mock_recmain.assert_not_called() 320 color.delegate = save_delegate 321 322 # Toggle off colorizing. 323 color.allow_colorizing = False 324 color.recolorize() 325 mock_recmain.assert_not_called() 326 color.allow_colorizing = True 327 328 # Colorizing in progress. 329 color.colorizing = True 330 color.recolorize() 331 mock_recmain.assert_not_called() 332 color.colorizing = False 333 334 # Colorizing is done, but not completed, so rescheduled. 335 color.recolorize() 336 self.assertFalse(color.stop_colorizing) 337 self.assertFalse(color.colorizing) 338 mock_recmain.assert_called() 339 eq(mock_recmain.call_count, 1) 340 # Rescheduled when TODO tag still exists. 341 eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer') 342 343 # No changes to text, so no scheduling added. 344 text.tag_remove('TODO', '1.0', 'end') 345 color.recolorize() 346 self.assertFalse(color.stop_colorizing) 347 self.assertFalse(color.colorizing) 348 mock_recmain.assert_called() 349 eq(mock_recmain.call_count, 2) 350 self.assertIsNone(color.after_id) 351 352 @mock.patch.object(colorizer.ColorDelegator, 'notify_range') 353 def test_recolorize_main(self, mock_notify): 354 text = self.text 355 color = self.color 356 eq = self.assertEqual 357 358 text.insert('insert', source) 359 expected = (('1.0', ('KEYWORD',)), ('1.2', ()), ('1.3', ('KEYWORD',)), 360 ('1.7', ()), ('1.9', ('BUILTIN',)), ('1.14', ('STRING',)), 361 ('1.19', ('COMMENT',)), 362 ('2.1', ('KEYWORD',)), ('2.18', ()), ('2.25', ('COMMENT',)), 363 ('3.6', ('BUILTIN',)), ('3.12', ('KEYWORD',)), ('3.21', ('COMMENT',)), 364 ('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()), 365 ('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)), 366 ('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()), 367 ('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)), 368 ('7.12', ()), ('7.14', ('STRING',)), 369 # SYNC at the end of every line. 370 ('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)), 371 ) 372 373 # Nothing marked to do therefore no tags in text. 374 text.tag_remove('TODO', '1.0', 'end') 375 color.recolorize_main() 376 for tag in text.tag_names(): 377 with self.subTest(tag=tag): 378 eq(text.tag_ranges(tag), ()) 379 380 # Source marked for processing. 381 text.tag_add('TODO', '1.0', 'end') 382 # Check some indexes. 383 color.recolorize_main() 384 for index, expected_tags in expected: 385 with self.subTest(index=index): 386 eq(text.tag_names(index), expected_tags) 387 388 # Check for some tags for ranges. 389 eq(text.tag_nextrange('TODO', '1.0'), ()) 390 eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2')) 391 eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43')) 392 eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0')) 393 eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53')) 394 eq(text.tag_nextrange('STRING', '7.0'), ('7.0', '7.3')) 395 eq(text.tag_nextrange('STRING', '7.3'), ('7.5', '7.12')) 396 eq(text.tag_nextrange('STRING', '7.12'), ('7.14', '7.17')) 397 eq(text.tag_nextrange('STRING', '7.17'), ('7.19', '7.26')) 398 eq(text.tag_nextrange('SYNC', '7.0'), ('7.26', '9.0')) 399 400 @mock.patch.object(colorizer.ColorDelegator, 'recolorize') 401 @mock.patch.object(colorizer.ColorDelegator, 'notify_range') 402 def test_removecolors(self, mock_notify, mock_recolorize): 403 text = self.text 404 color = self.color 405 text.insert('insert', source) 406 407 color.recolorize_main() 408 # recolorize_main doesn't add these tags. 409 text.tag_add("ERROR", "1.0") 410 text.tag_add("TODO", "1.0") 411 text.tag_add("hit", "1.0") 412 for tag in color.tagdefs: 413 with self.subTest(tag=tag): 414 self.assertNotEqual(text.tag_ranges(tag), ()) 415 416 color.removecolors() 417 for tag in color.tagdefs: 418 with self.subTest(tag=tag): 419 self.assertEqual(text.tag_ranges(tag), ()) 420 421 422if __name__ == '__main__': 423 unittest.main(verbosity=2) 424