3 # Copyright 2000, Mojam Media, Inc., all rights reserved.
4 # Author: Skip Montanaro
6 # Copyright 1999, Bioreason, Inc., all rights reserved.
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
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')
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.
63 import sys
, os
, string
, marshal
, tempfile
, copy
, operator
66 outfile
.write("""Usage: %s [OPTIONS] <file> [ARGS]
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
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.)
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
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
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
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
123 # check if the module is a proper submodule of something on
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
133 # Now check that __file__ isn't in one of the directories
135 # must be a built-in, so we must ignore
136 self
._ignore
[modulename
] = 1
139 # Ignore a file when it contains one of the ignorable paths
141 # The '+ os.sep' is to ensure that d is a parent directory,
142 # as compared to cases like:
144 # filename = "/usr/local.py"
146 # d = "/usr/local.py"
147 # filename = "/usr/local.py"
148 if string
.find(filename
, d
+ os
.sep
) == 0:
149 self
._ignore
[modulename
] = 1
152 # Tried the different ways, so we don't ignore this module
153 self
._ignore
[modulename
] = 0
158 dict = __main__
.__dict
__
161 exec cmd
in dict, dict
165 def runctx(trace
, cmd
, globals=None, locals=None):
166 if globals is None: globals = {}
167 if locals is None: locals = {}
170 exec cmd
in dict, dict
174 def runfunc(trace
, func
, *args
, **kw
):
178 result
= apply(func
, args
, kw
)
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"""
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
])
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"""
215 # This code was filched from the `dis' module then modified
223 if op
== dis
.SET_LINENO
:
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
:
238 def _find_LINENO(code
):
239 """return all of the SET_LINENO information from a code object"""
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
))
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).
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
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 ''
278 for i
in range(len(n
)):
279 n
[i
] = n
[i
].split(os
.sep
)
282 for i
in range(len(prefix
)):
283 if prefix
[:i
+1] <> item
[:i
+1]:
287 return os
.sep
.join(prefix
)
289 def create_results_log(results
, dirname
= ".", show_missing
= 1,
290 save_counts
= 0, summary
= 0, coverdir
= None):
292 # turn the counts data ("(filename, lineno) = count") into something
293 # accessible on a per-file basis
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
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
))
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
314 # generate file paths for the coverage files we are going to write...
316 tfdir
= tempfile
.gettempdir()
317 for key
in per_file
.keys():
320 # skip some "files" we don't care about...
321 if filename
== "<string>":
323 # are these caused by code compiled using exec or something?
324 if filename
.startswith(tfdir
):
327 modulename
= os
.path
.split(results
.modules
[key
])[1]
329 if filename
.endswith(".pyc") or filename
.endswith(".pyo"):
330 filename
= filename
[:-1]
333 listfilename
= os
.path
.join(coverdir
, modulename
+ ".cover")
335 # XXX this is almost certainly not portable!!!
336 fndir
= os
.path
.dirname(filename
)
337 if os
.path
.isabs(filename
):
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")
349 listfilename
= os
.path
.join(coverpath
, modulename
+ ".cover")
351 # Get the original lines from the .py file
353 lines
= open(filename
, 'r').readlines()
355 print >> sys
.stderr
, "trace: Could not open %s for reading " \
356 "because: %s - skipping" % (`filename`
, err
.strerror
)
360 outfile
= open(listfilename
, 'w')
363 '%s: Could not open %s for writing because: %s" \
364 "- skipping\n' % ("trace", `listfilename`
, err
.strerror
))
367 # If desired, get a list of the line numbers which represent
368 # executable content (returned as a dict for better lookup speed)
370 executable_linenos
= find_executable_linenos(filename
)
372 executable_linenos
= {}
376 lines_hit
= per_file
[key
]
377 for i
in range(len(lines
)):
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])
386 n_lines
= n_lines
+ 1
387 elif blank
.match(line
):
388 # blank lines and comments are preceded by dots
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('>>>>>> ')
401 n_lines
= n_lines
+ 1
402 outfile
.write(string
.expandtabs(lines
[i
], 8))
406 if summary
and n_lines
:
407 percent
= int(100 * n_hits
/ n_lines
)
408 sums
[modulename
] = n_lines
, percent
, modulename
, filename
411 # try and store counts and module info into dirname
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"))
418 sys
.stderr
.write("cannot save counts/modules " \
419 "files because %s" % err
.strerror
)
424 print "lines cov% module (path)"
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.
437 def __init__(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
):
446 # something is fishy about getting the file name
447 filename
= frame
.f_globals
.get("__file__", None)
449 filename
= frame
.f_code
.co_filename
451 modulename
= frame
.f_globals
["__name__"]
453 # PyRun_String() for example
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
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
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
478 return CoverageResults(self
.counts
, self
.modules
)
481 def __init__(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,
488 def trace(self
, frame
, why
, arg
):
490 filename
= frame
.f_code
.co_filename
492 modulename
= frame
.f_globals
["__name__"]
494 # PyRun_String() for example
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
503 ignore_it
= self
.ignore
.names(filename
, modulename
)
504 sys
.settrace(self
.trace
)
507 lineno
= frame
.f_lineno
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
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])
524 print '%s(%d): ??' % (modulename
, lineno
)
530 print >> sys
.stderr
, "%s: %s" % (sys
.argv
[0], msg
)
533 def main(argv
= None):
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=",
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" \
564 for opt
, val
in opts
:
569 if opt
== "--version":
570 sys
.stdout
.write("trace 2.0\n")
573 if opt
== "-t" or opt
== "--trace":
577 if opt
== "-c" or opt
== "--count":
581 if opt
== "-r" or opt
== "--report":
585 if opt
== "-R" or opt
== "--no-report":
589 if opt
== "-f" or opt
== "--file":
593 if opt
== "-d" or opt
== "--logdir":
597 if opt
== "-m" or opt
== "--missing":
601 if opt
== "-C" or opt
== "--coverdir":
605 if opt
== "-s" or opt
== "--summary":
609 if opt
== "--ignore-module":
610 ignore_modules
.append(val
)
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
)
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
):
650 "trace: WARNING, --logdir directory %s is not available\n" %
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
)
663 run(t
.trace
, 'execfile(' + `progname`
+ ')')
665 _err_exit("Cannot run file %s because: %s" % \
666 (`sys
.argv
[0]`
, err
.strerror
))
671 run(t
.trace
, 'execfile(' + `progname`
+ ')')
673 _err_exit("Cannot run file %s because: %s" % \
674 (`sys
.argv
[0]`
, err
.strerror
))
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]
684 # add in archived data, if available
686 old_counts
, old_modules
= marshal
.load(open(counts_file
, 'rb'))
690 results
.update(CoverageResults(old_counts
, old_modules
))
693 create_results_log(results
, logdir
, missing
,
694 summary
=summary
, coverdir
=coverdir
)
698 marshal
.dump( (results
.counts
, results
.modules
),
699 open(counts_file
, 'wb'))
701 _err_exit("Cannot save counts file %s because: %s" % \
702 (`counts_file`
, err
.strerror
))
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
)
711 assert 0, "Should never get here"
713 if __name__
=='__main__':