diff options
Diffstat (limited to 'lib/python2.7/trace.py')
-rw-r--r-- | lib/python2.7/trace.py | 819 |
1 files changed, 819 insertions, 0 deletions
diff --git a/lib/python2.7/trace.py b/lib/python2.7/trace.py new file mode 100644 index 0000000..6e8e837 --- /dev/null +++ b/lib/python2.7/trace.py @@ -0,0 +1,819 @@ +#!/usr/bin/env python2 + +# 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 + trace.py --trackcalls spam.py eggs + +Sample use, programmatically + import sys + + # create a Trace object, telling it what to ignore, and whether to + # do tracing or line-counting or both. + tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0, + count=1) + # run the new command using the given tracer + tracer.run('main()') + # make a report, placing output in /tmp + r = tracer.results() + r.write_results(show_missing=True, coverdir="/tmp") +""" + +import linecache +import os +import re +import sys +import time +import token +import tokenize +import inspect +import gc +import dis +try: + import cPickle + pickle = cPickle +except ImportError: + import pickle + +try: + import threading +except ImportError: + _settrace = sys.settrace + + def _unsettrace(): + sys.settrace(None) +else: + def _settrace(func): + threading.settrace(func) + sys.settrace(func) + + def _unsettrace(): + sys.settrace(None) + threading.settrace(None) + +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. +-l, --listfuncs Keep track of which functions are executed at least + once and write the results to sys.stdout after the + program exits. +-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'. + +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.) +-g, --timing Prefix each line with the time since the program started. + Only used while tracing. + +Filters, may be repeated multiple times: +--ignore-module=<mod> Ignore the given module(s) and its submodules + (if it is a package). Accepts comma separated + list of module names +--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*(#.*)?$') + +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 modulename in self._ignore: + 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 occurred, 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 + +def modname(path): + """Return a plausible module name for the patch.""" + + base = os.path.basename(path) + filename, ext = os.path.splitext(base) + return filename + +def fullmodname(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. + + comparepath = os.path.normcase(path) + longest = "" + for dir in sys.path: + dir = os.path.normcase(dir) + if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep: + if len(dir) > len(longest): + longest = dir + + if longest: + base = path[len(longest) + 1:] + else: + base = path + # the drive letter is never part of the module name + drive, base = os.path.splitdrive(base) + base = base.replace(os.sep, ".") + if os.altsep: + base = base.replace(os.altsep, ".") + filename, ext = os.path.splitext(base) + return filename.lstrip(".") + +class CoverageResults: + def __init__(self, counts=None, calledfuncs=None, infile=None, + callers=None, outfile=None): + 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.callers = callers + if self.callers is None: + self.callers = {} + self.callers = self.callers.copy() + self.infile = infile + self.outfile = outfile + if self.infile: + # Try to merge existing counts file. + try: + 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" + % (self.infile, err)) + + def update(self, other): + """Merge in the data from another CoverageResults""" + counts = self.counts + calledfuncs = self.calledfuncs + callers = self.callers + 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(): + calledfuncs[key] = 1 + + for key in other_callers.keys(): + callers[key] = 1 + + def write_results(self, show_missing=True, summary=False, coverdir=None): + """ + @param coverdir + """ + if self.calledfuncs: + print + print "functions called:" + calls = self.calledfuncs.keys() + calls.sort() + for filename, modulename, funcname in calls: + print ("filename: %s, modulename: %s, funcname: %s" + % (filename, modulename, funcname)) + + if self.callers: + print + print "calling relationships:" + calls = self.callers.keys() + calls.sort() + lastfile = lastcfile = "" + for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) in calls: + if pfile != lastfile: + print + print "***", pfile, "***" + lastfile = pfile + lastcfile = "" + if cfile != pfile and lastcfile != cfile: + print " -->", cfile + 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 + per_file = {} + 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 + sums = {} + + for filename, count in per_file.iteritems(): + # skip some "files" we don't care about... + if filename == "<string>": + continue + if filename.startswith("<doctest "): + continue + + if filename.endswith((".pyc", ".pyo")): + filename = filename[:-1] + + if coverdir is None: + dir = os.path.dirname(os.path.abspath(filename)) + modulename = modname(filename) + else: + dir = coverdir + if not os.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) + if show_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) + + if summary and n_lines: + percent = 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, self.callers), + open(self.outfile, 'wb'), 1) + except IOError, err: + 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.""" + + try: + outfile = open(path, "w") + except IOError, err: + print >> sys.stderr, ("trace: Could not open %r for writing: %s" + "- skipping" % (path, err)) + return 0, 0 + + n_lines = 0 + n_hits = 0 + for i, line in enumerate(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) + if lineno in lines_hit: + outfile.write("%5d: " % lines_hit[lineno]) + n_hits += 1 + n_lines += 1 + elif rx_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 COVER + if lineno in lnotab and not PRAGMA_NOCOVER in lines[i]: + outfile.write(">>>>>> ") + n_lines += 1 + else: + outfile.write(" ") + outfile.write(lines[i].expandtabs(8)) + outfile.close() + + return n_hits, n_lines + +def find_lines_from_code(code, strs): + """Return dict where keys are lines in the line number table.""" + linenos = {} + + for _, lineno in dis.findlinestarts(code): + if lineno not in strs: + linenos[lineno] = 1 + + return linenos + +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 + for c in code.co_consts: + if inspect.iscode(c): + # find another code object, so recurse into it + linenos.update(find_lines(c, strs)) + return linenos + +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 + 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.INDENT + f = open(filename) + for ttype, tstr, start, end, line in tokenize.generate_tokens(f.readline): + if ttype == token.STRING: + if prev_ttype == token.INDENT: + sline, scol = start + eline, ecol = end + for i in range(sline, eline + 1): + d[i] = 1 + prev_ttype = ttype + f.close() + return d + +def find_executable_linenos(filename): + """Return dict where keys are line numbers in the line number table.""" + try: + prog = open(filename, "rU").read() + except IOError, err: + print >> sys.stderr, ("Not printing coverage data for %r: %s" + % (filename, err)) + return {} + code = compile(prog, filename, "exec") + strs = find_strings(filename) + return find_lines(code, strs) + +class Trace: + def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0, + ignoremods=(), ignoredirs=(), infile=None, outfile=None, + timing=False): + """ + @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 + @param timing true iff timing information be displayed + """ + 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 = {} + self._callers = {} + self._caller_cache = {} + self.start_time = None + if timing: + self.start_time = time.time() + if countcallers: + self.globaltrace = self.globaltrace_trackcallers + elif 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__ + self.runctx(cmd, dict, dict) + + def runctx(self, cmd, globals=None, locals=None): + if globals is None: globals = {} + if locals is None: locals = {} + if not self.donothing: + _settrace(self.globaltrace) + try: + exec cmd in globals, locals + finally: + if not self.donothing: + _unsettrace() + + def runfunc(self, func, *args, **kw): + result = None + if not self.donothing: + sys.settrace(self.globaltrace) + try: + result = func(*args, **kw) + finally: + if not self.donothing: + sys.settrace(None) + return result + + def file_module_function_of(self, frame): + code = frame.f_code + filename = code.co_filename + if filename: + modulename = modname(filename) + else: + modulename = None + + funcname = code.co_name + clsname = None + if code in self._caller_cache: + if self._caller_cache[code] is not None: + clsname = self._caller_cache[code] + else: + 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 inspect.isfunction(f)] + # require len(func) == 1 to avoid ambiguity caused by calls to + # new.function(): "In the face of ambiguity, refuse the + # temptation to guess." + if len(funcs) == 1: + dicts = [d for d in gc.get_referrers(funcs[0]) + if isinstance(d, dict)] + if len(dicts) == 1: + classes = [c for c in gc.get_referrers(dicts[0]) + if hasattr(c, "__bases__")] + if len(classes) == 1: + # ditto for new.classobj() + clsname = classes[0].__name__ + # 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 + if clsname is not None: + 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. + """ + if why == 'call': + # 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. + """ + if why == 'call': + 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. + """ + if why == 'call': + code = frame.f_code + filename = frame.f_globals.get('__file__', None) + if 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) + if not ignore_it: + if self.trace: + print (" --- modulename: %s, funcname: %s" + % (modulename, code.co_name)) + return self.localtrace + else: + return None + + def localtrace_trace_and_count(self, frame, why, arg): + if why == "line": + # record the file name and line number of every trace + filename = frame.f_code.co_filename + lineno = frame.f_lineno + key = filename, lineno + self.counts[key] = self.counts.get(key, 0) + 1 + + if self.start_time: + print '%.2f' % (time.time() - self.start_time), + bname = os.path.basename(filename) + print "%s(%d): %s" % (bname, lineno, + linecache.getline(filename, lineno)), + return self.localtrace + + def localtrace_trace(self, frame, why, arg): + if why == "line": + # record the file name and line number of every trace + filename = frame.f_code.co_filename + lineno = frame.f_lineno + + if self.start_time: + print '%.2f' % (time.time() - self.start_time), + bname = os.path.basename(filename) + print "%s(%d): %s" % (bname, lineno, + linecache.getline(filename, lineno)), + 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, + callers=self._callers) + +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:lTg", + ["help", "version", "trace", "count", + "report", "no-report", "summary", + "file=", "missing", + "ignore-module=", "ignore-dir=", + "coverdir=", "listfuncs", + "trackcalls", "timing"]) + + 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 + countcallers = False + timing = 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 == "-T" or opt == "--trackcalls": + countcallers = True + continue + + if opt == "-l" or opt == "--listfuncs": + listfuncs = True + continue + + if opt == "-g" or opt == "--timing": + timing = 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": + for mod in val.split(","): + ignore_modules.append(mod.strip()) + 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 or trace or report or listfuncs or countcallers): + _err_exit("must specify one of --trace, --count, --report, " + "--listfuncs, or --trackcalls") + + 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, + countcallers=countcallers, ignoremods=ignore_modules, + ignoredirs=ignore_dirs, infile=counts_file, + outfile=counts_file, timing=timing) + try: + with open(progname) as fp: + code = compile(fp.read(), progname, 'exec') + # try to emulate __main__ namespace as much as possible + globs = { + '__file__': progname, + '__name__': '__main__', + '__package__': None, + '__cached__': None, + } + t.runctx(code, globs, globs) + except IOError, err: + _err_exit("Cannot run file %r 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() |