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
36 Sample use, programmatically
37 # create a Trace object, telling it what to ignore, and whether to
38 # do tracing or line-counting or both.
39 trace = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0,
41 # run the new command using the given trace
42 trace.run(coverage.globaltrace, 'main()')
43 # make a report, telling it where you want output
45 r.write_results(show_missing=True)
64 outfile
.write("""Usage: %s [OPTIONS] <file> [ARGS]
67 --help Display this help then exit.
68 --version Output version information then exit.
70 Otherwise, exactly one of the following three options must be given:
71 -t, --trace Print each line to sys.stdout before it is executed.
72 -c, --count Count the number of times each line is executed
73 and write the counts to <module>.cover for each
74 module executed, in the module's directory.
75 See also `--coverdir', `--file', `--no-report' below.
76 -r, --report Generate a report from a counts file; do not execute
77 any code. `--file' must specify the results file to
78 read, which must have been created in a previous run
79 with `--count --file=FILE'.
82 -f, --file=<file> File to accumulate counts over several runs.
83 -R, --no-report Do not generate the coverage report files.
84 Useful if you want to accumulate over several runs.
85 -C, --coverdir=<dir> Directory where the report files. The coverage
86 report for <package>.<module> is written to file
87 <dir>/<package>/<module>.cover.
88 -m, --missing Annotate executable lines that were not executed
90 -s, --summary Write a brief summary on stdout for each file.
91 (Can only be used with --count or --report.)
93 Filters, may be repeated multiple times:
94 --ignore-module=<mod> Ignore the given module and its submodules
96 --ignore-dir=<dir> Ignore files in the given directory (multiple
97 directories can be joined by os.pathsep).
100 PRAGMA_NOCOVER
= "#pragma NO COVER"
102 # Simple rx to find lines with no code.
103 rx_blank
= re
.compile(r
'^\s*(#.*)?$')
106 def __init__(self
, modules
= None, dirs
= None):
107 self
._mods
= modules
or []
108 self
._dirs
= dirs
or []
110 self
._dirs
= map(os
.path
.normpath
, self
._dirs
)
111 self
._ignore
= { '<string>': 1 }
113 def names(self
, filename
, modulename
):
114 if self
._ignore
.has_key(modulename
):
115 return self
._ignore
[modulename
]
117 # haven't seen this one before, so see if the module name is
118 # on the ignore list. Need to take some care since ignoring
119 # "cmp" musn't mean ignoring "cmpcache" but ignoring
120 # "Spam" must also mean ignoring "Spam.Eggs".
121 for mod
in self
._mods
:
122 if mod
== modulename
: # Identical names, so ignore
123 self
._ignore
[modulename
] = 1
125 # check if the module is a proper submodule of something on
128 # (will not overflow since if the first n characters are the
129 # same and the name has not already occured, then the size
130 # of "name" is greater than that of "mod")
131 if mod
== modulename
[:n
] and modulename
[n
] == '.':
132 self
._ignore
[modulename
] = 1
135 # Now check that __file__ isn't in one of the directories
137 # must be a built-in, so we must ignore
138 self
._ignore
[modulename
] = 1
141 # Ignore a file when it contains one of the ignorable paths
143 # The '+ os.sep' is to ensure that d is a parent directory,
144 # as compared to cases like:
146 # filename = "/usr/local.py"
148 # d = "/usr/local.py"
149 # filename = "/usr/local.py"
150 if filename
.startswith(d
+ os
.sep
):
151 self
._ignore
[modulename
] = 1
154 # Tried the different ways, so we don't ignore this module
155 self
._ignore
[modulename
] = 0
159 """Return a plausible module name for the patch."""
161 base
= os
.path
.basename(path
)
162 filename
, ext
= os
.path
.splitext(base
)
165 def fullmodname(path
):
166 """Return a plausible module name for the path."""
168 # If the file 'path' is part of a package, then the filename isn't
169 # enough to uniquely identify it. Try to do the right thing by
170 # looking in sys.path for the longest matching prefix. We'll
171 # assume that the rest is the package name.
175 if path
.startswith(dir) and path
[len(dir)] == os
.path
.sep
:
176 if len(dir) > len(longest
):
179 base
= path
[len(longest
) + 1:].replace("/", ".")
180 filename
, ext
= os
.path
.splitext(base
)
183 class CoverageResults
:
184 def __init__(self
, counts
=None, calledfuncs
=None, infile
=None,
187 if self
.counts
is None:
189 self
.counter
= self
.counts
.copy() # map (filename, lineno) to count
190 self
.calledfuncs
= calledfuncs
191 if self
.calledfuncs
is None:
192 self
.calledfuncs
= {}
193 self
.calledfuncs
= self
.calledfuncs
.copy()
195 self
.outfile
= outfile
197 # Try and merge existing counts file.
198 # This code understand a couple of old trace.py formats.
200 thingie
= pickle
.load(open(self
.infile
, 'r'))
201 if isinstance(thingie
, dict):
202 self
.update(self
.__class
__(thingie
))
203 elif isinstance(thingie
, tuple) and len(thingie
) == 2:
204 counts
, calledfuncs
= thingie
205 self
.update(self
.__class
__(counts
, calledfuncs
))
206 except (IOError, EOFError), err
:
207 print >> sys
.stderr
, ("Skipping counts file %r: %s"
208 % (self
.infile
, err
))
209 except pickle
.UnpicklingError
:
210 self
.update(self
.__class
__(marshal
.load(open(self
.infile
))))
212 def update(self
, other
):
213 """Merge in the data from another CoverageResults"""
215 calledfuncs
= self
.calledfuncs
216 other_counts
= other
.counts
217 other_calledfuncs
= other
.calledfuncs
219 for key
in other_counts
.keys():
220 counts
[key
] = counts
.get(key
, 0) + other_counts
[key
]
222 for key
in other_calledfuncs
.keys():
225 def write_results(self
, show_missing
=True, summary
=False, coverdir
=None):
229 for filename
, modulename
, funcname
in self
.calledfuncs
.keys():
230 print ("filename: %s, modulename: %s, funcname: %s"
231 % (filename
, modulename
, funcname
))
233 # turn the counts data ("(filename, lineno) = count") into something
234 # accessible on a per-file basis
236 for filename
, lineno
in self
.counts
.keys():
237 lines_hit
= per_file
[filename
] = per_file
.get(filename
, {})
238 lines_hit
[lineno
] = self
.counts
[(filename
, lineno
)]
240 # accumulate summary info, if needed
243 for filename
, count
in per_file
.iteritems():
244 # skip some "files" we don't care about...
245 if filename
== "<string>":
248 if filename
.endswith(".pyc") or filename
.endswith(".pyo"):
249 filename
= filename
[:-1]
252 dir = os
.path
.dirname(os
.path
.abspath(filename
))
253 modulename
= modname(filename
)
256 if not os
.path
.exists(dir):
258 modulename
= fullmodname(filename
)
260 # If desired, get a list of the line numbers which represent
261 # executable content (returned as a dict for better lookup speed)
263 lnotab
= find_executable_linenos(filename
)
267 source
= linecache
.getlines(filename
)
268 coverpath
= os
.path
.join(dir, modulename
+ ".cover")
269 n_hits
, n_lines
= self
.write_results_file(coverpath
, source
,
272 if summary
and n_lines
:
273 percent
= int(100 * n_hits
/ n_lines
)
274 sums
[modulename
] = n_lines
, percent
, modulename
, filename
279 print "lines cov% module (path)"
281 n_lines
, percent
, modulename
, filename
= sums
[m
]
282 print "%5d %3d%% %s (%s)" % sums
[m
]
285 # try and store counts and module info into self.outfile
287 pickle
.dump((self
.counts
, self
.calledfuncs
),
288 open(self
.outfile
, 'w'), 1)
290 print >> sys
.stderr
, "Can't save counts files because %s" % err
292 def write_results_file(self
, path
, lines
, lnotab
, lines_hit
):
293 """Return a coverage results file in path."""
296 outfile
= open(path
, "w")
298 print >> sys
.stderr
, ("trace: Could not open %r for writing: %s"
299 "- skipping" % (path
, err
))
304 for i
, line
in enumerate(lines
):
306 # do the blank/comment match to try to mark more lines
307 # (help the reader find stuff that hasn't been covered)
308 if lineno
in lines_hit
:
309 outfile
.write("%5d: " % lines_hit
[lineno
])
312 elif rx_blank
.match(line
):
315 # lines preceded by no marks weren't hit
316 # Highlight them if so indicated, unless the line contains
318 if lineno
in lnotab
and not PRAGMA_NOCOVER
in lines
[i
]:
319 outfile
.write(">>>>>> ")
323 outfile
.write(lines
[i
].expandtabs(8))
326 return n_hits
, n_lines
328 def find_lines_from_code(code
, strs
):
329 """Return dict where keys are lines in the line number table."""
332 line_increments
= [ord(c
) for c
in code
.co_lnotab
[1::2]]
333 table_length
= len(line_increments
)
336 lineno
= code
.co_firstlineno
337 for li
in line_increments
:
339 if lineno
not in strs
:
344 def find_lines(code
, strs
):
345 """Return lineno dict for all code objects reachable from code."""
346 # get all of the lineno information from the code of this scope level
347 linenos
= find_lines_from_code(code
, strs
)
349 # and check the constants for references to other code objects
350 for c
in code
.co_consts
:
351 if isinstance(c
, types
.CodeType
):
352 # find another code object, so recurse into it
353 linenos
.update(find_lines(c
, strs
))
356 def find_strings(filename
):
357 """Return a dict of possible docstring positions.
359 The dict maps line numbers to strings. There is an entry for
360 line that contains only a string or a part of a triple-quoted
364 # If the first token is a string, then it's the module docstring.
365 # Add this special case so that the test in the loop passes.
366 prev_ttype
= token
.INDENT
368 for ttype
, tstr
, start
, end
, line
in tokenize
.generate_tokens(f
.readline
):
369 if ttype
== token
.STRING
:
370 if prev_ttype
== token
.INDENT
:
373 for i
in range(sline
, eline
+ 1):
379 def find_executable_linenos(filename
):
380 """Return dict where keys are line numbers in the line number table."""
381 assert filename
.endswith('.py')
383 prog
= open(filename
).read()
385 print >> sys
.stderr
, ("Not printing coverage data for %r: %s"
388 code
= compile(prog
, filename
, "exec")
389 strs
= find_strings(filename
)
390 return find_lines(code
, strs
)
393 def __init__(self
, count
=1, trace
=1, countfuncs
=0, ignoremods
=(),
394 ignoredirs
=(), infile
=None, outfile
=None):
396 @param count true iff it should count number of times each
398 @param trace true iff it should print out each line that is
400 @param countfuncs true iff it should just output a list of
401 (filename, modulename, funcname,) for functions
402 that were called at least once; This overrides
404 @param ignoremods a list of the names of modules to ignore
405 @param ignoredirs a list of the names of directories to ignore
406 all of the (recursive) contents of
407 @param infile file from which to read stored counts to be
408 added into the results
409 @param outfile file in which to write the results
412 self
.outfile
= outfile
413 self
.ignore
= Ignore(ignoremods
, ignoredirs
)
414 self
.counts
= {} # keys are (filename, linenumber)
415 self
.blabbed
= {} # for debugging
416 self
.pathtobasename
= {} # for memoizing os.path.basename
419 self
._calledfuncs
= {}
421 self
.globaltrace
= self
.globaltrace_countfuncs
422 elif trace
and count
:
423 self
.globaltrace
= self
.globaltrace_lt
424 self
.localtrace
= self
.localtrace_trace_and_count
426 self
.globaltrace
= self
.globaltrace_lt
427 self
.localtrace
= self
.localtrace_trace
429 self
.globaltrace
= self
.globaltrace_lt
430 self
.localtrace
= self
.localtrace_count
432 # Ahem -- do nothing? Okay.
437 dict = __main__
.__dict
__
438 if not self
.donothing
:
439 sys
.settrace(self
.globaltrace
)
441 exec cmd
in dict, dict
443 if not self
.donothing
:
446 def runctx(self
, cmd
, globals=None, locals=None):
447 if globals is None: globals = {}
448 if locals is None: locals = {}
449 if not self
.donothing
:
450 sys
.settrace(self
.globaltrace
)
452 exec cmd
in globals, locals
454 if not self
.donothing
:
457 def runfunc(self
, func
, *args
, **kw
):
459 if not self
.donothing
:
460 sys
.settrace(self
.globaltrace
)
462 result
= func(*args
, **kw
)
464 if not self
.donothing
:
468 def globaltrace_countfuncs(self
, frame
, why
, arg
):
469 """Handler for call events.
471 Adds (filename, modulename, funcname) to the self._calledfuncs dict.
475 filename
= code
.co_filename
476 funcname
= code
.co_name
478 modulename
= modname(filename
)
481 self
._calledfuncs
[(filename
, modulename
, funcname
)] = 1
483 def globaltrace_lt(self
, frame
, why
, arg
):
484 """Handler for call events.
486 If the code block being entered is to be ignored, returns `None',
487 else returns self.localtrace.
491 filename
= code
.co_filename
493 # XXX modname() doesn't work right for packages, so
494 # the ignore support won't work right for packages
495 modulename
= modname(filename
)
496 if modulename
is not None:
497 ignore_it
= self
.ignore
.names(filename
, modulename
)
500 print (" --- modulename: %s, funcname: %s"
501 % (modulename
, code
.co_name
))
502 return self
.localtrace
506 def localtrace_trace_and_count(self
, frame
, why
, arg
):
508 # record the file name and line number of every trace
509 filename
= frame
.f_code
.co_filename
510 lineno
= frame
.f_lineno
511 key
= filename
, lineno
512 self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
514 bname
= os
.path
.basename(filename
)
515 print "%s(%d): %s" % (bname
, lineno
,
516 linecache
.getline(filename
, lineno
)),
517 return self
.localtrace
519 def localtrace_trace(self
, frame
, why
, arg
):
521 # record the file name and line number of every trace
522 filename
= frame
.f_code
.co_filename
523 lineno
= frame
.f_lineno
525 bname
= os
.path
.basename(filename
)
526 print "%s(%d): %s" % (bname
, lineno
,
527 linecache
.getline(filename
, lineno
)),
528 return self
.localtrace
530 def localtrace_count(self
, frame
, why
, arg
):
532 filename
= frame
.f_code
.co_filename
533 lineno
= frame
.f_lineno
534 key
= filename
, lineno
535 self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
536 return self
.localtrace
539 return CoverageResults(self
.counts
, infile
=self
.infile
,
540 outfile
=self
.outfile
,
541 calledfuncs
=self
._calledfuncs
)
544 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
553 opts
, prog_argv
= getopt
.getopt(argv
[1:], "tcrRf:d:msC:l",
554 ["help", "version", "trace", "count",
555 "report", "no-report", "summary",
557 "ignore-module=", "ignore-dir=",
558 "coverdir=", "listfuncs",])
560 except getopt
.error
, msg
:
561 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
562 sys
.stderr
.write("Try `%s --help' for more information\n"
578 for opt
, val
in opts
:
583 if opt
== "--version":
584 sys
.stdout
.write("trace 2.0\n")
587 if opt
== "-l" or opt
== "--listfuncs":
591 if opt
== "-t" or opt
== "--trace":
595 if opt
== "-c" or opt
== "--count":
599 if opt
== "-r" or opt
== "--report":
603 if opt
== "-R" or opt
== "--no-report":
607 if opt
== "-f" or opt
== "--file":
611 if opt
== "-m" or opt
== "--missing":
615 if opt
== "-C" or opt
== "--coverdir":
619 if opt
== "-s" or opt
== "--summary":
623 if opt
== "--ignore-module":
624 ignore_modules
.append(val
)
627 if opt
== "--ignore-dir":
628 for s
in val
.split(os
.pathsep
):
629 s
= os
.path
.expandvars(s
)
630 # should I also call expanduser? (after all, could use $HOME)
632 s
= s
.replace("$prefix",
633 os
.path
.join(sys
.prefix
, "lib",
634 "python" + sys
.version
[:3]))
635 s
= s
.replace("$exec_prefix",
636 os
.path
.join(sys
.exec_prefix
, "lib",
637 "python" + sys
.version
[:3]))
638 s
= os
.path
.normpath(s
)
639 ignore_dirs
.append(s
)
642 assert 0, "Should never get here"
644 if listfuncs
and (count
or trace
):
645 _err_exit("cannot specify both --listfuncs and (--trace or --count)")
647 if not count
and not trace
and not report
and not listfuncs
:
648 _err_exit("must specify one of --trace, --count, --report or "
651 if report
and no_report
:
652 _err_exit("cannot specify both --report and --no-report")
654 if report
and not counts_file
:
655 _err_exit("--report requires a --file")
657 if no_report
and len(prog_argv
) == 0:
658 _err_exit("missing name of file to run")
660 # everything is ready
662 results
= CoverageResults(infile
=counts_file
, outfile
=counts_file
)
663 results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)
666 progname
= prog_argv
[0]
667 sys
.path
[0] = os
.path
.split(progname
)[0]
669 t
= Trace(count
, trace
, countfuncs
=listfuncs
,
670 ignoremods
=ignore_modules
, ignoredirs
=ignore_dirs
,
671 infile
=counts_file
, outfile
=counts_file
)
673 t
.run('execfile(' + `progname`
+ ')')
675 _err_exit("Cannot run file %r because: %s" % (sys
.argv
[0], err
))
679 results
= t
.results()
682 results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)
684 if __name__
=='__main__':