#!/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 executionSample use, command line: trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs trace.py -t --ignore-dir '$prefix' spam.py eggsSample 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=True)"""importlinecacheimportmarshalimportosimportreimportsysimportthreadingimporttokenimporttokenizeimporttypestry:importcPicklepickle=cPickleexceptImportError:importpickledefusage(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.-l, --listfuncs Keep track of which functions are executed at least once 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'.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)."""%sys.argv[0])PRAGMA_NOCOVER="#pragma NO COVER"# Simple rx to find lines with no code.rx_blank=re.compile(r'^\s*(#.*)?$')classIgnore:def__init__(self,modules=None,dirs=None):self._mods=modulesor[]self._dirs=dirsor[]self._dirs=map(os.path.normpath,self._dirs)self._ignore={'<string>':1}defnames(self,filename,modulename):ifself._ignore.has_key(modulename):returnself._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".formodinself._mods:ifmod==modulename:# Identical names, so ignoreself._ignore[modulename]=1return1# check if the module is a proper submodule of something on# the ignore listn=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")ifmod==modulename[:n]andmodulename[n]=='.':self._ignore[modulename]=1return1# Now check that __file__ isn't in one of the directoriesiffilenameisNone:# must be a built-in, so we must ignoreself._ignore[modulename]=1return1# Ignore a file when it contains one of the ignorable pathsfordinself._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"iffilename.startswith(d+os.sep):self._ignore[modulename]=1return1# Tried the different ways, so we don't ignore this moduleself._ignore[modulename]=0return0defmodname(path):"""Return a plausible module name for the patch."""base=os.path.basename(path)filename,ext=os.path.splitext(base)returnfilenamedeffullmodname(path):"""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.longest=""fordirinsys.path:ifpath.startswith(dir)andpath[len(dir)]==os.path.sep:iflen(dir)>len(longest):longest=diriflongest:base=path[len(longest)+1:]else:base=pathbase=base.replace(os.sep,".")ifos.altsep:base=base.replace(os.altsep,".")filename,ext=os.path.splitext(base)returnfilenameclassCoverageResults:def__init__(self,counts=None,calledfuncs=None,infile=None,outfile=None):self.counts=countsifself.countsisNone:self.counts={}self.counter=self.counts.copy()# map (filename, lineno) to countself.calledfuncs=calledfuncsifself.calledfuncsisNone:self.calledfuncs={}self.calledfuncs=self.calledfuncs.copy()self.infile=infileself.outfile=outfileifself.infile:# Try to merge existing counts file.try:counts,calledfuncs=pickle.load(open(self.infile,'rb'))self.update(self.__class__(counts,calledfuncs))except(IOError,EOFError,ValueError),err:print>>sys.stderr,("Skipping counts file %r: %s"%(self.infile,err))defupdate(self,other):"""Merge in the data from another CoverageResults"""counts=self.countscalledfuncs=self.calledfuncsother_counts=other.countsother_calledfuncs=other.calledfuncsforkeyinother_counts.keys():counts[key]=counts.get(key,0)+other_counts[key]forkeyinother_calledfuncs.keys():calledfuncs[key]=1defwrite_results(self,show_missing=True,summary=False,coverdir=None):""" @param coverdir """forfilename,modulename,funcnameinself.calledfuncs.keys():print("filename: %s, modulename: %s, funcname: %s"%(filename,modulename,funcname))# turn the counts data ("(filename, lineno) = count") into something# accessible on a per-file basisper_file={}forfilename,linenoinself.counts.keys():lines_hit=per_file[filename]=per_file.get(filename,{})lines_hit[lineno]=self.counts[(filename,lineno)]# accumulate summary info, if neededsums={}forfilename,countinper_file.iteritems():# skip some "files" we don't care about...iffilename=="<string>":continueiffilename.endswith(".pyc")orfilename.endswith(".pyo"):filename=filename[:-1]ifcoverdirisNone:dir=os.path.dirname(os.path.abspath(filename))modulename=modname(filename)else:dir=coverdirifnotos.path.exists(dir):os.makedirs(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)ifshow_missing:lnotab=find_executable_linenos(filename)else:lnotab={}source=linecache.getlines(filename)coverpath=os.path.join(dir,modulename+".cover")n_hits,n_lines=self.write_results_file(coverpath,source,lnotab,count)ifsummaryandn_lines:percent=int(100*n_hits/n_lines)sums[modulename]=n_lines,percent,modulename,filenameifsummaryandsums:mods=sums.keys()mods.sort()print"lines cov% module (path)"forminmods:n_lines,percent,modulename,filename=sums[m]print"%5d%3d%%%s (%s)"%sums[m]ifself.outfile:# try and store counts and module info into self.outfiletry:pickle.dump((self.counts,self.calledfuncs),open(self.outfile,'wb'),1)exceptIOError,err:print>>sys.stderr,"Can't save counts files because %s"%errdefwrite_results_file(self,path,lines,lnotab,lines_hit):"""Return a coverage results file in path."""try:outfile=open(path,"w")exceptIOError,err:print>>sys.stderr,("trace: Could not open %r for writing: %s""- skipping"%(path,err))return0,0n_lines=0n_hits=0fori,lineinenumerate(lines):lineno=i+1# do the blank/comment match to try to mark more lines# (help the reader find stuff that hasn't been covered)iflinenoinlines_hit:outfile.write("%5d: "%lines_hit[lineno])n_hits+=1n_lines+=1elifrx_blank.match(line):outfile.write(" ")else:# lines preceded by no marks weren't hit# Highlight them if so indicated, unless the line contains# #pragma: NO COVERiflinenoinlnotabandnotPRAGMA_NOCOVERinlines[i]:outfile.write(">>>>>> ")n_lines+=1else:outfile.write(" ")outfile.write(lines[i].expandtabs(8))outfile.close()returnn_hits,n_linesdeffind_lines_from_code(code,strs):"""Return dict where keys are lines in the line number table."""linenos={}line_increments=[ord(c)forcincode.co_lnotab[1::2]]table_length=len(line_increments)docstring=Falselineno=code.co_firstlinenoforliinline_increments:lineno+=liiflinenonotinstrs:linenos[lineno]=1returnlinenosdeffind_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 levellinenos=find_lines_from_code(code,strs)# and check the constants for references to other code objectsforcincode.co_consts:ifisinstance(c,types.CodeType):# find another code object, so recurse into itlinenos.update(find_lines(c,strs))returnlinenosdeffind_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 string. """d={}# 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.INDENTf=open(filename)forttype,tstr,start,end,lineintokenize.generate_tokens(f.readline):ifttype==token.STRING:ifprev_ttype==token.INDENT:sline,scol=starteline,ecol=endforiinrange(sline,eline+1):d[i]=1prev_ttype=ttypef.close()returnddeffind_executable_linenos(filename):"""Return dict where keys are line numbers in the line number table."""assertfilename.endswith('.py')try:prog=open(filename,"rU").read()exceptIOError,err:print>>sys.stderr,("Not printing coverage data for %r: %s"%(filename,err))return{}code=compile(prog,filename,"exec")strs=find_strings(filename)returnfind_lines(code,strs)classTrace: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 line is executed @param trace true iff it should print out each line that is being counted @param countfuncs true iff it should just output a list of (filename, modulename, funcname,) for functions that were called at least once; This overrides `count' and `trace' @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 added into the results @param outfile file in which to write the results """self.infile=infileself.outfile=outfileself.ignore=Ignore(ignoremods,ignoredirs)self.counts={}# keys are (filename, linenumber)self.blabbed={}# for debuggingself.pathtobasename={}# for memoizing os.path.basenameself.donothing=0self.trace=traceself._calledfuncs={}ifcountfuncs:self.globaltrace=self.globaltrace_countfuncseliftraceandcount:self.globaltrace=self.globaltrace_ltself.localtrace=self.localtrace_trace_and_counteliftrace:self.globaltrace=self.globaltrace_ltself.localtrace=self.localtrace_traceelifcount:self.globaltrace=self.globaltrace_ltself.localtrace=self.localtrace_countelse:# Ahem -- do nothing? Okay.self.donothing=1defrun(self,cmd):import__main__dict=__main__.__dict__ifnotself.donothing:sys.settrace(self.globaltrace)threading.settrace(self.globaltrace)try:execcmdindict,dictfinally:ifnotself.donothing:sys.settrace(None)threading.settrace(None)defrunctx(self,cmd,globals=None,locals=None):ifglobalsisNone:globals={}iflocalsisNone:locals={}ifnotself.donothing:sys.settrace(self.globaltrace)threading.settrace(self.globaltrace)try:execcmdinglobals,localsfinally:ifnotself.donothing:sys.settrace(None)threading.settrace(None)defrunfunc(self,func,*args,**kw):result=Noneifnotself.donothing:sys.settrace(self.globaltrace)try:result=func(*args,**kw)finally:ifnotself.donothing:sys.settrace(None)returnresultdefglobaltrace_countfuncs(self,frame,why,arg):"""Handler for call events. Adds (filename, modulename, funcname) to the self._calledfuncs dict. """ifwhy=='call':code=frame.f_codefilename=code.co_filenamefuncname=code.co_nameiffilename:modulename=modname(filename)else:modulename=Noneself._calledfuncs[(filename,modulename,funcname)]=1defglobaltrace_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. """ifwhy=='call':code=frame.f_codefilename=code.co_filenameiffilename:# XXX modname() doesn't work right for packages, so# the ignore support won't work right for packagesmodulename=modname(filename)ifmodulenameisnotNone:ignore_it=self.ignore.names(filename,modulename)ifnotignore_it:ifself.trace:print(" --- modulename: %s, funcname: %s"%(modulename,code.co_name))returnself.localtraceelse:returnNonedeflocaltrace_trace_and_count(self,frame,why,arg):ifwhy=="line":# record the file name and line number of every tracefilename=frame.f_code.co_filenamelineno=frame.f_linenokey=filename,linenoself.counts[key]=self.counts.get(key,0)+1bname=os.path.basename(filename)print"%s(%d): %s"%(bname,lineno,linecache.getline(filename,lineno)),returnself.localtracedeflocaltrace_trace(self,frame,why,arg):ifwhy=="line":# record the file name and line number of every tracefilename=frame.f_code.co_filenamelineno=frame.f_linenobname=os.path.basename(filename)print"%s(%d): %s"%(bname,lineno,linecache.getline(filename,lineno)),returnself.localtracedeflocaltrace_count(self,frame,why,arg):ifwhy=="line":filename=frame.f_code.co_filenamelineno=frame.f_linenokey=filename,linenoself.counts[key]=self.counts.get(key,0)+1returnself.localtracedefresults(self):returnCoverageResults(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)defmain(argv=None):importgetoptifargvisNone:argv=sys.argvtry: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",])exceptgetopt.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=0count=0report=0no_report=0counts_file=Nonemissing=0ignore_modules=[]ignore_dirs=[]coverdir=Nonesummary=0listfuncs=Falseforopt,valinopts:ifopt=="--help":usage(sys.stdout)sys.exit(0)ifopt=="--version":sys.stdout.write("trace 2.0\n")sys.exit(0)ifopt=="-l"oropt=="--listfuncs":listfuncs=Truecontinueifopt=="-t"oropt=="--trace":trace=1continueifopt=="-c"oropt=="--count":count=1continueifopt=="-r"oropt=="--report":report=1continueifopt=="-R"oropt=="--no-report":no_report=1continueifopt=="-f"oropt=="--file":counts_file=valcontinueifopt=="-m"oropt=="--missing":missing=1continueifopt=="-C"oropt=="--coverdir":coverdir=valcontinueifopt=="-s"oropt=="--summary":summary=1continueifopt=="--ignore-module":ignore_modules.append(val)continueifopt=="--ignore-dir":forsinval.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)continueassert0,"Should never get here"iflistfuncsand(countortrace):_err_exit("cannot specify both --listfuncs and (--trace or --count)")ifnotcountandnottraceandnotreportandnotlistfuncs:_err_exit("must specify one of --trace, --count, --report or ""--listfuncs")ifreportandno_report:_err_exit("cannot specify both --report and --no-report")ifreportandnotcounts_file:_err_exit("--report requires a --file")ifno_reportandlen(prog_argv)==0:_err_exit("missing name of file to run")# everything is readyifreport:results=CoverageResults(infile=counts_file,outfile=counts_file)results.write_results(missing,summary=summary,coverdir=coverdir)else:sys.argv=prog_argvprogname=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`+')')exceptIOError,err:_err_exit("Cannot run file %r because: %s"%(sys.argv[0],err))exceptSystemExit:passresults=t.results()ifnotno_report:results.write_results(missing,summary=summary,coverdir=coverdir)if__name__=='__main__':main()