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=1)
48 import sys
, os
, tempfile
, types
, copy
, operator
, inspect
, exceptions
, marshal
55 # DEBUG_MODE=1 # make this true to get printouts which help you understand what's going on
58 outfile
.write("""Usage: %s [OPTIONS] <file> [ARGS]
61 --help Display this help then exit.
62 --version Output version information then exit.
64 Otherwise, exactly one of the following three options must be given:
65 -t, --trace Print each line to sys.stdout before it is executed.
66 -c, --count Count the number of times each line is executed
67 and write the counts to <module>.cover for each
68 module executed, in the module's directory.
69 See also `--coverdir', `--file', `--no-report' below.
70 -r, --report Generate a report from a counts file; do not execute
71 any code. `--file' must specify the results file to
72 read, which must have been created in a previous run
73 with `--count --file=FILE'.
76 -f, --file=<file> File to accumulate counts over several runs.
77 -R, --no-report Do not generate the coverage report files.
78 Useful if you want to accumulate over several runs.
79 -C, --coverdir=<dir> Directory where the report files. The coverage
80 report for <package>.<module> is written to file
81 <dir>/<package>/<module>.cover.
82 -m, --missing Annotate executable lines that were not executed
84 -s, --summary Write a brief summary on stdout for each file.
85 (Can only be used with --count or --report.)
87 Filters, may be repeated multiple times:
88 --ignore-module=<mod> Ignore the given module and its submodules
90 --ignore-dir=<dir> Ignore files in the given directory (multiple
91 directories can be joined by os.pathsep).
95 def __init__(self
, modules
= None, dirs
= None):
96 self
._mods
= modules
or []
97 self
._dirs
= dirs
or []
99 self
._dirs
= map(os
.path
.normpath
, self
._dirs
)
100 self
._ignore
= { '<string>': 1 }
102 def names(self
, filename
, modulename
):
103 if self
._ignore
.has_key(modulename
):
104 return self
._ignore
[modulename
]
106 # haven't seen this one before, so see if the module name is
107 # on the ignore list. Need to take some care since ignoring
108 # "cmp" musn't mean ignoring "cmpcache" but ignoring
109 # "Spam" must also mean ignoring "Spam.Eggs".
110 for mod
in self
._mods
:
111 if mod
== modulename
: # Identical names, so ignore
112 self
._ignore
[modulename
] = 1
114 # check if the module is a proper submodule of something on
117 # (will not overflow since if the first n characters are the
118 # same and the name has not already occured, then the size
119 # of "name" is greater than that of "mod")
120 if mod
== modulename
[:n
] and modulename
[n
] == '.':
121 self
._ignore
[modulename
] = 1
124 # Now check that __file__ isn't in one of the directories
126 # must be a built-in, so we must ignore
127 self
._ignore
[modulename
] = 1
130 # Ignore a file when it contains one of the ignorable paths
132 # The '+ os.sep' is to ensure that d is a parent directory,
133 # as compared to cases like:
135 # filename = "/usr/local.py"
137 # d = "/usr/local.py"
138 # filename = "/usr/local.py"
139 if filename
.startswith(d
+ os
.sep
):
140 self
._ignore
[modulename
] = 1
143 # Tried the different ways, so we don't ignore this module
144 self
._ignore
[modulename
] = 0
147 class CoverageResults
:
148 def __init__(self
, counts
=None, calledfuncs
=None, infile
=None,
151 if self
.counts
is None:
153 self
.counter
= self
.counts
.copy() # map (filename, lineno) to count
154 self
.calledfuncs
= calledfuncs
155 if self
.calledfuncs
is None:
156 self
.calledfuncs
= {}
157 self
.calledfuncs
= self
.calledfuncs
.copy()
159 self
.outfile
= outfile
161 # try and merge existing counts file
163 thingie
= pickle
.load(open(self
.infile
, 'r'))
164 if type(thingie
) is types
.DictType
:
165 # backwards compatibility for old trace.py after
166 # Zooko touched it but before calledfuncs --Zooko
168 self
.update(self
.__class
__(thingie
))
169 elif type(thingie
) is types
.TupleType
and len(thingie
) == 2:
170 counts
, calledfuncs
= thingie
171 self
.update(self
.__class
__(counts
, calledfuncs
))
172 except (IOError, EOFError):
174 except pickle
.UnpicklingError
:
175 # backwards compatibility for old trace.py before
176 # Zooko touched it --Zooko 2001-10-24
177 self
.update(self
.__class
__(marshal
.load(open(self
.infile
))))
179 def update(self
, other
):
180 """Merge in the data from another CoverageResults"""
182 calledfuncs
= self
.calledfuncs
183 other_counts
= other
.counts
184 other_calledfuncs
= other
.calledfuncs
186 for key
in other_counts
.keys():
187 if key
!= 'calledfuncs':
188 # backwards compatibility for abortive attempt to
189 # stuff calledfuncs into self.counts, by Zooko
191 counts
[key
] = counts
.get(key
, 0) + other_counts
[key
]
193 for key
in other_calledfuncs
.keys():
196 def write_results(self
, show_missing
= 1, summary
= 0, coverdir
= None):
200 for filename
, modulename
, funcname
in self
.calledfuncs
.keys():
201 print ("filename: %s, modulename: %s, funcname: %s"
202 % (filename
, modulename
, funcname
))
205 # turn the counts data ("(filename, lineno) = count") into something
206 # accessible on a per-file basis
208 for thingie
in self
.counts
.keys():
209 if thingie
!= "calledfuncs":
210 # backwards compatibility for abortive attempt to
211 # stuff calledfuncs into self.counts, by Zooko --Zooko
213 filename
, lineno
= thingie
214 lines_hit
= per_file
[filename
] = per_file
.get(filename
, {})
215 lines_hit
[lineno
] = self
.counts
[(filename
, lineno
)]
217 # there are many places where this is insufficient, like a blank
218 # line embedded in a multiline string.
219 blank
= re
.compile(r
'^\s*(#.*)?$')
221 # accumulate summary info, if needed
224 # generate file paths for the coverage files we are going to write...
226 tfdir
= tempfile
.gettempdir()
227 for key
in per_file
.keys():
230 # skip some "files" we don't care about...
231 if filename
== "<string>":
233 # are these caused by code compiled using exec or something?
234 if filename
.startswith(tfdir
):
237 modulename
= inspect
.getmodulename(filename
)
239 if filename
.endswith(".pyc") or filename
.endswith(".pyo"):
240 filename
= filename
[:-1]
243 thiscoverdir
= coverdir
245 thiscoverdir
= os
.path
.dirname(os
.path
.abspath(filename
))
247 # the code from here to "<<<" is the contents of the `fileutil.make_dirs()' function in the Mojo Nation project. --Zooko 2001-10-14
248 # http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/mojonation/evil/common/fileutil.py?rev=HEAD&content-type=text/vnd.viewcvs-markup
251 os
.makedirs(thiscoverdir
)
255 if not os
.path
.isdir(thiscoverdir
):
258 raise exceptions
.IOError, "unknown error prevented creation of directory: %s" % thiscoverdir
# careful not to construct an IOError with a 2-tuple, as that has a special meaning...
261 # build list file name by appending a ".cover" to the module name
262 # and sticking it into the specified directory
263 if "." in modulename
:
264 # A module in a package
265 finalname
= modulename
.split(".")[-1]
266 listfilename
= os
.path
.join(thiscoverdir
, finalname
+ ".cover")
268 listfilename
= os
.path
.join(thiscoverdir
, modulename
+ ".cover")
270 # Get the original lines from the .py file
272 lines
= open(filename
, 'r').readlines()
274 sys
.stderr
.write("trace: Could not open %s for reading because: %s - skipping\n" % (`filename`
, err
))
278 outfile
= open(listfilename
, 'w')
281 '%s: Could not open %s for writing because: %s" \
282 "- skipping\n' % ("trace", `listfilename`
, err
))
285 # If desired, get a list of the line numbers which represent
286 # executable content (returned as a dict for better lookup speed)
288 executable_linenos
= find_executable_linenos(filename
)
290 executable_linenos
= {}
294 lines_hit
= per_file
[key
]
295 for i
in range(len(lines
)):
298 # do the blank/comment match to try to mark more lines
299 # (help the reader find stuff that hasn't been covered)
300 if lines_hit
.has_key(i
+1):
301 # count precedes the lines that we captured
302 outfile
.write('%5d: ' % lines_hit
[i
+1])
304 n_lines
= n_lines
+ 1
305 elif blank
.match(line
):
306 # blank lines and comments are preceded by dots
309 # lines preceded by no marks weren't hit
310 # Highlight them if so indicated, unless the line contains
311 # '#pragma: NO COVER' (it is possible to embed this into
312 # the text as a non-comment; no easy fix)
313 if executable_linenos
.has_key(i
+1) and \
314 lines
[i
].find(' '.join(['#pragma', 'NO COVER'])) == -1:
315 outfile
.write('>>>>>> ')
318 n_lines
= n_lines
+ 1
319 outfile
.write(lines
[i
].expandtabs(8))
323 if summary
and n_lines
:
324 percent
= int(100 * n_hits
/ n_lines
)
325 sums
[modulename
] = n_lines
, percent
, modulename
, filename
330 print "lines cov% module (path)"
332 n_lines
, percent
, modulename
, filename
= sums
[m
]
333 print "%5d %3d%% %s (%s)" % sums
[m
]
336 # try and store counts and module info into self.outfile
338 pickle
.dump((self
.counts
, self
.calledfuncs
),
339 open(self
.outfile
, 'w'), 1)
341 sys
.stderr
.write("cannot save counts files because %s" % err
)
343 def _find_LINENO_from_code(code
):
344 """return the numbers of the lines containing the source code that
345 was compiled into code"""
348 line_increments
= [ord(c
) for c
in code
.co_lnotab
[1::2]]
349 table_length
= len(line_increments
)
351 lineno
= code
.co_firstlineno
353 for li
in line_increments
:
360 def _find_LINENO(code
):
361 """return all of the lineno information from a code object"""
364 # get all of the lineno information from the code of this scope level
365 linenos
= _find_LINENO_from_code(code
)
367 # and check the constants for references to other code objects
368 for c
in code
.co_consts
:
369 if type(c
) == types
.CodeType
:
370 # find another code object, so recurse into it
371 linenos
.update(_find_LINENO(c
))
374 def find_executable_linenos(filename
):
375 """return a dict of the line numbers from executable statements in a file
380 assert filename
.endswith('.py')
382 prog
= open(filename
).read()
383 ast
= parser
.suite(prog
)
384 code
= parser
.compileast(ast
, filename
)
386 return _find_LINENO(code
)
388 ### XXX because os.path.commonprefix seems broken by my way of thinking...
389 def commonprefix(dirs
):
390 "Given a list of pathnames, returns the longest common leading component"
391 if not dirs
: return ''
393 for i
in range(len(n
)):
394 n
[i
] = n
[i
].split(os
.sep
)
397 for i
in range(len(prefix
)):
398 if prefix
[:i
+1] <> item
[:i
+1]:
402 return os
.sep
.join(prefix
)
405 def __init__(self
, count
=1, trace
=1, countfuncs
=0, ignoremods
=(),
406 ignoredirs
=(), infile
=None, outfile
=None):
408 @param count true iff it should count number of times each
410 @param trace true iff it should print out each line that is
412 @param countfuncs true iff it should just output a list of
413 (filename, modulename, funcname,) for functions
414 that were called at least once; This overrides
416 @param ignoremods a list of the names of modules to ignore
417 @param ignoredirs a list of the names of directories to ignore
418 all of the (recursive) contents of
419 @param infile file from which to read stored counts to be
420 added into the results
421 @param outfile file in which to write the results
424 self
.outfile
= outfile
425 self
.ignore
= Ignore(ignoremods
, ignoredirs
)
426 self
.counts
= {} # keys are (filename, linenumber)
427 self
.blabbed
= {} # for debugging
428 self
.pathtobasename
= {} # for memoizing os.path.basename
431 self
._calledfuncs
= {}
433 self
.globaltrace
= self
.globaltrace_countfuncs
434 elif trace
and count
:
435 self
.globaltrace
= self
.globaltrace_lt
436 self
.localtrace
= self
.localtrace_trace_and_count
438 self
.globaltrace
= self
.globaltrace_lt
439 self
.localtrace
= self
.localtrace_trace
441 self
.globaltrace
= self
.globaltrace_lt
442 self
.localtrace
= self
.localtrace_count
444 # Ahem -- do nothing? Okay.
449 dict = __main__
.__dict
__
450 if not self
.donothing
:
451 sys
.settrace(self
.globaltrace
)
453 exec cmd
in dict, dict
455 if not self
.donothing
:
458 def runctx(self
, cmd
, globals=None, locals=None):
459 if globals is None: globals = {}
460 if locals is None: locals = {}
461 if not self
.donothing
:
462 sys
.settrace(self
.globaltrace
)
464 exec cmd
in globals, locals
466 if not self
.donothing
:
469 def runfunc(self
, func
, *args
, **kw
):
471 if not self
.donothing
:
472 sys
.settrace(self
.globaltrace
)
474 result
= apply(func
, args
, kw
)
476 if not self
.donothing
:
480 def globaltrace_countfuncs(self
, frame
, why
, arg
):
482 Handles `call' events (why == 'call') and adds the (filename, modulename, funcname,) to the self._calledfuncs dict.
485 filename
, lineno
, funcname
, context
, lineindex
= \
486 inspect
.getframeinfo(frame
, 0)
488 modulename
= inspect
.getmodulename(filename
)
491 self
._calledfuncs
[(filename
, modulename
, funcname
,)] = 1
493 def globaltrace_lt(self
, frame
, why
, arg
):
495 Handles `call' events (why == 'call') and if the code block being entered is to be ignored then it returns `None', else it returns `self.localtrace'.
498 filename
, lineno
, funcname
, context
, lineindex
= \
499 inspect
.getframeinfo(frame
, 0)
501 modulename
= inspect
.getmodulename(filename
)
502 if modulename
is not None:
503 ignore_it
= self
.ignore
.names(filename
, modulename
)
506 print (" --- modulename: %s, funcname: %s"
507 % (modulename
, funcname
))
508 return self
.localtrace
510 # XXX why no filename?
513 def localtrace_trace_and_count(self
, frame
, why
, arg
):
515 # record the file name and line number of every trace
516 # XXX I wish inspect offered me an optimized
517 # `getfilename(frame)' to use in place of the presumably
518 # heavier `getframeinfo()'. --Zooko 2001-10-14
520 filename
, lineno
, funcname
, context
, lineindex
= \
521 inspect
.getframeinfo(frame
, 1)
522 key
= filename
, lineno
523 self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
525 # XXX not convinced that this memoizing is a performance
526 # win -- I don't know enough about Python guts to tell.
529 bname
= self
.pathtobasename
.get(filename
)
532 # Using setdefault faster than two separate lines?
534 bname
= self
.pathtobasename
.setdefault(filename
,
535 os
.path
.basename(filename
))
537 print "%s(%d): %s" % (bname
, lineno
, context
[lineindex
]),
539 # Uh.. sometimes getframeinfo gives me a context of
540 # length 1 and a lineindex of -2. Oh well.
542 return self
.localtrace
544 def localtrace_trace(self
, frame
, why
, arg
):
546 # XXX shouldn't do the count increment when arg is
547 # exception? But be careful to return self.localtrace
548 # when arg is exception! ? --Zooko 2001-10-14
550 # record the file name and line number of every trace XXX
551 # I wish inspect offered me an optimized
552 # `getfilename(frame)' to use in place of the presumably
553 # heavier `getframeinfo()'. --Zooko 2001-10-14
554 filename
, lineno
, funcname
, context
, lineindex
= \
555 inspect
.getframeinfo(frame
)
557 # XXX not convinced that this memoizing is a performance
558 # win -- I don't know enough about Python guts to tell.
560 bname
= self
.pathtobasename
.get(filename
)
562 # Using setdefault faster than two separate lines?
564 bname
= self
.pathtobasename
.setdefault(filename
, os
.path
.basename(filename
))
565 if context
is not None:
567 print "%s(%d): %s" % (bname
, lineno
, context
[lineindex
]),
569 # Uh.. sometimes getframeinfo gives me a context of length 1 and a lineindex of -2. Oh well.
572 print "%s(???): ???" % bname
573 return self
.localtrace
575 def localtrace_count(self
, frame
, why
, arg
):
577 filename
= frame
.f_code
.co_filename
578 lineno
= frame
.f_lineno
579 key
= filename
, lineno
580 self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
581 return self
.localtrace
584 return CoverageResults(self
.counts
, infile
=self
.infile
,
585 outfile
=self
.outfile
,
586 calledfuncs
=self
._calledfuncs
)
589 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
598 opts
, prog_argv
= getopt
.getopt(argv
[1:], "tcrRf:d:msC:l",
599 ["help", "version", "trace", "count",
600 "report", "no-report", "summary",
602 "ignore-module=", "ignore-dir=",
603 "coverdir=", "listfuncs",])
605 except getopt
.error
, msg
:
606 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
607 sys
.stderr
.write("Try `%s --help' for more information\n"
623 for opt
, val
in opts
:
628 if opt
== "--version":
629 sys
.stdout
.write("trace 2.0\n")
632 if opt
== "-l" or opt
== "--listfuncs":
636 if opt
== "-t" or opt
== "--trace":
640 if opt
== "-c" or opt
== "--count":
644 if opt
== "-r" or opt
== "--report":
648 if opt
== "-R" or opt
== "--no-report":
652 if opt
== "-f" or opt
== "--file":
656 if opt
== "-m" or opt
== "--missing":
660 if opt
== "-C" or opt
== "--coverdir":
664 if opt
== "-s" or opt
== "--summary":
668 if opt
== "--ignore-module":
669 ignore_modules
.append(val
)
672 if opt
== "--ignore-dir":
673 for s
in val
.split(os
.pathsep
):
674 s
= os
.path
.expandvars(s
)
675 # should I also call expanduser? (after all, could use $HOME)
677 s
= s
.replace("$prefix",
678 os
.path
.join(sys
.prefix
, "lib",
679 "python" + sys
.version
[:3]))
680 s
= s
.replace("$exec_prefix",
681 os
.path
.join(sys
.exec_prefix
, "lib",
682 "python" + sys
.version
[:3]))
683 s
= os
.path
.normpath(s
)
684 ignore_dirs
.append(s
)
687 assert 0, "Should never get here"
689 if listfuncs
and (count
or trace
):
690 _err_exit("cannot specify both --listfuncs and (--trace or --count)")
692 if not count
and not trace
and not report
and not listfuncs
:
693 _err_exit("must specify one of --trace, --count, --report or --listfuncs")
695 if report
and no_report
:
696 _err_exit("cannot specify both --report and --no-report")
698 if report
and not counts_file
:
699 _err_exit("--report requires a --file")
701 if no_report
and len(prog_argv
) == 0:
702 _err_exit("missing name of file to run")
704 # everything is ready
706 results
= CoverageResults(infile
=counts_file
, outfile
=counts_file
)
707 results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)
710 progname
= prog_argv
[0]
711 sys
.path
[0] = os
.path
.split(progname
)[0]
713 t
= Trace(count
, trace
, countfuncs
=listfuncs
,
714 ignoremods
=ignore_modules
, ignoredirs
=ignore_dirs
,
715 infile
=counts_file
, outfile
=counts_file
)
717 t
.run('execfile(' + `progname`
+ ')')
719 _err_exit("Cannot run file %s because: %s" % (`sys
.argv
[0]`
, err
))
723 results
= t
.results()
726 results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)
728 if __name__
=='__main__':