blob: 35dc71f33e75ba39d997ebe2c7f8db31889ce578 [file] [log] [blame]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +00001#!/usr/bin/env python
2
3# Copyright 2000, Mojam Media, Inc., all rights reserved.
4# Author: Skip Montanaro
5#
6# Copyright 1999, Bioreason, Inc., all rights reserved.
7# Author: Andrew Dalke
8#
9# Copyright 1995-1997, Automatrix, Inc., all rights reserved.
10# Author: Skip Montanaro
11#
12# Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
13#
14#
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.
22#
23#
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
32
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
37
38"""
39program/module to trace Python program or function execution
40
41Sample 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
44
45Sample 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()')
52
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)
57
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"""
62
63import sys, os, string, marshal, tempfile, copy, operator
64
65def usage(outfile):
66 outfile.write("""Usage: %s [OPTIONS] <file> [ARGS]
67
68Execution:
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)
Jeremy Hylton66a7e572001-05-08 04:20:52 +000077 -s,--summary Generate a brief summary for each file. (Can only
78 be used with -c or -r.)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +000079
80I/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.
Jeremy Hylton66a7e572001-05-08 04:20:52 +000090 -C,--coverdir= Generate .cover files in this directory
Jeremy Hyltonda1ec462000-08-03 19:26:21 +000091
92Selection: 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)
97
98 The selection options can be listed multiple times to ignore different
99modules.
100""" % sys.argv[0])
101
102
103class Ignore:
104 def __init__(self, modules = None, dirs = None):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000105 self._mods = modules or []
106 self._dirs = dirs or []
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000107
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000108 self._ignore = { '<string>': 1 }
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000109
110
111 def names(self, filename, modulename):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000112 if self._ignore.has_key(modulename):
113 return self._ignore[modulename]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000114
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000115 # 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
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000132
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000133 # 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
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000138
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000139 # 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
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000151
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000152 # Tried the different ways, so we don't ignore this module
153 self._ignore[modulename] = 0
154 return 0
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000155
156def run(trace, cmd):
157 import __main__
158 dict = __main__.__dict__
159 sys.settrace(trace)
160 try:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000161 exec cmd in dict, dict
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000162 finally:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000163 sys.settrace(None)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000164
165def 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:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000170 exec cmd in dict, dict
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000171 finally:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000172 sys.settrace(None)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000173
174def runfunc(trace, func, *args, **kw):
175 result = None
176 sys.settrace(trace)
177 try:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000178 result = apply(func, args, kw)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000179 finally:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000180 sys.settrace(None)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000181 return result
182
183
184class CoverageResults:
185 def __init__(self, counts = {}, modules = {}):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000186 self.counts = counts.copy() # map (filename, lineno) to count
187 self.modules = modules.copy() # map filenames to modules
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000188
189 def update(self, other):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000190 """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
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000195
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000196 for key in other_counts.keys():
197 counts[key] = counts.get(key, 0) + other_counts[key]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000198
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000199 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], \
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000203 "Strange! filename %s has two different module " \
204 "names: %s and %s" % \
205 (key, modules[key], other_modules[key])
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000206 else:
207 modules[key] = other_modules[key]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000208
209# Given a code string, return the SET_LINENO information
210def _find_LINENO_from_string(co_code):
211 """return all of the SET_LINENO information from a code string"""
212 import dis
213 linenos = {}
214
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:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000221 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
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000236 return linenos
237
238def _find_LINENO(code):
239 """return all of the SET_LINENO information from a code object"""
240 import types
241
242 # get all of the lineno information from the code of this scope level
243 linenos = _find_LINENO_from_string(code.co_code)
244
245 # and check the constants for references to other code objects
246 for c in code.co_consts:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000247 if type(c) == types.CodeType:
248 # find another code object, so recurse into it
249 linenos.update(_find_LINENO(c))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000250 return linenos
251
252def find_executable_linenos(filename):
253 """return a dict of the line numbers from executable statements in a file
254
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).
257
258 """
259 import parser
260
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000261 assert filename.endswith('.py')
262
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000263 prog = open(filename).read()
264 ast = parser.suite(prog)
265 code = parser.compileast(ast, filename)
266
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?
Tim Peters70c43782001-01-17 08:48:39 +0000270
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000271 return _find_LINENO(code)
272
273### XXX because os.path.commonprefix seems broken by my way of thinking...
274def 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)
Tim Peters70c43782001-01-17 08:48:39 +0000288
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000289def create_results_log(results, dirname = ".", show_missing = 1,
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000290 save_counts = 0, summary = 0, coverdir = None):
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000291 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, {})
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000297 lines_hit[lineno] = results.counts[(filename, lineno)]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000298
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
Tim Peters70c43782001-01-17 08:48:39 +0000306
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000307 # there are many places where this is insufficient, like a blank
308 # line embedded in a multiline string.
309 blank = re.compile(r'^\s*(#.*)?$')
310
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000311 # accumulate summary info, if needed
312 sums = {}
313
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000314 # 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
Tim Peters70c43782001-01-17 08:48:39 +0000319
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000320 # 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
326
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000327 modulename = os.path.split(results.modules[key])[1]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000328
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000329 if filename.endswith(".pyc") or filename.endswith(".pyo"):
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000330 filename = filename[:-1]
331
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000332 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)
341
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")
350
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000351 # Get the original lines from the .py file
352 try:
353 lines = open(filename, 'r').readlines()
354 except IOError, err:
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000355 print >> sys.stderr, "trace: Could not open %s for reading " \
356 "because: %s - skipping" % (`filename`, err.strerror)
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000357 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000358
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000359 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
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000366
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000367 # 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 = {}
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000373
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000374 n_lines = 0
375 n_hits = 0
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000376 lines_hit = per_file[key]
377 for i in range(len(lines)):
378 line = lines[i]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000379
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000380 # 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])
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000385 n_hits = n_hits + 1
386 n_lines = n_lines + 1
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000387 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],
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000397 string.join(['#pragma', 'NO COVER'])) == -1:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000398 outfile.write('>>>>>> ')
399 else:
400 outfile.write(' '*7)
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000401 n_lines = n_lines + 1
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000402 outfile.write(string.expandtabs(lines[i], 8))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000403
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000404 outfile.close()
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000405
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000406 if summary and n_lines:
407 percent = int(100 * n_hits / n_lines)
408 sums[modulename] = n_lines, percent, modulename, filename
409
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000410 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:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000418 sys.stderr.write("cannot save counts/modules " \
419 "files because %s" % err.strerror)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000420
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000421 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]
428
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000429# 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.
435
436class Coverage:
437 def __init__(self, ignore = Ignore()):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000438 self.ignore = ignore
439 self.ignore_names = ignore._ignore # access ignore's cache (speed hack)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000440
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000441 self.counts = {} # keys are (filename, linenumber)
442 self.modules = {} # maps filename -> module name
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000443
444 def trace(self, frame, why, arg):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000445 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
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000450 try:
451 modulename = frame.f_globals["__name__"]
452 except KeyError:
453 # PyRun_String() for example
454 # XXX what to do?
455 modulename = None
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000456
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000457 # 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)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000464
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000465 # record the module name for every file
466 self.modules[filename] = modulename
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000467
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000468 if not ignore_it:
469 lineno = frame.f_lineno
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000470
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000471 # record the file name and line number of every trace
472 key = (filename, lineno)
473 self.counts[key] = self.counts.get(key, 0) + 1
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000474
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000475 return self.trace
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000476
477 def results(self):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000478 return CoverageResults(self.counts, self.modules)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000479
480class Trace:
481 def __init__(self, ignore = Ignore()):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000482 self.ignore = ignore
483 self.ignore_names = ignore._ignore # access ignore's cache (speed hack)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000484
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000485 self.files = {'<string>': None} # stores lines from the .py file,
486 # or None
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000487
488 def trace(self, frame, why, arg):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000489 if why == 'line':
490 filename = frame.f_code.co_filename
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000491 try:
492 modulename = frame.f_globals["__name__"]
493 except KeyError:
494 # PyRun_String() for example
495 # XXX what to do?
496 modulename = None
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000497
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000498 # 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)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000505
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000506 if not ignore_it:
507 lineno = frame.f_lineno
508 files = self.files
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000509
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000510 if filename != '<string>' and not files.has_key(filename):
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000511 files[filename] = map(string.rstrip,
512 open(filename).readlines())
513
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000514 # If you want to see filenames (the original behaviour), try:
515 # modulename = filename
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000516 # or, prettier but confusing when several files have the
517 # same name
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000518 # modulename = os.path.basename(filename)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000519
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000520 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)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000525
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000526 return self.trace
Tim Peters70c43782001-01-17 08:48:39 +0000527
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000528
529def _err_exit(msg):
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000530 print >> sys.stderr, "%s: %s" % (sys.argv[0], msg)
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000531 sys.exit(1)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000532
533def main(argv = None):
534 import getopt
535
536 if argv is None:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000537 argv = sys.argv
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000538 try:
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000539 opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:",
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000540 ["help", "version", "trace", "count",
541 "report", "no-report",
542 "file=", "logdir=", "missing",
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000543 "ignore-module=", "ignore-dir=",
544 "coverdir="])
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000545
546 except getopt.error, msg:
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000547 print >> sys.stderr, "%s: %s" % (sys.argv[0], msg)
548 print >> sys.stderr, "Try `%s --help' for more information" \
549 % sys.argv[0]
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000550 sys.exit(1)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000551
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 = []
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000561 coverdir = None
562 summary = 0
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000563
564 for opt, val in opts:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000565 if opt == "--help":
566 usage(sys.stdout)
567 sys.exit(0)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000568
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000569 if opt == "--version":
570 sys.stdout.write("trace 2.0\n")
571 sys.exit(0)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000572
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000573 if opt == "-t" or opt == "--trace":
574 trace = 1
575 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000576
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000577 if opt == "-c" or opt == "--count":
578 count = 1
579 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000580
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000581 if opt == "-r" or opt == "--report":
582 report = 1
583 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000584
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000585 if opt == "-R" or opt == "--no-report":
586 no_report = 1
587 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000588
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000589 if opt == "-f" or opt == "--file":
590 counts_file = val
591 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000592
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000593 if opt == "-d" or opt == "--logdir":
594 logdir = val
595 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000596
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000597 if opt == "-m" or opt == "--missing":
598 missing = 1
599 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000600
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000601 if opt == "-C" or opt == "--coverdir":
602 coverdir = val
603 continue
604
605 if opt == "-s" or opt == "--summary":
606 summary = 1
607 continue
608
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000609 if opt == "--ignore-module":
610 ignore_modules.append(val)
611 continue
612
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)
617
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
627
628 assert 0, "Should never get here"
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000629
630 if len(prog_argv) == 0:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000631 _err_exit("missing name of file to run")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000632
633 if count + trace + report > 1:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000634 _err_exit("can only specify one of --trace, --count or --report")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000635
636 if count + trace + report == 0:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000637 _err_exit("must specify one of --trace, --count or --report")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000638
639 if report and counts_file is None:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000640 _err_exit("--report requires a --file")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000641
642 if report and no_report:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000643 _err_exit("cannot specify both --report and --no-report")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000644
645 if logdir is not None:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000646 # 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`)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000652
653 sys.argv = prog_argv
654 progname = prog_argv[0]
655 if eval(sys.version[:3])>1.3:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000656 sys.path[0] = os.path.split(progname)[0] # ???
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000657
658 # everything is ready
659 ignore = Ignore(ignore_modules, ignore_dirs)
660 if trace:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000661 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))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000667
668 elif count:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000669 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
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000677
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000678 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]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000682
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000683 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))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000691
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000692 if not no_report:
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000693 create_results_log(results, logdir, missing,
694 summary=summary, coverdir=coverdir)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000695
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000696 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))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000703
704 elif report:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000705 old_counts, old_modules = marshal.load(open(counts_file, 'rb'))
706 results = CoverageResults(old_counts, old_modules)
Jeremy Hylton66a7e572001-05-08 04:20:52 +0000707 create_results_log(results, logdir, missing,
708 summary=summary, coverdir=coverdir)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000709
710 else:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000711 assert 0, "Should never get here"
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000712
713if __name__=='__main__':
714 main()