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)
79 -f,--file= File name for accumulating results over several runs.
80 (No file name means do not archive results)
81 -d,--logdir= Directory to use when writing annotated log files.
82 Log files are the module __name__ with `.` replaced
83 by os.sep and with '.pyl' added.
84 -m,--missing Annotate all executable lines which were not executed
86 -R,--no-report Do not generate the annotated reports. Useful if
87 you want to accumulate several over tests.
89 Selection: Do not trace or log lines from ...
90 --ignore-module=[string] modules with the given __name__, and submodules
92 --ignore-dir=[string] files in the stated directory (multiple
93 directories can be joined by os.pathsep)
95 The selection options can be listed multiple times to ignore different
101 def __init__(self
, modules
= None, dirs
= None):
102 self
._mods
= modules
or []
103 self
._dirs
= dirs
or []
105 self
._ignore
= { '<string>': 1 }
108 def names(self
, filename
, modulename
):
109 if self
._ignore
.has_key(modulename
):
110 return self
._ignore
[modulename
]
112 # haven't seen this one before, so see if the module name is
113 # on the ignore list. Need to take some care since ignoring
114 # "cmp" musn't mean ignoring "cmpcache" but ignoring
115 # "Spam" must also mean ignoring "Spam.Eggs".
116 for mod
in self
._mods
:
117 if mod
== modulename
: # Identical names, so ignore
118 self
._ignore
[modulename
] = 1
120 # check if the module is a proper submodule of something on
123 # (will not overflow since if the first n characters are the
124 # same and the name has not already occured, then the size
125 # of "name" is greater than that of "mod")
126 if mod
== modulename
[:n
] and modulename
[n
] == '.':
127 self
._ignore
[modulename
] = 1
130 # Now check that __file__ isn't in one of the directories
132 # must be a built-in, so we must ignore
133 self
._ignore
[modulename
] = 1
136 # Ignore a file when it contains one of the ignorable paths
138 # The '+ os.sep' is to ensure that d is a parent directory,
139 # as compared to cases like:
141 # filename = "/usr/local.py"
143 # d = "/usr/local.py"
144 # filename = "/usr/local.py"
145 if string
.find(filename
, d
+ os
.sep
) == 0:
146 self
._ignore
[modulename
] = 1
149 # Tried the different ways, so we don't ignore this module
150 self
._ignore
[modulename
] = 0
156 dict = __main__
.__dict
__
159 exec cmd
in dict, dict
163 def runctx(trace
, cmd
, globals=None, locals=None):
164 if globals is None: globals = {}
165 if locals is None: locals = {}
168 exec cmd
in dict, dict
172 def runfunc(trace
, func
, *args
, **kw
):
176 result
= apply(func
, args
, kw
)
182 class CoverageResults
:
183 def __init__(self
, counts
= {}, modules
= {}):
184 self
.counts
= counts
.copy() # map (filename, lineno) to count
185 self
.modules
= modules
.copy() # map filenames to modules
187 def update(self
, other
):
188 """Merge in the data from another CoverageResults"""
190 other_counts
= other
.counts
191 modules
= self
.modules
192 other_modules
= other
.modules
194 for key
in other_counts
.keys():
195 counts
[key
] = counts
.get(key
, 0) + other_counts
[key
]
197 for key
in other_modules
.keys():
198 if modules
.has_key(key
):
199 # make sure they point to the same file
200 assert modules
[key
] == other_modules
[key
], \
201 "Strange! filename %s has two different module names" % \
202 (key
, modules
[key
], other_module
[key
])
204 modules
[key
] = other_modules
[key
]
206 # Given a code string, return the SET_LINENO information
207 def _find_LINENO_from_string(co_code
):
208 """return all of the SET_LINENO information from a code string"""
212 # This code was filched from the `dis' module then modified
220 if op
== dis
.SET_LINENO
:
222 # two SET_LINENO in a row, so the previous didn't
223 # indicate anything. This occurs with triple
224 # quoted strings (?). Remove the old one.
225 del linenos
[prev_lineno
]
226 prev_lineno
= ord(co_code
[i
+1]) + ord(co_code
[i
+2])*256
227 linenos
[prev_lineno
] = 1
228 if op
>= dis
.HAVE_ARGUMENT
:
235 def _find_LINENO(code
):
236 """return all of the SET_LINENO information from a code object"""
239 # get all of the lineno information from the code of this scope level
240 linenos
= _find_LINENO_from_string(code
.co_code
)
242 # and check the constants for references to other code objects
243 for c
in code
.co_consts
:
244 if type(c
) == types
.CodeType
:
245 # find another code object, so recurse into it
246 linenos
.update(_find_LINENO(c
))
249 def find_executable_linenos(filename
):
250 """return a dict of the line numbers from executable statements in a file
252 Works by finding all of the code-like objects in the module then searching
253 the byte code for 'SET_LINENO' terms (so this won't work one -O files).
258 prog
= open(filename
).read()
259 ast
= parser
.suite(prog
)
260 code
= parser
.compileast(ast
, filename
)
262 # The only way I know to find line numbers is to look for the
263 # SET_LINENO instructions. Isn't there some way to get it from
266 return _find_LINENO(code
)
268 ### XXX because os.path.commonprefix seems broken by my way of thinking...
269 def commonprefix(dirs
):
270 "Given a list of pathnames, returns the longest common leading component"
271 if not dirs
: return ''
273 for i
in range(len(n
)):
274 n
[i
] = n
[i
].split(os
.sep
)
277 for i
in range(len(prefix
)):
278 if prefix
[:i
+1] <> item
[:i
+1]:
282 return os
.sep
.join(prefix
)
284 def create_results_log(results
, dirname
= ".", show_missing
= 1,
287 # turn the counts data ("(filename, lineno) = count") into something
288 # accessible on a per-file basis
290 for filename
, lineno
in results
.counts
.keys():
291 lines_hit
= per_file
[filename
] = per_file
.get(filename
, {})
292 lines_hit
[lineno
] = results
.counts
[(filename
, lineno
)]
294 # try and merge existing counts and modules file from dirname
296 counts
= marshal
.load(open(os
.path
.join(dirname
, "counts")))
297 modules
= marshal
.load(open(os
.path
.join(dirname
, "modules")))
298 results
.update(results
.__class
__(counts
, modules
))
302 # there are many places where this is insufficient, like a blank
303 # line embedded in a multiline string.
304 blank
= re
.compile(r
'^\s*(#.*)?$')
306 # generate file paths for the coverage files we are going to write...
308 tfdir
= tempfile
.gettempdir()
309 for key
in per_file
.keys():
312 # skip some "files" we don't care about...
313 if filename
== "<string>":
315 # are these caused by code compiled using exec or something?
316 if filename
.startswith(tfdir
):
319 # XXX this is almost certainly not portable!!!
320 fndir
= os
.path
.dirname(filename
)
321 if filename
[:1] == os
.sep
:
322 coverpath
= os
.path
.join(dirname
, "."+fndir
)
324 coverpath
= os
.path
.join(dirname
, fndir
)
326 if filename
.endswith(".pyc") or filename
.endswith(".pyo"):
327 filename
= filename
[:-1]
329 # Get the original lines from the .py file
331 lines
= open(filename
, 'r').readlines()
334 "%s: Could not open %s for reading because: %s - skipping\n" % \
335 ("trace", `filename`
, err
.strerror
))
338 modulename
= os
.path
.split(results
.modules
[key
])[1]
340 # build list file name by appending a ".cover" to the module name
341 # and sticking it into the specified directory
342 listfilename
= os
.path
.join(coverpath
, modulename
+ ".cover")
343 #sys.stderr.write("modulename: %(modulename)s\n"
344 # "filename: %(filename)s\n"
345 # "coverpath: %(coverpath)s\n"
346 # "listfilename: %(listfilename)s\n"
347 # "dirname: %(dirname)s\n"
350 outfile
= open(listfilename
, 'w')
353 '%s: Could not open %s for writing because: %s - skipping\n' %
354 ("trace", `listfilename`
, err
.strerror
))
357 # If desired, get a list of the line numbers which represent
358 # executable content (returned as a dict for better lookup speed)
360 executable_linenos
= find_executable_linenos(filename
)
362 executable_linenos
= {}
364 lines_hit
= per_file
[key
]
365 for i
in range(len(lines
)):
368 # do the blank/comment match to try to mark more lines
369 # (help the reader find stuff that hasn't been covered)
370 if lines_hit
.has_key(i
+1):
371 # count precedes the lines that we captured
372 outfile
.write('%5d: ' % lines_hit
[i
+1])
373 elif blank
.match(line
):
374 # blank lines and comments are preceded by dots
377 # lines preceded by no marks weren't hit
378 # Highlight them if so indicated, unless the line contains
379 # '#pragma: NO COVER' (it is possible to embed this into
380 # the text as a non-comment; no easy fix)
381 if executable_linenos
.has_key(i
+1) and \
382 string
.find(lines
[i
],
383 string
.join(['#pragma', 'NO COVER'])) == -1:
384 outfile
.write('>>>>>> ')
387 outfile
.write(string
.expandtabs(lines
[i
], 8))
392 # try and store counts and module info into dirname
394 marshal
.dump(results
.counts
,
395 open(os
.path
.join(dirname
, "counts"), "w"))
396 marshal
.dump(results
.modules
,
397 open(os
.path
.join(dirname
, "modules"), "w"))
399 sys
.stderr
.write("cannot save counts/modules files because %s" %
402 # There is a lot of code shared between these two classes even though
403 # it is straightforward to make a super class to share code. However,
404 # for performance reasons (remember, this is called at every step) I
405 # wanted to keep everything to a single function call. Also, by
406 # staying within a single scope, I don't have to temporarily nullify
407 # sys.settrace, which would slow things down even more.
410 def __init__(self
, ignore
= Ignore()):
412 self
.ignore_names
= ignore
._ignore
# access ignore's cache (speed hack)
414 self
.counts
= {} # keys are (filename, linenumber)
415 self
.modules
= {} # maps filename -> module name
417 def trace(self
, frame
, why
, arg
):
419 # something is fishy about getting the file name
420 filename
= frame
.f_globals
.get("__file__", None)
422 filename
= frame
.f_code
.co_filename
423 modulename
= frame
.f_globals
["__name__"]
425 # We do this next block to keep from having to make methods
426 # calls, which also requires resetting the trace
427 ignore_it
= self
.ignore_names
.get(modulename
, -1)
428 if ignore_it
== -1: # unknown filename
430 ignore_it
= self
.ignore
.names(filename
, modulename
)
431 sys
.settrace(self
.trace
)
433 # record the module name for every file
434 self
.modules
[filename
] = modulename
437 lineno
= frame
.f_lineno
439 # record the file name and line number of every trace
440 key
= (filename
, lineno
)
441 self
.counts
[key
] = self
.counts
.get(key
, 0) + 1
446 return CoverageResults(self
.counts
, self
.modules
)
449 def __init__(self
, ignore
= Ignore()):
451 self
.ignore_names
= ignore
._ignore
# access ignore's cache (speed hack)
453 self
.files
= {'<string>': None} # stores lines from the .py file, or None
455 def trace(self
, frame
, why
, arg
):
457 filename
= frame
.f_code
.co_filename
458 modulename
= frame
.f_globals
["__name__"]
460 # We do this next block to keep from having to make methods
461 # calls, which also requires resetting the trace
462 ignore_it
= self
.ignore_names
.get(modulename
, -1)
463 if ignore_it
== -1: # unknown filename
465 ignore_it
= self
.ignore
.names(filename
, modulename
)
466 sys
.settrace(self
.trace
)
469 lineno
= frame
.f_lineno
472 if filename
!= '<string>' and not files
.has_key(filename
):
473 files
[filename
] = map(string
.rstrip
,
474 open(filename
).readlines())
476 # If you want to see filenames (the original behaviour), try:
477 # modulename = filename
478 # or, prettier but confusing when several files have the same name
479 # modulename = os.path.basename(filename)
481 if files
[filename
] != None:
482 print '%s(%d): %s' % (os
.path
.basename(filename
), lineno
,
483 files
[filename
][lineno
-1])
485 print '%s(%d): ??' % (modulename
, lineno
)
491 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
494 def main(argv
= None):
500 opts
, prog_argv
= getopt
.getopt(argv
[1:], "tcrRf:d:m",
501 ["help", "version", "trace", "count",
502 "report", "no-report",
503 "file=", "logdir=", "missing",
504 "ignore-module=", "ignore-dir="])
506 except getopt
.error
, msg
:
507 sys
.stderr
.write("%s: %s\n" % (sys
.argv
[0], msg
))
508 sys
.stderr
.write("Try `%s --help' for more information\n" % sys
.argv
[0])
521 for opt
, val
in opts
:
526 if opt
== "--version":
527 sys
.stdout
.write("trace 2.0\n")
530 if opt
== "-t" or opt
== "--trace":
534 if opt
== "-c" or opt
== "--count":
538 if opt
== "-r" or opt
== "--report":
542 if opt
== "-R" or opt
== "--no-report":
546 if opt
== "-f" or opt
== "--file":
550 if opt
== "-d" or opt
== "--logdir":
554 if opt
== "-m" or opt
== "--missing":
558 if opt
== "--ignore-module":
559 ignore_modules
.append(val
)
562 if opt
== "--ignore-dir":
563 for s
in string
.split(val
, os
.pathsep
):
564 s
= os
.path
.expandvars(s
)
565 # should I also call expanduser? (after all, could use $HOME)
567 s
= string
.replace(s
, "$prefix",
568 os
.path
.join(sys
.prefix
, "lib",
569 "python" + sys
.version
[:3]))
570 s
= string
.replace(s
, "$exec_prefix",
571 os
.path
.join(sys
.exec_prefix
, "lib",
572 "python" + sys
.version
[:3]))
573 s
= os
.path
.normpath(s
)
574 ignore_dirs
.append(s
)
577 assert 0, "Should never get here"
579 if len(prog_argv
) == 0:
580 _err_exit("missing name of file to run")
582 if count
+ trace
+ report
> 1:
583 _err_exit("can only specify one of --trace, --count or --report")
585 if count
+ trace
+ report
== 0:
586 _err_exit("must specify one of --trace, --count or --report")
588 if report
and counts_file
is None:
589 _err_exit("--report requires a --file")
591 if report
and no_report
:
592 _err_exit("cannot specify both --report and --no-report")
594 if logdir
is not None:
595 # warn if the directory doesn't exist, but keep on going
596 # (is this the correct behaviour?)
597 if not os
.path
.isdir(logdir
):
599 "trace: WARNING, --logdir directory %s is not available\n" %
603 progname
= prog_argv
[0]
604 if eval(sys
.version
[:3])>1.3:
605 sys
.path
[0] = os
.path
.split(progname
)[0] # ???
607 # everything is ready
608 ignore
= Ignore(ignore_modules
, ignore_dirs
)
612 run(t
.trace
, 'execfile(' + `progname`
+ ')')
614 _err_exit("Cannot run file %s because: %s" % \
615 (`sys
.argv
[0]`
, err
.strerror
))
620 run(t
.trace
, 'execfile(' + `progname`
+ ')')
622 _err_exit("Cannot run file %s because: %s" % \
623 (`sys
.argv
[0]`
, err
.strerror
))
627 results
= t
.results()
628 # Add another lookup from the program's file name to its import name
629 # This give the right results, but I'm not sure why ...
630 results
.modules
[progname
] = os
.path
.splitext(progname
)[0]
633 # add in archived data, if available
635 old_counts
, old_modules
= marshal
.load(open(counts_file
, 'rb'))
639 results
.update(CoverageResults(old_counts
, old_modules
))
642 create_results_log(results
, logdir
, missing
)
646 marshal
.dump( (results
.counts
, results
.modules
),
647 open(counts_file
, 'wb'))
649 _err_exit("Cannot save counts file %s because: %s" % \
650 (`counts_file`
, err
.strerror
))
653 old_counts
, old_modules
= marshal
.load(open(counts_file
, 'rb'))
654 results
= CoverageResults(old_counts
, old_modules
)
655 create_results_log(results
, logdir
, missing
)
658 assert 0, "Should never get here"
660 if __name__
=='__main__':