2003-02-18 23:06:17 +08:00
#!/usr/bin/env python
# portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
# err... reserved and offered to the public under the terms of the
# Python 2.2 license.
# Author: Zooko O'Whielacronx
# http://zooko.com/
# mailto:zooko@zooko.com
#
# Copyright 2000, Mojam Media, Inc., all rights reserved.
# Author: Skip Montanaro
#
# Copyright 1999, Bioreason, Inc., all rights reserved.
# Author: Andrew Dalke
#
# Copyright 1995-1997, Automatrix, Inc., all rights reserved.
# Author: Skip Montanaro
#
# 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
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 ,
count = 1 )
# run the new command using the given trace
trace . run ( coverage . globaltrace , ' main() ' )
# make a report, telling it where you want output
r = trace . results ( )
r . write_results ( show_missing = 1 )
"""
import sys , os , tempfile , types , copy , operator , inspect , exceptions , marshal
try :
import cPickle
pickle = cPickle
except ImportError :
import pickle
# DEBUG_MODE=1 # make this true to get printouts which help you understand what's going on
def usage ( outfile ) :
outfile . write ( """ Usage: %s [OPTIONS] <file> [ARGS]
Meta - options :
- - 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.
- 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 ' .
Modifiers :
- 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
with ' >>>>>> ' .
- 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
( if it is a package ) .
- - ignore - dir = < dir > Ignore files in the given directory ( multiple
directories can be joined by os . pathsep ) .
""" % s ys.argv[0])
class Ignore :
def __init__ ( self , modules = None , dirs = None ) :
self . _mods = modules or [ ]
self . _dirs = dirs 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".
for mod in self . _mods :
if mod == modulename : # Identical names, so ignore
self . _ignore [ modulename ] = 1
return 1
# check if the module is a proper submodule of something on
# the ignore list
n = len ( mod )
# (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
return 1
# Now check that __file__ isn't in one of the directories
if filename is None :
# must be a built-in, so we must ignore
self . _ignore [ modulename ] = 1
return 1
# Ignore a file when it contains one of the ignorable paths
for d in self . _dirs :
# The '+ os.sep' is to ensure that d is a parent directory,
# as compared to cases like:
# d = "/usr/local"
# filename = "/usr/local.py"
# or
# d = "/usr/local.py"
# filename = "/usr/local.py"
if filename . startswith ( d + os . sep ) :
self . _ignore [ modulename ] = 1
return 1
# Tried the different ways, so we don't ignore this module
self . _ignore [ modulename ] = 0
return 0
class CoverageResults :
def __init__ ( self , counts = None , calledfuncs = None , infile = None ,
2003-02-19 10:35:07 +08:00
outfile = None ) :
2003-02-18 23:06:17 +08:00
self . counts = counts
if self . counts is None :
self . counts = { }
self . counter = self . counts . copy ( ) # map (filename, lineno) to count
self . calledfuncs = calledfuncs
if self . calledfuncs is None :
self . calledfuncs = { }
self . calledfuncs = self . calledfuncs . copy ( )
self . infile = infile
self . outfile = outfile
if self . infile :
# try and merge existing counts file
try :
thingie = pickle . load ( open ( self . infile , ' r ' ) )
if type ( thingie ) is types . DictType :
# backwards compatibility for old trace.py after
# Zooko touched it but before calledfuncs --Zooko
2003-02-19 10:35:07 +08:00
# 2001-10-24
2003-02-18 23:06:17 +08:00
self . update ( self . __class__ ( thingie ) )
elif type ( thingie ) is types . TupleType and len ( thingie ) == 2 :
counts , calledfuncs = thingie
self . update ( self . __class__ ( counts , calledfuncs ) )
except ( IOError , EOFError ) :
pass
except pickle . UnpicklingError :
# backwards compatibility for old trace.py before
2003-02-19 10:35:07 +08:00
# Zooko touched it --Zooko 2001-10-24
2003-02-18 23:06:17 +08:00
self . update ( self . __class__ ( marshal . load ( open ( self . infile ) ) ) )
def update ( self , other ) :
""" Merge in the data from another CoverageResults """
counts = self . counts
calledfuncs = self . calledfuncs
other_counts = other . counts
other_calledfuncs = other . calledfuncs
for key in other_counts . keys ( ) :
if key != ' calledfuncs ' :
# backwards compatibility for abortive attempt to
# stuff calledfuncs into self.counts, by Zooko
2003-02-19 10:35:07 +08:00
# --Zooko 2001-10-24
2003-02-18 23:06:17 +08:00
counts [ key ] = counts . get ( key , 0 ) + other_counts [ key ]
for key in other_calledfuncs . keys ( ) :
calledfuncs [ key ] = 1
def write_results ( self , show_missing = 1 , summary = 0 , coverdir = None ) :
"""
@param coverdir
"""
for filename , modulename , funcname in self . calledfuncs . keys ( ) :
print ( " filename: %s , modulename: %s , funcname: %s "
% ( filename , modulename , funcname ) )
import re
# turn the counts data ("(filename, lineno) = count") into something
# accessible on a per-file basis
per_file = { }
for thingie in self . counts . keys ( ) :
if thingie != " calledfuncs " :
# backwards compatibility for abortive attempt to
# stuff calledfuncs into self.counts, by Zooko --Zooko
# 2001-10-24
filename , lineno = thingie
lines_hit = per_file [ filename ] = per_file . get ( filename , { } )
lines_hit [ lineno ] = self . counts [ ( filename , lineno ) ]
# there are many places where this is insufficient, like a blank
# line embedded in a multiline string.
blank = re . compile ( r ' ^ \ s*(#.*)?$ ' )
# accumulate summary info, if needed
sums = { }
# generate file paths for the coverage files we are going to write...
fnlist = [ ]
tfdir = tempfile . gettempdir ( )
for key in per_file . keys ( ) :
filename = key
# skip some "files" we don't care about...
if filename == " <string> " :
continue
# are these caused by code compiled using exec or something?
if filename . startswith ( tfdir ) :
continue
modulename = inspect . getmodulename ( filename )
if filename . endswith ( " .pyc " ) or filename . endswith ( " .pyo " ) :
filename = filename [ : - 1 ]
if coverdir :
thiscoverdir = coverdir
else :
thiscoverdir = os . path . dirname ( os . path . abspath ( filename ) )
# the code from here to "<<<" is the contents of the `fileutil.make_dirs()' function in the Mojo Nation project. --Zooko 2001-10-14
# http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/mojonation/evil/common/fileutil.py?rev=HEAD&content-type=text/vnd.viewcvs-markup
tx = None
try :
os . makedirs ( thiscoverdir )
except OSError , x :
tx = x
if not os . path . isdir ( thiscoverdir ) :
if tx :
raise tx
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...
# <<<
# build list file name by appending a ".cover" to the module name
# and sticking it into the specified directory
if " . " in modulename :
# A module in a package
finalname = modulename . split ( " . " ) [ - 1 ]
listfilename = os . path . join ( thiscoverdir , finalname + " .cover " )
else :
listfilename = os . path . join ( thiscoverdir , modulename + " .cover " )
# Get the original lines from the .py file
try :
lines = open ( filename , ' r ' ) . readlines ( )
except IOError , err :
sys . stderr . write ( " trace: Could not open %s for reading because: %s - skipping \n " % ( ` filename ` , err ) )
continue
try :
outfile = open ( listfilename , ' w ' )
except IOError , err :
sys . stderr . write (
' %s : Could not open %s for writing because: %s " \
" - skipping \n ' % ( " trace " , `listfilename`, err))
continue
# If desired, get a list of the line numbers which represent
# executable content (returned as a dict for better lookup speed)
if show_missing :
executable_linenos = find_executable_linenos ( filename )
else :
executable_linenos = { }
n_lines = 0
n_hits = 0
lines_hit = per_file [ key ]
for i in range ( len ( lines ) ) :
line = lines [ i ]
# do the blank/comment match to try to mark more lines
# (help the reader find stuff that hasn't been covered)
if lines_hit . has_key ( i + 1 ) :
# count precedes the lines that we captured
outfile . write ( ' %5d : ' % lines_hit [ i + 1 ] )
n_hits = n_hits + 1
n_lines = n_lines + 1
elif blank . match ( line ) :
# blank lines and comments are preceded by dots
outfile . write ( ' . ' )
else :
# lines preceded by no marks weren't hit
# Highlight them if so indicated, unless the line contains
# '#pragma: NO COVER' (it is possible to embed this into
# the text as a non-comment; no easy fix)
if executable_linenos . has_key ( i + 1 ) and \
lines [ i ] . find ( ' ' . join ( [ ' #pragma ' , ' NO COVER ' ] ) ) == - 1 :
outfile . write ( ' >>>>>> ' )
else :
outfile . write ( ' ' * 7 )
n_lines = n_lines + 1
outfile . write ( lines [ i ] . expandtabs ( 8 ) )
outfile . close ( )
if summary and n_lines :
percent = int ( 100 * n_hits / n_lines )
sums [ modulename ] = n_lines , percent , modulename , filename
if summary and sums :
mods = sums . keys ( )
mods . sort ( )
print " lines cov % module (path) "
for m in mods :
n_lines , percent , modulename , filename = sums [ m ]
print " %5d %3d %% %s ( %s ) " % sums [ m ]
if self . outfile :
# try and store counts and module info into self.outfile
try :
pickle . dump ( ( self . counts , self . calledfuncs ) ,
open ( self . outfile , ' w ' ) , 1 )
except IOError , err :
sys . stderr . write ( " cannot save counts files because %s " % err )
def _find_LINENO_from_code ( code ) :
""" return the numbers of the lines containing the source code that
was compiled into code """
linenos = { }
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 :
linenos [ lineno ] = 1
lineno + = li
linenos [ lineno ] = 1
return linenos
def _find_LINENO ( code ) :
""" return all of the lineno information from a code object """
import types
# get all of the lineno information from the code of this scope level
linenos = _find_LINENO_from_code ( code )
# and check the constants for references to other code objects
for c in code . co_consts :
if type ( c ) == types . CodeType :
# find another code object, so recurse into it
linenos . update ( _find_LINENO ( c ) )
return linenos
def find_executable_linenos ( filename ) :
""" return a dict of the line numbers from executable statements in a file
"""
import parser
assert filename . endswith ( ' .py ' )
prog = open ( filename ) . read ( )
ast = parser . suite ( prog )
code = parser . compileast ( ast , filename )
return _find_LINENO ( code )
### XXX because os.path.commonprefix seems broken by my way of thinking...
def commonprefix ( dirs ) :
" Given a list of pathnames, returns the longest common leading component "
if not dirs : return ' '
n = copy . copy ( dirs )
for i in range ( len ( n ) ) :
n [ i ] = n [ i ] . split ( os . sep )
prefix = n [ 0 ]
for item in n :
for i in range ( len ( prefix ) ) :
if prefix [ : i + 1 ] < > item [ : i + 1 ] :
prefix = prefix [ : i ]
if i == 0 : return ' '
break
return os . sep . join ( prefix )
class Trace :
def __init__ ( self , count = 1 , trace = 1 , countfuncs = 0 , ignoremods = ( ) ,
ignoredirs = ( ) , infile = None , outfile = None ) :
"""
@param count true iff it should count number of times each
2003-02-19 10:35:07 +08:00
line is executed
2003-02-18 23:06:17 +08:00
@param trace true iff it should print out each line that is
2003-02-19 10:35:07 +08:00
being counted
2003-02-18 23:06:17 +08:00
@param countfuncs true iff it should just output a list of
( filename , modulename , funcname , ) for functions
that were called at least once ; This overrides
2003-02-19 10:35:07 +08:00
` count ' and `trace '
2003-02-18 23:06:17 +08:00
@param ignoremods a list of the names of modules to ignore
@param ignoredirs a list of the names of directories to ignore
2003-02-19 10:35:07 +08:00
all of the ( recursive ) contents of
2003-02-18 23:06:17 +08:00
@param infile file from which to read stored counts to be
2003-02-19 10:35:07 +08:00
added into the results
2003-02-18 23:06:17 +08:00
@param outfile file in which to write the results
"""
self . infile = infile
self . outfile = outfile
self . ignore = Ignore ( ignoremods , ignoredirs )
self . counts = { } # keys are (filename, linenumber)
self . blabbed = { } # for debugging
self . pathtobasename = { } # for memoizing os.path.basename
self . donothing = 0
self . trace = trace
self . _calledfuncs = { }
if countfuncs :
self . globaltrace = self . globaltrace_countfuncs
elif trace and count :
self . globaltrace = self . globaltrace_lt
self . localtrace = self . localtrace_trace_and_count
elif trace :
self . globaltrace = self . globaltrace_lt
self . localtrace = self . localtrace_trace
elif count :
self . globaltrace = self . globaltrace_lt
self . localtrace = self . localtrace_count
else :
# Ahem -- do nothing? Okay.
self . donothing = 1
def run ( self , cmd ) :
import __main__
dict = __main__ . __dict__
if not self . donothing :
sys . settrace ( self . globaltrace )
try :
exec cmd in dict , dict
finally :
if not self . donothing :
sys . settrace ( None )
def runctx ( self , cmd , globals = None , locals = None ) :
if globals is None : globals = { }
if locals is None : locals = { }
if not self . donothing :
sys . settrace ( self . globaltrace )
try :
exec cmd in globals , locals
finally :
if not self . donothing :
sys . settrace ( None )
def runfunc ( self , func , * args , * * kw ) :
result = None
if not self . donothing :
sys . settrace ( self . globaltrace )
try :
2003-02-28 04:14:51 +08:00
result = func ( * args , * * kw )
2003-02-18 23:06:17 +08:00
finally :
if not self . donothing :
sys . settrace ( None )
return result
def globaltrace_countfuncs ( self , frame , why , arg ) :
"""
Handles ` call ' events (why == ' call ' ) and adds the (filename, modulename, funcname,) to the self._calledfuncs dict.
"""
if why == ' call ' :
filename , lineno , funcname , context , lineindex = \
inspect . getframeinfo ( frame , 0 )
if filename :
modulename = inspect . getmodulename ( filename )
else :
modulename = None
self . _calledfuncs [ ( filename , modulename , funcname , ) ] = 1
def globaltrace_lt ( self , frame , why , arg ) :
"""
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 ' .
"""
if why == ' call ' :
filename , lineno , funcname , context , lineindex = \
inspect . getframeinfo ( frame , 0 )
if filename :
modulename = inspect . getmodulename ( filename )
if modulename is not None :
ignore_it = self . ignore . names ( filename , modulename )
if not ignore_it :
if self . trace :
print ( " --- modulename: %s , funcname: %s "
% ( modulename , funcname ) )
return self . localtrace
else :
# XXX why no filename?
return None
def localtrace_trace_and_count ( self , frame , why , arg ) :
if why == ' line ' :
# record the file name and line number of every trace
# XXX I wish inspect offered me an optimized
# `getfilename(frame)' to use in place of the presumably
# heavier `getframeinfo()'. --Zooko 2001-10-14
2003-02-19 10:35:07 +08:00
2003-02-18 23:06:17 +08:00
filename , lineno , funcname , context , lineindex = \
inspect . getframeinfo ( frame , 1 )
key = filename , lineno
self . counts [ key ] = self . counts . get ( key , 0 ) + 1
2003-02-19 10:35:07 +08:00
2003-02-18 23:06:17 +08:00
# XXX not convinced that this memoizing is a performance
# win -- I don't know enough about Python guts to tell.
# --Zooko 2001-10-14
2003-02-19 10:35:07 +08:00
2003-02-18 23:06:17 +08:00
bname = self . pathtobasename . get ( filename )
if bname is None :
2003-02-19 10:35:07 +08:00
2003-02-18 23:06:17 +08:00
# Using setdefault faster than two separate lines?
# --Zooko 2001-10-14
bname = self . pathtobasename . setdefault ( filename ,
os . path . basename ( filename ) )
try :
print " %s ( %d ): %s " % ( bname , lineno , context [ lineindex ] ) ,
except IndexError :
# Uh.. sometimes getframeinfo gives me a context of
# length 1 and a lineindex of -2. Oh well.
pass
return self . localtrace
def localtrace_trace ( self , frame , why , arg ) :
if why == ' line ' :
# XXX shouldn't do the count increment when arg is
# exception? But be careful to return self.localtrace
# when arg is exception! ? --Zooko 2001-10-14
# record the file name and line number of every trace XXX
# I wish inspect offered me an optimized
# `getfilename(frame)' to use in place of the presumably
# heavier `getframeinfo()'. --Zooko 2001-10-14
filename , lineno , funcname , context , lineindex = \
inspect . getframeinfo ( frame )
2003-02-19 10:35:07 +08:00
2003-02-18 23:06:17 +08:00
# XXX not convinced that this memoizing is a performance
# win -- I don't know enough about Python guts to tell.
# --Zooko 2001-10-14
bname = self . pathtobasename . get ( filename )
if bname is None :
# Using setdefault faster than two separate lines?
# --Zooko 2001-10-14
bname = self . pathtobasename . setdefault ( filename , os . path . basename ( filename ) )
if context is not None :
try :
print " %s ( %d ): %s " % ( bname , lineno , context [ lineindex ] ) ,
except IndexError :
# Uh.. sometimes getframeinfo gives me a context of length 1 and a lineindex of -2. Oh well.
pass
else :
print " %s (???): ??? " % bname
return self . localtrace
def localtrace_count ( self , frame , why , arg ) :
if why == ' line ' :
filename = frame . f_code . co_filename
lineno = frame . f_lineno
key = filename , lineno
self . counts [ key ] = self . counts . get ( key , 0 ) + 1
return self . localtrace
def results ( self ) :
return CoverageResults ( self . counts , infile = self . infile ,
outfile = self . outfile ,
calledfuncs = self . _calledfuncs )
def _err_exit ( msg ) :
sys . stderr . write ( " %s : %s \n " % ( sys . argv [ 0 ] , msg ) )
sys . exit ( 1 )
def main ( argv = None ) :
import getopt
if argv is None :
argv = sys . argv
try :
opts , prog_argv = getopt . getopt ( argv [ 1 : ] , " tcrRf:d:msC:l " ,
[ " help " , " version " , " trace " , " count " ,
" report " , " no-report " , " summary " ,
" file= " , " missing " ,
" 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 . argv [ 0 ] )
sys . exit ( 1 )
trace = 0
count = 0
report = 0
no_report = 0
counts_file = None
missing = 0
ignore_modules = [ ]
ignore_dirs = [ ]
coverdir = None
summary = 0
listfuncs = False
for opt , val in opts :
if opt == " --help " :
usage ( sys . stdout )
sys . exit ( 0 )
if opt == " --version " :
sys . stdout . write ( " trace 2.0 \n " )
sys . exit ( 0 )
if opt == " -l " or opt == " --listfuncs " :
listfuncs = True
continue
if opt == " -t " or opt == " --trace " :
trace = 1
continue
if opt == " -c " or opt == " --count " :
count = 1
continue
if opt == " -r " or opt == " --report " :
report = 1
continue
if opt == " -R " or opt == " --no-report " :
no_report = 1
continue
if opt == " -f " or opt == " --file " :
counts_file = val
continue
if opt == " -m " or opt == " --missing " :
missing = 1
continue
if opt == " -C " or opt == " --coverdir " :
coverdir = val
continue
if opt == " -s " or opt == " --summary " :
summary = 1
continue
if opt == " --ignore-module " :
ignore_modules . append ( val )
continue
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)
s = s . replace ( " $prefix " ,
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 ] ) )
s = os . path . normpath ( s )
ignore_dirs . append ( s )
continue
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 and not trace and not report and not listfuncs :
_err_exit ( " must specify one of --trace, --count, --report or --listfuncs " )
if report and no_report :
_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 " )
# everything is ready
if report :
results = CoverageResults ( infile = counts_file , outfile = counts_file )
results . write_results ( missing , summary = summary , coverdir = coverdir )
else :
sys . argv = prog_argv
progname = prog_argv [ 0 ]
sys . path [ 0 ] = os . path . split ( progname ) [ 0 ]
t = Trace ( count , trace , countfuncs = listfuncs ,
ignoremods = ignore_modules , ignoredirs = ignore_dirs ,
infile = counts_file , outfile = counts_file )
try :
t . run ( ' execfile( ' + ` progname ` + ' ) ' )
except IOError , err :
_err_exit ( " Cannot run file %s because: %s " % ( ` sys . argv [ 0 ] ` , err ) )
except SystemExit :
pass
results = t . results ( )
if not no_report :
results . write_results ( missing , summary = summary , coverdir = coverdir )
if __name__ == ' __main__ ' :
main ( )