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 (self.my_py_filename, firstlineno_calling + 2): 11,
191 (self.my_py_filename, firstlineno_calling + 3): 1,
192 (self.my_py_filename, firstlineno_called + 1): 10,
193 }
194 self.assertEqual(self.tracer.results().counts, expected)
195
196 def test_traced_decorated_function(self):
197 self.tracer.runfunc(traced_decorated_function)
198
199 firstlineno = get_firstlineno(traced_decorated_function)
200 expected = {
201 (self.my_py_filename, firstlineno + 1): 1,
202 (self.my_py_filename, firstlineno + 2): 1,
203 (self.my_py_filename, firstlineno + 3): 1,
204 (self.my_py_filename, firstlineno + 4): 1,
205 (self.my_py_filename, firstlineno + 5): 1,
206 (self.my_py_filename, firstlineno + 6): 1,
207 (self.my_py_filename, firstlineno + 7): 2,
208 (self.my_py_filename, firstlineno + 8): 2,
209 (self.my_py_filename, firstlineno + 9): 2,
210 (self.my_py_filename, firstlineno + 10): 1,
211 (self.my_py_filename, firstlineno + 11): 1,
212 }
213 self.assertEqual(self.tracer.results().counts, expected)
214
215 def test_linear_methods(self):
216 # XXX todo: later add 'static_method_linear' and 'class_method_linear'
217 # here, once issue1764286 is resolved
218 #
219 for methname in ['inst_method_linear',]:
220 tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
221 traced_obj = TracedClass(25)
222 method = getattr(traced_obj, methname)
223 tracer.runfunc(method, 20)
224
225 firstlineno = get_firstlineno(method)
226 expected = {
227 (self.my_py_filename, firstlineno + 1): 1,
228 }
229 self.assertEqual(tracer.results().counts, expected)
230
231
232 class ESC[4;38;5;81mTestRunExecCounts(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
233 """A simple sanity test of line-counting, via runctx (exec)"""
234 def setUp(self):
235 self.my_py_filename = fix_ext_py(__file__)
236 self.addCleanup(sys.settrace, sys.gettrace())
237
238 def test_exec_counts(self):
239 self.tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
240 code = r'''traced_func_loop(2, 5)'''
241 code = compile(code, __file__, 'exec')
242 self.tracer.runctx(code, globals(), vars())
243
244 firstlineno = get_firstlineno(traced_func_loop)
245 expected = {
246 (self.my_py_filename, firstlineno + 1): 1,
247 (self.my_py_filename, firstlineno + 2): 6,
248 (self.my_py_filename, firstlineno + 3): 5,
249 (self.my_py_filename, firstlineno + 4): 1,
250 }
251
252 # When used through 'run', some other spurious counts are produced, like
253 # the settrace of threading, which we ignore, just making sure that the
254 # counts fo traced_func_loop were right.
255 #
256 for k in expected.keys():
257 self.assertEqual(self.tracer.results().counts[k], expected[k])
258
259
260 class ESC[4;38;5;81mTestFuncs(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
261 """White-box testing of funcs tracing"""
262 def setUp(self):
263 self.addCleanup(sys.settrace, sys.gettrace())
264 self.tracer = Trace(count=0, trace=0, countfuncs=1)
265 self.filemod = my_file_and_modname()
266 self._saved_tracefunc = sys.gettrace()
267
268 def tearDown(self):
269 if self._saved_tracefunc is not None:
270 sys.settrace(self._saved_tracefunc)
271
272 def test_simple_caller(self):
273 self.tracer.runfunc(traced_func_simple_caller, 1)
274
275 expected = {
276 self.filemod + ('traced_func_simple_caller',): 1,
277 self.filemod + ('traced_func_linear',): 1,
278 }
279 self.assertEqual(self.tracer.results().calledfuncs, expected)
280
281 def test_arg_errors(self):
282 res = self.tracer.runfunc(traced_capturer, 1, 2, self=3, func=4)
283 self.assertEqual(res, ((1, 2), {'self': 3, 'func': 4}))
284 with self.assertRaises(TypeError):
285 self.tracer.runfunc(func=traced_capturer, arg=1)
286 with self.assertRaises(TypeError):
287 self.tracer.runfunc()
288
289 def test_loop_caller_importing(self):
290 self.tracer.runfunc(traced_func_importing_caller, 1)
291
292 expected = {
293 self.filemod + ('traced_func_simple_caller',): 1,
294 self.filemod + ('traced_func_linear',): 1,
295 self.filemod + ('traced_func_importing_caller',): 1,
296 self.filemod + ('traced_func_importing',): 1,
297 (fix_ext_py(testmod.__file__), 'testmod', 'func'): 1,
298 }
299 self.assertEqual(self.tracer.results().calledfuncs, expected)
300
301 @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
302 'pre-existing trace function throws off measurements')
303 def test_inst_method_calling(self):
304 obj = TracedClass(20)
305 self.tracer.runfunc(obj.inst_method_calling, 1)
306
307 expected = {
308 self.filemod + ('TracedClass.inst_method_calling',): 1,
309 self.filemod + ('TracedClass.inst_method_linear',): 1,
310 self.filemod + ('traced_func_linear',): 1,
311 }
312 self.assertEqual(self.tracer.results().calledfuncs, expected)
313
314 def test_traced_decorated_function(self):
315 self.tracer.runfunc(traced_decorated_function)
316
317 expected = {
318 self.filemod + ('traced_decorated_function',): 1,
319 self.filemod + ('decorator_fabric',): 1,
320 self.filemod + ('decorator2',): 1,
321 self.filemod + ('decorator1',): 1,
322 self.filemod + ('func',): 1,
323 }
324 self.assertEqual(self.tracer.results().calledfuncs, expected)
325
326
327 class ESC[4;38;5;81mTestCallers(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
328 """White-box testing of callers tracing"""
329 def setUp(self):
330 self.addCleanup(sys.settrace, sys.gettrace())
331 self.tracer = Trace(count=0, trace=0, countcallers=1)
332 self.filemod = my_file_and_modname()
333
334 @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
335 'pre-existing trace function throws off measurements')
336 def test_loop_caller_importing(self):
337 self.tracer.runfunc(traced_func_importing_caller, 1)
338
339 expected = {
340 ((os.path.splitext(trace.__file__)[0] + '.py', 'trace', 'Trace.runfunc'),
341 (self.filemod + ('traced_func_importing_caller',))): 1,
342 ((self.filemod + ('traced_func_simple_caller',)),
343 (self.filemod + ('traced_func_linear',))): 1,
344 ((self.filemod + ('traced_func_importing_caller',)),
345 (self.filemod + ('traced_func_simple_caller',))): 1,
346 ((self.filemod + ('traced_func_importing_caller',)),
347 (self.filemod + ('traced_func_importing',))): 1,
348 ((self.filemod + ('traced_func_importing',)),
349 (fix_ext_py(testmod.__file__), 'testmod', 'func')): 1,
350 }
351 self.assertEqual(self.tracer.results().callers, expected)
352
353
354 # Created separately for issue #3821
355 class ESC[4;38;5;81mTestCoverage(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
356 def setUp(self):
357 self.addCleanup(sys.settrace, sys.gettrace())
358
359 def tearDown(self):
360 rmtree(TESTFN)
361 unlink(TESTFN)
362
363 DEFAULT_SCRIPT = '''if True:
364 import unittest
365 from test.test_pprint import QueryTestCase
366 loader = unittest.TestLoader()
367 tests = loader.loadTestsFromTestCase(QueryTestCase)
368 tests(unittest.TestResult())
369 '''
370 def _coverage(self, tracer, cmd=DEFAULT_SCRIPT):
371 tracer.run(cmd)
372 r = tracer.results()
373 r.write_results(show_missing=True, summary=True, coverdir=TESTFN)
374
375 @requires_resource('cpu')
376 def test_coverage(self):
377 tracer = trace.Trace(trace=0, count=1)
378 with captured_stdout() as stdout:
379 self._coverage(tracer)
380 stdout = stdout.getvalue()
381 self.assertIn("pprint.py", stdout)
382 self.assertIn("case.py", stdout) # from unittest
383 files = os.listdir(TESTFN)
384 self.assertIn("pprint.cover", files)
385 self.assertIn("unittest.case.cover", files)
386
387 def test_coverage_ignore(self):
388 # Ignore all files, nothing should be traced nor printed
389 libpath = os.path.normpath(os.path.dirname(os.path.dirname(__file__)))
390 # sys.prefix does not work when running from a checkout
391 tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,
392 libpath], trace=0, count=1)
393 with captured_stdout() as stdout:
394 self._coverage(tracer)
395 if os.path.exists(TESTFN):
396 files = os.listdir(TESTFN)
397 self.assertEqual(files, ['_importlib.cover']) # Ignore __import__
398
399 def test_issue9936(self):
400 tracer = trace.Trace(trace=0, count=1)
401 modname = 'test.tracedmodules.testmod'
402 # Ensure that the module is executed in import
403 if modname in sys.modules:
404 del sys.modules[modname]
405 cmd = ("import test.tracedmodules.testmod as t;"
406 "t.func(0); t.func2();")
407 with captured_stdout() as stdout:
408 self._coverage(tracer, cmd)
409 stdout.seek(0)
410 stdout.readline()
411 coverage = {}
412 for line in stdout:
413 lines, cov, module = line.split()[:3]
414 coverage[module] = (int(lines), int(cov[:-1]))
415 # XXX This is needed to run regrtest.py as a script
416 modname = trace._fullmodname(sys.modules[modname].__file__)
417 self.assertIn(modname, coverage)
418 self.assertEqual(coverage[modname], (5, 100))
419
420 def test_coverageresults_update(self):
421 # Update empty CoverageResults with a non-empty infile.
422 infile = TESTFN + '-infile'
423 with open(infile, 'wb') as f:
424 dump(({}, {}, {'caller': 1}), f, protocol=1)
425 self.addCleanup(unlink, infile)
426 results = trace.CoverageResults({}, {}, infile, {})
427 self.assertEqual(results.callers, {'caller': 1})
428
429 ### Tests that don't mess with sys.settrace and can be traced
430 ### themselves TODO: Skip tests that do mess with sys.settrace when
431 ### regrtest is invoked with -T option.
432 class ESC[4;38;5;81mTest_Ignore(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
433 def test_ignored(self):
434 jn = os.path.join
435 ignore = trace._Ignore(['x', 'y.z'], [jn('foo', 'bar')])
436 self.assertTrue(ignore.names('x.py', 'x'))
437 self.assertFalse(ignore.names('xy.py', 'xy'))
438 self.assertFalse(ignore.names('y.py', 'y'))
439 self.assertTrue(ignore.names(jn('foo', 'bar', 'baz.py'), 'baz'))
440 self.assertFalse(ignore.names(jn('bar', 'z.py'), 'z'))
441 # Matched before.
442 self.assertTrue(ignore.names(jn('bar', 'baz.py'), 'baz'))
443
444 # Created for Issue 31908 -- CLI utility not writing cover files
445 class ESC[4;38;5;81mTestCoverageCommandLineOutput(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
446
447 codefile = 'tmp.py'
448 coverfile = 'tmp.cover'
449
450 def setUp(self):
451 with open(self.codefile, 'w', encoding='iso-8859-15') as f:
452 f.write(textwrap.dedent('''\
453 # coding: iso-8859-15
454 x = 'spœm'
455 if []:
456 print('unreachable')
457 '''))
458
459 def tearDown(self):
460 unlink(self.codefile)
461 unlink(self.coverfile)
462
463 def test_cover_files_written_no_highlight(self):
464 # Test also that the cover file for the trace module is not created
465 # (issue #34171).
466 tracedir = os.path.dirname(os.path.abspath(trace.__file__))
467 tracecoverpath = os.path.join(tracedir, 'trace.cover')
468 unlink(tracecoverpath)
469
470 argv = '-m trace --count'.split() + [self.codefile]
471 status, stdout, stderr = assert_python_ok(*argv)
472 self.assertEqual(stderr, b'')
473 self.assertFalse(os.path.exists(tracecoverpath))
474 self.assertTrue(os.path.exists(self.coverfile))
475 with open(self.coverfile, encoding='iso-8859-15') as f:
476 self.assertEqual(f.read(),
477 " # coding: iso-8859-15\n"
478 " 1: x = 'spœm'\n"
479 " 1: if []:\n"
480 " print('unreachable')\n"
481 )
482
483 def test_cover_files_written_with_highlight(self):
484 argv = '-m trace --count --missing'.split() + [self.codefile]
485 status, stdout, stderr = assert_python_ok(*argv)
486 self.assertTrue(os.path.exists(self.coverfile))
487 with open(self.coverfile, encoding='iso-8859-15') as f:
488 self.assertEqual(f.read(), textwrap.dedent('''\
489 # coding: iso-8859-15
490 1: x = 'spœm'
491 1: if []:
492 >>>>>> print('unreachable')
493 '''))
494
495 class ESC[4;38;5;81mTestCommandLine(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
496
497 def test_failures(self):
498 _errors = (
499 (b'progname is missing: required with the main options', '-l', '-T'),
500 (b'cannot specify both --listfuncs and (--trace or --count)', '-lc'),
501 (b'argument -R/--no-report: not allowed with argument -r/--report', '-rR'),
502 (b'must specify one of --trace, --count, --report, --listfuncs, or --trackcalls', '-g'),
503 (b'-r/--report requires -f/--file', '-r'),
504 (b'--summary can only be used with --count or --report', '-sT'),
505 (b'unrecognized arguments: -y', '-y'))
506 for message, *args in _errors:
507 *_, stderr = assert_python_failure('-m', 'trace', *args)
508 self.assertIn(message, stderr)
509
510 def test_listfuncs_flag_success(self):
511 filename = TESTFN + '.py'
512 modulename = os.path.basename(TESTFN)
513 with open(filename, 'w', encoding='utf-8') as fd:
514 self.addCleanup(unlink, filename)
515 fd.write("a = 1\n")
516 status, stdout, stderr = assert_python_ok('-m', 'trace', '-l', filename,
517 PYTHONIOENCODING='utf-8')
518 self.assertIn(b'functions called:', stdout)
519 expected = f'filename: {filename}, modulename: {modulename}, funcname: <module>'
520 self.assertIn(expected.encode(), stdout)
521
522 def test_sys_argv_list(self):
523 with open(TESTFN, 'w', encoding='utf-8') as fd:
524 self.addCleanup(unlink, TESTFN)
525 fd.write("import sys\n")
526 fd.write("print(type(sys.argv))\n")
527
528 status, direct_stdout, stderr = assert_python_ok(TESTFN)
529 status, trace_stdout, stderr = assert_python_ok('-m', 'trace', '-l', TESTFN,
530 PYTHONIOENCODING='utf-8')
531 self.assertIn(direct_stdout.strip(), trace_stdout)
532
533 def test_count_and_summary(self):
534 filename = f'{TESTFN}.py'
535 coverfilename = f'{TESTFN}.cover'
536 modulename = os.path.basename(TESTFN)
537 with open(filename, 'w', encoding='utf-8') as fd:
538 self.addCleanup(unlink, filename)
539 self.addCleanup(unlink, coverfilename)
540 fd.write(textwrap.dedent("""\
541 x = 1
542 y = 2
543
544 def f():
545 return x + y
546
547 for i in range(10):
548 f()
549 """))
550 status, stdout, _ = assert_python_ok('-m', 'trace', '-cs', filename,
551 PYTHONIOENCODING='utf-8')
552 stdout = stdout.decode()
553 self.assertEqual(status, 0)
554 self.assertIn('lines cov% module (path)', stdout)
555 self.assertIn(f'6 100% {modulename} ({filename})', stdout)
556
557 def test_run_as_module(self):
558 assert_python_ok('-m', 'trace', '-l', '--module', 'timeit', '-n', '1')
559 assert_python_failure('-m', 'trace', '-l', '--module', 'not_a_module_zzz')
560
561
562 if __name__ == '__main__':
563 unittest.main()