Merged release21-maint changes.
[python/dscho.git] / Tools / scripts / trace.py
blob35dc71f33e75ba39d997ebe2c7f8db31889ce578
1 #!/usr/bin/env python
3 # Copyright 2000, Mojam Media, Inc., all rights reserved.
4 # Author: Skip Montanaro
6 # Copyright 1999, Bioreason, Inc., all rights reserved.
7 # Author: Andrew Dalke
9 # Copyright 1995-1997, Automatrix, Inc., all rights reserved.
10 # Author: Skip Montanaro
12 # Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
15 # Permission to use, copy, modify, and distribute this Python software and
16 # its associated documentation for any purpose without fee is hereby
17 # granted, provided that the above copyright notice appears in all copies,
18 # and that both that copyright notice and this permission notice appear in
19 # supporting documentation, and that the name of neither Automatrix,
20 # Bioreason or Mojam Media be used in advertising or publicity pertaining to
21 # distribution of the software without specific, written prior permission.
24 # Summary of recent changes:
25 # Support for files with the same basename (submodules in packages)
26 # Expanded the idea of how to ignore files or modules
27 # Split tracing and counting into different classes
28 # Extracted count information and reporting from the count class
29 # Added some ability to detect which missing lines could be executed
30 # Added pseudo-pragma to prohibit complaining about unexecuted lines
31 # Rewrote the main program
33 # Summary of older changes:
34 # Added run-time display of statements being executed
35 # Incorporated portability and performance fixes from Greg Stein
36 # Incorporated main program from Michael Scharf
38 """
39 program/module to trace Python program or function execution
41 Sample use, command line:
42 trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
43 trace.py -t --ignore-dir '$prefix' spam.py eggs
45 Sample use, programmatically (still more complicated than it should be)
46 # create an Ignore option, telling it what you want to ignore
47 ignore = trace.Ignore(dirs = [sys.prefix, sys.exec_prefix])
48 # create a Coverage object, telling it what to ignore
49 coverage = trace.Coverage(ignore)
50 # run the new command using the given trace
51 trace.run(coverage.trace, 'main()')
53 # make a report, telling it where you want output
54 t = trace.create_results_log(coverage.results(),
55 '/usr/local/Automatrix/concerts/coverage')
56 show_missing = 1)
58 The Trace class can be instantited instead of the Coverage class if
59 runtime display of executable lines is desired instead of statement
60 converage measurement.
61 """
63 import sys, os, string, marshal, tempfile, copy, operator
65 def usage(outfile):
66 outfile.write("""Usage: %s [OPTIONS] <file> [ARGS]
68 Execution:
69 --help Display this help then exit.
70 --version Output version information then exit.
71 -t,--trace Print the line to be executed to sys.stdout.
72 -c,--count Count the number of times a line is executed.
73 Results are written in the results file, if given.
74 -r,--report Generate a report from a results file; do not
75 execute any code.
76 (One of `-t', `-c' or `-r' must be specified)
77 -s,--summary Generate a brief summary for each file. (Can only
78 be used with -c or -r.)
80 I/O:
81 -f,--file= File name for accumulating results over several runs.
82 (No file name means do not archive results)
83 -d,--logdir= Directory to use when writing annotated log files.
84 Log files are the module __name__ with `.` replaced
85 by os.sep and with '.pyl' added.
86 -m,--missing Annotate all executable lines which were not executed
87 with a '>>>>>> '.
88 -R,--no-report Do not generate the annotated reports. Useful if
89 you want to accumulate several over tests.
90 -C,--coverdir= Generate .cover files in this directory
92 Selection: Do not trace or log lines from ...
93 --ignore-module=[string] modules with the given __name__, and submodules
94 of that module
95 --ignore-dir=[string] files in the stated directory (multiple
96 directories can be joined by os.pathsep)
98 The selection options can be listed multiple times to ignore different
99 modules.
100 """ % sys.argv[0])
103 class Ignore:
104 def __init__(self, modules = None, dirs = None):
105 self._mods = modules or []
106 self._dirs = dirs or []
108 self._ignore = { '<string>': 1 }
111 def names(self, filename, modulename):
112 if self._ignore.has_key(modulename):
113 return self._ignore[modulename]
115 # haven't seen this one before, so see if the module name is
116 # on the ignore list. Need to take some care since ignoring
117 # "cmp" musn't mean ignoring "cmpcache" but ignoring
118 # "Spam" must also mean ignoring "Spam.Eggs".
119 for mod in self._mods:
120 if mod == modulename: # Identical names, so ignore
121 self._ignore[modulename] = 1
122 return 1
123 # check if the module is a proper submodule of something on
124 # the ignore list
125 n = len(mod)
126 # (will not overflow since if the first n characters are the
127 # same and the name has not already occured, then the size
128 # of "name" is greater than that of "mod")
129 if mod == modulename[:n] and modulename[n] == '.':
130 self._ignore[modulename] = 1
131 return 1
133 # Now check that __file__ isn't in one of the directories
134 if filename is None:
135 # must be a built-in, so we must ignore
136 self._ignore[modulename] = 1
137 return 1
139 # Ignore a file when it contains one of the ignorable paths
140 for d in self._dirs:
141 # The '+ os.sep' is to ensure that d is a parent directory,
142 # as compared to cases like:
143 # d = "/usr/local"
144 # filename = "/usr/local.py"
145 # or
146 # d = "/usr/local.py"
147 # filename = "/usr/local.py"
148 if string.find(filename, d + os.sep) == 0:
149 self._ignore[modulename] = 1
150 return 1
152 # Tried the different ways, so we don't ignore this module
153 self._ignore[modulename] = 0
154 return 0
156 def run(trace, cmd):
157 import __main__
158 dict = __main__.__dict__
159 sys.settrace(trace)
160 try:
161 exec cmd in dict, dict
162 finally:
163 sys.settrace(None)
165 def runctx(trace, cmd, globals=None, locals=None):
166 if globals is None: globals = {}
167 if locals is None: locals = {}
168 sys.settrace(trace)
169 try:
170 exec cmd in dict, dict
171 finally:
172 sys.settrace(None)
174 def runfunc(trace, func, *args, **kw):
175 result = None
176 sys.settrace(trace)
177 try:
178 result = apply(func, args, kw)
179 finally:
180 sys.settrace(None)
181 return result
184 class CoverageResults:
185 def __init__(self, counts = {}, modules = {}):
186 self.counts = counts.copy() # map (filename, lineno) to count
187 self.modules = modules.copy() # map filenames to modules
189 def update(self, other):
190 """Merge in the data from another CoverageResults"""
191 counts = self.counts
192 other_counts = other.counts
193 modules = self.modules
194 other_modules = other.modules
196 for key in other_counts.keys():
197 counts[key] = counts.get(key, 0) + other_counts[key]
199 for key in other_modules.keys():
200 if modules.has_key(key):
201 # make sure they point to the same file
202 assert modules[key] == other_modules[key], \
203 "Strange! filename %s has two different module " \
204 "names: %s and %s" % \
205 (key, modules[key], other_modules[key])
206 else:
207 modules[key] = other_modules[key]
209 # Given a code string, return the SET_LINENO information
210 def _find_LINENO_from_string(co_code):
211 """return all of the SET_LINENO information from a code string"""
212 import dis
213 linenos = {}
215 # This code was filched from the `dis' module then modified
216 n = len(co_code)
217 i = 0
218 prev_op = None
219 prev_lineno = 0
220 while i < n:
221 c = co_code[i]
222 op = ord(c)
223 if op == dis.SET_LINENO:
224 if prev_op == op:
225 # two SET_LINENO in a row, so the previous didn't
226 # indicate anything. This occurs with triple
227 # quoted strings (?). Remove the old one.
228 del linenos[prev_lineno]
229 prev_lineno = ord(co_code[i+1]) + ord(co_code[i+2])*256
230 linenos[prev_lineno] = 1
231 if op >= dis.HAVE_ARGUMENT:
232 i = i + 3
233 else:
234 i = i + 1
235 prev_op = op
236 return linenos
238 def _find_LINENO(code):
239 """return all of the SET_LINENO information from a code object"""
240 import types
242 # get all of the lineno information from the code of this scope level
243 linenos = _find_LINENO_from_string(code.co_code)
245 # and check the constants for references to other code objects
246 for c in code.co_consts:
247 if type(c) == types.CodeType:
248 # find another code object, so recurse into it
249 linenos.update(_find_LINENO(c))
250 return linenos
252 def find_executable_linenos(filename):
253 """return a dict of the line numbers from executable statements in a file
255 Works by finding all of the code-like objects in the module then searching
256 the byte code for 'SET_LINENO' terms (so this won't work one -O files).
259 import parser
261 assert filename.endswith('.py')
263 prog = open(filename).read()
264 ast = parser.suite(prog)
265 code = parser.compileast(ast, filename)
267 # The only way I know to find line numbers is to look for the
268 # SET_LINENO instructions. Isn't there some way to get it from
269 # the AST?
271 return _find_LINENO(code)
273 ### XXX because os.path.commonprefix seems broken by my way of thinking...
274 def commonprefix(dirs):
275 "Given a list of pathnames, returns the longest common leading component"
276 if not dirs: return ''
277 n = copy.copy(dirs)
278 for i in range(len(n)):
279 n[i] = n[i].split(os.sep)
280 prefix = n[0]
281 for item in n:
282 for i in range(len(prefix)):
283 if prefix[:i+1] <> item[:i+1]:
284 prefix = prefix[:i]
285 if i == 0: return ''
286 break
287 return os.sep.join(prefix)
289 def create_results_log(results, dirname = ".", show_missing = 1,
290 save_counts = 0, summary = 0, coverdir = None):
291 import re
292 # turn the counts data ("(filename, lineno) = count") into something
293 # accessible on a per-file basis
294 per_file = {}
295 for filename, lineno in results.counts.keys():
296 lines_hit = per_file[filename] = per_file.get(filename, {})
297 lines_hit[lineno] = results.counts[(filename, lineno)]
299 # try and merge existing counts and modules file from dirname
300 try:
301 counts = marshal.load(open(os.path.join(dirname, "counts")))
302 modules = marshal.load(open(os.path.join(dirname, "modules")))
303 results.update(results.__class__(counts, modules))
304 except IOError:
305 pass
307 # there are many places where this is insufficient, like a blank
308 # line embedded in a multiline string.
309 blank = re.compile(r'^\s*(#.*)?$')
311 # accumulate summary info, if needed
312 sums = {}
314 # generate file paths for the coverage files we are going to write...
315 fnlist = []
316 tfdir = tempfile.gettempdir()
317 for key in per_file.keys():
318 filename = key
320 # skip some "files" we don't care about...
321 if filename == "<string>":
322 continue
323 # are these caused by code compiled using exec or something?
324 if filename.startswith(tfdir):
325 continue
327 modulename = os.path.split(results.modules[key])[1]
329 if filename.endswith(".pyc") or filename.endswith(".pyo"):
330 filename = filename[:-1]
332 if coverdir:
333 listfilename = os.path.join(coverdir, modulename + ".cover")
334 else:
335 # XXX this is almost certainly not portable!!!
336 fndir = os.path.dirname(filename)
337 if os.path.isabs(filename):
338 coverpath = fndir
339 else:
340 coverpath = os.path.join(dirname, fndir)
342 # build list file name by appending a ".cover" to the module name
343 # and sticking it into the specified directory
344 if "." in modulename:
345 # A module in a package
346 finalname = modulename.split(".")[-1]
347 listfilename = os.path.join(coverpath, finalname + ".cover")
348 else:
349 listfilename = os.path.join(coverpath, modulename + ".cover")
351 # Get the original lines from the .py file
352 try:
353 lines = open(filename, 'r').readlines()
354 except IOError, err:
355 print >> sys.stderr, "trace: Could not open %s for reading " \
356 "because: %s - skipping" % (`filename`, err.strerror)
357 continue
359 try:
360 outfile = open(listfilename, 'w')
361 except IOError, err:
362 sys.stderr.write(
363 '%s: Could not open %s for writing because: %s" \
364 "- skipping\n' % ("trace", `listfilename`, err.strerror))
365 continue
367 # If desired, get a list of the line numbers which represent
368 # executable content (returned as a dict for better lookup speed)
369 if show_missing:
370 executable_linenos = find_executable_linenos(filename)
371 else:
372 executable_linenos = {}
374 n_lines = 0
375 n_hits = 0
376 lines_hit = per_file[key]
377 for i in range(len(lines)):
378 line = lines[i]
380 # do the blank/comment match to try to mark more lines
381 # (help the reader find stuff that hasn't been covered)
382 if lines_hit.has_key(i+1):
383 # count precedes the lines that we captured
384 outfile.write('%5d: ' % lines_hit[i+1])
385 n_hits = n_hits + 1
386 n_lines = n_lines + 1
387 elif blank.match(line):
388 # blank lines and comments are preceded by dots
389 outfile.write(' . ')
390 else:
391 # lines preceded by no marks weren't hit
392 # Highlight them if so indicated, unless the line contains
393 # '#pragma: NO COVER' (it is possible to embed this into
394 # the text as a non-comment; no easy fix)
395 if executable_linenos.has_key(i+1) and \
396 string.find(lines[i],
397 string.join(['#pragma', 'NO COVER'])) == -1:
398 outfile.write('>>>>>> ')
399 else:
400 outfile.write(' '*7)
401 n_lines = n_lines + 1
402 outfile.write(string.expandtabs(lines[i], 8))
404 outfile.close()
406 if summary and n_lines:
407 percent = int(100 * n_hits / n_lines)
408 sums[modulename] = n_lines, percent, modulename, filename
410 if save_counts:
411 # try and store counts and module info into dirname
412 try:
413 marshal.dump(results.counts,
414 open(os.path.join(dirname, "counts"), "w"))
415 marshal.dump(results.modules,
416 open(os.path.join(dirname, "modules"), "w"))
417 except IOError, err:
418 sys.stderr.write("cannot save counts/modules " \
419 "files because %s" % err.strerror)
421 if summary and sums:
422 mods = sums.keys()
423 mods.sort()
424 print "lines cov% module (path)"
425 for m in mods:
426 n_lines, percent, modulename, filename = sums[m]
427 print "%5d %3d%% %s (%s)" % sums[m]
429 # There is a lot of code shared between these two classes even though
430 # it is straightforward to make a super class to share code. However,
431 # for performance reasons (remember, this is called at every step) I
432 # wanted to keep everything to a single function call. Also, by
433 # staying within a single scope, I don't have to temporarily nullify
434 # sys.settrace, which would slow things down even more.
436 class Coverage:
437 def __init__(self, ignore = Ignore()):
438 self.ignore = ignore
439 self.ignore_names = ignore._ignore # access ignore's cache (speed hack)
441 self.counts = {} # keys are (filename, linenumber)
442 self.modules = {} # maps filename -> module name
444 def trace(self, frame, why, arg):
445 if why == 'line':
446 # something is fishy about getting the file name
447 filename = frame.f_globals.get("__file__", None)
448 if filename is None:
449 filename = frame.f_code.co_filename
450 try:
451 modulename = frame.f_globals["__name__"]
452 except KeyError:
453 # PyRun_String() for example
454 # XXX what to do?
455 modulename = None
457 # We do this next block to keep from having to make methods
458 # calls, which also requires resetting the trace
459 ignore_it = self.ignore_names.get(modulename, -1)
460 if ignore_it == -1: # unknown filename
461 sys.settrace(None)
462 ignore_it = self.ignore.names(filename, modulename)
463 sys.settrace(self.trace)
465 # record the module name for every file
466 self.modules[filename] = modulename
468 if not ignore_it:
469 lineno = frame.f_lineno
471 # record the file name and line number of every trace
472 key = (filename, lineno)
473 self.counts[key] = self.counts.get(key, 0) + 1
475 return self.trace
477 def results(self):
478 return CoverageResults(self.counts, self.modules)
480 class Trace:
481 def __init__(self, ignore = Ignore()):
482 self.ignore = ignore
483 self.ignore_names = ignore._ignore # access ignore's cache (speed hack)
485 self.files = {'<string>': None} # stores lines from the .py file,
486 # or None
488 def trace(self, frame, why, arg):
489 if why == 'line':
490 filename = frame.f_code.co_filename
491 try:
492 modulename = frame.f_globals["__name__"]
493 except KeyError:
494 # PyRun_String() for example
495 # XXX what to do?
496 modulename = None
498 # We do this next block to keep from having to make methods
499 # calls, which also requires resetting the trace
500 ignore_it = self.ignore_names.get(modulename, -1)
501 if ignore_it == -1: # unknown filename
502 sys.settrace(None)
503 ignore_it = self.ignore.names(filename, modulename)
504 sys.settrace(self.trace)
506 if not ignore_it:
507 lineno = frame.f_lineno
508 files = self.files
510 if filename != '<string>' and not files.has_key(filename):
511 files[filename] = map(string.rstrip,
512 open(filename).readlines())
514 # If you want to see filenames (the original behaviour), try:
515 # modulename = filename
516 # or, prettier but confusing when several files have the
517 # same name
518 # modulename = os.path.basename(filename)
520 if files[filename] != None:
521 print '%s(%d): %s' % (os.path.basename(filename), lineno,
522 files[filename][lineno-1])
523 else:
524 print '%s(%d): ??' % (modulename, lineno)
526 return self.trace
529 def _err_exit(msg):
530 print >> sys.stderr, "%s: %s" % (sys.argv[0], msg)
531 sys.exit(1)
533 def main(argv = None):
534 import getopt
536 if argv is None:
537 argv = sys.argv
538 try:
539 opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:",
540 ["help", "version", "trace", "count",
541 "report", "no-report",
542 "file=", "logdir=", "missing",
543 "ignore-module=", "ignore-dir=",
544 "coverdir="])
546 except getopt.error, msg:
547 print >> sys.stderr, "%s: %s" % (sys.argv[0], msg)
548 print >> sys.stderr, "Try `%s --help' for more information" \
549 % sys.argv[0]
550 sys.exit(1)
552 trace = 0
553 count = 0
554 report = 0
555 no_report = 0
556 counts_file = None
557 logdir = "."
558 missing = 0
559 ignore_modules = []
560 ignore_dirs = []
561 coverdir = None
562 summary = 0
564 for opt, val in opts:
565 if opt == "--help":
566 usage(sys.stdout)
567 sys.exit(0)
569 if opt == "--version":
570 sys.stdout.write("trace 2.0\n")
571 sys.exit(0)
573 if opt == "-t" or opt == "--trace":
574 trace = 1
575 continue
577 if opt == "-c" or opt == "--count":
578 count = 1
579 continue
581 if opt == "-r" or opt == "--report":
582 report = 1
583 continue
585 if opt == "-R" or opt == "--no-report":
586 no_report = 1
587 continue
589 if opt == "-f" or opt == "--file":
590 counts_file = val
591 continue
593 if opt == "-d" or opt == "--logdir":
594 logdir = val
595 continue
597 if opt == "-m" or opt == "--missing":
598 missing = 1
599 continue
601 if opt == "-C" or opt == "--coverdir":
602 coverdir = val
603 continue
605 if opt == "-s" or opt == "--summary":
606 summary = 1
607 continue
609 if opt == "--ignore-module":
610 ignore_modules.append(val)
611 continue
613 if opt == "--ignore-dir":
614 for s in string.split(val, os.pathsep):
615 s = os.path.expandvars(s)
616 # should I also call expanduser? (after all, could use $HOME)
618 s = string.replace(s, "$prefix",
619 os.path.join(sys.prefix, "lib",
620 "python" + sys.version[:3]))
621 s = string.replace(s, "$exec_prefix",
622 os.path.join(sys.exec_prefix, "lib",
623 "python" + sys.version[:3]))
624 s = os.path.normpath(s)
625 ignore_dirs.append(s)
626 continue
628 assert 0, "Should never get here"
630 if len(prog_argv) == 0:
631 _err_exit("missing name of file to run")
633 if count + trace + report > 1:
634 _err_exit("can only specify one of --trace, --count or --report")
636 if count + trace + report == 0:
637 _err_exit("must specify one of --trace, --count or --report")
639 if report and counts_file is None:
640 _err_exit("--report requires a --file")
642 if report and no_report:
643 _err_exit("cannot specify both --report and --no-report")
645 if logdir is not None:
646 # warn if the directory doesn't exist, but keep on going
647 # (is this the correct behaviour?)
648 if not os.path.isdir(logdir):
649 sys.stderr.write(
650 "trace: WARNING, --logdir directory %s is not available\n" %
651 `logdir`)
653 sys.argv = prog_argv
654 progname = prog_argv[0]
655 if eval(sys.version[:3])>1.3:
656 sys.path[0] = os.path.split(progname)[0] # ???
658 # everything is ready
659 ignore = Ignore(ignore_modules, ignore_dirs)
660 if trace:
661 t = Trace(ignore)
662 try:
663 run(t.trace, 'execfile(' + `progname` + ')')
664 except IOError, err:
665 _err_exit("Cannot run file %s because: %s" % \
666 (`sys.argv[0]`, err.strerror))
668 elif count:
669 t = Coverage(ignore)
670 try:
671 run(t.trace, 'execfile(' + `progname` + ')')
672 except IOError, err:
673 _err_exit("Cannot run file %s because: %s" % \
674 (`sys.argv[0]`, err.strerror))
675 except SystemExit:
676 pass
678 results = t.results()
679 # Add another lookup from the program's file name to its import name
680 # This give the right results, but I'm not sure why ...
681 results.modules[progname] = os.path.splitext(progname)[0]
683 if counts_file:
684 # add in archived data, if available
685 try:
686 old_counts, old_modules = marshal.load(open(counts_file, 'rb'))
687 except IOError:
688 pass
689 else:
690 results.update(CoverageResults(old_counts, old_modules))
692 if not no_report:
693 create_results_log(results, logdir, missing,
694 summary=summary, coverdir=coverdir)
696 if counts_file:
697 try:
698 marshal.dump( (results.counts, results.modules),
699 open(counts_file, 'wb'))
700 except IOError, err:
701 _err_exit("Cannot save counts file %s because: %s" % \
702 (`counts_file`, err.strerror))
704 elif report:
705 old_counts, old_modules = marshal.load(open(counts_file, 'rb'))
706 results = CoverageResults(old_counts, old_modules)
707 create_results_log(results, logdir, missing,
708 summary=summary, coverdir=coverdir)
710 else:
711 assert 0, "Should never get here"
713 if __name__=='__main__':
714 main()