Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | #!/usr/bin/env python |
2 | ||
3 | # portions copyright 2001, Autonomous Zones Industries, Inc., all rights... | |
4 | # err... reserved and offered to the public under the terms of the | |
5 | # Python 2.2 license. | |
6 | # Author: Zooko O'Whielacronx | |
7 | # http://zooko.com/ | |
8 | # mailto:zooko@zooko.com | |
9 | # | |
10 | # Copyright 2000, Mojam Media, Inc., all rights reserved. | |
11 | # Author: Skip Montanaro | |
12 | # | |
13 | # Copyright 1999, Bioreason, Inc., all rights reserved. | |
14 | # Author: Andrew Dalke | |
15 | # | |
16 | # Copyright 1995-1997, Automatrix, Inc., all rights reserved. | |
17 | # Author: Skip Montanaro | |
18 | # | |
19 | # Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved. | |
20 | # | |
21 | # | |
22 | # Permission to use, copy, modify, and distribute this Python software and | |
23 | # its associated documentation for any purpose without fee is hereby | |
24 | # granted, provided that the above copyright notice appears in all copies, | |
25 | # and that both that copyright notice and this permission notice appear in | |
26 | # supporting documentation, and that the name of neither Automatrix, | |
27 | # Bioreason or Mojam Media be used in advertising or publicity pertaining to | |
28 | # distribution of the software without specific, written prior permission. | |
29 | # | |
30 | """program/module to trace Python program or function execution | |
31 | ||
32 | Sample use, command line: | |
33 | trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs | |
34 | trace.py -t --ignore-dir '$prefix' spam.py eggs | |
35 | trace.py --trackcalls spam.py eggs | |
36 | ||
37 | Sample use, programmatically | |
38 | # create a Trace object, telling it what to ignore, and whether to | |
39 | # do tracing or line-counting or both. | |
40 | trace = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0, | |
41 | count=1) | |
42 | # run the new command using the given trace | |
43 | trace.run('main()') | |
44 | # make a report, telling it where you want output | |
45 | r = trace.results() | |
46 | r.write_results(show_missing=True) | |
47 | """ | |
48 | ||
49 | import linecache | |
50 | import os | |
51 | import re | |
52 | import sys | |
53 | import threading | |
54 | import token | |
55 | import tokenize | |
56 | import types | |
57 | import gc | |
58 | ||
59 | try: | |
60 | import cPickle | |
61 | pickle = cPickle | |
62 | except ImportError: | |
63 | import pickle | |
64 | ||
65 | def usage(outfile): | |
66 | outfile.write("""Usage: %s [OPTIONS] <file> [ARGS] | |
67 | ||
68 | Meta-options: | |
69 | --help Display this help then exit. | |
70 | --version Output version information then exit. | |
71 | ||
72 | Otherwise, exactly one of the following three options must be given: | |
73 | -t, --trace Print each line to sys.stdout before it is executed. | |
74 | -c, --count Count the number of times each line is executed | |
75 | and write the counts to <module>.cover for each | |
76 | module executed, in the module's directory. | |
77 | See also `--coverdir', `--file', `--no-report' below. | |
78 | -l, --listfuncs Keep track of which functions are executed at least | |
79 | once and write the results to sys.stdout after the | |
80 | program exits. | |
81 | -T, --trackcalls Keep track of caller/called pairs and write the | |
82 | results to sys.stdout after the program exits. | |
83 | -r, --report Generate a report from a counts file; do not execute | |
84 | any code. `--file' must specify the results file to | |
85 | read, which must have been created in a previous run | |
86 | with `--count --file=FILE'. | |
87 | ||
88 | Modifiers: | |
89 | -f, --file=<file> File to accumulate counts over several runs. | |
90 | -R, --no-report Do not generate the coverage report files. | |
91 | Useful if you want to accumulate over several runs. | |
92 | -C, --coverdir=<dir> Directory where the report files. The coverage | |
93 | report for <package>.<module> is written to file | |
94 | <dir>/<package>/<module>.cover. | |
95 | -m, --missing Annotate executable lines that were not executed | |
96 | with '>>>>>> '. | |
97 | -s, --summary Write a brief summary on stdout for each file. | |
98 | (Can only be used with --count or --report.) | |
99 | ||
100 | Filters, may be repeated multiple times: | |
101 | --ignore-module=<mod> Ignore the given module and its submodules | |
102 | (if it is a package). | |
103 | --ignore-dir=<dir> Ignore files in the given directory (multiple | |
104 | directories can be joined by os.pathsep). | |
105 | """ % sys.argv[0]) | |
106 | ||
107 | PRAGMA_NOCOVER = "#pragma NO COVER" | |
108 | ||
109 | # Simple rx to find lines with no code. | |
110 | rx_blank = re.compile(r'^\s*(#.*)?$') | |
111 | ||
112 | class Ignore: | |
113 | def __init__(self, modules = None, dirs = None): | |
114 | self._mods = modules or [] | |
115 | self._dirs = dirs or [] | |
116 | ||
117 | self._dirs = map(os.path.normpath, self._dirs) | |
118 | self._ignore = { '<string>': 1 } | |
119 | ||
120 | def names(self, filename, modulename): | |
121 | if self._ignore.has_key(modulename): | |
122 | return self._ignore[modulename] | |
123 | ||
124 | # haven't seen this one before, so see if the module name is | |
125 | # on the ignore list. Need to take some care since ignoring | |
126 | # "cmp" musn't mean ignoring "cmpcache" but ignoring | |
127 | # "Spam" must also mean ignoring "Spam.Eggs". | |
128 | for mod in self._mods: | |
129 | if mod == modulename: # Identical names, so ignore | |
130 | self._ignore[modulename] = 1 | |
131 | return 1 | |
132 | # check if the module is a proper submodule of something on | |
133 | # the ignore list | |
134 | n = len(mod) | |
135 | # (will not overflow since if the first n characters are the | |
136 | # same and the name has not already occured, then the size | |
137 | # of "name" is greater than that of "mod") | |
138 | if mod == modulename[:n] and modulename[n] == '.': | |
139 | self._ignore[modulename] = 1 | |
140 | return 1 | |
141 | ||
142 | # Now check that __file__ isn't in one of the directories | |
143 | if filename is None: | |
144 | # must be a built-in, so we must ignore | |
145 | self._ignore[modulename] = 1 | |
146 | return 1 | |
147 | ||
148 | # Ignore a file when it contains one of the ignorable paths | |
149 | for d in self._dirs: | |
150 | # The '+ os.sep' is to ensure that d is a parent directory, | |
151 | # as compared to cases like: | |
152 | # d = "/usr/local" | |
153 | # filename = "/usr/local.py" | |
154 | # or | |
155 | # d = "/usr/local.py" | |
156 | # filename = "/usr/local.py" | |
157 | if filename.startswith(d + os.sep): | |
158 | self._ignore[modulename] = 1 | |
159 | return 1 | |
160 | ||
161 | # Tried the different ways, so we don't ignore this module | |
162 | self._ignore[modulename] = 0 | |
163 | return 0 | |
164 | ||
165 | def modname(path): | |
166 | """Return a plausible module name for the patch.""" | |
167 | ||
168 | base = os.path.basename(path) | |
169 | filename, ext = os.path.splitext(base) | |
170 | return filename | |
171 | ||
172 | def fullmodname(path): | |
173 | """Return a plausible module name for the path.""" | |
174 | ||
175 | # If the file 'path' is part of a package, then the filename isn't | |
176 | # enough to uniquely identify it. Try to do the right thing by | |
177 | # looking in sys.path for the longest matching prefix. We'll | |
178 | # assume that the rest is the package name. | |
179 | ||
180 | longest = "" | |
181 | for dir in sys.path: | |
182 | if path.startswith(dir) and path[len(dir)] == os.path.sep: | |
183 | if len(dir) > len(longest): | |
184 | longest = dir | |
185 | ||
186 | if longest: | |
187 | base = path[len(longest) + 1:] | |
188 | else: | |
189 | base = path | |
190 | base = base.replace(os.sep, ".") | |
191 | if os.altsep: | |
192 | base = base.replace(os.altsep, ".") | |
193 | filename, ext = os.path.splitext(base) | |
194 | return filename | |
195 | ||
196 | class CoverageResults: | |
197 | def __init__(self, counts=None, calledfuncs=None, infile=None, | |
198 | callers=None, outfile=None): | |
199 | self.counts = counts | |
200 | if self.counts is None: | |
201 | self.counts = {} | |
202 | self.counter = self.counts.copy() # map (filename, lineno) to count | |
203 | self.calledfuncs = calledfuncs | |
204 | if self.calledfuncs is None: | |
205 | self.calledfuncs = {} | |
206 | self.calledfuncs = self.calledfuncs.copy() | |
207 | self.callers = callers | |
208 | if self.callers is None: | |
209 | self.callers = {} | |
210 | self.callers = self.callers.copy() | |
211 | self.infile = infile | |
212 | self.outfile = outfile | |
213 | if self.infile: | |
214 | # Try to merge existing counts file. | |
215 | try: | |
216 | counts, calledfuncs, callers = \ | |
217 | pickle.load(open(self.infile, 'rb')) | |
218 | self.update(self.__class__(counts, calledfuncs, callers)) | |
219 | except (IOError, EOFError, ValueError), err: | |
220 | print >> sys.stderr, ("Skipping counts file %r: %s" | |
221 | % (self.infile, err)) | |
222 | ||
223 | def update(self, other): | |
224 | """Merge in the data from another CoverageResults""" | |
225 | counts = self.counts | |
226 | calledfuncs = self.calledfuncs | |
227 | callers = self.callers | |
228 | other_counts = other.counts | |
229 | other_calledfuncs = other.calledfuncs | |
230 | other_callers = other.callers | |
231 | ||
232 | for key in other_counts.keys(): | |
233 | counts[key] = counts.get(key, 0) + other_counts[key] | |
234 | ||
235 | for key in other_calledfuncs.keys(): | |
236 | calledfuncs[key] = 1 | |
237 | ||
238 | for key in other_callers.keys(): | |
239 | callers[key] = 1 | |
240 | ||
241 | def write_results(self, show_missing=True, summary=False, coverdir=None): | |
242 | """ | |
243 | @param coverdir | |
244 | """ | |
245 | if self.calledfuncs: | |
246 | ||
247 | print "functions called:" | |
248 | calls = self.calledfuncs.keys() | |
249 | calls.sort() | |
250 | for filename, modulename, funcname in calls: | |
251 | print ("filename: %s, modulename: %s, funcname: %s" | |
252 | % (filename, modulename, funcname)) | |
253 | ||
254 | if self.callers: | |
255 | ||
256 | print "calling relationships:" | |
257 | calls = self.callers.keys() | |
258 | calls.sort() | |
259 | lastfile = lastcfile = "" | |
260 | for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) in calls: | |
261 | if pfile != lastfile: | |
262 | ||
263 | print "***", pfile, "***" | |
264 | lastfile = pfile | |
265 | lastcfile = "" | |
266 | if cfile != pfile and lastcfile != cfile: | |
267 | print " -->", cfile | |
268 | lastcfile = cfile | |
269 | print " %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc) | |
270 | ||
271 | # turn the counts data ("(filename, lineno) = count") into something | |
272 | # accessible on a per-file basis | |
273 | per_file = {} | |
274 | for filename, lineno in self.counts.keys(): | |
275 | lines_hit = per_file[filename] = per_file.get(filename, {}) | |
276 | lines_hit[lineno] = self.counts[(filename, lineno)] | |
277 | ||
278 | # accumulate summary info, if needed | |
279 | sums = {} | |
280 | ||
281 | for filename, count in per_file.iteritems(): | |
282 | # skip some "files" we don't care about... | |
283 | if filename == "<string>": | |
284 | continue | |
285 | ||
286 | if filename.endswith(".pyc") or filename.endswith(".pyo"): | |
287 | filename = filename[:-1] | |
288 | ||
289 | if coverdir is None: | |
290 | dir = os.path.dirname(os.path.abspath(filename)) | |
291 | modulename = modname(filename) | |
292 | else: | |
293 | dir = coverdir | |
294 | if not os.path.exists(dir): | |
295 | os.makedirs(dir) | |
296 | modulename = fullmodname(filename) | |
297 | ||
298 | # If desired, get a list of the line numbers which represent | |
299 | # executable content (returned as a dict for better lookup speed) | |
300 | if show_missing: | |
301 | lnotab = find_executable_linenos(filename) | |
302 | else: | |
303 | lnotab = {} | |
304 | ||
305 | source = linecache.getlines(filename) | |
306 | coverpath = os.path.join(dir, modulename + ".cover") | |
307 | n_hits, n_lines = self.write_results_file(coverpath, source, | |
308 | lnotab, count) | |
309 | ||
310 | if summary and n_lines: | |
311 | percent = int(100 * n_hits / n_lines) | |
312 | sums[modulename] = n_lines, percent, modulename, filename | |
313 | ||
314 | if summary and sums: | |
315 | mods = sums.keys() | |
316 | mods.sort() | |
317 | print "lines cov% module (path)" | |
318 | for m in mods: | |
319 | n_lines, percent, modulename, filename = sums[m] | |
320 | print "%5d %3d%% %s (%s)" % sums[m] | |
321 | ||
322 | if self.outfile: | |
323 | # try and store counts and module info into self.outfile | |
324 | try: | |
325 | pickle.dump((self.counts, self.calledfuncs, self.callers), | |
326 | open(self.outfile, 'wb'), 1) | |
327 | except IOError, err: | |
328 | print >> sys.stderr, "Can't save counts files because %s" % err | |
329 | ||
330 | def write_results_file(self, path, lines, lnotab, lines_hit): | |
331 | """Return a coverage results file in path.""" | |
332 | ||
333 | try: | |
334 | outfile = open(path, "w") | |
335 | except IOError, err: | |
336 | print >> sys.stderr, ("trace: Could not open %r for writing: %s" | |
337 | "- skipping" % (path, err)) | |
338 | return 0, 0 | |
339 | ||
340 | n_lines = 0 | |
341 | n_hits = 0 | |
342 | for i, line in enumerate(lines): | |
343 | lineno = i + 1 | |
344 | # do the blank/comment match to try to mark more lines | |
345 | # (help the reader find stuff that hasn't been covered) | |
346 | if lineno in lines_hit: | |
347 | outfile.write("%5d: " % lines_hit[lineno]) | |
348 | n_hits += 1 | |
349 | n_lines += 1 | |
350 | elif rx_blank.match(line): | |
351 | outfile.write(" ") | |
352 | else: | |
353 | # lines preceded by no marks weren't hit | |
354 | # Highlight them if so indicated, unless the line contains | |
355 | # #pragma: NO COVER | |
356 | if lineno in lnotab and not PRAGMA_NOCOVER in lines[i]: | |
357 | outfile.write(">>>>>> ") | |
358 | n_lines += 1 | |
359 | else: | |
360 | outfile.write(" ") | |
361 | outfile.write(lines[i].expandtabs(8)) | |
362 | outfile.close() | |
363 | ||
364 | return n_hits, n_lines | |
365 | ||
366 | def find_lines_from_code(code, strs): | |
367 | """Return dict where keys are lines in the line number table.""" | |
368 | linenos = {} | |
369 | ||
370 | line_increments = [ord(c) for c in code.co_lnotab[1::2]] | |
371 | table_length = len(line_increments) | |
372 | docstring = False | |
373 | ||
374 | lineno = code.co_firstlineno | |
375 | for li in line_increments: | |
376 | lineno += li | |
377 | if lineno not in strs: | |
378 | linenos[lineno] = 1 | |
379 | ||
380 | return linenos | |
381 | ||
382 | def find_lines(code, strs): | |
383 | """Return lineno dict for all code objects reachable from code.""" | |
384 | # get all of the lineno information from the code of this scope level | |
385 | linenos = find_lines_from_code(code, strs) | |
386 | ||
387 | # and check the constants for references to other code objects | |
388 | for c in code.co_consts: | |
389 | if isinstance(c, types.CodeType): | |
390 | # find another code object, so recurse into it | |
391 | linenos.update(find_lines(c, strs)) | |
392 | return linenos | |
393 | ||
394 | def find_strings(filename): | |
395 | """Return a dict of possible docstring positions. | |
396 | ||
397 | The dict maps line numbers to strings. There is an entry for | |
398 | line that contains only a string or a part of a triple-quoted | |
399 | string. | |
400 | """ | |
401 | d = {} | |
402 | # If the first token is a string, then it's the module docstring. | |
403 | # Add this special case so that the test in the loop passes. | |
404 | prev_ttype = token.INDENT | |
405 | f = open(filename) | |
406 | for ttype, tstr, start, end, line in tokenize.generate_tokens(f.readline): | |
407 | if ttype == token.STRING: | |
408 | if prev_ttype == token.INDENT: | |
409 | sline, scol = start | |
410 | eline, ecol = end | |
411 | for i in range(sline, eline + 1): | |
412 | d[i] = 1 | |
413 | prev_ttype = ttype | |
414 | f.close() | |
415 | return d | |
416 | ||
417 | def find_executable_linenos(filename): | |
418 | """Return dict where keys are line numbers in the line number table.""" | |
419 | try: | |
420 | prog = open(filename, "rU").read() | |
421 | except IOError, err: | |
422 | print >> sys.stderr, ("Not printing coverage data for %r: %s" | |
423 | % (filename, err)) | |
424 | return {} | |
425 | code = compile(prog, filename, "exec") | |
426 | strs = find_strings(filename) | |
427 | return find_lines(code, strs) | |
428 | ||
429 | class Trace: | |
430 | def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0, | |
431 | ignoremods=(), ignoredirs=(), infile=None, outfile=None): | |
432 | """ | |
433 | @param count true iff it should count number of times each | |
434 | line is executed | |
435 | @param trace true iff it should print out each line that is | |
436 | being counted | |
437 | @param countfuncs true iff it should just output a list of | |
438 | (filename, modulename, funcname,) for functions | |
439 | that were called at least once; This overrides | |
440 | `count' and `trace' | |
441 | @param ignoremods a list of the names of modules to ignore | |
442 | @param ignoredirs a list of the names of directories to ignore | |
443 | all of the (recursive) contents of | |
444 | @param infile file from which to read stored counts to be | |
445 | added into the results | |
446 | @param outfile file in which to write the results | |
447 | """ | |
448 | self.infile = infile | |
449 | self.outfile = outfile | |
450 | self.ignore = Ignore(ignoremods, ignoredirs) | |
451 | self.counts = {} # keys are (filename, linenumber) | |
452 | self.blabbed = {} # for debugging | |
453 | self.pathtobasename = {} # for memoizing os.path.basename | |
454 | self.donothing = 0 | |
455 | self.trace = trace | |
456 | self._calledfuncs = {} | |
457 | self._callers = {} | |
458 | self._caller_cache = {} | |
459 | if countcallers: | |
460 | self.globaltrace = self.globaltrace_trackcallers | |
461 | elif countfuncs: | |
462 | self.globaltrace = self.globaltrace_countfuncs | |
463 | elif trace and count: | |
464 | self.globaltrace = self.globaltrace_lt | |
465 | self.localtrace = self.localtrace_trace_and_count | |
466 | elif trace: | |
467 | self.globaltrace = self.globaltrace_lt | |
468 | self.localtrace = self.localtrace_trace | |
469 | elif count: | |
470 | self.globaltrace = self.globaltrace_lt | |
471 | self.localtrace = self.localtrace_count | |
472 | else: | |
473 | # Ahem -- do nothing? Okay. | |
474 | self.donothing = 1 | |
475 | ||
476 | def run(self, cmd): | |
477 | import __main__ | |
478 | dict = __main__.__dict__ | |
479 | if not self.donothing: | |
480 | sys.settrace(self.globaltrace) | |
481 | threading.settrace(self.globaltrace) | |
482 | try: | |
483 | exec cmd in dict, dict | |
484 | finally: | |
485 | if not self.donothing: | |
486 | sys.settrace(None) | |
487 | threading.settrace(None) | |
488 | ||
489 | def runctx(self, cmd, globals=None, locals=None): | |
490 | if globals is None: globals = {} | |
491 | if locals is None: locals = {} | |
492 | if not self.donothing: | |
493 | sys.settrace(self.globaltrace) | |
494 | threading.settrace(self.globaltrace) | |
495 | try: | |
496 | exec cmd in globals, locals | |
497 | finally: | |
498 | if not self.donothing: | |
499 | sys.settrace(None) | |
500 | threading.settrace(None) | |
501 | ||
502 | def runfunc(self, func, *args, **kw): | |
503 | result = None | |
504 | if not self.donothing: | |
505 | sys.settrace(self.globaltrace) | |
506 | try: | |
507 | result = func(*args, **kw) | |
508 | finally: | |
509 | if not self.donothing: | |
510 | sys.settrace(None) | |
511 | return result | |
512 | ||
513 | def file_module_function_of(self, frame): | |
514 | code = frame.f_code | |
515 | filename = code.co_filename | |
516 | if filename: | |
517 | modulename = modname(filename) | |
518 | else: | |
519 | modulename = None | |
520 | ||
521 | funcname = code.co_name | |
522 | clsname = None | |
523 | if code in self._caller_cache: | |
524 | if self._caller_cache[code] is not None: | |
525 | clsname = self._caller_cache[code] | |
526 | else: | |
527 | self._caller_cache[code] = None | |
528 | ## use of gc.get_referrers() was suggested by Michael Hudson | |
529 | # all functions which refer to this code object | |
530 | funcs = [f for f in gc.get_referrers(code) | |
531 | if hasattr(f, "func_doc")] | |
532 | # require len(func) == 1 to avoid ambiguity caused by calls to | |
533 | # new.function(): "In the face of ambiguity, refuse the | |
534 | # temptation to guess." | |
535 | if len(funcs) == 1: | |
536 | dicts = [d for d in gc.get_referrers(funcs[0]) | |
537 | if isinstance(d, dict)] | |
538 | if len(dicts) == 1: | |
539 | classes = [c for c in gc.get_referrers(dicts[0]) | |
540 | if hasattr(c, "__bases__")] | |
541 | if len(classes) == 1: | |
542 | # ditto for new.classobj() | |
543 | clsname = str(classes[0]) | |
544 | # cache the result - assumption is that new.* is | |
545 | # not called later to disturb this relationship | |
546 | # _caller_cache could be flushed if functions in | |
547 | # the new module get called. | |
548 | self._caller_cache[code] = clsname | |
549 | if clsname is not None: | |
550 | # final hack - module name shows up in str(cls), but we've already | |
551 | # computed module name, so remove it | |
552 | clsname = clsname.split(".")[1:] | |
553 | clsname = ".".join(clsname) | |
554 | funcname = "%s.%s" % (clsname, funcname) | |
555 | ||
556 | return filename, modulename, funcname | |
557 | ||
558 | def globaltrace_trackcallers(self, frame, why, arg): | |
559 | """Handler for call events. | |
560 | ||
561 | Adds information about who called who to the self._callers dict. | |
562 | """ | |
563 | if why == 'call': | |
564 | # XXX Should do a better job of identifying methods | |
565 | this_func = self.file_module_function_of(frame) | |
566 | parent_func = self.file_module_function_of(frame.f_back) | |
567 | self._callers[(parent_func, this_func)] = 1 | |
568 | ||
569 | def globaltrace_countfuncs(self, frame, why, arg): | |
570 | """Handler for call events. | |
571 | ||
572 | Adds (filename, modulename, funcname) to the self._calledfuncs dict. | |
573 | """ | |
574 | if why == 'call': | |
575 | this_func = self.file_module_function_of(frame) | |
576 | self._calledfuncs[this_func] = 1 | |
577 | ||
578 | def globaltrace_lt(self, frame, why, arg): | |
579 | """Handler for call events. | |
580 | ||
581 | If the code block being entered is to be ignored, returns `None', | |
582 | else returns self.localtrace. | |
583 | """ | |
584 | if why == 'call': | |
585 | code = frame.f_code | |
586 | filename = code.co_filename | |
587 | if filename: | |
588 | # XXX modname() doesn't work right for packages, so | |
589 | # the ignore support won't work right for packages | |
590 | modulename = modname(filename) | |
591 | if modulename is not None: | |
592 | ignore_it = self.ignore.names(filename, modulename) | |
593 | if not ignore_it: | |
594 | if self.trace: | |
595 | print (" --- modulename: %s, funcname: %s" | |
596 | % (modulename, code.co_name)) | |
597 | return self.localtrace | |
598 | else: | |
599 | return None | |
600 | ||
601 | def localtrace_trace_and_count(self, frame, why, arg): | |
602 | if why == "line": | |
603 | # record the file name and line number of every trace | |
604 | filename = frame.f_code.co_filename | |
605 | lineno = frame.f_lineno | |
606 | key = filename, lineno | |
607 | self.counts[key] = self.counts.get(key, 0) + 1 | |
608 | ||
609 | bname = os.path.basename(filename) | |
610 | print "%s(%d): %s" % (bname, lineno, | |
611 | linecache.getline(filename, lineno)), | |
612 | return self.localtrace | |
613 | ||
614 | def localtrace_trace(self, frame, why, arg): | |
615 | if why == "line": | |
616 | # record the file name and line number of every trace | |
617 | filename = frame.f_code.co_filename | |
618 | lineno = frame.f_lineno | |
619 | ||
620 | bname = os.path.basename(filename) | |
621 | print "%s(%d): %s" % (bname, lineno, | |
622 | linecache.getline(filename, lineno)), | |
623 | return self.localtrace | |
624 | ||
625 | def localtrace_count(self, frame, why, arg): | |
626 | if why == "line": | |
627 | filename = frame.f_code.co_filename | |
628 | lineno = frame.f_lineno | |
629 | key = filename, lineno | |
630 | self.counts[key] = self.counts.get(key, 0) + 1 | |
631 | return self.localtrace | |
632 | ||
633 | def results(self): | |
634 | return CoverageResults(self.counts, infile=self.infile, | |
635 | outfile=self.outfile, | |
636 | calledfuncs=self._calledfuncs, | |
637 | callers=self._callers) | |
638 | ||
639 | def _err_exit(msg): | |
640 | sys.stderr.write("%s: %s\n" % (sys.argv[0], msg)) | |
641 | sys.exit(1) | |
642 | ||
643 | def main(argv=None): | |
644 | import getopt | |
645 | ||
646 | if argv is None: | |
647 | argv = sys.argv | |
648 | try: | |
649 | opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:lT", | |
650 | ["help", "version", "trace", "count", | |
651 | "report", "no-report", "summary", | |
652 | "file=", "missing", | |
653 | "ignore-module=", "ignore-dir=", | |
654 | "coverdir=", "listfuncs", | |
655 | "trackcalls"]) | |
656 | ||
657 | except getopt.error, msg: | |
658 | sys.stderr.write("%s: %s\n" % (sys.argv[0], msg)) | |
659 | sys.stderr.write("Try `%s --help' for more information\n" | |
660 | % sys.argv[0]) | |
661 | sys.exit(1) | |
662 | ||
663 | trace = 0 | |
664 | count = 0 | |
665 | report = 0 | |
666 | no_report = 0 | |
667 | counts_file = None | |
668 | missing = 0 | |
669 | ignore_modules = [] | |
670 | ignore_dirs = [] | |
671 | coverdir = None | |
672 | summary = 0 | |
673 | listfuncs = False | |
674 | countcallers = False | |
675 | ||
676 | for opt, val in opts: | |
677 | if opt == "--help": | |
678 | usage(sys.stdout) | |
679 | sys.exit(0) | |
680 | ||
681 | if opt == "--version": | |
682 | sys.stdout.write("trace 2.0\n") | |
683 | sys.exit(0) | |
684 | ||
685 | if opt == "-T" or opt == "--trackcalls": | |
686 | countcallers = True | |
687 | continue | |
688 | ||
689 | if opt == "-l" or opt == "--listfuncs": | |
690 | listfuncs = True | |
691 | continue | |
692 | ||
693 | if opt == "-t" or opt == "--trace": | |
694 | trace = 1 | |
695 | continue | |
696 | ||
697 | if opt == "-c" or opt == "--count": | |
698 | count = 1 | |
699 | continue | |
700 | ||
701 | if opt == "-r" or opt == "--report": | |
702 | report = 1 | |
703 | continue | |
704 | ||
705 | if opt == "-R" or opt == "--no-report": | |
706 | no_report = 1 | |
707 | continue | |
708 | ||
709 | if opt == "-f" or opt == "--file": | |
710 | counts_file = val | |
711 | continue | |
712 | ||
713 | if opt == "-m" or opt == "--missing": | |
714 | missing = 1 | |
715 | continue | |
716 | ||
717 | if opt == "-C" or opt == "--coverdir": | |
718 | coverdir = val | |
719 | continue | |
720 | ||
721 | if opt == "-s" or opt == "--summary": | |
722 | summary = 1 | |
723 | continue | |
724 | ||
725 | if opt == "--ignore-module": | |
726 | ignore_modules.append(val) | |
727 | continue | |
728 | ||
729 | if opt == "--ignore-dir": | |
730 | for s in val.split(os.pathsep): | |
731 | s = os.path.expandvars(s) | |
732 | # should I also call expanduser? (after all, could use $HOME) | |
733 | ||
734 | s = s.replace("$prefix", | |
735 | os.path.join(sys.prefix, "lib", | |
736 | "python" + sys.version[:3])) | |
737 | s = s.replace("$exec_prefix", | |
738 | os.path.join(sys.exec_prefix, "lib", | |
739 | "python" + sys.version[:3])) | |
740 | s = os.path.normpath(s) | |
741 | ignore_dirs.append(s) | |
742 | continue | |
743 | ||
744 | assert 0, "Should never get here" | |
745 | ||
746 | if listfuncs and (count or trace): | |
747 | _err_exit("cannot specify both --listfuncs and (--trace or --count)") | |
748 | ||
749 | if not (count or trace or report or listfuncs or countcallers): | |
750 | _err_exit("must specify one of --trace, --count, --report, " | |
751 | "--listfuncs, or --trackcalls") | |
752 | ||
753 | if report and no_report: | |
754 | _err_exit("cannot specify both --report and --no-report") | |
755 | ||
756 | if report and not counts_file: | |
757 | _err_exit("--report requires a --file") | |
758 | ||
759 | if no_report and len(prog_argv) == 0: | |
760 | _err_exit("missing name of file to run") | |
761 | ||
762 | # everything is ready | |
763 | if report: | |
764 | results = CoverageResults(infile=counts_file, outfile=counts_file) | |
765 | results.write_results(missing, summary=summary, coverdir=coverdir) | |
766 | else: | |
767 | sys.argv = prog_argv | |
768 | progname = prog_argv[0] | |
769 | sys.path[0] = os.path.split(progname)[0] | |
770 | ||
771 | t = Trace(count, trace, countfuncs=listfuncs, | |
772 | countcallers=countcallers, ignoremods=ignore_modules, | |
773 | ignoredirs=ignore_dirs, infile=counts_file, | |
774 | outfile=counts_file) | |
775 | try: | |
776 | t.run('execfile(%r)' % (progname,)) | |
777 | except IOError, err: | |
778 | _err_exit("Cannot run file %r because: %s" % (sys.argv[0], err)) | |
779 | except SystemExit: | |
780 | pass | |
781 | ||
782 | results = t.results() | |
783 | ||
784 | if not no_report: | |
785 | results.write_results(missing, summary=summary, coverdir=coverdir) | |
786 | ||
787 | if __name__=='__main__': | |
788 | main() |