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
35
trace.py --trackcalls spam.py eggs
37
Sample use, programmatically
40
# create a Trace object, telling it what to ignore, and whether to
41
# do tracing or line-counting or both.
42
tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0,
44
# run the new command using the given tracer
46
# make a report, placing output in /tmp
48
r.write_results(show_missing=True, coverdir="/tmp")
65
outfile.write("""Usage: %s [OPTIONS] <file> [ARGS]
68
--help Display this help then exit.
69
--version Output version information then exit.
71
Otherwise, exactly one of the following three options must be given:
72
-t, --trace Print each line to sys.stdout before it is executed.
73
-c, --count Count the number of times each line is executed
74
and write the counts to <module>.cover for each
75
module executed, in the module's directory.
76
See also `--coverdir', `--file', `--no-report' below.
77
-l, --listfuncs Keep track of which functions are executed at least
78
once and write the results to sys.stdout after the
80
-T, --trackcalls Keep track of caller/called pairs and write the
81
results to sys.stdout after the program exits.
82
-r, --report Generate a report from a counts file; do not execute
83
any code. `--file' must specify the results file to
84
read, which must have been created in a previous run
85
with `--count --file=FILE'.
88
-f, --file=<file> File to accumulate counts over several runs.
89
-R, --no-report Do not generate the coverage report files.
90
Useful if you want to accumulate over several runs.
91
-C, --coverdir=<dir> Directory where the report files. The coverage
92
report for <package>.<module> is written to file
93
<dir>/<package>/<module>.cover.
94
-m, --missing Annotate executable lines that were not executed
96
-s, --summary Write a brief summary on stdout for each file.
97
(Can only be used with --count or --report.)
98
-g, --timing Prefix each line with the time since the program started.
99
Only used while tracing.
101
Filters, may be repeated multiple times:
102
--ignore-module=<mod> Ignore the given module(s) and its submodules
103
(if it is a package). Accepts comma separated
105
--ignore-dir=<dir> Ignore files in the given directory (multiple
106
directories can be joined by os.pathsep).
109
PRAGMA_NOCOVER = "#pragma NO COVER"
111
# Simple rx to find lines with no code.
112
rx_blank = re.compile(r'^\s*(#.*)?$')
115
def __init__(self, modules = None, dirs = None):
116
self._mods = modules or []
117
self._dirs = dirs or []
119
self._dirs = map(os.path.normpath, self._dirs)
120
self._ignore = { '<string>': 1 }
122
def names(self, filename, modulename):
123
if modulename in self._ignore:
124
return self._ignore[modulename]
126
# haven't seen this one before, so see if the module name is
127
# on the ignore list. Need to take some care since ignoring
128
# "cmp" musn't mean ignoring "cmpcache" but ignoring
129
# "Spam" must also mean ignoring "Spam.Eggs".
130
for mod in self._mods:
131
if mod == modulename: # Identical names, so ignore
132
self._ignore[modulename] = 1
134
# check if the module is a proper submodule of something on
137
# (will not overflow since if the first n characters are the
138
# same and the name has not already occurred, then the size
139
# of "name" is greater than that of "mod")
140
if mod == modulename[:n] and modulename[n] == '.':
141
self._ignore[modulename] = 1
144
# Now check that __file__ isn't in one of the directories
146
# must be a built-in, so we must ignore
147
self._ignore[modulename] = 1
150
# Ignore a file when it contains one of the ignorable paths
152
# The '+ os.sep' is to ensure that d is a parent directory,
153
# as compared to cases like:
155
# filename = "/usr/local.py"
157
# d = "/usr/local.py"
158
# filename = "/usr/local.py"
159
if filename.startswith(d + os.sep):
160
self._ignore[modulename] = 1
163
# Tried the different ways, so we don't ignore this module
164
self._ignore[modulename] = 0
168
"""Return a plausible module name for the patch."""
170
base = os.path.basename(path)
171
filename, ext = os.path.splitext(base)
174
def fullmodname(path):
175
"""Return a plausible module name for the path."""
177
# If the file 'path' is part of a package, then the filename isn't
178
# enough to uniquely identify it. Try to do the right thing by
179
# looking in sys.path for the longest matching prefix. We'll
180
# assume that the rest is the package name.
182
comparepath = os.path.normcase(path)
185
dir = os.path.normcase(dir)
186
if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep:
187
if len(dir) > len(longest):
191
base = path[len(longest) + 1:]
194
base = base.replace(os.sep, ".")
196
base = base.replace(os.altsep, ".")
197
filename, ext = os.path.splitext(base)
200
class CoverageResults:
201
def __init__(self, counts=None, calledfuncs=None, infile=None,
202
callers=None, outfile=None):
204
if self.counts is None:
206
self.counter = self.counts.copy() # map (filename, lineno) to count
207
self.calledfuncs = calledfuncs
208
if self.calledfuncs is None:
209
self.calledfuncs = {}
210
self.calledfuncs = self.calledfuncs.copy()
211
self.callers = callers
212
if self.callers is None:
214
self.callers = self.callers.copy()
216
self.outfile = outfile
218
# Try to merge existing counts file.
220
counts, calledfuncs, callers = \
221
pickle.load(open(self.infile, 'rb'))
222
self.update(self.__class__(counts, calledfuncs, callers))
223
except (IOError, EOFError, ValueError) as err:
224
print(("Skipping counts file %r: %s"
225
% (self.infile, err)), file=sys.stderr)
227
def update(self, other):
228
"""Merge in the data from another CoverageResults"""
230
calledfuncs = self.calledfuncs
231
callers = self.callers
232
other_counts = other.counts
233
other_calledfuncs = other.calledfuncs
234
other_callers = other.callers
236
for key in other_counts.keys():
237
counts[key] = counts.get(key, 0) + other_counts[key]
239
for key in other_calledfuncs.keys():
242
for key in other_callers.keys():
245
def write_results(self, show_missing=True, summary=False, coverdir=None):
251
print("functions called:")
252
for filename, modulename, funcname in sorted(calls.keys()):
253
print(("filename: %s, modulename: %s, funcname: %s"
254
% (filename, modulename, funcname)))
258
print("calling relationships:")
259
lastfile = lastcfile = ""
260
for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) in sorted(self.callers.keys()):
261
if pfile != lastfile:
263
print("***", pfile, "***")
266
if cfile != pfile and lastcfile != cfile:
269
print(" %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc))
271
# turn the counts data ("(filename, lineno) = count") into something
272
# accessible on a per-file basis
274
for filename, lineno in self.counts.keys():
275
lines_hit = per_file[filename] = per_file.get(filename, {})
276
lines_hit[lineno] = self.counts[(filename, lineno)]
278
# accumulate summary info, if needed
281
for filename, count in per_file.items():
282
# skip some "files" we don't care about...
283
if filename == "<string>":
285
if filename.startswith("<doctest "):
288
if filename.endswith((".pyc", ".pyo")):
289
filename = filename[:-1]
292
dir = os.path.dirname(os.path.abspath(filename))
293
modulename = modname(filename)
296
if not os.path.exists(dir):
298
modulename = fullmodname(filename)
300
# If desired, get a list of the line numbers which represent
301
# executable content (returned as a dict for better lookup speed)
303
lnotab = find_executable_linenos(filename)
307
source = linecache.getlines(filename)
308
coverpath = os.path.join(dir, modulename + ".cover")
309
n_hits, n_lines = self.write_results_file(coverpath, source,
312
if summary and n_lines:
313
percent = int(100 * n_hits / n_lines)
314
sums[modulename] = n_lines, percent, modulename, filename
317
print("lines cov% module (path)")
318
for m in sorted(sums.keys()):
319
n_lines, percent, modulename, filename = sums[m]
320
print("%5d %3d%% %s (%s)" % sums[m])
323
# try and store counts and module info into self.outfile
325
pickle.dump((self.counts, self.calledfuncs, self.callers),
326
open(self.outfile, 'wb'), 1)
327
except IOError as err:
328
print("Can't save counts files because %s" % err, file=sys.stderr)
330
def write_results_file(self, path, lines, lnotab, lines_hit):
331
"""Return a coverage results file in path."""
334
outfile = open(path, "w")
335
except IOError as err:
336
print(("trace: Could not open %r for writing: %s"
337
"- skipping" % (path, err)), file=sys.stderr)
342
for i, line in enumerate(lines):
344
# do the blank/comment match to try to mark more lines
345
# (help the reader find stuff that hasn't been covered)
346
if lineno in lines_hit:
347
outfile.write("%5d: " % lines_hit[lineno])
350
elif rx_blank.match(line):
353
# lines preceded by no marks weren't hit
354
# Highlight them if so indicated, unless the line contains
356
if lineno in lnotab and not PRAGMA_NOCOVER in lines[i]:
357
outfile.write(">>>>>> ")
361
outfile.write(lines[i].expandtabs(8))
364
return n_hits, n_lines
366
def find_lines_from_code(code, strs):
367
"""Return dict where keys are lines in the line number table."""
370
line_increments = code.co_lnotab[1::2]
371
table_length = len(line_increments)
374
lineno = code.co_firstlineno
375
for li in line_increments:
377
if lineno not in strs:
382
def find_lines(code, strs):
383
"""Return lineno dict for all code objects reachable from code."""
384
# get all of the lineno information from the code of this scope level
385
linenos = find_lines_from_code(code, strs)
387
# and check the constants for references to other code objects
388
for c in code.co_consts:
389
if isinstance(c, types.CodeType):
390
# find another code object, so recurse into it
391
linenos.update(find_lines(c, strs))
394
def find_strings(filename):
395
"""Return a dict of possible docstring positions.
397
The dict maps line numbers to strings. There is an entry for
398
line that contains only a string or a part of a triple-quoted
402
# If the first token is a string, then it's the module docstring.
403
# Add this special case so that the test in the loop passes.
404
prev_ttype = token.INDENT
406
for ttype, tstr, start, end, line in tokenize.generate_tokens(f.readline):
407
if ttype == token.STRING:
408
if prev_ttype == token.INDENT:
411
for i in range(sline, eline + 1):
417
def find_executable_linenos(filename):
418
"""Return dict where keys are line numbers in the line number table."""
420
prog = open(filename, "rU").read()
421
except IOError as err:
422
print(("Not printing coverage data for %r: %s"
423
% (filename, err)), file=sys.stderr)
425
code = compile(prog, filename, "exec")
426
strs = find_strings(filename)
427
return find_lines(code, strs)
430
def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0,
431
ignoremods=(), ignoredirs=(), infile=None, outfile=None,
434
@param count true iff it should count number of times each
436
@param trace true iff it should print out each line that is
438
@param countfuncs true iff it should just output a list of
439
(filename, modulename, funcname,) for functions
440
that were called at least once; This overrides
442
@param ignoremods a list of the names of modules to ignore
443
@param ignoredirs a list of the names of directories to ignore
444
all of the (recursive) contents of
445
@param infile file from which to read stored counts to be
446
added into the results
447
@param outfile file in which to write the results
448
@param timing true iff timing information be displayed
451
self.outfile = outfile
452
self.ignore = Ignore(ignoremods, ignoredirs)
453
self.counts = {} # keys are (filename, linenumber)
454
self.blabbed = {} # for debugging
455
self.pathtobasename = {} # for memoizing os.path.basename
458
self._calledfuncs = {}
460
self._caller_cache = {}
461
self.start_time = None
463
self.start_time = time.time()
465
self.globaltrace = self.globaltrace_trackcallers
467
self.globaltrace = self.globaltrace_countfuncs
468
elif trace and count:
469
self.globaltrace = self.globaltrace_lt
470
self.localtrace = self.localtrace_trace_and_count
472
self.globaltrace = self.globaltrace_lt
473
self.localtrace = self.localtrace_trace
475
self.globaltrace = self.globaltrace_lt
476
self.localtrace = self.localtrace_count
478
# Ahem -- do nothing? Okay.
483
dict = __main__.__dict__
484
if not self.donothing:
485
sys.settrace(self.globaltrace)
486
threading.settrace(self.globaltrace)
488
exec(cmd, dict, dict)
490
if not self.donothing:
492
threading.settrace(None)
494
def runctx(self, cmd, globals=None, locals=None):
495
if globals is None: globals = {}
496
if locals is None: locals = {}
497
if not self.donothing:
498
sys.settrace(self.globaltrace)
499
threading.settrace(self.globaltrace)
501
exec(cmd, globals, locals)
503
if not self.donothing:
505
threading.settrace(None)
507
def runfunc(self, func, *args, **kw):
509
if not self.donothing:
510
sys.settrace(self.globaltrace)
512
result = func(*args, **kw)
514
if not self.donothing:
518
def file_module_function_of(self, frame):
520
filename = code.co_filename
522
modulename = modname(filename)
526
funcname = code.co_name
528
if code in self._caller_cache:
529
if self._caller_cache[code] is not None:
530
clsname = self._caller_cache[code]
532
self._caller_cache[code] = None
533
## use of gc.get_referrers() was suggested by Michael Hudson
534
# all functions which refer to this code object
535
funcs = [f for f in gc.get_referrers(code)
536
if hasattr(f, "__doc__")]
537
# require len(func) == 1 to avoid ambiguity caused by calls to
538
# new.function(): "In the face of ambiguity, refuse the
539
# temptation to guess."
541
dicts = [d for d in gc.get_referrers(funcs[0])
542
if isinstance(d, dict)]
544
classes = [c for c in gc.get_referrers(dicts[0])
545
if hasattr(c, "__bases__")]
546
if len(classes) == 1:
547
# ditto for new.classobj()
548
clsname = str(classes[0])
549
# cache the result - assumption is that new.* is
550
# not called later to disturb this relationship
551
# _caller_cache could be flushed if functions in
552
# the new module get called.
553
self._caller_cache[code] = clsname
554
if clsname is not None:
555
# final hack - module name shows up in str(cls), but we've already
556
# computed module name, so remove it
557
clsname = clsname.split(".")[1:]
558
clsname = ".".join(clsname)
559
funcname = "%s.%s" % (clsname, funcname)
561
return filename, modulename, funcname
563
def globaltrace_trackcallers(self, frame, why, arg):
564
"""Handler for call events.
566
Adds information about who called who to the self._callers dict.
569
# XXX Should do a better job of identifying methods
570
this_func = self.file_module_function_of(frame)
571
parent_func = self.file_module_function_of(frame.f_back)
572
self._callers[(parent_func, this_func)] = 1
574
def globaltrace_countfuncs(self, frame, why, arg):
575
"""Handler for call events.
577
Adds (filename, modulename, funcname) to the self._calledfuncs dict.
580
this_func = self.file_module_function_of(frame)
581
self._calledfuncs[this_func] = 1
583
def globaltrace_lt(self, frame, why, arg):
584
"""Handler for call events.
586
If the code block being entered is to be ignored, returns `None',
587
else returns self.localtrace.
591
filename = frame.f_globals.get('__file__', None)
593
# XXX modname() doesn't work right for packages, so
594
# the ignore support won't work right for packages
595
modulename = modname(filename)
596
if modulename is not None:
597
ignore_it = self.ignore.names(filename, modulename)
600
print((" --- modulename: %s, funcname: %s"
601
% (modulename, code.co_name)))
602
return self.localtrace
606
def localtrace_trace_and_count(self, frame, why, arg):
608
# record the file name and line number of every trace
609
filename = frame.f_code.co_filename
610
lineno = frame.f_lineno
611
key = filename, lineno
612
self.counts[key] = self.counts.get(key, 0) + 1
615
print('%.2f' % (time.time() - self.start_time), end=' ')
616
bname = os.path.basename(filename)
617
print("%s(%d): %s" % (bname, lineno,
618
linecache.getline(filename, lineno)), end=' ')
619
return self.localtrace
621
def localtrace_trace(self, frame, why, arg):
623
# record the file name and line number of every trace
624
filename = frame.f_code.co_filename
625
lineno = frame.f_lineno
628
print('%.2f' % (time.time() - self.start_time), end=' ')
629
bname = os.path.basename(filename)
630
print("%s(%d): %s" % (bname, lineno,
631
linecache.getline(filename, lineno)), end=' ')
632
return self.localtrace
634
def localtrace_count(self, frame, why, arg):
636
filename = frame.f_code.co_filename
637
lineno = frame.f_lineno
638
key = filename, lineno
639
self.counts[key] = self.counts.get(key, 0) + 1
640
return self.localtrace
643
return CoverageResults(self.counts, infile=self.infile,
644
outfile=self.outfile,
645
calledfuncs=self._calledfuncs,
646
callers=self._callers)
649
sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
658
opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:lTg",
659
["help", "version", "trace", "count",
660
"report", "no-report", "summary",
662
"ignore-module=", "ignore-dir=",
663
"coverdir=", "listfuncs",
664
"trackcalls", "timing"])
666
except getopt.error as msg:
667
sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
668
sys.stderr.write("Try `%s --help' for more information\n"
686
for opt, val in opts:
691
if opt == "--version":
692
sys.stdout.write("trace 2.0\n")
695
if opt == "-T" or opt == "--trackcalls":
699
if opt == "-l" or opt == "--listfuncs":
703
if opt == "-g" or opt == "--timing":
707
if opt == "-t" or opt == "--trace":
711
if opt == "-c" or opt == "--count":
715
if opt == "-r" or opt == "--report":
719
if opt == "-R" or opt == "--no-report":
723
if opt == "-f" or opt == "--file":
727
if opt == "-m" or opt == "--missing":
731
if opt == "-C" or opt == "--coverdir":
735
if opt == "-s" or opt == "--summary":
739
if opt == "--ignore-module":
740
for mod in val.split(","):
741
ignore_modules.append(mod.strip())
744
if opt == "--ignore-dir":
745
for s in val.split(os.pathsep):
746
s = os.path.expandvars(s)
747
# should I also call expanduser? (after all, could use $HOME)
749
s = s.replace("$prefix",
750
os.path.join(sys.prefix, "lib",
751
"python" + sys.version[:3]))
752
s = s.replace("$exec_prefix",
753
os.path.join(sys.exec_prefix, "lib",
754
"python" + sys.version[:3]))
755
s = os.path.normpath(s)
756
ignore_dirs.append(s)
759
assert 0, "Should never get here"
761
if listfuncs and (count or trace):
762
_err_exit("cannot specify both --listfuncs and (--trace or --count)")
764
if not (count or trace or report or listfuncs or countcallers):
765
_err_exit("must specify one of --trace, --count, --report, "
766
"--listfuncs, or --trackcalls")
768
if report and no_report:
769
_err_exit("cannot specify both --report and --no-report")
771
if report and not counts_file:
772
_err_exit("--report requires a --file")
774
if no_report and len(prog_argv) == 0:
775
_err_exit("missing name of file to run")
777
# everything is ready
779
results = CoverageResults(infile=counts_file, outfile=counts_file)
780
results.write_results(missing, summary=summary, coverdir=coverdir)
783
progname = prog_argv[0]
784
sys.path[0] = os.path.split(progname)[0]
786
t = Trace(count, trace, countfuncs=listfuncs,
787
countcallers=countcallers, ignoremods=ignore_modules,
788
ignoredirs=ignore_dirs, infile=counts_file,
789
outfile=counts_file, timing=timing)
796
t.run('exec(%r)' % (script,))
797
except IOError as err:
798
_err_exit("Cannot run file %r because: %s" % (sys.argv[0], err))
802
results = t.results()
805
results.write_results(missing, summary=summary, coverdir=coverdir)
807
if __name__=='__main__':