• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import os
2import sys
3from test.support import TESTFN, rmtree, unlink, captured_stdout
4from test.support.script_helper import assert_python_ok, assert_python_failure
5import textwrap
6import unittest
7
8import trace
9from trace import Trace
10
11from test.tracedmodules import testmod
12
13#------------------------------- Utilities -----------------------------------#
14
15def fix_ext_py(filename):
16    """Given a .pyc filename converts it to the appropriate .py"""
17    if filename.endswith('.pyc'):
18        filename = filename[:-1]
19    return filename
20
21def my_file_and_modname():
22    """The .py file and module name of this file (__file__)"""
23    modname = os.path.splitext(os.path.basename(__file__))[0]
24    return fix_ext_py(__file__), modname
25
26def get_firstlineno(func):
27    return func.__code__.co_firstlineno
28
29#-------------------- Target functions for tracing ---------------------------#
30#
31# The relative line numbers of lines in these functions matter for verifying
32# tracing. Please modify the appropriate tests if you change one of the
33# functions. Absolute line numbers don't matter.
34#
35
36def traced_func_linear(x, y):
37    a = x
38    b = y
39    c = a + b
40    return c
41
42def traced_func_loop(x, y):
43    c = x
44    for i in range(5):
45        c += y
46    return c
47
48def traced_func_importing(x, y):
49    return x + y + testmod.func(1)
50
51def traced_func_simple_caller(x):
52    c = traced_func_linear(x, x)
53    return c + x
54
55def traced_func_importing_caller(x):
56    k = traced_func_simple_caller(x)
57    k += traced_func_importing(k, x)
58    return k
59
60def traced_func_generator(num):
61    c = 5       # executed once
62    for i in range(num):
63        yield i + c
64
65def traced_func_calling_generator():
66    k = 0
67    for i in traced_func_generator(10):
68        k += i
69
70def traced_doubler(num):
71    return num * 2
72
73def traced_caller_list_comprehension():
74    k = 10
75    mylist = [traced_doubler(i) for i in range(k)]
76    return mylist
77
78
79class TracedClass(object):
80    def __init__(self, x):
81        self.a = x
82
83    def inst_method_linear(self, y):
84        return self.a + y
85
86    def inst_method_calling(self, x):
87        c = self.inst_method_linear(x)
88        return c + traced_func_linear(x, c)
89
90    @classmethod
91    def class_method_linear(cls, y):
92        return y * 2
93
94    @staticmethod
95    def static_method_linear(y):
96        return y * 2
97
98
99#------------------------------ Test cases -----------------------------------#
100
101
102class TestLineCounts(unittest.TestCase):
103    """White-box testing of line-counting, via runfunc"""
104    def setUp(self):
105        self.addCleanup(sys.settrace, sys.gettrace())
106        self.tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
107        self.my_py_filename = fix_ext_py(__file__)
108
109    def test_traced_func_linear(self):
110        result = self.tracer.runfunc(traced_func_linear, 2, 5)
111        self.assertEqual(result, 7)
112
113        # all lines are executed once
114        expected = {}
115        firstlineno = get_firstlineno(traced_func_linear)
116        for i in range(1, 5):
117            expected[(self.my_py_filename, firstlineno +  i)] = 1
118
119        self.assertEqual(self.tracer.results().counts, expected)
120
121    def test_traced_func_loop(self):
122        self.tracer.runfunc(traced_func_loop, 2, 3)
123
124        firstlineno = get_firstlineno(traced_func_loop)
125        expected = {
126            (self.my_py_filename, firstlineno + 1): 1,
127            (self.my_py_filename, firstlineno + 2): 6,
128            (self.my_py_filename, firstlineno + 3): 5,
129            (self.my_py_filename, firstlineno + 4): 1,
130        }
131        self.assertEqual(self.tracer.results().counts, expected)
132
133    def test_traced_func_importing(self):
134        self.tracer.runfunc(traced_func_importing, 2, 5)
135
136        firstlineno = get_firstlineno(traced_func_importing)
137        expected = {
138            (self.my_py_filename, firstlineno + 1): 1,
139            (fix_ext_py(testmod.__file__), 2): 1,
140            (fix_ext_py(testmod.__file__), 3): 1,
141        }
142
143        self.assertEqual(self.tracer.results().counts, expected)
144
145    def test_trace_func_generator(self):
146        self.tracer.runfunc(traced_func_calling_generator)
147
148        firstlineno_calling = get_firstlineno(traced_func_calling_generator)
149        firstlineno_gen = get_firstlineno(traced_func_generator)
150        expected = {
151            (self.my_py_filename, firstlineno_calling + 1): 1,
152            (self.my_py_filename, firstlineno_calling + 2): 11,
153            (self.my_py_filename, firstlineno_calling + 3): 10,
154            (self.my_py_filename, firstlineno_gen + 1): 1,
155            (self.my_py_filename, firstlineno_gen + 2): 11,
156            (self.my_py_filename, firstlineno_gen + 3): 10,
157        }
158        self.assertEqual(self.tracer.results().counts, expected)
159
160    def test_trace_list_comprehension(self):
161        self.tracer.runfunc(traced_caller_list_comprehension)
162
163        firstlineno_calling = get_firstlineno(traced_caller_list_comprehension)
164        firstlineno_called = get_firstlineno(traced_doubler)
165        expected = {
166            (self.my_py_filename, firstlineno_calling + 1): 1,
167            # List compehentions work differently in 3.x, so the count
168            # below changed compared to 2.x.
169            (self.my_py_filename, firstlineno_calling + 2): 12,
170            (self.my_py_filename, firstlineno_calling + 3): 1,
171            (self.my_py_filename, firstlineno_called + 1): 10,
172        }
173        self.assertEqual(self.tracer.results().counts, expected)
174
175
176    def test_linear_methods(self):
177        # XXX todo: later add 'static_method_linear' and 'class_method_linear'
178        # here, once issue1764286 is resolved
179        #
180        for methname in ['inst_method_linear',]:
181            tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
182            traced_obj = TracedClass(25)
183            method = getattr(traced_obj, methname)
184            tracer.runfunc(method, 20)
185
186            firstlineno = get_firstlineno(method)
187            expected = {
188                (self.my_py_filename, firstlineno + 1): 1,
189            }
190            self.assertEqual(tracer.results().counts, expected)
191
192class TestRunExecCounts(unittest.TestCase):
193    """A simple sanity test of line-counting, via runctx (exec)"""
194    def setUp(self):
195        self.my_py_filename = fix_ext_py(__file__)
196        self.addCleanup(sys.settrace, sys.gettrace())
197
198    def test_exec_counts(self):
199        self.tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
200        code = r'''traced_func_loop(2, 5)'''
201        code = compile(code, __file__, 'exec')
202        self.tracer.runctx(code, globals(), vars())
203
204        firstlineno = get_firstlineno(traced_func_loop)
205        expected = {
206            (self.my_py_filename, firstlineno + 1): 1,
207            (self.my_py_filename, firstlineno + 2): 6,
208            (self.my_py_filename, firstlineno + 3): 5,
209            (self.my_py_filename, firstlineno + 4): 1,
210        }
211
212        # When used through 'run', some other spurious counts are produced, like
213        # the settrace of threading, which we ignore, just making sure that the
214        # counts fo traced_func_loop were right.
215        #
216        for k in expected.keys():
217            self.assertEqual(self.tracer.results().counts[k], expected[k])
218
219
220class TestFuncs(unittest.TestCase):
221    """White-box testing of funcs tracing"""
222    def setUp(self):
223        self.addCleanup(sys.settrace, sys.gettrace())
224        self.tracer = Trace(count=0, trace=0, countfuncs=1)
225        self.filemod = my_file_and_modname()
226        self._saved_tracefunc = sys.gettrace()
227
228    def tearDown(self):
229        if self._saved_tracefunc is not None:
230            sys.settrace(self._saved_tracefunc)
231
232    def test_simple_caller(self):
233        self.tracer.runfunc(traced_func_simple_caller, 1)
234
235        expected = {
236            self.filemod + ('traced_func_simple_caller',): 1,
237            self.filemod + ('traced_func_linear',): 1,
238        }
239        self.assertEqual(self.tracer.results().calledfuncs, expected)
240
241    def test_loop_caller_importing(self):
242        self.tracer.runfunc(traced_func_importing_caller, 1)
243
244        expected = {
245            self.filemod + ('traced_func_simple_caller',): 1,
246            self.filemod + ('traced_func_linear',): 1,
247            self.filemod + ('traced_func_importing_caller',): 1,
248            self.filemod + ('traced_func_importing',): 1,
249            (fix_ext_py(testmod.__file__), 'testmod', 'func'): 1,
250        }
251        self.assertEqual(self.tracer.results().calledfuncs, expected)
252
253    @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
254                     'pre-existing trace function throws off measurements')
255    def test_inst_method_calling(self):
256        obj = TracedClass(20)
257        self.tracer.runfunc(obj.inst_method_calling, 1)
258
259        expected = {
260            self.filemod + ('TracedClass.inst_method_calling',): 1,
261            self.filemod + ('TracedClass.inst_method_linear',): 1,
262            self.filemod + ('traced_func_linear',): 1,
263        }
264        self.assertEqual(self.tracer.results().calledfuncs, expected)
265
266
267class TestCallers(unittest.TestCase):
268    """White-box testing of callers tracing"""
269    def setUp(self):
270        self.addCleanup(sys.settrace, sys.gettrace())
271        self.tracer = Trace(count=0, trace=0, countcallers=1)
272        self.filemod = my_file_and_modname()
273
274    @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
275                     'pre-existing trace function throws off measurements')
276    def test_loop_caller_importing(self):
277        self.tracer.runfunc(traced_func_importing_caller, 1)
278
279        expected = {
280            ((os.path.splitext(trace.__file__)[0] + '.py', 'trace', 'Trace.runfunc'),
281                (self.filemod + ('traced_func_importing_caller',))): 1,
282            ((self.filemod + ('traced_func_simple_caller',)),
283                (self.filemod + ('traced_func_linear',))): 1,
284            ((self.filemod + ('traced_func_importing_caller',)),
285                (self.filemod + ('traced_func_simple_caller',))): 1,
286            ((self.filemod + ('traced_func_importing_caller',)),
287                (self.filemod + ('traced_func_importing',))): 1,
288            ((self.filemod + ('traced_func_importing',)),
289                (fix_ext_py(testmod.__file__), 'testmod', 'func')): 1,
290        }
291        self.assertEqual(self.tracer.results().callers, expected)
292
293
294# Created separately for issue #3821
295class TestCoverage(unittest.TestCase):
296    def setUp(self):
297        self.addCleanup(sys.settrace, sys.gettrace())
298
299    def tearDown(self):
300        rmtree(TESTFN)
301        unlink(TESTFN)
302
303    def _coverage(self, tracer,
304                  cmd='import test.support, test.test_pprint;'
305                      'test.support.run_unittest(test.test_pprint.QueryTestCase)'):
306        tracer.run(cmd)
307        r = tracer.results()
308        r.write_results(show_missing=True, summary=True, coverdir=TESTFN)
309
310    def test_coverage(self):
311        tracer = trace.Trace(trace=0, count=1)
312        with captured_stdout() as stdout:
313            self._coverage(tracer)
314        stdout = stdout.getvalue()
315        self.assertIn("pprint.py", stdout)
316        self.assertIn("case.py", stdout)   # from unittest
317        files = os.listdir(TESTFN)
318        self.assertIn("pprint.cover", files)
319        self.assertIn("unittest.case.cover", files)
320
321    def test_coverage_ignore(self):
322        # Ignore all files, nothing should be traced nor printed
323        libpath = os.path.normpath(os.path.dirname(os.__file__))
324        # sys.prefix does not work when running from a checkout
325        tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,
326                             libpath], trace=0, count=1)
327        with captured_stdout() as stdout:
328            self._coverage(tracer)
329        if os.path.exists(TESTFN):
330            files = os.listdir(TESTFN)
331            self.assertEqual(files, ['_importlib.cover'])  # Ignore __import__
332
333    def test_issue9936(self):
334        tracer = trace.Trace(trace=0, count=1)
335        modname = 'test.tracedmodules.testmod'
336        # Ensure that the module is executed in import
337        if modname in sys.modules:
338            del sys.modules[modname]
339        cmd = ("import test.tracedmodules.testmod as t;"
340               "t.func(0); t.func2();")
341        with captured_stdout() as stdout:
342            self._coverage(tracer, cmd)
343        stdout.seek(0)
344        stdout.readline()
345        coverage = {}
346        for line in stdout:
347            lines, cov, module = line.split()[:3]
348            coverage[module] = (int(lines), int(cov[:-1]))
349        # XXX This is needed to run regrtest.py as a script
350        modname = trace._fullmodname(sys.modules[modname].__file__)
351        self.assertIn(modname, coverage)
352        self.assertEqual(coverage[modname], (5, 100))
353
354### Tests that don't mess with sys.settrace and can be traced
355### themselves TODO: Skip tests that do mess with sys.settrace when
356### regrtest is invoked with -T option.
357class Test_Ignore(unittest.TestCase):
358    def test_ignored(self):
359        jn = os.path.join
360        ignore = trace._Ignore(['x', 'y.z'], [jn('foo', 'bar')])
361        self.assertTrue(ignore.names('x.py', 'x'))
362        self.assertFalse(ignore.names('xy.py', 'xy'))
363        self.assertFalse(ignore.names('y.py', 'y'))
364        self.assertTrue(ignore.names(jn('foo', 'bar', 'baz.py'), 'baz'))
365        self.assertFalse(ignore.names(jn('bar', 'z.py'), 'z'))
366        # Matched before.
367        self.assertTrue(ignore.names(jn('bar', 'baz.py'), 'baz'))
368
369# Created for Issue 31908 -- CLI utility not writing cover files
370class TestCoverageCommandLineOutput(unittest.TestCase):
371
372    codefile = 'tmp.py'
373    coverfile = 'tmp.cover'
374
375    def setUp(self):
376        with open(self.codefile, 'w') as f:
377            f.write(textwrap.dedent('''\
378                x = 42
379                if []:
380                    print('unreachable')
381            '''))
382
383    def tearDown(self):
384        unlink(self.codefile)
385        unlink(self.coverfile)
386
387    def test_cover_files_written_no_highlight(self):
388        # Test also that the cover file for the trace module is not created
389        # (issue #34171).
390        tracedir = os.path.dirname(os.path.abspath(trace.__file__))
391        tracecoverpath = os.path.join(tracedir, 'trace.cover')
392        unlink(tracecoverpath)
393
394        argv = '-m trace --count'.split() + [self.codefile]
395        status, stdout, stderr = assert_python_ok(*argv)
396        self.assertEqual(stderr, b'')
397        self.assertFalse(os.path.exists(tracecoverpath))
398        self.assertTrue(os.path.exists(self.coverfile))
399        with open(self.coverfile) as f:
400            self.assertEqual(f.read(),
401                "    1: x = 42\n"
402                "    1: if []:\n"
403                "           print('unreachable')\n"
404            )
405
406    def test_cover_files_written_with_highlight(self):
407        argv = '-m trace --count --missing'.split() + [self.codefile]
408        status, stdout, stderr = assert_python_ok(*argv)
409        self.assertTrue(os.path.exists(self.coverfile))
410        with open(self.coverfile) as f:
411            self.assertEqual(f.read(), textwrap.dedent('''\
412                    1: x = 42
413                    1: if []:
414                >>>>>>     print('unreachable')
415            '''))
416
417class TestCommandLine(unittest.TestCase):
418
419    def test_failures(self):
420        _errors = (
421            (b'filename is missing: required with the main options', '-l', '-T'),
422            (b'cannot specify both --listfuncs and (--trace or --count)', '-lc'),
423            (b'argument -R/--no-report: not allowed with argument -r/--report', '-rR'),
424            (b'must specify one of --trace, --count, --report, --listfuncs, or --trackcalls', '-g'),
425            (b'-r/--report requires -f/--file', '-r'),
426            (b'--summary can only be used with --count or --report', '-sT'),
427            (b'unrecognized arguments: -y', '-y'))
428        for message, *args in _errors:
429            *_, stderr = assert_python_failure('-m', 'trace', *args)
430            self.assertIn(message, stderr)
431
432    def test_listfuncs_flag_success(self):
433        with open(TESTFN, 'w') as fd:
434            self.addCleanup(unlink, TESTFN)
435            fd.write("a = 1\n")
436            status, stdout, stderr = assert_python_ok('-m', 'trace', '-l', TESTFN)
437            self.assertIn(b'functions called:', stdout)
438
439    def test_sys_argv_list(self):
440        with open(TESTFN, 'w') as fd:
441            self.addCleanup(unlink, TESTFN)
442            fd.write("import sys\n")
443            fd.write("print(type(sys.argv))\n")
444
445        status, direct_stdout, stderr = assert_python_ok(TESTFN)
446        status, trace_stdout, stderr = assert_python_ok('-m', 'trace', '-l', TESTFN)
447        self.assertIn(direct_stdout.strip(), trace_stdout)
448
449    def test_count_and_summary(self):
450        filename = f'{TESTFN}.py'
451        coverfilename = f'{TESTFN}.cover'
452        with open(filename, 'w') as fd:
453            self.addCleanup(unlink, filename)
454            self.addCleanup(unlink, coverfilename)
455            fd.write(textwrap.dedent("""\
456                x = 1
457                y = 2
458
459                def f():
460                    return x + y
461
462                for i in range(10):
463                    f()
464            """))
465        status, stdout, _ = assert_python_ok('-m', 'trace', '-cs', filename)
466        stdout = stdout.decode()
467        self.assertEqual(status, 0)
468        self.assertIn('lines   cov%   module   (path)', stdout)
469        self.assertIn(f'6   100%   {TESTFN}   ({filename})', stdout)
470
471if __name__ == '__main__':
472    unittest.main()
473