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