1""" Tests for the linecache module """ 2 3import linecache 4import unittest 5import os.path 6import tempfile 7import tokenize 8from importlib.machinery import ModuleSpec 9from test import support 10from test.support import os_helper 11 12 13FILENAME = linecache.__file__ 14NONEXISTENT_FILENAME = FILENAME + '.missing' 15INVALID_NAME = '!@$)(!@#_1' 16EMPTY = '' 17TEST_PATH = os.path.dirname(__file__) 18MODULES = "linecache abc".split() 19MODULE_PATH = os.path.dirname(FILENAME) 20 21SOURCE_1 = ''' 22" Docstring " 23 24def function(): 25 return result 26 27''' 28 29SOURCE_2 = ''' 30def f(): 31 return 1 + 1 32 33a = f() 34 35''' 36 37SOURCE_3 = ''' 38def f(): 39 return 3''' # No ending newline 40 41 42class TempFile: 43 44 def setUp(self): 45 super().setUp() 46 with tempfile.NamedTemporaryFile(delete=False) as fp: 47 self.file_name = fp.name 48 fp.write(self.file_byte_string) 49 self.addCleanup(os_helper.unlink, self.file_name) 50 51 52class GetLineTestsGoodData(TempFile): 53 # file_list = ['list\n', 'of\n', 'good\n', 'strings\n'] 54 55 def setUp(self): 56 self.file_byte_string = ''.join(self.file_list).encode('utf-8') 57 super().setUp() 58 59 def test_getline(self): 60 with tokenize.open(self.file_name) as fp: 61 for index, line in enumerate(fp): 62 if not line.endswith('\n'): 63 line += '\n' 64 65 cached_line = linecache.getline(self.file_name, index + 1) 66 self.assertEqual(line, cached_line) 67 68 def test_getlines(self): 69 lines = linecache.getlines(self.file_name) 70 self.assertEqual(lines, self.file_list) 71 72 73class GetLineTestsBadData(TempFile): 74 # file_byte_string = b'Bad data goes here' 75 76 def test_getline(self): 77 self.assertEqual(linecache.getline(self.file_name, 1), '') 78 79 def test_getlines(self): 80 self.assertEqual(linecache.getlines(self.file_name), []) 81 82 83class EmptyFile(GetLineTestsGoodData, unittest.TestCase): 84 file_list = [] 85 86 def test_getlines(self): 87 lines = linecache.getlines(self.file_name) 88 self.assertEqual(lines, ['\n']) 89 90 91class SingleEmptyLine(GetLineTestsGoodData, unittest.TestCase): 92 file_list = ['\n'] 93 94 95class GoodUnicode(GetLineTestsGoodData, unittest.TestCase): 96 file_list = ['á\n', 'b\n', 'abcdef\n', 'ááááá\n'] 97 98class BadUnicode_NoDeclaration(GetLineTestsBadData, unittest.TestCase): 99 file_byte_string = b'\n\x80abc' 100 101class BadUnicode_WithDeclaration(GetLineTestsBadData, unittest.TestCase): 102 file_byte_string = b'# coding=utf-8\n\x80abc' 103 104 105class FakeLoader: 106 def get_source(self, fullname): 107 return f'source for {fullname}' 108 109 110class NoSourceLoader: 111 def get_source(self, fullname): 112 return None 113 114 115class LineCacheTests(unittest.TestCase): 116 117 def test_getline(self): 118 getline = linecache.getline 119 120 # Bad values for line number should return an empty string 121 self.assertEqual(getline(FILENAME, 2**15), EMPTY) 122 self.assertEqual(getline(FILENAME, -1), EMPTY) 123 124 # Float values currently raise TypeError, should it? 125 self.assertRaises(TypeError, getline, FILENAME, 1.1) 126 127 # Bad filenames should return an empty string 128 self.assertEqual(getline(EMPTY, 1), EMPTY) 129 self.assertEqual(getline(INVALID_NAME, 1), EMPTY) 130 131 # Check module loading 132 for entry in MODULES: 133 filename = os.path.join(MODULE_PATH, entry) + '.py' 134 with open(filename, encoding='utf-8') as file: 135 for index, line in enumerate(file): 136 self.assertEqual(line, getline(filename, index + 1)) 137 138 # Check that bogus data isn't returned (issue #1309567) 139 empty = linecache.getlines('a/b/c/__init__.py') 140 self.assertEqual(empty, []) 141 142 def test_no_ending_newline(self): 143 self.addCleanup(os_helper.unlink, os_helper.TESTFN) 144 with open(os_helper.TESTFN, "w", encoding='utf-8') as fp: 145 fp.write(SOURCE_3) 146 lines = linecache.getlines(os_helper.TESTFN) 147 self.assertEqual(lines, ["\n", "def f():\n", " return 3\n"]) 148 149 def test_clearcache(self): 150 cached = [] 151 for entry in MODULES: 152 filename = os.path.join(MODULE_PATH, entry) + '.py' 153 cached.append(filename) 154 linecache.getline(filename, 1) 155 156 # Are all files cached? 157 self.assertNotEqual(cached, []) 158 cached_empty = [fn for fn in cached if fn not in linecache.cache] 159 self.assertEqual(cached_empty, []) 160 161 # Can we clear the cache? 162 linecache.clearcache() 163 cached_empty = [fn for fn in cached if fn in linecache.cache] 164 self.assertEqual(cached_empty, []) 165 166 def test_checkcache(self): 167 getline = linecache.getline 168 # Create a source file and cache its contents 169 source_name = os_helper.TESTFN + '.py' 170 self.addCleanup(os_helper.unlink, source_name) 171 with open(source_name, 'w', encoding='utf-8') as source: 172 source.write(SOURCE_1) 173 getline(source_name, 1) 174 175 # Keep a copy of the old contents 176 source_list = [] 177 with open(source_name, encoding='utf-8') as source: 178 for index, line in enumerate(source): 179 self.assertEqual(line, getline(source_name, index + 1)) 180 source_list.append(line) 181 182 with open(source_name, 'w', encoding='utf-8') as source: 183 source.write(SOURCE_2) 184 185 # Try to update a bogus cache entry 186 linecache.checkcache('dummy') 187 188 # Check that the cache matches the old contents 189 for index, line in enumerate(source_list): 190 self.assertEqual(line, getline(source_name, index + 1)) 191 192 # Update the cache and check whether it matches the new source file 193 linecache.checkcache(source_name) 194 with open(source_name, encoding='utf-8') as source: 195 for index, line in enumerate(source): 196 self.assertEqual(line, getline(source_name, index + 1)) 197 source_list.append(line) 198 199 def test_lazycache_no_globals(self): 200 lines = linecache.getlines(FILENAME) 201 linecache.clearcache() 202 self.assertEqual(False, linecache.lazycache(FILENAME, None)) 203 self.assertEqual(lines, linecache.getlines(FILENAME)) 204 205 def test_lazycache_smoke(self): 206 lines = linecache.getlines(NONEXISTENT_FILENAME, globals()) 207 linecache.clearcache() 208 self.assertEqual( 209 True, linecache.lazycache(NONEXISTENT_FILENAME, globals())) 210 self.assertEqual(1, len(linecache.cache[NONEXISTENT_FILENAME])) 211 # Note here that we're looking up a nonexistent filename with no 212 # globals: this would error if the lazy value wasn't resolved. 213 self.assertEqual(lines, linecache.getlines(NONEXISTENT_FILENAME)) 214 215 def test_lazycache_provide_after_failed_lookup(self): 216 linecache.clearcache() 217 lines = linecache.getlines(NONEXISTENT_FILENAME, globals()) 218 linecache.clearcache() 219 linecache.getlines(NONEXISTENT_FILENAME) 220 linecache.lazycache(NONEXISTENT_FILENAME, globals()) 221 self.assertEqual(lines, linecache.updatecache(NONEXISTENT_FILENAME)) 222 223 def test_lazycache_check(self): 224 linecache.clearcache() 225 linecache.lazycache(NONEXISTENT_FILENAME, globals()) 226 linecache.checkcache() 227 228 def test_lazycache_bad_filename(self): 229 linecache.clearcache() 230 self.assertEqual(False, linecache.lazycache('', globals())) 231 self.assertEqual(False, linecache.lazycache('<foo>', globals())) 232 233 def test_lazycache_already_cached(self): 234 linecache.clearcache() 235 lines = linecache.getlines(NONEXISTENT_FILENAME, globals()) 236 self.assertEqual( 237 False, 238 linecache.lazycache(NONEXISTENT_FILENAME, globals())) 239 self.assertEqual(4, len(linecache.cache[NONEXISTENT_FILENAME])) 240 241 def test_memoryerror(self): 242 lines = linecache.getlines(FILENAME) 243 self.assertTrue(lines) 244 def raise_memoryerror(*args, **kwargs): 245 raise MemoryError 246 with support.swap_attr(linecache, 'updatecache', raise_memoryerror): 247 lines2 = linecache.getlines(FILENAME) 248 self.assertEqual(lines2, lines) 249 250 linecache.clearcache() 251 with support.swap_attr(linecache, 'updatecache', raise_memoryerror): 252 lines3 = linecache.getlines(FILENAME) 253 self.assertEqual(lines3, []) 254 self.assertEqual(linecache.getlines(FILENAME), lines) 255 256 def test_loader(self): 257 filename = 'scheme://path' 258 259 for loader in (None, object(), NoSourceLoader()): 260 linecache.clearcache() 261 module_globals = {'__name__': 'a.b.c', '__loader__': loader} 262 self.assertEqual(linecache.getlines(filename, module_globals), []) 263 264 linecache.clearcache() 265 module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader()} 266 self.assertEqual(linecache.getlines(filename, module_globals), 267 ['source for a.b.c\n']) 268 269 for spec in (None, object(), ModuleSpec('', FakeLoader())): 270 linecache.clearcache() 271 module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader(), 272 '__spec__': spec} 273 self.assertEqual(linecache.getlines(filename, module_globals), 274 ['source for a.b.c\n']) 275 276 linecache.clearcache() 277 spec = ModuleSpec('x.y.z', FakeLoader()) 278 module_globals = {'__name__': 'a.b.c', '__loader__': spec.loader, 279 '__spec__': spec} 280 self.assertEqual(linecache.getlines(filename, module_globals), 281 ['source for x.y.z\n']) 282 283 def test_invalid_names(self): 284 for name, desc in [ 285 ('\x00', 'NUL bytes filename'), 286 (__file__ + '\x00', 'filename with embedded NUL bytes'), 287 # A filename with surrogate codes. A UnicodeEncodeError is raised 288 # by os.stat() upon querying, which is a subclass of ValueError. 289 ("\uD834\uDD1E.py", 'surrogate codes (MUSICAL SYMBOL G CLEF)'), 290 # For POSIX platforms, an OSError will be raised but for Windows 291 # platforms, a ValueError is raised due to the path_t converter. 292 # See: https://github.com/python/cpython/issues/122170 293 ('a' * 1_000_000, 'very long filename'), 294 ]: 295 with self.subTest(f'updatecache: {desc}'): 296 linecache.clearcache() 297 lines = linecache.updatecache(name) 298 self.assertListEqual(lines, []) 299 self.assertNotIn(name, linecache.cache) 300 301 # hack into the cache (it shouldn't be allowed 302 # but we never know what people do...) 303 for key, fullname in [(name, 'ok'), ('key', name), (name, name)]: 304 with self.subTest(f'checkcache: {desc}', 305 key=key, fullname=fullname): 306 linecache.clearcache() 307 linecache.cache[key] = (0, 1234, [], fullname) 308 linecache.checkcache(key) 309 self.assertNotIn(key, linecache.cache) 310 311 # just to be sure that we did not mess with cache 312 linecache.clearcache() 313 314 315class LineCacheInvalidationTests(unittest.TestCase): 316 def setUp(self): 317 super().setUp() 318 linecache.clearcache() 319 self.deleted_file = os_helper.TESTFN + '.1' 320 self.modified_file = os_helper.TESTFN + '.2' 321 self.unchanged_file = os_helper.TESTFN + '.3' 322 323 for fname in (self.deleted_file, 324 self.modified_file, 325 self.unchanged_file): 326 self.addCleanup(os_helper.unlink, fname) 327 with open(fname, 'w', encoding='utf-8') as source: 328 source.write(f'print("I am {fname}")') 329 330 self.assertNotIn(fname, linecache.cache) 331 linecache.getlines(fname) 332 self.assertIn(fname, linecache.cache) 333 334 os.remove(self.deleted_file) 335 with open(self.modified_file, 'w', encoding='utf-8') as source: 336 source.write('print("was modified")') 337 338 def test_checkcache_for_deleted_file(self): 339 linecache.checkcache(self.deleted_file) 340 self.assertNotIn(self.deleted_file, linecache.cache) 341 self.assertIn(self.modified_file, linecache.cache) 342 self.assertIn(self.unchanged_file, linecache.cache) 343 344 def test_checkcache_for_modified_file(self): 345 linecache.checkcache(self.modified_file) 346 self.assertIn(self.deleted_file, linecache.cache) 347 self.assertNotIn(self.modified_file, linecache.cache) 348 self.assertIn(self.unchanged_file, linecache.cache) 349 350 def test_checkcache_with_no_parameter(self): 351 linecache.checkcache() 352 self.assertNotIn(self.deleted_file, linecache.cache) 353 self.assertNotIn(self.modified_file, linecache.cache) 354 self.assertIn(self.unchanged_file, linecache.cache) 355 356 357if __name__ == "__main__": 358 unittest.main() 359