# portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
# err... reserved and offered to the public under the terms of the
# Author: Zooko O'Whielacronx
# Copyright 2000, Mojam Media, Inc., all rights reserved.
# Copyright 1999, Bioreason, Inc., all rights reserved.
# Copyright 1995-1997, Automatrix, Inc., all rights reserved.
# Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
# Permission to use, copy, modify, and distribute this Python software and
# its associated documentation for any purpose without fee is hereby
# granted, provided that the above copyright notice appears in all copies,
# and that both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of neither Automatrix,
# Bioreason or Mojam Media be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior permission.
"""program/module to trace Python program or function execution
Sample use, command line:
trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
trace.py -t --ignore-dir '$prefix' spam.py eggs
trace.py --trackcalls spam.py eggs
Sample use, programmatically
# create a Trace object, telling it what to ignore, and whether to
# do tracing or line-counting or both.
trace = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0,
# run the new command using the given trace
# make a report, telling it where you want output
r.write_results(show_missing=True)
outfile
.write("""Usage: %s [OPTIONS] <file> [ARGS]
--help Display this help then exit.
--version Output version information then exit.
Otherwise, exactly one of the following three options must be given:
-t, --trace Print each line to sys.stdout before it is executed.
-c, --count Count the number of times each line is executed
and write the counts to <module>.cover for each
module executed, in the module's directory.
See also `--coverdir', `--file', `--no-report' below.
-l, --listfuncs Keep track of which functions are executed at least
once and write the results to sys.stdout after the
-T, --trackcalls Keep track of caller/called pairs and write the
results to sys.stdout after the program exits.
-r, --report Generate a report from a counts file; do not execute
any code. `--file' must specify the results file to
read, which must have been created in a previous run
with `--count --file=FILE'.
-f, --file=<file> File to accumulate counts over several runs.
-R, --no-report Do not generate the coverage report files.
Useful if you want to accumulate over several runs.
-C, --coverdir=<dir> Directory where the report files. The coverage
report for <package>.<module> is written to file
<dir>/<package>/<module>.cover.
-m, --missing Annotate executable lines that were not executed
-s, --summary Write a brief summary on stdout for each file.
(Can only be used with --count or --report.)
Filters, may be repeated multiple times:
--ignore-module=<mod> Ignore the given module and its submodules
--ignore-dir=<dir> Ignore files in the given directory (multiple
directories can be joined by os.pathsep).
PRAGMA_NOCOVER
= "#pragma NO COVER"
# Simple rx to find lines with no code.
rx_blank
= re
.compile(r
'^\s*(#.*)?$')
def __init__(self
, modules
= None, dirs
= None):
self
._mods
= modules
or []
self
._dirs
= map(os
.path
.normpath
, self
._dirs
)
self
._ignore
= { '<string>': 1 }
def names(self
, filename
, modulename
):
if self
._ignore
.has_key(modulename
):
return self
._ignore
[modulename
]
# haven't seen this one before, so see if the module name is
# on the ignore list. Need to take some care since ignoring
# "cmp" musn't mean ignoring "cmpcache" but ignoring
# "Spam" must also mean ignoring "Spam.Eggs".
if mod
== modulename
: # Identical names, so ignore
self
._ignore
[modulename
] = 1
# check if the module is a proper submodule of something on
# (will not overflow since if the first n characters are the
# same and the name has not already occured, then the size
# of "name" is greater than that of "mod")
if mod
== modulename
[:n
] and modulename
[n
] == '.':
self
._ignore
[modulename
] = 1
# Now check that __file__ isn't in one of the directories
# must be a built-in, so we must ignore
self
._ignore
[modulename
] = 1
# Ignore a file when it contains one of the ignorable paths
# The '+ os.sep' is to ensure that d is a parent directory,
# as compared to cases like:
# filename = "/usr/local.py"
# filename = "/usr/local.py"
if filename
.startswith(d
+ os
.sep
):
self
._ignore
[modulename
] = 1
# Tried the different ways, so we don't ignore this module
self
._ignore
[modulename
] = 0
"""Return a plausible module name for the patch."""
base
= os
.path
.basename(path
)
filename
, ext
= os
.path
.splitext(base
)
"""Return a plausible module name for the path."""
# If the file 'path' is part of a package, then the filename isn't
# enough to uniquely identify it. Try to do the right thing by
# looking in sys.path for the longest matching prefix. We'll
# assume that the rest is the package name.
if path
.startswith(dir) and path
[len(dir)] == os
.path
.sep
:
if len(dir) > len(longest
):
base
= path
[len(longest
) + 1:]
base
= base
.replace(os
.sep
, ".")
base
= base
.replace(os
.altsep
, ".")
filename
, ext
= os
.path
.splitext(base
)
def __init__(self
, counts
=None, calledfuncs
=None, infile
=None,
callers
=None, outfile
=None):
self
.counter
= self
.counts
.copy() # map (filename, lineno) to count
self
.calledfuncs
= calledfuncs
if self
.calledfuncs
is None:
self
.calledfuncs
= self
.calledfuncs
.copy()
self
.callers
= self
.callers
.copy()
# Try to merge existing counts file.
counts
, calledfuncs
, callers
= \
pickle
.load(open(self
.infile
, 'rb'))
self
.update(self
.__class
__(counts
, calledfuncs
, callers
))
except (IOError, EOFError, ValueError), err
:
print >> sys
.stderr
, ("Skipping counts file %r: %s"
"""Merge in the data from another CoverageResults"""
calledfuncs
= self
.calledfuncs
other_counts
= other
.counts
other_calledfuncs
= other
.calledfuncs
other_callers
= other
.callers
for key
in other_counts
.keys():
counts
[key
] = counts
.get(key
, 0) + other_counts
[key
]
for key
in other_calledfuncs
.keys():
for key
in other_callers
.keys():
def write_results(self
, show_missing
=True, summary
=False, coverdir
=None):
print "functions called:"
calls
= self
.calledfuncs
.keys()
for filename
, modulename
, funcname
in calls
:
print ("filename: %s, modulename: %s, funcname: %s"
% (filename
, modulename
, funcname
))
print "calling relationships:"
calls
= self
.callers
.keys()
lastfile
= lastcfile
= ""
for ((pfile
, pmod
, pfunc
), (cfile
, cmod
, cfunc
)) in calls
:
print "***", pfile
, "***"
if cfile
!= pfile
and lastcfile
!= cfile
:
print " %s.%s -> %s.%s" % (pmod
, pfunc
, cmod
, cfunc
)
# turn the counts data ("(filename, lineno) = count") into something
# accessible on a per-file basis
for filename
, lineno
in self
.counts
.keys():
lines_hit
= per_file
[filename
] = per_file
.get(filename
, {})
lines_hit
[lineno
] = self
.counts
[(filename
, lineno
)]
# accumulate summary info, if needed
for filename
, count
in per_file
.iteritems():
# skip some "files" we don't care about...
if filename
== "<string>":
if filename
.endswith(".pyc") or filename
.endswith(".pyo"):
dir = os
.path
.dirname(os
.path
.abspath(filename
))
modulename
= modname(filename
)
if not os
.path
.exists(dir):
modulename
= fullmodname(filename
)
# If desired, get a list of the line numbers which represent
# executable content (returned as a dict for better lookup speed)
lnotab
= find_executable_linenos(filename
)
source
= linecache
.getlines(filename
)
coverpath
= os
.path
.join(dir, modulename
+ ".cover")
n_hits
, n_lines
= self
.write_results_file(coverpath
, source
,
percent
= int(100 * n_hits
/ n_lines
)
sums
[modulename
] = n_lines
, percent
, modulename
, filename
print "lines cov% module (path)"
n_lines
, percent
, modulename
, filename
= sums
[m
]
print "%5d %3d%% %s (%s)" % sums
[m
]
# try and store counts and module info into self.outfile
pickle
.dump((self
.counts
, self
.calledfuncs
, self
.callers
),
open(self
.outfile
, 'wb'), 1)
print >> sys
.stderr
, "Can't save counts files because %s" % err
def write_results_file(self
, path
, lines
, lnotab
, lines_hit
):
"""Return a coverage results file in path."""
outfile
= open(path
, "w")
print >> sys
.stderr
, ("trace: Could not open %r for writing: %s"
"- skipping" % (path
, err
))
for i
, line
in enumerate(lines
):
# do the blank/comment match to try to mark more lines
# (help the reader find stuff that hasn't been covered)
outfile
.write("%5d: " % lines_hit
[lineno
])
elif rx_blank
.match(line
):
# lines preceded by no marks weren't hit
# Highlight them if so indicated, unless the line contains
if lineno
in lnotab
and not PRAGMA_NOCOVER
in lines
[i
]:
outfile
.write(lines
[i
].expandtabs(8))
def find_lines_from_code(code
, strs
):
"""Return dict where keys are lines in the line number table."""
line_increments
= [ord(c
) for c
in code
.co_lnotab
[1::2]]
table_length
= len(line_increments
)
lineno
= code
.co_firstlineno
for li
in line_increments
:
def find_lines(code
, strs
):
"""Return lineno dict for all code objects reachable from code."""
# get all of the lineno information from the code of this scope level
linenos
= find_lines_from_code(code
, strs
)
# and check the constants for references to other code objects
if isinstance(c
, types
.CodeType
):
# find another code object, so recurse into it
linenos
.update(find_lines(c
, strs
))
def find_strings(filename
):
"""Return a dict of possible docstring positions.
The dict maps line numbers to strings. There is an entry for
line that contains only a string or a part of a triple-quoted
# If the first token is a string, then it's the module docstring.
# Add this special case so that the test in the loop passes.
prev_ttype
= token
.INDENT
for ttype
, tstr
, start
, end
, line
in tokenize
.generate_tokens(f
.readline
):
if ttype
== token
.STRING
:
if prev_ttype
== token
.INDENT
:
for i
in range(sline
, eline
+ 1):
def find_executable_linenos(filename
):
"""Return dict where keys are line numbers in the line number table."""
prog
= open(filename
, "rU").read()
print >> sys
.stderr
, ("Not printing coverage data for %r: %s"
code
= compile(prog
, filename
, "exec")
strs
= find_strings(filename
)
return find_lines(code
, strs
)
def __init__(self
, count
=1, trace
=1, countfuncs
=0, countcallers
=0,
ignoremods
=(), ignoredirs
=(), infile
=None, outfile
=None):
@param count true iff it should count number of times each
@param trace true iff it should print out each line that is
@param countfuncs true iff it should just output a list of
(filename, modulename, funcname,) for functions
that were called at least once; This overrides
@param ignoremods a list of the names of modules to ignore
@param ignoredirs a list of the names of directories to ignore
all of the (recursive) contents of
@param infile file from which to read stored counts to be
@param outfile file in which to write the results
self
.ignore
= Ignore(ignoremods
, ignoredirs
)
self
.counts
= {} # keys are (filename, linenumber)
self
.blabbed
= {} # for debugging
self
.pathtobasename
= {} # for memoizing os.path.basename
self
.globaltrace
= self
.globaltrace_trackcallers
self
.globaltrace
= self
.globaltrace_countfuncs
self
.globaltrace
= self
.globaltrace_lt
self
.localtrace
= self
.localtrace_trace_and_count
self
.globaltrace
= self
.globaltrace_lt
self
.localtrace
= self
.localtrace_trace
self
.globaltrace
= self
.globaltrace_lt
self
.localtrace
= self
.localtrace_count
# Ahem -- do nothing? Okay.
sys
.settrace(self
.globaltrace
)
threading
.settrace(self
.globaltrace
)
def runctx(self
, cmd
, globals=None, locals=None):
if globals is None: globals = {}
if locals is None: locals = {}
sys
.settrace(self
.globaltrace
)
threading
.settrace(self
.globaltrace
)
exec cmd
in globals, locals
def runfunc(self
, func
, *args
, **kw
):
sys
.settrace(self
.globaltrace
)
result
= func(*args
, **kw
)
def file_module_function_of(self
, frame
):
filename
= code
.co_filename
modulename
= modname(filename
)
if code
in self
._caller
_cache
:
if self
._caller
_cache
[code
] is not None:
clsname
= self
._caller
_cache
[code
]
self
._caller
_cache
[code
] = None
## use of gc.get_referrers() was suggested by Michael Hudson
# all functions which refer to this code object
funcs
= [f
for f
in gc
.get_referrers(code
)
if hasattr(f
, "func_doc")]
# require len(func) == 1 to avoid ambiguity caused by calls to
# new.function(): "In the face of ambiguity, refuse the
dicts
= [d
for d
in gc
.get_referrers(funcs
[0])
classes
= [c
for c
in gc
.get_referrers(dicts
[0])
if hasattr(c
, "__bases__")]
# ditto for new.classobj()
clsname
= str(classes
[0])
# cache the result - assumption is that new.* is
# not called later to disturb this relationship
# _caller_cache could be flushed if functions in
# the new module get called.
self
._caller
_cache
[code
] = clsname
# final hack - module name shows up in str(cls), but we've already
# computed module name, so remove it
clsname
= clsname
.split(".")[1:]
clsname
= ".".join(clsname
)
funcname
= "%s.%s" % (clsname
, funcname
)
return filename
, modulename
, funcname
def globaltrace_trackcallers(self
, frame
, why
, arg
):
"""Handler for call events.
Adds information about who called who to the self._callers dict.
# XXX Should do a better job of identifying methods
this_func
= self
.file_module_function_of(frame
)
parent_func
= self
.file_module_function_of(frame
.f_back
)
self
._callers
[(parent_func
, this_func
)] = 1
def globaltrace_countfuncs(self
, frame
, why
, arg
):
"""Handler for call events.
Adds (filename, modulename, funcname) to the self._calledfuncs dict.
this_func
= self
.file_module_function_of(frame
)
self
._calledfuncs
[this_func
] = 1
def globaltrace_lt(self
, frame
, why
, arg
):
"""Handler for call events.
If the code block being entered is to be ignored, returns `None',
else returns self.localtrace.
filename
= code
.co_filename
# XXX modname() doesn't work right for packages, so
# the ignore support won't work right for packages
modulename
= modname(filename
)
if modulename
is not None:
ignore_it
= self
.ignore
.names(filename
, modulename
)
print (" --- modulename: %s, funcname: %s"
% (modulename
, code
.co_name
))
def localtrace_trace_and_count(self
, frame
, why
, arg
):
# record the file name and line number of every trace
filename
= frame
.f_code
.co_filename
self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
bname
= os
.path
.basename(filename
)
print "%s(%d): %s" % (bname
, lineno
,
linecache
.getline(filename
, lineno
)),
def localtrace_trace(self
, frame
, why
, arg
):
# record the file name and line number of every trace
filename
= frame
.f_code
.co_filename
bname
= os
.path
.basename(filename
)
print "%s(%d): %s" % (bname
, lineno
,
linecache
.getline(filename
, lineno
)),
def localtrace_count(self
, frame
, why
, arg
):
filename
= frame
.f_code
.co_filename
self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
return CoverageResults(self
.counts
, infile
=self
.infile
,
calledfuncs
=self
._calledfuncs
,
sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
opts
, prog_argv
= getopt
.getopt(argv
[1:], "tcrRf:d:msC:lT",
["help", "version", "trace", "count",
"report", "no-report", "summary",
"ignore-module=", "ignore-dir=",
"coverdir=", "listfuncs",
except getopt
.error
, msg
:
sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
sys
.stderr
.write("Try `%s --help' for more information\n"
sys
.stdout
.write("trace 2.0\n")
if opt
== "-T" or opt
== "--trackcalls":
if opt
== "-l" or opt
== "--listfuncs":
if opt
== "-t" or opt
== "--trace":
if opt
== "-c" or opt
== "--count":
if opt
== "-r" or opt
== "--report":
if opt
== "-R" or opt
== "--no-report":
if opt
== "-f" or opt
== "--file":
if opt
== "-m" or opt
== "--missing":
if opt
== "-C" or opt
== "--coverdir":
if opt
== "-s" or opt
== "--summary":
if opt
== "--ignore-module":
ignore_modules
.append(val
)
if opt
== "--ignore-dir":
for s
in val
.split(os
.pathsep
):
s
= os
.path
.expandvars(s
)
# should I also call expanduser? (after all, could use $HOME)
os
.path
.join(sys
.prefix
, "lib",
"python" + sys
.version
[:3]))
s
= s
.replace("$exec_prefix",
os
.path
.join(sys
.exec_prefix
, "lib",
"python" + sys
.version
[:3]))
assert 0, "Should never get here"
if listfuncs
and (count
or trace
):
_err_exit("cannot specify both --listfuncs and (--trace or --count)")
if not (count
or trace
or report
or listfuncs
or countcallers
):
_err_exit("must specify one of --trace, --count, --report, "
"--listfuncs, or --trackcalls")
_err_exit("cannot specify both --report and --no-report")
if report
and not counts_file
:
_err_exit("--report requires a --file")
if no_report
and len(prog_argv
) == 0:
_err_exit("missing name of file to run")
results
= CoverageResults(infile
=counts_file
, outfile
=counts_file
)
results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)
sys
.path
[0] = os
.path
.split(progname
)[0]
t
= Trace(count
, trace
, countfuncs
=listfuncs
,
countcallers
=countcallers
, ignoremods
=ignore_modules
,
ignoredirs
=ignore_dirs
, infile
=counts_file
,
t
.run('execfile(%r)' % (progname
,))
_err_exit("Cannot run file %r because: %s" % (sys
.argv
[0], err
))
results
.write_results(missing
, summary
=summary
, coverdir
=coverdir
)