• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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