3 # portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
4 # err... reserved and offered to the public under the terms of the
6 # Author: Zooko O'Whielacronx
8 # mailto:zooko@zooko.com
10 # Copyright 2000, Mojam Media, Inc., all rights reserved.
11 # Author: Skip Montanaro
13 # Copyright 1999, Bioreason, Inc., all rights reserved.
14 # Author: Andrew Dalke
16 # Copyright 1995-1997, Automatrix, Inc., all rights reserved.
17 # Author: Skip Montanaro
19 # Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
22 # Permission to use, copy, modify, and distribute this Python software and
23 # its associated documentation for any purpose without fee is hereby
24 # granted, provided that the above copyright notice appears in all copies,
25 # and that both that copyright notice and this permission notice appear in
26 # supporting documentation, and that the name of neither Automatrix,
27 # Bioreason or Mojam Media be used in advertising or publicity pertaining to
28 # distribution of the software without specific, written prior permission.
30 """program/module to trace Python program or function execution
32 Sample use, command line:
33 trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
34 trace.py -t --ignore-dir '$prefix' spam.py eggs
35 trace.py --trackcalls spam.py eggs
37 Sample use, programmatically
40 # create a Trace object, telling it what to ignore, and whether to
41 # do tracing or line-counting or both.
42 tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0,
44 # run the new command using the given tracer
46 # make a report, placing output in /tmp
48 r.write_results(show_missing=True, coverdir="/tmp")
66 outfile
.write("""Usage: %s [OPTIONS] <file> [ARGS]
69 --help Display this help then exit.
70 --version Output version information then exit.
72 Otherwise, exactly one of the following three options must be given:
73 -t, --trace Print each line to sys.stdout before it is executed.
74 -c, --count Count the number of times each line is executed
75 and write the counts to <module>.cover for each
76 module executed, in the module's directory.
77 See also `--coverdir', `--file', `--no-report' below.
78 -l, --listfuncs Keep track of which functions are executed at least
79 once and write the results to sys.stdout after the
81 -T, --trackcalls Keep track of caller/called pairs and write the
82 results to sys.stdout after the program exits.
83 -r, --report Generate a report from a counts file; do not execute
84 any code. `--file' must specify the results file to
85 read, which must have been created in a previous run
86 with `--count --file=FILE'.
89 -f, --file=<file> File to accumulate counts over several runs.
90 -R, --no-report Do not generate the coverage report files.
91 Useful if you want to accumulate over several runs.
92 -C, --coverdir=<dir> Directory where the report files. The coverage
93 report for <package>.<module> is written to file
94 <dir>/<package>/<module>.cover.
95 -m, --missing Annotate executable lines that were not executed
97 -s, --summary Write a brief summary on stdout for each file.
98 (Can only be used with --count or --report.)
99 -g, --timing Prefix each line with the time since the program started.
100 Only used while tracing.
102 Filters, may be repeated multiple times:
103 --ignore-module=<mod> Ignore the given module(s) and its submodules
104 (if it is a package). Accepts comma separated
106 --ignore-dir=<dir> Ignore files in the given directory (multiple
107 directories can be joined by os.pathsep).
110 PRAGMA_NOCOVER
= "#pragma NO COVER"
112 # Simple rx to find lines with no code.
113 rx_blank
= re
.compile(r
'^\s*(#.*)?$')
116 def __init__(self
, modules
= None, dirs
= None):
117 self
._mods
= modules
or []
118 self
._dirs
= dirs
or []
120 self
._dirs
= map(os
.path
.normpath
, self
._dirs
)
121 self
._ignore
= { '<string>': 1 }
123 def names(self
, filename
, modulename
):
124 if modulename
in self
._ignore
:
125 return self
._ignore
[modulename
]
127 # haven't seen this one before, so see if the module name is
128 # on the ignore list. Need to take some care since ignoring
129 # "cmp" musn't mean ignoring "cmpcache" but ignoring
130 # "Spam" must also mean ignoring "Spam.Eggs".
131 for mod
in self
._mods
:
132 if mod
== modulename
: # Identical names, so ignore
133 self
._ignore
[modulename
] = 1
135 # check if the module is a proper submodule of something on
138 # (will not overflow since if the first n characters are the
139 # same and the name has not already occurred, then the size
140 # of "name" is greater than that of "mod")
141 if mod
== modulename
[:n
] and modulename
[n
] == '.':
142 self
._ignore
[modulename
] = 1
145 # Now check that __file__ isn't in one of the directories
147 # must be a built-in, so we must ignore
148 self
._ignore
[modulename
] = 1
151 # Ignore a file when it contains one of the ignorable paths
153 # The '+ os.sep' is to ensure that d is a parent directory,
154 # as compared to cases like:
156 # filename = "/usr/local.py"
158 # d = "/usr/local.py"
159 # filename = "/usr/local.py"
160 if filename
.startswith(d
+ os
.sep
):
161 self
._ignore
[modulename
] = 1
164 # Tried the different ways, so we don't ignore this module
165 self
._ignore
[modulename
] = 0
169 """Return a plausible module name for the patch."""
171 base
= os
.path
.basename(path
)
172 filename
, ext
= os
.path
.splitext(base
)
175 def fullmodname(path
):
176 """Return a plausible module name for the path."""
178 # If the file 'path' is part of a package, then the filename isn't
179 # enough to uniquely identify it. Try to do the right thing by
180 # looking in sys.path for the longest matching prefix. We'll
181 # assume that the rest is the package name.
183 comparepath
= os
.path
.normcase(path
)
186 dir = os
.path
.normcase(dir)
187 if comparepath
.startswith(dir) and comparepath
[len(dir)] == os
.sep
:
188 if len(dir) > len(longest
):
192 base
= path
[len(longest
) + 1:]
195 base
= base
.replace(os
.sep
, ".")
197 base
= base
.replace(os
.altsep
, ".")
198 filename
, ext
= os
.path
.splitext(base
)
201 class CoverageResults
:
202 def __init__(self
, counts
=None, calledfuncs
=None, infile
=None,
203 callers
=None, outfile
=None):
205 if self
.counts
is None:
207 self
.counter
= self
.counts
.copy() # map (filename, lineno) to count
208 self
.calledfuncs
= calledfuncs
209 if self
.calledfuncs
is None:
210 self
.calledfuncs
= {}
211 self
.calledfuncs
= self
.calledfuncs
.copy()
212 self
.callers
= callers
213 if self
.callers
is None:
215 self
.callers
= self
.callers
.copy()
217 self
.outfile
= outfile
219 # Try to merge existing counts file.
221 counts
, calledfuncs
, callers
= \
222 pickle
.load(open(self
.infile
, 'rb'))
223 self
.update(self
.__class
__(counts
, calledfuncs
, callers
))
224 except (IOError, EOFError, ValueError) as err
:
225 print(("Skipping counts file %r: %s"
226 % (self
.infile
, err
)), file=sys
.stderr
)
228 def is_ignored_filename(self
, filename
):
229 """Return True if the filename does not refer to a file
230 we want to have reported.
232 return (filename
== "<string>" or
233 filename
.startswith("<doctest "))
235 def update(self
, other
):
236 """Merge in the data from another CoverageResults"""
238 calledfuncs
= self
.calledfuncs
239 callers
= self
.callers
240 other_counts
= other
.counts
241 other_calledfuncs
= other
.calledfuncs
242 other_callers
= other
.callers
244 for key
in other_counts
.keys():
245 counts
[key
] = counts
.get(key
, 0) + other_counts
[key
]
247 for key
in other_calledfuncs
.keys():
250 for key
in other_callers
.keys():
253 def write_results(self
, show_missing
=True, summary
=False, coverdir
=None):
259 print("functions called:")
260 for filename
, modulename
, funcname
in sorted(calls
.keys()):
261 print(("filename: %s, modulename: %s, funcname: %s"
262 % (filename
, modulename
, funcname
)))
266 print("calling relationships:")
267 lastfile
= lastcfile
= ""
268 for ((pfile
, pmod
, pfunc
), (cfile
, cmod
, cfunc
)) \
269 in sorted(self
.callers
.keys()):
270 if pfile
!= lastfile
:
272 print("***", pfile
, "***")
275 if cfile
!= pfile
and lastcfile
!= cfile
:
278 print(" %s.%s -> %s.%s" % (pmod
, pfunc
, cmod
, cfunc
))
280 # turn the counts data ("(filename, lineno) = count") into something
281 # accessible on a per-file basis
283 for filename
, lineno
in self
.counts
.keys():
284 lines_hit
= per_file
[filename
] = per_file
.get(filename
, {})
285 lines_hit
[lineno
] = self
.counts
[(filename
, lineno
)]
287 # accumulate summary info, if needed
290 for filename
, count
in per_file
.items():
291 if self
.is_ignored_filename(filename
):
294 if filename
.endswith((".pyc", ".pyo")):
295 filename
= filename
[:-1]
298 dir = os
.path
.dirname(os
.path
.abspath(filename
))
299 modulename
= modname(filename
)
302 if not os
.path
.exists(dir):
304 modulename
= fullmodname(filename
)
306 # If desired, get a list of the line numbers which represent
307 # executable content (returned as a dict for better lookup speed)
309 lnotab
= find_executable_linenos(filename
)
313 source
= linecache
.getlines(filename
)
314 coverpath
= os
.path
.join(dir, modulename
+ ".cover")
315 n_hits
, n_lines
= self
.write_results_file(coverpath
, source
,
318 if summary
and n_lines
:
319 percent
= int(100 * n_hits
/ n_lines
)
320 sums
[modulename
] = n_lines
, percent
, modulename
, filename
323 print("lines cov% module (path)")
324 for m
in sorted(sums
.keys()):
325 n_lines
, percent
, modulename
, filename
= sums
[m
]
326 print("%5d %3d%% %s (%s)" % sums
[m
])
329 # try and store counts and module info into self.outfile
331 pickle
.dump((self
.counts
, self
.calledfuncs
, self
.callers
),
332 open(self
.outfile
, 'wb'), 1)
333 except IOError as err
:
334 print("Can't save counts files because %s" % err
, file=sys
.stderr
)
336 def write_results_file(self
, path
, lines
, lnotab
, lines_hit
):
337 """Return a coverage results file in path."""
340 outfile
= open(path
, "w")
341 except IOError as err
:
342 print(("trace: Could not open %r for writing: %s"
343 "- skipping" % (path
, err
)), file=sys
.stderr
)
348 for i
, line
in enumerate(lines
):
350 # do the blank/comment match to try to mark more lines
351 # (help the reader find stuff that hasn't been covered)
352 if lineno
in lines_hit
:
353 outfile
.write("%5d: " % lines_hit
[lineno
])
356 elif rx_blank
.match(line
):
359 # lines preceded by no marks weren't hit
360 # Highlight them if so indicated, unless the line contains
362 if lineno
in lnotab
and not PRAGMA_NOCOVER
in lines
[i
]:
363 outfile
.write(">>>>>> ")
367 outfile
.write(lines
[i
].expandtabs(8))
370 return n_hits
, n_lines
372 def find_lines_from_code(code
, strs
):
373 """Return dict where keys are lines in the line number table."""
376 line_increments
= code
.co_lnotab
[1::2]
377 table_length
= len(line_increments
)
380 lineno
= code
.co_firstlineno
381 for li
in line_increments
:
383 if lineno
not in strs
:
388 def find_lines(code
, strs
):
389 """Return lineno dict for all code objects reachable from code."""
390 # get all of the lineno information from the code of this scope level
391 linenos
= find_lines_from_code(code
, strs
)
393 # and check the constants for references to other code objects
394 for c
in code
.co_consts
:
395 if isinstance(c
, types
.CodeType
):
396 # find another code object, so recurse into it
397 linenos
.update(find_lines(c
, strs
))
400 def find_strings(filename
, encoding
=None):
401 """Return a dict of possible docstring positions.
403 The dict maps line numbers to strings. There is an entry for
404 line that contains only a string or a part of a triple-quoted
408 # If the first token is a string, then it's the module docstring.
409 # Add this special case so that the test in the loop passes.
410 prev_ttype
= token
.INDENT
411 f
= open(filename
, encoding
=encoding
)
412 for ttype
, tstr
, start
, end
, line
in tokenize
.generate_tokens(f
.readline
):
413 if ttype
== token
.STRING
:
414 if prev_ttype
== token
.INDENT
:
417 for i
in range(sline
, eline
+ 1):
423 def find_executable_linenos(filename
):
424 """Return dict where keys are line numbers in the line number table."""
426 with io
.FileIO(filename
, 'r') as file:
427 encoding
, lines
= tokenize
.detect_encoding(file.readline
)
428 prog
= open(filename
, "r", encoding
=encoding
).read()
429 except IOError as err
:
430 print(("Not printing coverage data for %r: %s"
431 % (filename
, err
)), file=sys
.stderr
)
433 code
= compile(prog
, filename
, "exec")
434 strs
= find_strings(filename
, encoding
)
435 return find_lines(code
, strs
)
438 def __init__(self
, count
=1, trace
=1, countfuncs
=0, countcallers
=0,
439 ignoremods
=(), ignoredirs
=(), infile
=None, outfile
=None,
442 @param count true iff it should count number of times each
444 @param trace true iff it should print out each line that is
446 @param countfuncs true iff it should just output a list of
447 (filename, modulename, funcname,) for functions
448 that were called at least once; This overrides
450 @param ignoremods a list of the names of modules to ignore
451 @param ignoredirs a list of the names of directories to ignore
452 all of the (recursive) contents of
453 @param infile file from which to read stored counts to be
454 added into the results
455 @param outfile file in which to write the results
456 @param timing true iff timing information be displayed
459 self
.outfile
= outfile
460 self
.ignore
= Ignore(ignoremods
, ignoredirs
)
461 self
.counts
= {} # keys are (filename, linenumber)
462 self
.blabbed
= {} # for debugging
463 self
.pathtobasename
= {} # for memoizing os.path.basename
466 self
._calledfuncs
= {}
468 self
._caller
_cache
= {}
469 self
.start_time
= None
471 self
.start_time
= time
.time()
473 self
.globaltrace
= self
.globaltrace_trackcallers
475 self
.globaltrace
= self
.globaltrace_countfuncs
476 elif trace
and count
:
477 self
.globaltrace
= self
.globaltrace_lt
478 self
.localtrace
= self
.localtrace_trace_and_count
480 self
.globaltrace
= self
.globaltrace_lt
481 self
.localtrace
= self
.localtrace_trace
483 self
.globaltrace
= self
.globaltrace_lt
484 self
.localtrace
= self
.localtrace_count
486 # Ahem -- do nothing? Okay.
491 dict = __main__
.__dict
__
492 if not self
.donothing
:
493 sys
.settrace(self
.globaltrace
)
494 threading
.settrace(self
.globaltrace
)
496 exec(cmd
, dict, dict)
498 if not self
.donothing
:
500 threading
.settrace(None)
502 def runctx(self
, cmd
, globals=None, locals=None):
503 if globals is None: globals = {}
504 if locals is None: locals = {}
505 if not self
.donothing
:
506 sys
.settrace(self
.globaltrace
)
507 threading
.settrace(self
.globaltrace
)
509 exec(cmd
, globals, locals)
511 if not self
.donothing
:
513 threading
.settrace(None)
515 def runfunc(self
, func
, *args
, **kw
):
517 if not self
.donothing
:
518 sys
.settrace(self
.globaltrace
)
520 result
= func(*args
, **kw
)
522 if not self
.donothing
:
526 def file_module_function_of(self
, frame
):
528 filename
= code
.co_filename
530 modulename
= modname(filename
)
534 funcname
= code
.co_name
536 if code
in self
._caller
_cache
:
537 if self
._caller
_cache
[code
] is not None:
538 clsname
= self
._caller
_cache
[code
]
540 self
._caller
_cache
[code
] = None
541 ## use of gc.get_referrers() was suggested by Michael Hudson
542 # all functions which refer to this code object
543 funcs
= [f
for f
in gc
.get_referrers(code
)
544 if hasattr(f
, "__doc__")]
545 # require len(func) == 1 to avoid ambiguity caused by calls to
546 # new.function(): "In the face of ambiguity, refuse the
547 # temptation to guess."
549 dicts
= [d
for d
in gc
.get_referrers(funcs
[0])
550 if isinstance(d
, dict)]
552 classes
= [c
for c
in gc
.get_referrers(dicts
[0])
553 if hasattr(c
, "__bases__")]
554 if len(classes
) == 1:
555 # ditto for new.classobj()
556 clsname
= str(classes
[0])
557 # cache the result - assumption is that new.* is
558 # not called later to disturb this relationship
559 # _caller_cache could be flushed if functions in
560 # the new module get called.
561 self
._caller
_cache
[code
] = clsname
562 if clsname
is not None:
563 # final hack - module name shows up in str(cls), but we've already
564 # computed module name, so remove it
565 clsname
= clsname
.split(".")[1:]
566 clsname
= ".".join(clsname
)
567 funcname
= "%s.%s" % (clsname
, funcname
)
569 return filename
, modulename
, funcname
571 def globaltrace_trackcallers(self
, frame
, why
, arg
):
572 """Handler for call events.
574 Adds information about who called who to the self._callers dict.
577 # XXX Should do a better job of identifying methods
578 this_func
= self
.file_module_function_of(frame
)
579 parent_func
= self
.file_module_function_of(frame
.f_back
)
580 self
._callers
[(parent_func
, this_func
)] = 1
582 def globaltrace_countfuncs(self
, frame
, why
, arg
):
583 """Handler for call events.
585 Adds (filename, modulename, funcname) to the self._calledfuncs dict.
588 this_func
= self
.file_module_function_of(frame
)
589 self
._calledfuncs
[this_func
] = 1
591 def globaltrace_lt(self
, frame
, why
, arg
):
592 """Handler for call events.
594 If the code block being entered is to be ignored, returns `None',
595 else returns self.localtrace.
599 filename
= frame
.f_globals
.get('__file__', None)
601 # XXX modname() doesn't work right for packages, so
602 # the ignore support won't work right for packages
603 modulename
= modname(filename
)
604 if modulename
is not None:
605 ignore_it
= self
.ignore
.names(filename
, modulename
)
608 print((" --- modulename: %s, funcname: %s"
609 % (modulename
, code
.co_name
)))
610 return self
.localtrace
614 def localtrace_trace_and_count(self
, frame
, why
, arg
):
616 # record the file name and line number of every trace
617 filename
= frame
.f_code
.co_filename
618 lineno
= frame
.f_lineno
619 key
= filename
, lineno
620 self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
623 print('%.2f' % (time
.time() - self
.start_time
), end
=' ')
624 bname
= os
.path
.basename(filename
)
625 print("%s(%d): %s" % (bname
, lineno
,
626 linecache
.getline(filename
, lineno
)), end
=' ')
627 return self
.localtrace
629 def localtrace_trace(self
, frame
, why
, arg
):
631 # record the file name and line number of every trace
632 filename
= frame
.f_code
.co_filename
633 lineno
= frame
.f_lineno
636 print('%.2f' % (time
.time() - self
.start_time
), end
=' ')
637 bname
= os
.path
.basename(filename
)
638 print("%s(%d): %s" % (bname
, lineno
,
639 linecache
.getline(filename
, lineno
)), end
=' ')
640 return self
.localtrace
642 def localtrace_count(self
, frame
, why
, arg
):
644 filename
= frame
.f_code
.co_filename
645 lineno
= frame
.f_lineno
646 key
= filename
, lineno
647 self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
648 return self
.localtrace
651 return CoverageResults(self
.counts
, infile
=self
.infile
,
652 outfile
=self
.outfile
,
653 calledfuncs
=self
._calledfuncs
,
654 callers
=self
._callers
)
657 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
666 opts
, prog_argv
= getopt
.getopt(argv
[1:], "tcrRf:d:msC:lTg",
667 ["help", "version", "trace", "count",
668 "report", "no-report", "summary",
670 "ignore-module=", "ignore-dir=",
671 "coverdir=", "listfuncs",
672 "trackcalls", "timing"])
674 except getopt
.error
as msg
:
675 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
676 sys
.stderr
.write("Try `%s --help' for more information\n"
694 for opt
, val
in opts
:
699 if opt
== "--version":
700 sys
.stdout
.write("trace 2.0\n")
703 if opt
== "-T" or opt
== "--trackcalls":
707 if opt
== "-l" or opt
== "--listfuncs":
711 if opt
== "-g" or opt
== "--timing":
715 if opt
== "-t" or opt
== "--trace":
719 if opt
== "-c" or opt
== "--count":
723 if opt
== "-r" or opt
== "--report":
727 if opt
== "-R" or opt
== "--no-report":
731 if opt
== "-f" or opt
== "--file":
735 if opt
== "-m" or opt
== "--missing":
739 if opt
== "-C" or opt
== "--coverdir":
743 if opt
== "-s" or opt
== "--summary":
747 if opt
== "--ignore-module":
748 for mod
in val
.split(","):
749 ignore_modules
.append(mod
.strip())
752 if opt
== "--ignore-dir":
753 for s
in val
.split(os
.pathsep
):
754 s
= os
.path
.expandvars(s
)
755 # should I also call expanduser? (after all, could use $HOME)
757 s
= s
.replace("$prefix",
758 os
.path
.join(sys
.prefix
, "lib",
759 "python" + sys
.version
[:3]))
760 s
= s
.replace("$exec_prefix",
761 os
.path
.join(sys
.exec_prefix
, "lib",
762 "python" + sys
.version
[:3]))
763 s
= os
.path
.normpath(s
)
764 ignore_dirs
.append(s
)
767 assert 0, "Should never get here"
769 if listfuncs
and (count
or trace
):
770 _err_exit("cannot specify both --listfuncs and (--trace or --count)")
772 if not (count
or trace
or report
or listfuncs
or countcallers
):
773 _err_exit("must specify one of --trace, --count, --report, "
774 "--listfuncs, or --trackcalls")
776 if report
and no_report
:
777 _err_exit("cannot specify both --report and --no-report")
779 if report
and not counts_file
:
780 _err_exit("--report requires a --file")
782 if no_report
and len(prog_argv
) == 0:
783 _err_exit("missing name of file to run")
785 # everything is ready
787 results
= CoverageResults(infile
=counts_file
, outfile
=counts_file
)
788 results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)
791 progname
= prog_argv
[0]
792 sys
.path
[0] = os
.path
.split(progname
)[0]
794 t
= Trace(count
, trace
, countfuncs
=listfuncs
,
795 countcallers
=countcallers
, ignoremods
=ignore_modules
,
796 ignoredirs
=ignore_dirs
, infile
=counts_file
,
797 outfile
=counts_file
, timing
=timing
)
804 t
.run('exec(%r)' % (script
,))
805 except IOError as err
:
806 _err_exit("Cannot run file %r because: %s" % (sys
.argv
[0], err
))
810 results
= t
.results()
813 results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)
815 if __name__
=='__main__':