blob: 78715e0fc5edb386c3f372e6cdf2bdc09b80c172 [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)
77
78I/O:
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
85 with a '>>>>>> '.
86 -R,--no-report Do not generate the annotated reports. Useful if
87 you want to accumulate several over tests.
88
89Selection: Do not trace or log lines from ...
90 --ignore-module=[string] modules with the given __name__, and submodules
91 of that module
92 --ignore-dir=[string] files in the stated directory (multiple
93 directories can be joined by os.pathsep)
94
95 The selection options can be listed multiple times to ignore different
96modules.
97""" % sys.argv[0])
98
99
100class Ignore:
101 def __init__(self, modules = None, dirs = None):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000102 self._mods = modules or []
103 self._dirs = dirs or []
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000104
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000105 self._ignore = { '<string>': 1 }
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000106
107
108 def names(self, filename, modulename):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000109 if self._ignore.has_key(modulename):
110 return self._ignore[modulename]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000111
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000112 # 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
119 return 1
120 # check if the module is a proper submodule of something on
121 # the ignore list
122 n = len(mod)
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
128 return 1
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000129
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000130 # Now check that __file__ isn't in one of the directories
131 if filename is None:
132 # must be a built-in, so we must ignore
133 self._ignore[modulename] = 1
134 return 1
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000135
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000136 # Ignore a file when it contains one of the ignorable paths
137 for d in self._dirs:
138 # The '+ os.sep' is to ensure that d is a parent directory,
139 # as compared to cases like:
140 # d = "/usr/local"
141 # filename = "/usr/local.py"
142 # or
143 # d = "/usr/local.py"
144 # filename = "/usr/local.py"
145 if string.find(filename, d + os.sep) == 0:
146 self._ignore[modulename] = 1
147 return 1
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000148
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000149 # Tried the different ways, so we don't ignore this module
150 self._ignore[modulename] = 0
151 return 0
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000152
153def run(trace, cmd):
154 import __main__
155 dict = __main__.__dict__
156 sys.settrace(trace)
157 try:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000158 exec cmd in dict, dict
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000159 finally:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000160 sys.settrace(None)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000161
162def runctx(trace, cmd, globals=None, locals=None):
163 if globals is None: globals = {}
164 if locals is None: locals = {}
165 sys.settrace(trace)
166 try:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000167 exec cmd in dict, dict
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000168 finally:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000169 sys.settrace(None)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000170
171def runfunc(trace, func, *args, **kw):
172 result = None
173 sys.settrace(trace)
174 try:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000175 result = apply(func, args, kw)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000176 finally:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000177 sys.settrace(None)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000178 return result
179
180
181class CoverageResults:
182 def __init__(self, counts = {}, modules = {}):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000183 self.counts = counts.copy() # map (filename, lineno) to count
184 self.modules = modules.copy() # map filenames to modules
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000185
186 def update(self, other):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000187 """Merge in the data from another CoverageResults"""
188 counts = self.counts
189 other_counts = other.counts
190 modules = self.modules
191 other_modules = other.modules
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000192
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000193 for key in other_counts.keys():
194 counts[key] = counts.get(key, 0) + other_counts[key]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000195
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000196 for key in other_modules.keys():
197 if modules.has_key(key):
198 # make sure they point to the same file
199 assert modules[key] == other_modules[key], \
200 "Strange! filename %s has two different module names" % \
201 (key, modules[key], other_module[key])
202 else:
203 modules[key] = other_modules[key]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000204
205# Given a code string, return the SET_LINENO information
206def _find_LINENO_from_string(co_code):
207 """return all of the SET_LINENO information from a code string"""
208 import dis
209 linenos = {}
210
211 # This code was filched from the `dis' module then modified
212 n = len(co_code)
213 i = 0
214 prev_op = None
215 prev_lineno = 0
216 while i < n:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000217 c = co_code[i]
218 op = ord(c)
219 if op == dis.SET_LINENO:
220 if prev_op == op:
221 # two SET_LINENO in a row, so the previous didn't
222 # indicate anything. This occurs with triple
223 # quoted strings (?). Remove the old one.
224 del linenos[prev_lineno]
225 prev_lineno = ord(co_code[i+1]) + ord(co_code[i+2])*256
226 linenos[prev_lineno] = 1
227 if op >= dis.HAVE_ARGUMENT:
228 i = i + 3
229 else:
230 i = i + 1
231 prev_op = op
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000232 return linenos
233
234def _find_LINENO(code):
235 """return all of the SET_LINENO information from a code object"""
236 import types
237
238 # get all of the lineno information from the code of this scope level
239 linenos = _find_LINENO_from_string(code.co_code)
240
241 # and check the constants for references to other code objects
242 for c in code.co_consts:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000243 if type(c) == types.CodeType:
244 # find another code object, so recurse into it
245 linenos.update(_find_LINENO(c))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000246 return linenos
247
248def find_executable_linenos(filename):
249 """return a dict of the line numbers from executable statements in a file
250
251 Works by finding all of the code-like objects in the module then searching
252 the byte code for 'SET_LINENO' terms (so this won't work one -O files).
253
254 """
255 import parser
256
257 prog = open(filename).read()
258 ast = parser.suite(prog)
259 code = parser.compileast(ast, filename)
260
261 # The only way I know to find line numbers is to look for the
262 # SET_LINENO instructions. Isn't there some way to get it from
263 # the AST?
Tim Peters70c43782001-01-17 08:48:39 +0000264
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000265 return _find_LINENO(code)
266
267### XXX because os.path.commonprefix seems broken by my way of thinking...
268def commonprefix(dirs):
269 "Given a list of pathnames, returns the longest common leading component"
270 if not dirs: return ''
271 n = copy.copy(dirs)
272 for i in range(len(n)):
273 n[i] = n[i].split(os.sep)
274 prefix = n[0]
275 for item in n:
276 for i in range(len(prefix)):
277 if prefix[:i+1] <> item[:i+1]:
278 prefix = prefix[:i]
279 if i == 0: return ''
280 break
281 return os.sep.join(prefix)
Tim Peters70c43782001-01-17 08:48:39 +0000282
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000283def create_results_log(results, dirname = ".", show_missing = 1,
284 save_counts = 0):
285 import re
286 # turn the counts data ("(filename, lineno) = count") into something
287 # accessible on a per-file basis
288 per_file = {}
289 for filename, lineno in results.counts.keys():
290 lines_hit = per_file[filename] = per_file.get(filename, {})
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000291 lines_hit[lineno] = results.counts[(filename, lineno)]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000292
293 # try and merge existing counts and modules file from dirname
294 try:
295 counts = marshal.load(open(os.path.join(dirname, "counts")))
296 modules = marshal.load(open(os.path.join(dirname, "modules")))
297 results.update(results.__class__(counts, modules))
298 except IOError:
299 pass
Tim Peters70c43782001-01-17 08:48:39 +0000300
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000301 # there are many places where this is insufficient, like a blank
302 # line embedded in a multiline string.
303 blank = re.compile(r'^\s*(#.*)?$')
304
305 # generate file paths for the coverage files we are going to write...
306 fnlist = []
307 tfdir = tempfile.gettempdir()
308 for key in per_file.keys():
309 filename = key
Tim Peters70c43782001-01-17 08:48:39 +0000310
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000311 # skip some "files" we don't care about...
312 if filename == "<string>":
313 continue
314 # are these caused by code compiled using exec or something?
315 if filename.startswith(tfdir):
316 continue
317
318 # XXX this is almost certainly not portable!!!
319 fndir = os.path.dirname(filename)
320 if filename[:1] == os.sep:
321 coverpath = os.path.join(dirname, "."+fndir)
322 else:
323 coverpath = os.path.join(dirname, fndir)
324
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000325 if filename.endswith(".pyc") or filename.endswith(".pyo"):
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000326 filename = filename[:-1]
327
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000328 # Get the original lines from the .py file
329 try:
330 lines = open(filename, 'r').readlines()
331 except IOError, err:
332 sys.stderr.write("%s: Could not open %s for reading " \
333 "because: %s - skipping\n" % \
334 ("trace", `filename`, err.strerror))
335 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000336
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000337 modulename = os.path.split(results.modules[key])[1]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000338
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000339 # build list file name by appending a ".cover" to the module name
340 # and sticking it into the specified directory
341 listfilename = os.path.join(coverpath, modulename + ".cover")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000342 #sys.stderr.write("modulename: %(modulename)s\n"
343 # "filename: %(filename)s\n"
344 # "coverpath: %(coverpath)s\n"
345 # "listfilename: %(listfilename)s\n"
346 # "dirname: %(dirname)s\n"
347 # % locals())
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000348 try:
349 outfile = open(listfilename, 'w')
350 except IOError, err:
351 sys.stderr.write(
352 '%s: Could not open %s for writing because: %s" \
353 "- skipping\n' % ("trace", `listfilename`, err.strerror))
354 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000355
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000356 # If desired, get a list of the line numbers which represent
357 # executable content (returned as a dict for better lookup speed)
358 if show_missing:
359 executable_linenos = find_executable_linenos(filename)
360 else:
361 executable_linenos = {}
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000362
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000363 lines_hit = per_file[key]
364 for i in range(len(lines)):
365 line = lines[i]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000366
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000367 # do the blank/comment match to try to mark more lines
368 # (help the reader find stuff that hasn't been covered)
369 if lines_hit.has_key(i+1):
370 # count precedes the lines that we captured
371 outfile.write('%5d: ' % lines_hit[i+1])
372 elif blank.match(line):
373 # blank lines and comments are preceded by dots
374 outfile.write(' . ')
375 else:
376 # lines preceded by no marks weren't hit
377 # Highlight them if so indicated, unless the line contains
378 # '#pragma: NO COVER' (it is possible to embed this into
379 # the text as a non-comment; no easy fix)
380 if executable_linenos.has_key(i+1) and \
381 string.find(lines[i],
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000382 string.join(['#pragma', 'NO COVER'])) == -1:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000383 outfile.write('>>>>>> ')
384 else:
385 outfile.write(' '*7)
386 outfile.write(string.expandtabs(lines[i], 8))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000387
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000388 outfile.close()
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000389
390 if save_counts:
391 # try and store counts and module info into dirname
392 try:
393 marshal.dump(results.counts,
394 open(os.path.join(dirname, "counts"), "w"))
395 marshal.dump(results.modules,
396 open(os.path.join(dirname, "modules"), "w"))
397 except IOError, err:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000398 sys.stderr.write("cannot save counts/modules " \
399 "files because %s" % err.strerror)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000400
401# There is a lot of code shared between these two classes even though
402# it is straightforward to make a super class to share code. However,
403# for performance reasons (remember, this is called at every step) I
404# wanted to keep everything to a single function call. Also, by
405# staying within a single scope, I don't have to temporarily nullify
406# sys.settrace, which would slow things down even more.
407
408class Coverage:
409 def __init__(self, ignore = Ignore()):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000410 self.ignore = ignore
411 self.ignore_names = ignore._ignore # access ignore's cache (speed hack)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000412
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000413 self.counts = {} # keys are (filename, linenumber)
414 self.modules = {} # maps filename -> module name
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000415
416 def trace(self, frame, why, arg):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000417 if why == 'line':
418 # something is fishy about getting the file name
419 filename = frame.f_globals.get("__file__", None)
420 if filename is None:
421 filename = frame.f_code.co_filename
422 modulename = frame.f_globals["__name__"]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000423
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000424 # We do this next block to keep from having to make methods
425 # calls, which also requires resetting the trace
426 ignore_it = self.ignore_names.get(modulename, -1)
427 if ignore_it == -1: # unknown filename
428 sys.settrace(None)
429 ignore_it = self.ignore.names(filename, modulename)
430 sys.settrace(self.trace)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000431
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000432 # record the module name for every file
433 self.modules[filename] = modulename
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000434
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000435 if not ignore_it:
436 lineno = frame.f_lineno
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000437
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000438 # record the file name and line number of every trace
439 key = (filename, lineno)
440 self.counts[key] = self.counts.get(key, 0) + 1
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000441
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000442 return self.trace
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000443
444 def results(self):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000445 return CoverageResults(self.counts, self.modules)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000446
447class Trace:
448 def __init__(self, ignore = Ignore()):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000449 self.ignore = ignore
450 self.ignore_names = ignore._ignore # access ignore's cache (speed hack)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000451
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000452 self.files = {'<string>': None} # stores lines from the .py file, or None
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000453
454 def trace(self, frame, why, arg):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000455 if why == 'line':
456 filename = frame.f_code.co_filename
457 modulename = frame.f_globals["__name__"]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000458
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000459 # We do this next block to keep from having to make methods
460 # calls, which also requires resetting the trace
461 ignore_it = self.ignore_names.get(modulename, -1)
462 if ignore_it == -1: # unknown filename
463 sys.settrace(None)
464 ignore_it = self.ignore.names(filename, modulename)
465 sys.settrace(self.trace)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000466
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000467 if not ignore_it:
468 lineno = frame.f_lineno
469 files = self.files
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000470
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000471 if filename != '<string>' and not files.has_key(filename):
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000472 files[filename] = map(string.rstrip,
473 open(filename).readlines())
474
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000475 # If you want to see filenames (the original behaviour), try:
476 # modulename = filename
477 # or, prettier but confusing when several files have the same name
478 # modulename = os.path.basename(filename)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000479
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000480 if files[filename] != None:
481 print '%s(%d): %s' % (os.path.basename(filename), lineno,
482 files[filename][lineno-1])
483 else:
484 print '%s(%d): ??' % (modulename, lineno)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000485
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000486 return self.trace
Tim Peters70c43782001-01-17 08:48:39 +0000487
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000488
489def _err_exit(msg):
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000490 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
491 sys.exit(1)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000492
493def main(argv = None):
494 import getopt
495
496 if argv is None:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000497 argv = sys.argv
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000498 try:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000499 opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:m",
500 ["help", "version", "trace", "count",
501 "report", "no-report",
502 "file=", "logdir=", "missing",
503 "ignore-module=", "ignore-dir="])
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000504
505 except getopt.error, msg:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000506 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
507 sys.stderr.write("Try `%s --help' for more information\n" % sys.argv[0])
508 sys.exit(1)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000509
510 trace = 0
511 count = 0
512 report = 0
513 no_report = 0
514 counts_file = None
515 logdir = "."
516 missing = 0
517 ignore_modules = []
518 ignore_dirs = []
519
520 for opt, val in opts:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000521 if opt == "--help":
522 usage(sys.stdout)
523 sys.exit(0)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000524
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000525 if opt == "--version":
526 sys.stdout.write("trace 2.0\n")
527 sys.exit(0)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000528
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000529 if opt == "-t" or opt == "--trace":
530 trace = 1
531 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000532
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000533 if opt == "-c" or opt == "--count":
534 count = 1
535 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000536
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000537 if opt == "-r" or opt == "--report":
538 report = 1
539 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000540
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000541 if opt == "-R" or opt == "--no-report":
542 no_report = 1
543 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000544
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000545 if opt == "-f" or opt == "--file":
546 counts_file = val
547 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000548
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000549 if opt == "-d" or opt == "--logdir":
550 logdir = val
551 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000552
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000553 if opt == "-m" or opt == "--missing":
554 missing = 1
555 continue
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000556
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000557 if opt == "--ignore-module":
558 ignore_modules.append(val)
559 continue
560
561 if opt == "--ignore-dir":
562 for s in string.split(val, os.pathsep):
563 s = os.path.expandvars(s)
564 # should I also call expanduser? (after all, could use $HOME)
565
566 s = string.replace(s, "$prefix",
567 os.path.join(sys.prefix, "lib",
568 "python" + sys.version[:3]))
569 s = string.replace(s, "$exec_prefix",
570 os.path.join(sys.exec_prefix, "lib",
571 "python" + sys.version[:3]))
572 s = os.path.normpath(s)
573 ignore_dirs.append(s)
574 continue
575
576 assert 0, "Should never get here"
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000577
578 if len(prog_argv) == 0:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000579 _err_exit("missing name of file to run")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000580
581 if count + trace + report > 1:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000582 _err_exit("can only specify one of --trace, --count or --report")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000583
584 if count + trace + report == 0:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000585 _err_exit("must specify one of --trace, --count or --report")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000586
587 if report and counts_file is None:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000588 _err_exit("--report requires a --file")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000589
590 if report and no_report:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000591 _err_exit("cannot specify both --report and --no-report")
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000592
593 if logdir is not None:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000594 # warn if the directory doesn't exist, but keep on going
595 # (is this the correct behaviour?)
596 if not os.path.isdir(logdir):
597 sys.stderr.write(
598 "trace: WARNING, --logdir directory %s is not available\n" %
599 `logdir`)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000600
601 sys.argv = prog_argv
602 progname = prog_argv[0]
603 if eval(sys.version[:3])>1.3:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000604 sys.path[0] = os.path.split(progname)[0] # ???
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000605
606 # everything is ready
607 ignore = Ignore(ignore_modules, ignore_dirs)
608 if trace:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000609 t = Trace(ignore)
610 try:
611 run(t.trace, 'execfile(' + `progname` + ')')
612 except IOError, err:
613 _err_exit("Cannot run file %s because: %s" % \
614 (`sys.argv[0]`, err.strerror))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000615
616 elif count:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000617 t = Coverage(ignore)
618 try:
619 run(t.trace, 'execfile(' + `progname` + ')')
620 except IOError, err:
621 _err_exit("Cannot run file %s because: %s" % \
622 (`sys.argv[0]`, err.strerror))
623 except SystemExit:
624 pass
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000625
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000626 results = t.results()
627 # Add another lookup from the program's file name to its import name
628 # This give the right results, but I'm not sure why ...
629 results.modules[progname] = os.path.splitext(progname)[0]
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000630
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000631 if counts_file:
632 # add in archived data, if available
633 try:
634 old_counts, old_modules = marshal.load(open(counts_file, 'rb'))
635 except IOError:
636 pass
637 else:
638 results.update(CoverageResults(old_counts, old_modules))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000639
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000640 if not no_report:
641 create_results_log(results, logdir, missing)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000642
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000643 if counts_file:
644 try:
645 marshal.dump( (results.counts, results.modules),
646 open(counts_file, 'wb'))
647 except IOError, err:
648 _err_exit("Cannot save counts file %s because: %s" % \
649 (`counts_file`, err.strerror))
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000650
651 elif report:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000652 old_counts, old_modules = marshal.load(open(counts_file, 'rb'))
653 results = CoverageResults(old_counts, old_modules)
654 create_results_log(results, logdir, missing)
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000655
656 else:
Jeremy Hylton0b7b4b82000-09-18 01:46:01 +0000657 assert 0, "Should never get here"
Jeremy Hyltonda1ec462000-08-03 19:26:21 +0000658
659if __name__=='__main__':
660 main()