Show More
@@ -1,4 +1,4 b'' | |||
|
1 |
#!/usr/bin/ |
|
|
1 | #!/usr/bin/python | |
|
2 | 2 | # |
|
3 | 3 | # Perforce Defect Tracking Integration Project |
|
4 | 4 | # <http://www.ravenbrook.com/project/p4dti/> |
@@ -22,15 +22,20 b'' | |||
|
22 | 22 | # interface and limitations. See [GDR 2001-12-04b] for requirements and |
|
23 | 23 | # design. |
|
24 | 24 | |
|
25 | """Usage: | |
|
25 | r"""Usage: | |
|
26 | 26 | |
|
27 | coverage.py -x MODULE.py [ARG1 ARG2 ...] | |
|
27 | coverage.py -x [-p] MODULE.py [ARG1 ARG2 ...] | |
|
28 | 28 | Execute module, passing the given command-line arguments, collecting |
|
29 | coverage data. | |
|
29 | coverage data. With the -p option, write to a temporary file containing | |
|
30 | the machine name and process ID. | |
|
30 | 31 | |
|
31 | 32 | coverage.py -e |
|
32 | 33 | Erase collected coverage data. |
|
33 | 34 | |
|
35 | coverage.py -c | |
|
36 | Collect data from multiple coverage files (as created by -p option above) | |
|
37 | and store it into a single file representing the union of the coverage. | |
|
38 | ||
|
34 | 39 | coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ... |
|
35 | 40 | Report on the statement coverage for the given files. With the -m |
|
36 | 41 | option, show line numbers of the statements that weren't executed. |
@@ -49,16 +54,26 b' coverage.py -a [-d dir] [-o dir1,dir2,..' | |||
|
49 | 54 | Coverage data is saved in the file .coverage by default. Set the |
|
50 | 55 | COVERAGE_FILE environment variable to save it somewhere else.""" |
|
51 | 56 | |
|
52 |
__version__ = "2. |
|
|
57 | __version__ = "2.77.20070729" # see detailed history at the end of this file. | |
|
53 | 58 | |
|
54 | 59 | import compiler |
|
55 | 60 | import compiler.visitor |
|
61 | import glob | |
|
56 | 62 | import os |
|
57 | 63 | import re |
|
58 | 64 | import string |
|
65 | import symbol | |
|
59 | 66 | import sys |
|
60 | 67 | import threading |
|
68 | import token | |
|
61 | 69 | import types |
|
70 | from socket import gethostname | |
|
71 | ||
|
72 | # Python version compatibility | |
|
73 | try: | |
|
74 | strclass = basestring # new to 2.3 | |
|
75 | except: | |
|
76 | strclass = str | |
|
62 | 77 | |
|
63 | 78 | # 2. IMPLEMENTATION |
|
64 | 79 | # |
@@ -81,25 +96,29 b' import types' | |||
|
81 | 96 | # names to increase speed. |
|
82 | 97 | |
|
83 | 98 | class StatementFindingAstVisitor(compiler.visitor.ASTVisitor): |
|
99 | """ A visitor for a parsed Abstract Syntax Tree which finds executable | |
|
100 | statements. | |
|
101 | """ | |
|
84 | 102 | def __init__(self, statements, excluded, suite_spots): |
|
85 | 103 | compiler.visitor.ASTVisitor.__init__(self) |
|
86 | 104 | self.statements = statements |
|
87 | 105 | self.excluded = excluded |
|
88 | 106 | self.suite_spots = suite_spots |
|
89 | 107 | self.excluding_suite = 0 |
|
90 | ||
|
108 | ||
|
91 | 109 | def doRecursive(self, node): |
|
92 | self.recordNodeLine(node) | |
|
93 | 110 | for n in node.getChildNodes(): |
|
94 | 111 | self.dispatch(n) |
|
95 | 112 | |
|
96 | 113 | visitStmt = visitModule = doRecursive |
|
97 | ||
|
114 | ||
|
98 | 115 | def doCode(self, node): |
|
99 | 116 | if hasattr(node, 'decorators') and node.decorators: |
|
100 | 117 | self.dispatch(node.decorators) |
|
101 |
self. |
|
|
102 | ||
|
118 | self.recordAndDispatch(node.code) | |
|
119 | else: | |
|
120 | self.doSuite(node, node.code) | |
|
121 | ||
|
103 | 122 | visitFunction = visitClass = doCode |
|
104 | 123 | |
|
105 | 124 | def getFirstLine(self, node): |
@@ -119,17 +138,40 b' class StatementFindingAstVisitor(compile' | |||
|
119 | 138 | for n in node.getChildNodes(): |
|
120 | 139 | lineno = max(lineno, self.getLastLine(n)) |
|
121 | 140 | return lineno |
|
122 | ||
|
141 | ||
|
123 | 142 | def doStatement(self, node): |
|
124 | 143 | self.recordLine(self.getFirstLine(node)) |
|
125 | 144 | |
|
126 |
visitAssert = visitAssign = visitAssTuple = |
|
|
145 | visitAssert = visitAssign = visitAssTuple = visitPrint = \ | |
|
127 | 146 | visitPrintnl = visitRaise = visitSubscript = visitDecorators = \ |
|
128 | 147 | doStatement |
|
148 | ||
|
149 | def visitPass(self, node): | |
|
150 | # Pass statements have weird interactions with docstrings. If this | |
|
151 | # pass statement is part of one of those pairs, claim that the statement | |
|
152 | # is on the later of the two lines. | |
|
153 | l = node.lineno | |
|
154 | if l: | |
|
155 | lines = self.suite_spots.get(l, [l,l]) | |
|
156 | self.statements[lines[1]] = 1 | |
|
157 | ||
|
158 | def visitDiscard(self, node): | |
|
159 | # Discard nodes are statements that execute an expression, but then | |
|
160 | # discard the results. This includes function calls, so we can't | |
|
161 | # ignore them all. But if the expression is a constant, the statement | |
|
162 | # won't be "executed", so don't count it now. | |
|
163 | if node.expr.__class__.__name__ != 'Const': | |
|
164 | self.doStatement(node) | |
|
129 | 165 | |
|
130 | 166 | def recordNodeLine(self, node): |
|
131 | return self.recordLine(node.lineno) | |
|
132 | ||
|
167 | # Stmt nodes often have None, but shouldn't claim the first line of | |
|
168 | # their children (because the first child might be an ignorable line | |
|
169 | # like "global a"). | |
|
170 | if node.__class__.__name__ != 'Stmt': | |
|
171 | return self.recordLine(self.getFirstLine(node)) | |
|
172 | else: | |
|
173 | return 0 | |
|
174 | ||
|
133 | 175 | def recordLine(self, lineno): |
|
134 | 176 | # Returns a bool, whether the line is included or excluded. |
|
135 | 177 | if lineno: |
@@ -137,7 +179,7 b' class StatementFindingAstVisitor(compile' | |||
|
137 | 179 | # keyword. |
|
138 | 180 | if lineno in self.suite_spots: |
|
139 | 181 | lineno = self.suite_spots[lineno][0] |
|
140 | # If we're inside an exluded suite, record that this line was | |
|
182 | # If we're inside an excluded suite, record that this line was | |
|
141 | 183 | # excluded. |
|
142 | 184 | if self.excluding_suite: |
|
143 | 185 | self.excluded[lineno] = 1 |
@@ -153,9 +195,9 b' class StatementFindingAstVisitor(compile' | |||
|
153 | 195 | self.statements[lineno] = 1 |
|
154 | 196 | return 1 |
|
155 | 197 | return 0 |
|
156 | ||
|
198 | ||
|
157 | 199 | default = recordNodeLine |
|
158 | ||
|
200 | ||
|
159 | 201 | def recordAndDispatch(self, node): |
|
160 | 202 | self.recordNodeLine(node) |
|
161 | 203 | self.dispatch(node) |
@@ -166,7 +208,7 b' class StatementFindingAstVisitor(compile' | |||
|
166 | 208 | self.excluding_suite = 1 |
|
167 | 209 | self.recordAndDispatch(body) |
|
168 | 210 | self.excluding_suite = exsuite |
|
169 | ||
|
211 | ||
|
170 | 212 | def doPlainWordSuite(self, prevsuite, suite): |
|
171 | 213 | # Finding the exclude lines for else's is tricky, because they aren't |
|
172 | 214 | # present in the compiler parse tree. Look at the previous suite, |
@@ -180,15 +222,17 b' class StatementFindingAstVisitor(compile' | |||
|
180 | 222 | break |
|
181 | 223 | else: |
|
182 | 224 | self.doSuite(None, suite) |
|
183 | ||
|
225 | ||
|
184 | 226 | def doElse(self, prevsuite, node): |
|
185 | 227 | if node.else_: |
|
186 | 228 | self.doPlainWordSuite(prevsuite, node.else_) |
|
187 | ||
|
229 | ||
|
188 | 230 | def visitFor(self, node): |
|
189 | 231 | self.doSuite(node, node.body) |
|
190 | 232 | self.doElse(node.body, node) |
|
191 | 233 | |
|
234 | visitWhile = visitFor | |
|
235 | ||
|
192 | 236 | def visitIf(self, node): |
|
193 | 237 | # The first test has to be handled separately from the rest. |
|
194 | 238 | # The first test is credited to the line with the "if", but the others |
@@ -198,10 +242,6 b' class StatementFindingAstVisitor(compile' | |||
|
198 | 242 | self.doSuite(t, n) |
|
199 | 243 | self.doElse(node.tests[-1][1], node) |
|
200 | 244 | |
|
201 | def visitWhile(self, node): | |
|
202 | self.doSuite(node, node.body) | |
|
203 | self.doElse(node.body, node) | |
|
204 | ||
|
205 | 245 | def visitTryExcept(self, node): |
|
206 | 246 | self.doSuite(node, node.body) |
|
207 | 247 | for i in range(len(node.handlers)): |
@@ -216,11 +256,14 b' class StatementFindingAstVisitor(compile' | |||
|
216 | 256 | else: |
|
217 | 257 | self.doSuite(a, h) |
|
218 | 258 | self.doElse(node.handlers[-1][2], node) |
|
219 | ||
|
259 | ||
|
220 | 260 | def visitTryFinally(self, node): |
|
221 | 261 | self.doSuite(node, node.body) |
|
222 | 262 | self.doPlainWordSuite(node.body, node.final) |
|
223 | ||
|
263 | ||
|
264 | def visitWith(self, node): | |
|
265 | self.doSuite(node, node.body) | |
|
266 | ||
|
224 | 267 | def visitGlobal(self, node): |
|
225 | 268 | # "global" statements don't execute like others (they don't call the |
|
226 | 269 | # trace function), so don't record their line numbers. |
@@ -228,9 +271,9 b' class StatementFindingAstVisitor(compile' | |||
|
228 | 271 | |
|
229 | 272 | the_coverage = None |
|
230 | 273 | |
|
274 | class CoverageException(Exception): pass | |
|
275 | ||
|
231 | 276 | class coverage: |
|
232 | error = "coverage error" | |
|
233 | ||
|
234 | 277 | # Name of the cache file (unless environment variable is set). |
|
235 | 278 | cache_default = ".coverage" |
|
236 | 279 | |
@@ -240,7 +283,7 b' class coverage:' | |||
|
240 | 283 | # A dictionary with an entry for (Python source file name, line number |
|
241 | 284 | # in that file) if that line has been executed. |
|
242 | 285 | c = {} |
|
243 | ||
|
286 | ||
|
244 | 287 | # A map from canonical Python source file name to a dictionary in |
|
245 | 288 | # which there's an entry for each line number that has been |
|
246 | 289 | # executed. |
@@ -257,53 +300,58 b' class coverage:' | |||
|
257 | 300 | def __init__(self): |
|
258 | 301 | global the_coverage |
|
259 | 302 | if the_coverage: |
|
260 |
raise |
|
|
303 | raise CoverageException, "Only one coverage object allowed." | |
|
261 | 304 | self.usecache = 1 |
|
262 | 305 | self.cache = None |
|
306 | self.parallel_mode = False | |
|
263 | 307 | self.exclude_re = '' |
|
264 | 308 | self.nesting = 0 |
|
265 | 309 | self.cstack = [] |
|
266 | 310 | self.xstack = [] |
|
267 |
self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os. |
|
|
311 | self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.sep) | |
|
312 | self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') | |
|
268 | 313 | |
|
269 | # t(f, x, y). This method is passed to sys.settrace as a trace function. | |
|
270 | # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and | |
|
314 | # t(f, x, y). This method is passed to sys.settrace as a trace function. | |
|
315 | # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and | |
|
271 | 316 | # the arguments and return value of the trace function. |
|
272 | 317 | # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code |
|
273 | 318 | # objects. |
|
274 | ||
|
275 |
def t(self, f, w, |
|
|
276 | #print w, f.f_code.co_filename, f.f_lineno | |
|
319 | ||
|
320 | def t(self, f, w, unused): #pragma: no cover | |
|
277 | 321 | if w == 'line': |
|
322 | #print "Executing %s @ %d" % (f.f_code.co_filename, f.f_lineno) | |
|
278 | 323 | self.c[(f.f_code.co_filename, f.f_lineno)] = 1 |
|
279 | 324 | for c in self.cstack: |
|
280 | 325 | c[(f.f_code.co_filename, f.f_lineno)] = 1 |
|
281 | 326 | return self.t |
|
282 | ||
|
283 | def help(self, error=None): | |
|
327 | ||
|
328 | def help(self, error=None): #pragma: no cover | |
|
284 | 329 | if error: |
|
285 | 330 | print error |
|
286 | 331 | |
|
287 | 332 | print __doc__ |
|
288 | 333 | sys.exit(1) |
|
289 | 334 | |
|
290 | def command_line(self): | |
|
335 | def command_line(self, argv, help_fn=None): | |
|
291 | 336 | import getopt |
|
337 | help_fn = help_fn or self.help | |
|
292 | 338 | settings = {} |
|
293 | 339 | optmap = { |
|
294 | 340 | '-a': 'annotate', |
|
341 | '-c': 'collect', | |
|
295 | 342 | '-d:': 'directory=', |
|
296 | 343 | '-e': 'erase', |
|
297 | 344 | '-h': 'help', |
|
298 | 345 | '-i': 'ignore-errors', |
|
299 | 346 | '-m': 'show-missing', |
|
347 | '-p': 'parallel-mode', | |
|
300 | 348 | '-r': 'report', |
|
301 | 349 | '-x': 'execute', |
|
302 | '-o': 'omit=', | |
|
350 | '-o:': 'omit=', | |
|
303 | 351 | } |
|
304 | 352 | short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '') |
|
305 | 353 | long_opts = optmap.values() |
|
306 |
options, args = getopt.getopt( |
|
|
354 | options, args = getopt.getopt(argv, short_opts, long_opts) | |
|
307 | 355 | for o, a in options: |
|
308 | 356 | if optmap.has_key(o): |
|
309 | 357 | settings[optmap[o]] = 1 |
@@ -312,69 +360,84 b' class coverage:' | |||
|
312 | 360 | elif o[2:] in long_opts: |
|
313 | 361 | settings[o[2:]] = 1 |
|
314 | 362 | elif o[2:] + '=' in long_opts: |
|
315 | settings[o[2:]] = a | |
|
316 | else: | |
|
317 | self.help("Unknown option: '%s'." % o) | |
|
363 | settings[o[2:]+'='] = a | |
|
364 | else: #pragma: no cover | |
|
365 | pass # Can't get here, because getopt won't return anything unknown. | |
|
366 | ||
|
318 | 367 | if settings.get('help'): |
|
319 |
|
|
|
368 | help_fn() | |
|
369 | ||
|
320 | 370 | for i in ['erase', 'execute']: |
|
321 | for j in ['annotate', 'report']: | |
|
371 | for j in ['annotate', 'report', 'collect']: | |
|
322 | 372 | if settings.get(i) and settings.get(j): |
|
323 |
|
|
|
373 | help_fn("You can't specify the '%s' and '%s' " | |
|
324 | 374 | "options at the same time." % (i, j)) |
|
375 | ||
|
325 | 376 | args_needed = (settings.get('execute') |
|
326 | 377 | or settings.get('annotate') |
|
327 | 378 | or settings.get('report')) |
|
328 |
action = settings.get('erase') |
|
|
379 | action = (settings.get('erase') | |
|
380 | or settings.get('collect') | |
|
381 | or args_needed) | |
|
329 | 382 | if not action: |
|
330 |
|
|
|
383 | help_fn("You must specify at least one of -e, -x, -c, -r, or -a.") | |
|
331 | 384 | if not args_needed and args: |
|
332 |
|
|
|
333 | ||
|
385 | help_fn("Unexpected arguments: %s" % " ".join(args)) | |
|
386 | ||
|
387 | self.parallel_mode = settings.get('parallel-mode') | |
|
334 | 388 | self.get_ready() |
|
335 | self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]') | |
|
336 | 389 | |
|
337 | 390 | if settings.get('erase'): |
|
338 | 391 | self.erase() |
|
339 | 392 | if settings.get('execute'): |
|
340 | 393 | if not args: |
|
341 |
|
|
|
394 | help_fn("Nothing to do.") | |
|
342 | 395 | sys.argv = args |
|
343 | 396 | self.start() |
|
344 | 397 | import __main__ |
|
345 | 398 | sys.path[0] = os.path.dirname(sys.argv[0]) |
|
346 | 399 | execfile(sys.argv[0], __main__.__dict__) |
|
400 | if settings.get('collect'): | |
|
401 | self.collect() | |
|
347 | 402 | if not args: |
|
348 | 403 | args = self.cexecuted.keys() |
|
404 | ||
|
349 | 405 | ignore_errors = settings.get('ignore-errors') |
|
350 | 406 | show_missing = settings.get('show-missing') |
|
351 | directory = settings.get('directory') | |
|
352 | omit = filter(None, settings.get('omit', '').split(',')) | |
|
353 | omit += ['/<'] # Always skip /<string> etc. | |
|
407 | directory = settings.get('directory=') | |
|
408 | ||
|
409 | omit = settings.get('omit=') | |
|
410 | if omit is not None: | |
|
411 | omit = omit.split(',') | |
|
412 | else: | |
|
413 | omit = [] | |
|
354 | 414 | |
|
355 | 415 | if settings.get('report'): |
|
356 | 416 | self.report(args, show_missing, ignore_errors, omit_prefixes=omit) |
|
357 | 417 | if settings.get('annotate'): |
|
358 | 418 | self.annotate(args, directory, ignore_errors, omit_prefixes=omit) |
|
359 | 419 | |
|
360 | def use_cache(self, usecache): | |
|
420 | def use_cache(self, usecache, cache_file=None): | |
|
361 | 421 | self.usecache = usecache |
|
362 | ||
|
363 | def get_ready(self): | |
|
422 | if cache_file and not self.cache: | |
|
423 | self.cache_default = cache_file | |
|
424 | ||
|
425 | def get_ready(self, parallel_mode=False): | |
|
364 | 426 | if self.usecache and not self.cache: |
|
365 |
self.cache = os. |
|
|
366 | self.cache_default)) | |
|
427 | self.cache = os.environ.get(self.cache_env, self.cache_default) | |
|
428 | if self.parallel_mode: | |
|
429 | self.cache += "." + gethostname() + "." + str(os.getpid()) | |
|
367 | 430 | self.restore() |
|
368 | 431 | self.analysis_cache = {} |
|
369 | ||
|
370 | def start(self): | |
|
432 | ||
|
433 | def start(self, parallel_mode=False): | |
|
371 | 434 | self.get_ready() |
|
372 | 435 | if self.nesting == 0: #pragma: no cover |
|
373 | 436 | sys.settrace(self.t) |
|
374 | 437 | if hasattr(threading, 'settrace'): |
|
375 | 438 | threading.settrace(self.t) |
|
376 | 439 | self.nesting += 1 |
|
377 | ||
|
440 | ||
|
378 | 441 | def stop(self): |
|
379 | 442 | self.nesting -= 1 |
|
380 | 443 | if self.nesting == 0: #pragma: no cover |
@@ -383,12 +446,12 b' class coverage:' | |||
|
383 | 446 | threading.settrace(None) |
|
384 | 447 | |
|
385 | 448 | def erase(self): |
|
449 | self.get_ready() | |
|
386 | 450 | self.c = {} |
|
387 | 451 | self.analysis_cache = {} |
|
388 | 452 | self.cexecuted = {} |
|
389 | 453 | if self.cache and os.path.exists(self.cache): |
|
390 | 454 | os.remove(self.cache) |
|
391 | self.exclude_re = "" | |
|
392 | 455 | |
|
393 | 456 | def exclude(self, re): |
|
394 | 457 | if self.exclude_re: |
@@ -398,7 +461,7 b' class coverage:' | |||
|
398 | 461 | def begin_recursive(self): |
|
399 | 462 | self.cstack.append(self.c) |
|
400 | 463 | self.xstack.append(self.exclude_re) |
|
401 | ||
|
464 | ||
|
402 | 465 | def end_recursive(self): |
|
403 | 466 | self.c = self.cstack.pop() |
|
404 | 467 | self.exclude_re = self.xstack.pop() |
@@ -406,8 +469,6 b' class coverage:' | |||
|
406 | 469 | # save(). Save coverage data to the coverage cache. |
|
407 | 470 | |
|
408 | 471 | def save(self): |
|
409 | # move to directory that must exist. | |
|
410 | os.chdir(os.sep) | |
|
411 | 472 | if self.usecache and self.cache: |
|
412 | 473 | self.canonicalize_filenames() |
|
413 | 474 | cache = open(self.cache, 'wb') |
@@ -421,17 +482,45 b' class coverage:' | |||
|
421 | 482 | self.c = {} |
|
422 | 483 | self.cexecuted = {} |
|
423 | 484 | assert self.usecache |
|
424 |
if |
|
|
425 | return | |
|
485 | if os.path.exists(self.cache): | |
|
486 | self.cexecuted = self.restore_file(self.cache) | |
|
487 | ||
|
488 | def restore_file(self, file_name): | |
|
426 | 489 | try: |
|
427 |
cache = open( |
|
|
490 | cache = open(file_name, 'rb') | |
|
428 | 491 | import marshal |
|
429 | 492 | cexecuted = marshal.load(cache) |
|
430 | 493 | cache.close() |
|
431 | 494 | if isinstance(cexecuted, types.DictType): |
|
432 |
|
|
|
495 | return cexecuted | |
|
496 | else: | |
|
497 | return {} | |
|
433 | 498 | except: |
|
434 |
|
|
|
499 | return {} | |
|
500 | ||
|
501 | # collect(). Collect data in multiple files produced by parallel mode | |
|
502 | ||
|
503 | def collect(self): | |
|
504 | cache_dir, local = os.path.split(self.cache) | |
|
505 | for f in os.listdir(cache_dir or '.'): | |
|
506 | if not f.startswith(local): | |
|
507 | continue | |
|
508 | ||
|
509 | full_path = os.path.join(cache_dir, f) | |
|
510 | cexecuted = self.restore_file(full_path) | |
|
511 | self.merge_data(cexecuted) | |
|
512 | ||
|
513 | def merge_data(self, new_data): | |
|
514 | for file_name, file_data in new_data.items(): | |
|
515 | if self.cexecuted.has_key(file_name): | |
|
516 | self.merge_file_data(self.cexecuted[file_name], file_data) | |
|
517 | else: | |
|
518 | self.cexecuted[file_name] = file_data | |
|
519 | ||
|
520 | def merge_file_data(self, cache_data, new_data): | |
|
521 | for line_number in new_data.keys(): | |
|
522 | if not cache_data.has_key(line_number): | |
|
523 | cache_data[line_number] = new_data[line_number] | |
|
435 | 524 | |
|
436 | 525 | # canonical_filename(filename). Return a canonical filename for the |
|
437 | 526 | # file (that is, an absolute path with no redundant components and |
@@ -452,11 +541,14 b' class coverage:' | |||
|
452 | 541 | self.canonical_filename_cache[filename] = cf |
|
453 | 542 | return self.canonical_filename_cache[filename] |
|
454 | 543 | |
|
455 | # canonicalize_filenames(). Copy results from "c" to "cexecuted", | |
|
544 | # canonicalize_filenames(). Copy results from "c" to "cexecuted", | |
|
456 | 545 | # canonicalizing filenames on the way. Clear the "c" map. |
|
457 | 546 | |
|
458 | 547 | def canonicalize_filenames(self): |
|
459 | 548 | for filename, lineno in self.c.keys(): |
|
549 | if filename == '<string>': | |
|
550 | # Can't do anything useful with exec'd strings, so skip them. | |
|
551 | continue | |
|
460 | 552 | f = self.canonical_filename(filename) |
|
461 | 553 | if not self.cexecuted.has_key(f): |
|
462 | 554 | self.cexecuted[f] = {} |
@@ -468,18 +560,20 b' class coverage:' | |||
|
468 | 560 | def morf_filename(self, morf): |
|
469 | 561 | if isinstance(morf, types.ModuleType): |
|
470 | 562 | if not hasattr(morf, '__file__'): |
|
471 |
raise |
|
|
472 |
f |
|
|
563 | raise CoverageException, "Module has no __file__ attribute." | |
|
564 | f = morf.__file__ | |
|
473 | 565 | else: |
|
474 |
f |
|
|
475 |
return self.canonical_filename(f |
|
|
566 | f = morf | |
|
567 | return self.canonical_filename(f) | |
|
476 | 568 | |
|
477 | 569 | # analyze_morf(morf). Analyze the module or filename passed as |
|
478 | 570 | # the argument. If the source code can't be found, raise an error. |
|
479 | 571 | # Otherwise, return a tuple of (1) the canonical filename of the |
|
480 | 572 | # source code for the module, (2) a list of lines of statements |
|
481 |
# in the source code, |
|
|
482 | ||
|
573 | # in the source code, (3) a list of lines of excluded statements, | |
|
574 | # and (4), a map of line numbers to multi-line line number ranges, for | |
|
575 | # statements that cross lines. | |
|
576 | ||
|
483 | 577 | def analyze_morf(self, morf): |
|
484 | 578 | if self.analysis_cache.has_key(morf): |
|
485 | 579 | return self.analysis_cache[morf] |
@@ -487,30 +581,69 b' class coverage:' | |||
|
487 | 581 | ext = os.path.splitext(filename)[1] |
|
488 | 582 | if ext == '.pyc': |
|
489 | 583 | if not os.path.exists(filename[0:-1]): |
|
490 |
raise |
|
|
584 | raise CoverageException, ("No source for compiled code '%s'." | |
|
491 | 585 | % filename) |
|
492 | 586 | filename = filename[0:-1] |
|
493 | 587 | elif ext != '.py': |
|
494 |
raise |
|
|
588 | raise CoverageException, "File '%s' not Python source." % filename | |
|
495 | 589 | source = open(filename, 'r') |
|
496 | lines, excluded_lines = self.find_executable_statements( | |
|
590 | lines, excluded_lines, line_map = self.find_executable_statements( | |
|
497 | 591 | source.read(), exclude=self.exclude_re |
|
498 | 592 | ) |
|
499 | 593 | source.close() |
|
500 | result = filename, lines, excluded_lines | |
|
594 | result = filename, lines, excluded_lines, line_map | |
|
501 | 595 | self.analysis_cache[morf] = result |
|
502 | 596 | return result |
|
503 | 597 | |
|
598 | def first_line_of_tree(self, tree): | |
|
599 | while True: | |
|
600 | if len(tree) == 3 and type(tree[2]) == type(1): | |
|
601 | return tree[2] | |
|
602 | tree = tree[1] | |
|
603 | ||
|
604 | def last_line_of_tree(self, tree): | |
|
605 | while True: | |
|
606 | if len(tree) == 3 and type(tree[2]) == type(1): | |
|
607 | return tree[2] | |
|
608 | tree = tree[-1] | |
|
609 | ||
|
610 | def find_docstring_pass_pair(self, tree, spots): | |
|
611 | for i in range(1, len(tree)): | |
|
612 | if self.is_string_constant(tree[i]) and self.is_pass_stmt(tree[i+1]): | |
|
613 | first_line = self.first_line_of_tree(tree[i]) | |
|
614 | last_line = self.last_line_of_tree(tree[i+1]) | |
|
615 | self.record_multiline(spots, first_line, last_line) | |
|
616 | ||
|
617 | def is_string_constant(self, tree): | |
|
618 | try: | |
|
619 | return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.expr_stmt | |
|
620 | except: | |
|
621 | return False | |
|
622 | ||
|
623 | def is_pass_stmt(self, tree): | |
|
624 | try: | |
|
625 | return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.pass_stmt | |
|
626 | except: | |
|
627 | return False | |
|
628 | ||
|
629 | def record_multiline(self, spots, i, j): | |
|
630 | for l in range(i, j+1): | |
|
631 | spots[l] = (i, j) | |
|
632 | ||
|
504 | 633 | def get_suite_spots(self, tree, spots): |
|
505 | import symbol, token | |
|
634 | """ Analyze a parse tree to find suite introducers which span a number | |
|
635 | of lines. | |
|
636 | """ | |
|
506 | 637 | for i in range(1, len(tree)): |
|
507 |
if |
|
|
638 | if type(tree[i]) == type(()): | |
|
508 | 639 | if tree[i][0] == symbol.suite: |
|
509 | 640 | # Found a suite, look back for the colon and keyword. |
|
510 | 641 | lineno_colon = lineno_word = None |
|
511 | 642 | for j in range(i-1, 0, -1): |
|
512 | 643 | if tree[j][0] == token.COLON: |
|
513 | lineno_colon = tree[j][2] | |
|
644 | # Colons are never executed themselves: we want the | |
|
645 | # line number of the last token before the colon. | |
|
646 | lineno_colon = self.last_line_of_tree(tree[j-1]) | |
|
514 | 647 | elif tree[j][0] == token.NAME: |
|
515 | 648 | if tree[j][1] == 'elif': |
|
516 | 649 | # Find the line number of the first non-terminal |
@@ -532,8 +665,18 b' class coverage:' | |||
|
532 | 665 | if lineno_colon and lineno_word: |
|
533 | 666 | # Found colon and keyword, mark all the lines |
|
534 | 667 | # between the two with the two line numbers. |
|
535 |
for |
|
|
536 | spots[l] = (lineno_word, lineno_colon) | |
|
668 | self.record_multiline(spots, lineno_word, lineno_colon) | |
|
669 | ||
|
670 | # "pass" statements are tricky: different versions of Python | |
|
671 | # treat them differently, especially in the common case of a | |
|
672 | # function with a doc string and a single pass statement. | |
|
673 | self.find_docstring_pass_pair(tree[i], spots) | |
|
674 | ||
|
675 | elif tree[i][0] == symbol.simple_stmt: | |
|
676 | first_line = self.first_line_of_tree(tree[i]) | |
|
677 | last_line = self.last_line_of_tree(tree[i]) | |
|
678 | if first_line != last_line: | |
|
679 | self.record_multiline(spots, first_line, last_line) | |
|
537 | 680 | self.get_suite_spots(tree[i], spots) |
|
538 | 681 | |
|
539 | 682 | def find_executable_statements(self, text, exclude=None): |
@@ -547,10 +690,13 b' class coverage:' | |||
|
547 | 690 | if reExclude.search(lines[i]): |
|
548 | 691 | excluded[i+1] = 1 |
|
549 | 692 | |
|
693 | # Parse the code and analyze the parse tree to find out which statements | |
|
694 | # are multiline, and where suites begin and end. | |
|
550 | 695 | import parser |
|
551 | 696 | tree = parser.suite(text+'\n\n').totuple(1) |
|
552 | 697 | self.get_suite_spots(tree, suite_spots) |
|
553 | ||
|
698 | #print "Suite spots:", suite_spots | |
|
699 | ||
|
554 | 700 | # Use the compiler module to parse the text and find the executable |
|
555 | 701 | # statements. We add newlines to be impervious to final partial lines. |
|
556 | 702 | statements = {} |
@@ -562,7 +708,7 b' class coverage:' | |||
|
562 | 708 | lines.sort() |
|
563 | 709 | excluded_lines = excluded.keys() |
|
564 | 710 | excluded_lines.sort() |
|
565 | return lines, excluded_lines | |
|
711 | return lines, excluded_lines, suite_spots | |
|
566 | 712 | |
|
567 | 713 | # format_lines(statements, lines). Format a list of line numbers |
|
568 | 714 | # for printing by coalescing groups of lines as long as the lines |
@@ -595,7 +741,8 b' class coverage:' | |||
|
595 | 741 | return "%d" % start |
|
596 | 742 | else: |
|
597 | 743 | return "%d-%d" % (start, end) |
|
598 |
ret |
|
|
744 | ret = string.join(map(stringify, pairs), ", ") | |
|
745 | return ret | |
|
599 | 746 | |
|
600 | 747 | # Backward compatibility with version 1. |
|
601 | 748 | def analysis(self, morf): |
@@ -603,13 +750,17 b' class coverage:' | |||
|
603 | 750 | return f, s, m, mf |
|
604 | 751 | |
|
605 | 752 | def analysis2(self, morf): |
|
606 | filename, statements, excluded = self.analyze_morf(morf) | |
|
753 | filename, statements, excluded, line_map = self.analyze_morf(morf) | |
|
607 | 754 | self.canonicalize_filenames() |
|
608 | 755 | if not self.cexecuted.has_key(filename): |
|
609 | 756 | self.cexecuted[filename] = {} |
|
610 | 757 | missing = [] |
|
611 | 758 | for line in statements: |
|
612 | if not self.cexecuted[filename].has_key(line): | |
|
759 | lines = line_map.get(line, [line, line]) | |
|
760 | for l in range(lines[0], lines[1]+1): | |
|
761 | if self.cexecuted[filename].has_key(l): | |
|
762 | break | |
|
763 | else: | |
|
613 | 764 | missing.append(line) |
|
614 | 765 | return (filename, statements, excluded, missing, |
|
615 | 766 | self.format_lines(statements, missing)) |
@@ -647,6 +798,15 b' class coverage:' | |||
|
647 | 798 | def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]): |
|
648 | 799 | if not isinstance(morfs, types.ListType): |
|
649 | 800 | morfs = [morfs] |
|
801 | # On windows, the shell doesn't expand wildcards. Do it here. | |
|
802 | globbed = [] | |
|
803 | for morf in morfs: | |
|
804 | if isinstance(morf, strclass): | |
|
805 | globbed.extend(glob.glob(morf)) | |
|
806 | else: | |
|
807 | globbed.append(morf) | |
|
808 | morfs = globbed | |
|
809 | ||
|
650 | 810 | morfs = self.filter_by_prefix(morfs, omit_prefixes) |
|
651 | 811 | morfs.sort(self.morf_name_compare) |
|
652 | 812 | |
@@ -684,8 +844,8 b' class coverage:' | |||
|
684 | 844 | raise |
|
685 | 845 | except: |
|
686 | 846 | if not ignore_errors: |
|
687 |
typ |
|
|
688 |
print >>file, fmt_err % (name, typ |
|
|
847 | typ, msg = sys.exc_info()[0:2] | |
|
848 | print >>file, fmt_err % (name, typ, msg) | |
|
689 | 849 | if len(morfs) > 1: |
|
690 | 850 | print >>file, "-" * len(header) |
|
691 | 851 | if total_statements > 0: |
@@ -713,7 +873,7 b' class coverage:' | |||
|
713 | 873 | except: |
|
714 | 874 | if not ignore_errors: |
|
715 | 875 | raise |
|
716 | ||
|
876 | ||
|
717 | 877 | def annotate_file(self, filename, statements, excluded, missing, directory=None): |
|
718 | 878 | source = open(filename, 'r') |
|
719 | 879 | if directory: |
@@ -741,7 +901,7 b' class coverage:' | |||
|
741 | 901 | if self.blank_re.match(line): |
|
742 | 902 | dest.write(' ') |
|
743 | 903 | elif self.else_re.match(line): |
|
744 | # Special logic for lines containing only 'else:'. | |
|
904 | # Special logic for lines containing only 'else:'. | |
|
745 | 905 | # See [GDR 2001-12-04b, 3.2]. |
|
746 | 906 | if i >= len(statements) and j >= len(missing): |
|
747 | 907 | dest.write('! ') |
@@ -765,18 +925,41 b' class coverage:' | |||
|
765 | 925 | the_coverage = coverage() |
|
766 | 926 | |
|
767 | 927 | # Module functions call methods in the singleton object. |
|
768 | def use_cache(*args, **kw): return the_coverage.use_cache(*args, **kw) | |
|
769 |
|
|
|
770 | def stop(*args, **kw): return the_coverage.stop(*args, **kw) | |
|
771 | def erase(*args, **kw): return the_coverage.erase(*args, **kw) | |
|
772 | def begin_recursive(*args, **kw): return the_coverage.begin_recursive(*args, **kw) | |
|
773 | def end_recursive(*args, **kw): return the_coverage.end_recursive(*args, **kw) | |
|
774 | def exclude(*args, **kw): return the_coverage.exclude(*args, **kw) | |
|
775 |
|
|
|
776 | def analysis2(*args, **kw): return the_coverage.analysis2(*args, **kw) | |
|
777 | def report(*args, **kw): return the_coverage.report(*args, **kw) | |
|
778 |
|
|
|
779 | def annotate_file(*args, **kw): return the_coverage.annotate_file(*args, **kw) | |
|
928 | def use_cache(*args, **kw): | |
|
929 | return the_coverage.use_cache(*args, **kw) | |
|
930 | ||
|
931 | def start(*args, **kw): | |
|
932 | return the_coverage.start(*args, **kw) | |
|
933 | ||
|
934 | def stop(*args, **kw): | |
|
935 | return the_coverage.stop(*args, **kw) | |
|
936 | ||
|
937 | def erase(*args, **kw): | |
|
938 | return the_coverage.erase(*args, **kw) | |
|
939 | ||
|
940 | def begin_recursive(*args, **kw): | |
|
941 | return the_coverage.begin_recursive(*args, **kw) | |
|
942 | ||
|
943 | def end_recursive(*args, **kw): | |
|
944 | return the_coverage.end_recursive(*args, **kw) | |
|
945 | ||
|
946 | def exclude(*args, **kw): | |
|
947 | return the_coverage.exclude(*args, **kw) | |
|
948 | ||
|
949 | def analysis(*args, **kw): | |
|
950 | return the_coverage.analysis(*args, **kw) | |
|
951 | ||
|
952 | def analysis2(*args, **kw): | |
|
953 | return the_coverage.analysis2(*args, **kw) | |
|
954 | ||
|
955 | def report(*args, **kw): | |
|
956 | return the_coverage.report(*args, **kw) | |
|
957 | ||
|
958 | def annotate(*args, **kw): | |
|
959 | return the_coverage.annotate(*args, **kw) | |
|
960 | ||
|
961 | def annotate_file(*args, **kw): | |
|
962 | return the_coverage.annotate_file(*args, **kw) | |
|
780 | 963 | |
|
781 | 964 | # Save coverage data when Python exits. (The atexit module wasn't |
|
782 | 965 | # introduced until Python 2.0, so use sys.exitfunc when it's not |
@@ -789,7 +972,7 b' except ImportError:' | |||
|
789 | 972 | |
|
790 | 973 | # Command-line interface. |
|
791 | 974 | if __name__ == '__main__': |
|
792 | the_coverage.command_line() | |
|
975 | the_coverage.command_line(sys.argv[1:]) | |
|
793 | 976 | |
|
794 | 977 | |
|
795 | 978 | # A. REFERENCES |
@@ -850,7 +1033,7 b" if __name__ == '__main__':" | |||
|
850 | 1033 | # Thanks, Allen. |
|
851 | 1034 | # |
|
852 | 1035 | # 2005-12-02 NMB Call threading.settrace so that all threads are measured. |
|
853 | # Thanks Martin Fuzzey. Add a file argument to report so that reports can be | |
|
1036 | # Thanks Martin Fuzzey. Add a file argument to report so that reports can be | |
|
854 | 1037 | # captured to a different destination. |
|
855 | 1038 | # |
|
856 | 1039 | # 2005-12-03 NMB coverage.py can now measure itself. |
@@ -858,10 +1041,46 b" if __name__ == '__main__':" | |||
|
858 | 1041 | # 2005-12-04 NMB Adapted Greg Rogers' patch for using relative filenames, |
|
859 | 1042 | # and sorting and omitting files to report on. |
|
860 | 1043 | # |
|
1044 | # 2006-07-23 NMB Applied Joseph Tate's patch for function decorators. | |
|
1045 | # | |
|
1046 | # 2006-08-21 NMB Applied Sigve Tjora and Mark van der Wal's fixes for argument | |
|
1047 | # handling. | |
|
1048 | # | |
|
1049 | # 2006-08-22 NMB Applied Geoff Bache's parallel mode patch. | |
|
1050 | # | |
|
1051 | # 2006-08-23 NMB Refactorings to improve testability. Fixes to command-line | |
|
1052 | # logic for parallel mode and collect. | |
|
1053 | # | |
|
1054 | # 2006-08-25 NMB "#pragma: nocover" is excluded by default. | |
|
1055 | # | |
|
1056 | # 2006-09-10 NMB Properly ignore docstrings and other constant expressions that | |
|
1057 | # appear in the middle of a function, a problem reported by Tim Leslie. | |
|
1058 | # Minor changes to avoid lint warnings. | |
|
1059 | # | |
|
1060 | # 2006-09-17 NMB coverage.erase() shouldn't clobber the exclude regex. | |
|
1061 | # Change how parallel mode is invoked, and fix erase() so that it erases the | |
|
1062 | # cache when called programmatically. | |
|
1063 | # | |
|
1064 | # 2007-07-21 NMB In reports, ignore code executed from strings, since we can't | |
|
1065 | # do anything useful with it anyway. | |
|
1066 | # Better file handling on Linux, thanks Guillaume Chazarain. | |
|
1067 | # Better shell support on Windows, thanks Noel O'Boyle. | |
|
1068 | # Python 2.2 support maintained, thanks Catherine Proulx. | |
|
1069 | # | |
|
1070 | # 2007-07-22 NMB Python 2.5 now fully supported. The method of dealing with | |
|
1071 | # multi-line statements is now less sensitive to the exact line that Python | |
|
1072 | # reports during execution. Pass statements are handled specially so that their | |
|
1073 | # disappearance during execution won't throw off the measurement. | |
|
1074 | # | |
|
1075 | # 2007-07-23 NMB Now Python 2.5 is *really* fully supported: the body of the | |
|
1076 | # new with statement is counted as executable. | |
|
1077 | # | |
|
1078 | # 2007-07-29 NMB Better packaging. | |
|
1079 | ||
|
861 | 1080 | # C. COPYRIGHT AND LICENCE |
|
862 | 1081 | # |
|
863 | 1082 | # Copyright 2001 Gareth Rees. All rights reserved. |
|
864 |
# Copyright 2004-200 |
|
|
1083 | # Copyright 2004-2007 Ned Batchelder. All rights reserved. | |
|
865 | 1084 | # |
|
866 | 1085 | # Redistribution and use in source and binary forms, with or without |
|
867 | 1086 | # modification, are permitted provided that the following conditions are |
@@ -888,4 +1107,4 b" if __name__ == '__main__':" | |||
|
888 | 1107 | # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH |
|
889 | 1108 | # DAMAGE. |
|
890 | 1109 | # |
|
891 |
# $Id: coverage.py |
|
|
1110 | # $Id: coverage.py 74 2007-07-29 22:28:35Z nedbat $ |
General Comments 0
You need to be logged in to leave comments.
Login now