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