comparison tests/coverage.py @ 2066:a5d072f38a4a

add coverage.py module to tests directory. written by ned batchelder and gareth rees.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Thu, 13 Apr 2006 15:10:03 -0700
parents
children 3094becfd8e8
comparison
equal deleted inserted replaced
2058:7e0dd64b0718 2066:a5d072f38a4a
1 #!/usr/bin/python
2 #
3 # Perforce Defect Tracking Integration Project
4 # <http://www.ravenbrook.com/project/p4dti/>
5 #
6 # COVERAGE.PY -- COVERAGE TESTING
7 #
8 # Gareth Rees, Ravenbrook Limited, 2001-12-04
9 # Ned Batchelder, 2004-12-12
10 # http://nedbatchelder.com/code/modules/coverage.html
11 #
12 #
13 # 1. INTRODUCTION
14 #
15 # This module provides coverage testing for Python code.
16 #
17 # The intended readership is all Python developers.
18 #
19 # This document is not confidential.
20 #
21 # See [GDR 2001-12-04a] for the command-line interface, programmatic
22 # interface and limitations. See [GDR 2001-12-04b] for requirements and
23 # design.
24
25 """Usage:
26
27 coverage.py -x MODULE.py [ARG1 ARG2 ...]
28 Execute module, passing the given command-line arguments, collecting
29 coverage data.
30
31 coverage.py -e
32 Erase collected coverage data.
33
34 coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
35 Report on the statement coverage for the given files. With the -m
36 option, show line numbers of the statements that weren't executed.
37
38 coverage.py -a [-d dir] [-o dir1,dir2,...] FILE1 FILE2 ...
39 Make annotated copies of the given files, marking statements that
40 are executed with > and statements that are missed with !. With
41 the -d option, make the copies in that directory. Without the -d
42 option, make each copy in the same directory as the original.
43
44 -o dir,dir2,...
45 Omit reporting or annotating files when their filename path starts with
46 a directory listed in the omit list.
47 e.g. python coverage.py -i -r -o c:\python23,lib\enthought\traits
48
49 Coverage data is saved in the file .coverage by default. Set the
50 COVERAGE_FILE environment variable to save it somewhere else."""
51
52 __version__ = "2.5.20051204" # see detailed history at the end of this file.
53
54 import compiler
55 import compiler.visitor
56 import os
57 import re
58 import string
59 import sys
60 import threading
61 import types
62
63 # 2. IMPLEMENTATION
64 #
65 # This uses the "singleton" pattern.
66 #
67 # The word "morf" means a module object (from which the source file can
68 # be deduced by suitable manipulation of the __file__ attribute) or a
69 # filename.
70 #
71 # When we generate a coverage report we have to canonicalize every
72 # filename in the coverage dictionary just in case it refers to the
73 # module we are reporting on. It seems a shame to throw away this
74 # information so the data in the coverage dictionary is transferred to
75 # the 'cexecuted' dictionary under the canonical filenames.
76 #
77 # The coverage dictionary is called "c" and the trace function "t". The
78 # reason for these short names is that Python looks up variables by name
79 # at runtime and so execution time depends on the length of variables!
80 # In the bottleneck of this application it's appropriate to abbreviate
81 # names to increase speed.
82
83 class StatementFindingAstVisitor(compiler.visitor.ASTVisitor):
84 def __init__(self, statements, excluded, suite_spots):
85 compiler.visitor.ASTVisitor.__init__(self)
86 self.statements = statements
87 self.excluded = excluded
88 self.suite_spots = suite_spots
89 self.excluding_suite = 0
90
91 def doRecursive(self, node):
92 self.recordNodeLine(node)
93 for n in node.getChildNodes():
94 self.dispatch(n)
95
96 visitStmt = visitModule = doRecursive
97
98 def doCode(self, node):
99 if hasattr(node, 'decorators') and node.decorators:
100 self.dispatch(node.decorators)
101 self.doSuite(node, node.code)
102
103 visitFunction = visitClass = doCode
104
105 def getFirstLine(self, node):
106 # Find the first line in the tree node.
107 lineno = node.lineno
108 for n in node.getChildNodes():
109 f = self.getFirstLine(n)
110 if lineno and f:
111 lineno = min(lineno, f)
112 else:
113 lineno = lineno or f
114 return lineno
115
116 def getLastLine(self, node):
117 # Find the first line in the tree node.
118 lineno = node.lineno
119 for n in node.getChildNodes():
120 lineno = max(lineno, self.getLastLine(n))
121 return lineno
122
123 def doStatement(self, node):
124 self.recordLine(self.getFirstLine(node))
125
126 visitAssert = visitAssign = visitAssTuple = visitDiscard = visitPrint = \
127 visitPrintnl = visitRaise = visitSubscript = visitDecorators = \
128 doStatement
129
130 def recordNodeLine(self, node):
131 return self.recordLine(node.lineno)
132
133 def recordLine(self, lineno):
134 # Returns a bool, whether the line is included or excluded.
135 if lineno:
136 # Multi-line tests introducing suites have to get charged to their
137 # keyword.
138 if lineno in self.suite_spots:
139 lineno = self.suite_spots[lineno][0]
140 # If we're inside an exluded suite, record that this line was
141 # excluded.
142 if self.excluding_suite:
143 self.excluded[lineno] = 1
144 return 0
145 # If this line is excluded, or suite_spots maps this line to
146 # another line that is exlcuded, then we're excluded.
147 elif self.excluded.has_key(lineno) or \
148 self.suite_spots.has_key(lineno) and \
149 self.excluded.has_key(self.suite_spots[lineno][1]):
150 return 0
151 # Otherwise, this is an executable line.
152 else:
153 self.statements[lineno] = 1
154 return 1
155 return 0
156
157 default = recordNodeLine
158
159 def recordAndDispatch(self, node):
160 self.recordNodeLine(node)
161 self.dispatch(node)
162
163 def doSuite(self, intro, body, exclude=0):
164 exsuite = self.excluding_suite
165 if exclude or (intro and not self.recordNodeLine(intro)):
166 self.excluding_suite = 1
167 self.recordAndDispatch(body)
168 self.excluding_suite = exsuite
169
170 def doPlainWordSuite(self, prevsuite, suite):
171 # Finding the exclude lines for else's is tricky, because they aren't
172 # present in the compiler parse tree. Look at the previous suite,
173 # and find its last line. If any line between there and the else's
174 # first line are excluded, then we exclude the else.
175 lastprev = self.getLastLine(prevsuite)
176 firstelse = self.getFirstLine(suite)
177 for l in range(lastprev+1, firstelse):
178 if self.suite_spots.has_key(l):
179 self.doSuite(None, suite, exclude=self.excluded.has_key(l))
180 break
181 else:
182 self.doSuite(None, suite)
183
184 def doElse(self, prevsuite, node):
185 if node.else_:
186 self.doPlainWordSuite(prevsuite, node.else_)
187
188 def visitFor(self, node):
189 self.doSuite(node, node.body)
190 self.doElse(node.body, node)
191
192 def visitIf(self, node):
193 # The first test has to be handled separately from the rest.
194 # The first test is credited to the line with the "if", but the others
195 # are credited to the line with the test for the elif.
196 self.doSuite(node, node.tests[0][1])
197 for t, n in node.tests[1:]:
198 self.doSuite(t, n)
199 self.doElse(node.tests[-1][1], node)
200
201 def visitWhile(self, node):
202 self.doSuite(node, node.body)
203 self.doElse(node.body, node)
204
205 def visitTryExcept(self, node):
206 self.doSuite(node, node.body)
207 for i in range(len(node.handlers)):
208 a, b, h = node.handlers[i]
209 if not a:
210 # It's a plain "except:". Find the previous suite.
211 if i > 0:
212 prev = node.handlers[i-1][2]
213 else:
214 prev = node.body
215 self.doPlainWordSuite(prev, h)
216 else:
217 self.doSuite(a, h)
218 self.doElse(node.handlers[-1][2], node)
219
220 def visitTryFinally(self, node):
221 self.doSuite(node, node.body)
222 self.doPlainWordSuite(node.body, node.final)
223
224 def visitGlobal(self, node):
225 # "global" statements don't execute like others (they don't call the
226 # trace function), so don't record their line numbers.
227 pass
228
229 the_coverage = None
230
231 class coverage:
232 error = "coverage error"
233
234 # Name of the cache file (unless environment variable is set).
235 cache_default = ".coverage"
236
237 # Environment variable naming the cache file.
238 cache_env = "COVERAGE_FILE"
239
240 # A dictionary with an entry for (Python source file name, line number
241 # in that file) if that line has been executed.
242 c = {}
243
244 # A map from canonical Python source file name to a dictionary in
245 # which there's an entry for each line number that has been
246 # executed.
247 cexecuted = {}
248
249 # Cache of results of calling the analysis2() method, so that you can
250 # specify both -r and -a without doing double work.
251 analysis_cache = {}
252
253 # Cache of results of calling the canonical_filename() method, to
254 # avoid duplicating work.
255 canonical_filename_cache = {}
256
257 def __init__(self):
258 global the_coverage
259 if the_coverage:
260 raise self.error, "Only one coverage object allowed."
261 self.usecache = 1
262 self.cache = None
263 self.exclude_re = ''
264 self.nesting = 0
265 self.cstack = []
266 self.xstack = []
267 self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.path.sep)
268
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
271 # the arguments and return value of the trace function.
272 # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code
273 # objects.
274
275 def t(self, f, w, a): #pragma: no cover
276 #print w, f.f_code.co_filename, f.f_lineno
277 if w == 'line':
278 self.c[(f.f_code.co_filename, f.f_lineno)] = 1
279 for c in self.cstack:
280 c[(f.f_code.co_filename, f.f_lineno)] = 1
281 return self.t
282
283 def help(self, error=None):
284 if error:
285 print error
286 print
287 print __doc__
288 sys.exit(1)
289
290 def command_line(self):
291 import getopt
292 settings = {}
293 optmap = {
294 '-a': 'annotate',
295 '-d:': 'directory=',
296 '-e': 'erase',
297 '-h': 'help',
298 '-i': 'ignore-errors',
299 '-m': 'show-missing',
300 '-r': 'report',
301 '-x': 'execute',
302 '-o': 'omit=',
303 }
304 short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
305 long_opts = optmap.values()
306 options, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
307 for o, a in options:
308 if optmap.has_key(o):
309 settings[optmap[o]] = 1
310 elif optmap.has_key(o + ':'):
311 settings[optmap[o + ':']] = a
312 elif o[2:] in long_opts:
313 settings[o[2:]] = 1
314 elif o[2:] + '=' in long_opts:
315 settings[o[2:]] = a
316 else:
317 self.help("Unknown option: '%s'." % o)
318 if settings.get('help'):
319 self.help()
320 for i in ['erase', 'execute']:
321 for j in ['annotate', 'report']:
322 if settings.get(i) and settings.get(j):
323 self.help("You can't specify the '%s' and '%s' "
324 "options at the same time." % (i, j))
325 args_needed = (settings.get('execute')
326 or settings.get('annotate')
327 or settings.get('report'))
328 action = settings.get('erase') or args_needed
329 if not action:
330 self.help("You must specify at least one of -e, -x, -r, or -a.")
331 if not args_needed and args:
332 self.help("Unexpected arguments %s." % args)
333
334 self.get_ready()
335 self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
336
337 if settings.get('erase'):
338 self.erase()
339 if settings.get('execute'):
340 if not args:
341 self.help("Nothing to do.")
342 sys.argv = args
343 self.start()
344 import __main__
345 sys.path[0] = os.path.dirname(sys.argv[0])
346 execfile(sys.argv[0], __main__.__dict__)
347 if not args:
348 args = self.cexecuted.keys()
349 ignore_errors = settings.get('ignore-errors')
350 show_missing = settings.get('show-missing')
351 directory = settings.get('directory=')
352 omit = settings.get('omit=')
353 if omit is not None:
354 omit = omit.split(',')
355 else:
356 omit = []
357
358 if settings.get('report'):
359 self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
360 if settings.get('annotate'):
361 self.annotate(args, directory, ignore_errors, omit_prefixes=omit)
362
363 def use_cache(self, usecache):
364 self.usecache = usecache
365
366 def get_ready(self):
367 if self.usecache and not self.cache:
368 self.cache = os.environ.get(self.cache_env, self.cache_default)
369 self.restore()
370 self.analysis_cache = {}
371
372 def start(self):
373 self.get_ready()
374 if self.nesting == 0: #pragma: no cover
375 sys.settrace(self.t)
376 if hasattr(threading, 'settrace'):
377 threading.settrace(self.t)
378 self.nesting += 1
379
380 def stop(self):
381 self.nesting -= 1
382 if self.nesting == 0: #pragma: no cover
383 sys.settrace(None)
384 if hasattr(threading, 'settrace'):
385 threading.settrace(None)
386
387 def erase(self):
388 self.c = {}
389 self.analysis_cache = {}
390 self.cexecuted = {}
391 if self.cache and os.path.exists(self.cache):
392 os.remove(self.cache)
393 self.exclude_re = ""
394
395 def exclude(self, re):
396 if self.exclude_re:
397 self.exclude_re += "|"
398 self.exclude_re += "(" + re + ")"
399
400 def begin_recursive(self):
401 self.cstack.append(self.c)
402 self.xstack.append(self.exclude_re)
403
404 def end_recursive(self):
405 self.c = self.cstack.pop()
406 self.exclude_re = self.xstack.pop()
407
408 # save(). Save coverage data to the coverage cache.
409
410 def save(self):
411 if self.usecache and self.cache:
412 self.canonicalize_filenames()
413 cache = open(self.cache, 'wb')
414 import marshal
415 marshal.dump(self.cexecuted, cache)
416 cache.close()
417
418 # restore(). Restore coverage data from the coverage cache (if it exists).
419
420 def restore(self):
421 self.c = {}
422 self.cexecuted = {}
423 assert self.usecache
424 if not os.path.exists(self.cache):
425 return
426 try:
427 cache = open(self.cache, 'rb')
428 import marshal
429 cexecuted = marshal.load(cache)
430 cache.close()
431 if isinstance(cexecuted, types.DictType):
432 self.cexecuted = cexecuted
433 except:
434 pass
435
436 # canonical_filename(filename). Return a canonical filename for the
437 # file (that is, an absolute path with no redundant components and
438 # normalized case). See [GDR 2001-12-04b, 3.3].
439
440 def canonical_filename(self, filename):
441 if not self.canonical_filename_cache.has_key(filename):
442 f = filename
443 if os.path.isabs(f) and not os.path.exists(f):
444 f = os.path.basename(f)
445 if not os.path.isabs(f):
446 for path in [os.curdir] + sys.path:
447 g = os.path.join(path, f)
448 if os.path.exists(g):
449 f = g
450 break
451 cf = os.path.normcase(os.path.abspath(f))
452 self.canonical_filename_cache[filename] = cf
453 return self.canonical_filename_cache[filename]
454
455 # canonicalize_filenames(). Copy results from "c" to "cexecuted",
456 # canonicalizing filenames on the way. Clear the "c" map.
457
458 def canonicalize_filenames(self):
459 for filename, lineno in self.c.keys():
460 f = self.canonical_filename(filename)
461 if not self.cexecuted.has_key(f):
462 self.cexecuted[f] = {}
463 self.cexecuted[f][lineno] = 1
464 self.c = {}
465
466 # morf_filename(morf). Return the filename for a module or file.
467
468 def morf_filename(self, morf):
469 if isinstance(morf, types.ModuleType):
470 if not hasattr(morf, '__file__'):
471 raise self.error, "Module has no __file__ attribute."
472 file = morf.__file__
473 else:
474 file = morf
475 return self.canonical_filename(file)
476
477 # analyze_morf(morf). Analyze the module or filename passed as
478 # the argument. If the source code can't be found, raise an error.
479 # Otherwise, return a tuple of (1) the canonical filename of the
480 # source code for the module, (2) a list of lines of statements
481 # in the source code, and (3) a list of lines of excluded statements.
482
483 def analyze_morf(self, morf):
484 if self.analysis_cache.has_key(morf):
485 return self.analysis_cache[morf]
486 filename = self.morf_filename(morf)
487 ext = os.path.splitext(filename)[1]
488 if ext == '.pyc':
489 if not os.path.exists(filename[0:-1]):
490 raise self.error, ("No source for compiled code '%s'."
491 % filename)
492 filename = filename[0:-1]
493 elif ext != '.py':
494 raise self.error, "File '%s' not Python source." % filename
495 source = open(filename, 'r')
496 lines, excluded_lines = self.find_executable_statements(
497 source.read(), exclude=self.exclude_re
498 )
499 source.close()
500 result = filename, lines, excluded_lines
501 self.analysis_cache[morf] = result
502 return result
503
504 def get_suite_spots(self, tree, spots):
505 import symbol, token
506 for i in range(1, len(tree)):
507 if type(tree[i]) == type(()):
508 if tree[i][0] == symbol.suite:
509 # Found a suite, look back for the colon and keyword.
510 lineno_colon = lineno_word = None
511 for j in range(i-1, 0, -1):
512 if tree[j][0] == token.COLON:
513 lineno_colon = tree[j][2]
514 elif tree[j][0] == token.NAME:
515 if tree[j][1] == 'elif':
516 # Find the line number of the first non-terminal
517 # after the keyword.
518 t = tree[j+1]
519 while t and token.ISNONTERMINAL(t[0]):
520 t = t[1]
521 if t:
522 lineno_word = t[2]
523 else:
524 lineno_word = tree[j][2]
525 break
526 elif tree[j][0] == symbol.except_clause:
527 # "except" clauses look like:
528 # ('except_clause', ('NAME', 'except', lineno), ...)
529 if tree[j][1][0] == token.NAME:
530 lineno_word = tree[j][1][2]
531 break
532 if lineno_colon and lineno_word:
533 # Found colon and keyword, mark all the lines
534 # between the two with the two line numbers.
535 for l in range(lineno_word, lineno_colon+1):
536 spots[l] = (lineno_word, lineno_colon)
537 self.get_suite_spots(tree[i], spots)
538
539 def find_executable_statements(self, text, exclude=None):
540 # Find lines which match an exclusion pattern.
541 excluded = {}
542 suite_spots = {}
543 if exclude:
544 reExclude = re.compile(exclude)
545 lines = text.split('\n')
546 for i in range(len(lines)):
547 if reExclude.search(lines[i]):
548 excluded[i+1] = 1
549
550 import parser
551 tree = parser.suite(text+'\n\n').totuple(1)
552 self.get_suite_spots(tree, suite_spots)
553
554 # Use the compiler module to parse the text and find the executable
555 # statements. We add newlines to be impervious to final partial lines.
556 statements = {}
557 ast = compiler.parse(text+'\n\n')
558 visitor = StatementFindingAstVisitor(statements, excluded, suite_spots)
559 compiler.walk(ast, visitor, walker=visitor)
560
561 lines = statements.keys()
562 lines.sort()
563 excluded_lines = excluded.keys()
564 excluded_lines.sort()
565 return lines, excluded_lines
566
567 # format_lines(statements, lines). Format a list of line numbers
568 # for printing by coalescing groups of lines as long as the lines
569 # represent consecutive statements. This will coalesce even if
570 # there are gaps between statements, so if statements =
571 # [1,2,3,4,5,10,11,12,13,14] and lines = [1,2,5,10,11,13,14] then
572 # format_lines will return "1-2, 5-11, 13-14".
573
574 def format_lines(self, statements, lines):
575 pairs = []
576 i = 0
577 j = 0
578 start = None
579 pairs = []
580 while i < len(statements) and j < len(lines):
581 if statements[i] == lines[j]:
582 if start == None:
583 start = lines[j]
584 end = lines[j]
585 j = j + 1
586 elif start:
587 pairs.append((start, end))
588 start = None
589 i = i + 1
590 if start:
591 pairs.append((start, end))
592 def stringify(pair):
593 start, end = pair
594 if start == end:
595 return "%d" % start
596 else:
597 return "%d-%d" % (start, end)
598 return string.join(map(stringify, pairs), ", ")
599
600 # Backward compatibility with version 1.
601 def analysis(self, morf):
602 f, s, _, m, mf = self.analysis2(morf)
603 return f, s, m, mf
604
605 def analysis2(self, morf):
606 filename, statements, excluded = self.analyze_morf(morf)
607 self.canonicalize_filenames()
608 if not self.cexecuted.has_key(filename):
609 self.cexecuted[filename] = {}
610 missing = []
611 for line in statements:
612 if not self.cexecuted[filename].has_key(line):
613 missing.append(line)
614 return (filename, statements, excluded, missing,
615 self.format_lines(statements, missing))
616
617 def relative_filename(self, filename):
618 """ Convert filename to relative filename from self.relative_dir.
619 """
620 return filename.replace(self.relative_dir, "")
621
622 def morf_name(self, morf):
623 """ Return the name of morf as used in report.
624 """
625 if isinstance(morf, types.ModuleType):
626 return morf.__name__
627 else:
628 return self.relative_filename(os.path.splitext(morf)[0])
629
630 def filter_by_prefix(self, morfs, omit_prefixes):
631 """ Return list of morfs where the morf name does not begin
632 with any one of the omit_prefixes.
633 """
634 filtered_morfs = []
635 for morf in morfs:
636 for prefix in omit_prefixes:
637 if self.morf_name(morf).startswith(prefix):
638 break
639 else:
640 filtered_morfs.append(morf)
641
642 return filtered_morfs
643
644 def morf_name_compare(self, x, y):
645 return cmp(self.morf_name(x), self.morf_name(y))
646
647 def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]):
648 if not isinstance(morfs, types.ListType):
649 morfs = [morfs]
650 morfs = self.filter_by_prefix(morfs, omit_prefixes)
651 morfs.sort(self.morf_name_compare)
652
653 max_name = max([5,] + map(len, map(self.morf_name, morfs)))
654 fmt_name = "%%- %ds " % max_name
655 fmt_err = fmt_name + "%s: %s"
656 header = fmt_name % "Name" + " Stmts Exec Cover"
657 fmt_coverage = fmt_name + "% 6d % 6d % 5d%%"
658 if show_missing:
659 header = header + " Missing"
660 fmt_coverage = fmt_coverage + " %s"
661 if not file:
662 file = sys.stdout
663 print >>file, header
664 print >>file, "-" * len(header)
665 total_statements = 0
666 total_executed = 0
667 for morf in morfs:
668 name = self.morf_name(morf)
669 try:
670 _, statements, _, missing, readable = self.analysis2(morf)
671 n = len(statements)
672 m = n - len(missing)
673 if n > 0:
674 pc = 100.0 * m / n
675 else:
676 pc = 100.0
677 args = (name, n, m, pc)
678 if show_missing:
679 args = args + (readable,)
680 print >>file, fmt_coverage % args
681 total_statements = total_statements + n
682 total_executed = total_executed + m
683 except KeyboardInterrupt: #pragma: no cover
684 raise
685 except:
686 if not ignore_errors:
687 type, msg = sys.exc_info()[0:2]
688 print >>file, fmt_err % (name, type, msg)
689 if len(morfs) > 1:
690 print >>file, "-" * len(header)
691 if total_statements > 0:
692 pc = 100.0 * total_executed / total_statements
693 else:
694 pc = 100.0
695 args = ("TOTAL", total_statements, total_executed, pc)
696 if show_missing:
697 args = args + ("",)
698 print >>file, fmt_coverage % args
699
700 # annotate(morfs, ignore_errors).
701
702 blank_re = re.compile(r"\s*(#|$)")
703 else_re = re.compile(r"\s*else\s*:\s*(#|$)")
704
705 def annotate(self, morfs, directory=None, ignore_errors=0, omit_prefixes=[]):
706 morfs = self.filter_by_prefix(morfs, omit_prefixes)
707 for morf in morfs:
708 try:
709 filename, statements, excluded, missing, _ = self.analysis2(morf)
710 self.annotate_file(filename, statements, excluded, missing, directory)
711 except KeyboardInterrupt:
712 raise
713 except:
714 if not ignore_errors:
715 raise
716
717 def annotate_file(self, filename, statements, excluded, missing, directory=None):
718 source = open(filename, 'r')
719 if directory:
720 dest_file = os.path.join(directory,
721 os.path.basename(filename)
722 + ',cover')
723 else:
724 dest_file = filename + ',cover'
725 dest = open(dest_file, 'w')
726 lineno = 0
727 i = 0
728 j = 0
729 covered = 1
730 while 1:
731 line = source.readline()
732 if line == '':
733 break
734 lineno = lineno + 1
735 while i < len(statements) and statements[i] < lineno:
736 i = i + 1
737 while j < len(missing) and missing[j] < lineno:
738 j = j + 1
739 if i < len(statements) and statements[i] == lineno:
740 covered = j >= len(missing) or missing[j] > lineno
741 if self.blank_re.match(line):
742 dest.write(' ')
743 elif self.else_re.match(line):
744 # Special logic for lines containing only 'else:'.
745 # See [GDR 2001-12-04b, 3.2].
746 if i >= len(statements) and j >= len(missing):
747 dest.write('! ')
748 elif i >= len(statements) or j >= len(missing):
749 dest.write('> ')
750 elif statements[i] == missing[j]:
751 dest.write('! ')
752 else:
753 dest.write('> ')
754 elif lineno in excluded:
755 dest.write('- ')
756 elif covered:
757 dest.write('> ')
758 else:
759 dest.write('! ')
760 dest.write(line)
761 source.close()
762 dest.close()
763
764 # Singleton object.
765 the_coverage = coverage()
766
767 # Module functions call methods in the singleton object.
768 def use_cache(*args, **kw): return the_coverage.use_cache(*args, **kw)
769 def start(*args, **kw): return the_coverage.start(*args, **kw)
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 def analysis(*args, **kw): return the_coverage.analysis(*args, **kw)
776 def analysis2(*args, **kw): return the_coverage.analysis2(*args, **kw)
777 def report(*args, **kw): return the_coverage.report(*args, **kw)
778 def annotate(*args, **kw): return the_coverage.annotate(*args, **kw)
779 def annotate_file(*args, **kw): return the_coverage.annotate_file(*args, **kw)
780
781 # Save coverage data when Python exits. (The atexit module wasn't
782 # introduced until Python 2.0, so use sys.exitfunc when it's not
783 # available.)
784 try:
785 import atexit
786 atexit.register(the_coverage.save)
787 except ImportError:
788 sys.exitfunc = the_coverage.save
789
790 # Command-line interface.
791 if __name__ == '__main__':
792 the_coverage.command_line()
793
794
795 # A. REFERENCES
796 #
797 # [GDR 2001-12-04a] "Statement coverage for Python"; Gareth Rees;
798 # Ravenbrook Limited; 2001-12-04;
799 # <http://www.nedbatchelder.com/code/modules/rees-coverage.html>.
800 #
801 # [GDR 2001-12-04b] "Statement coverage for Python: design and
802 # analysis"; Gareth Rees; Ravenbrook Limited; 2001-12-04;
803 # <http://www.nedbatchelder.com/code/modules/rees-design.html>.
804 #
805 # [van Rossum 2001-07-20a] "Python Reference Manual (releae 2.1.1)";
806 # Guide van Rossum; 2001-07-20;
807 # <http://www.python.org/doc/2.1.1/ref/ref.html>.
808 #
809 # [van Rossum 2001-07-20b] "Python Library Reference"; Guido van Rossum;
810 # 2001-07-20; <http://www.python.org/doc/2.1.1/lib/lib.html>.
811 #
812 #
813 # B. DOCUMENT HISTORY
814 #
815 # 2001-12-04 GDR Created.
816 #
817 # 2001-12-06 GDR Added command-line interface and source code
818 # annotation.
819 #
820 # 2001-12-09 GDR Moved design and interface to separate documents.
821 #
822 # 2001-12-10 GDR Open cache file as binary on Windows. Allow
823 # simultaneous -e and -x, or -a and -r.
824 #
825 # 2001-12-12 GDR Added command-line help. Cache analysis so that it
826 # only needs to be done once when you specify -a and -r.
827 #
828 # 2001-12-13 GDR Improved speed while recording. Portable between
829 # Python 1.5.2 and 2.1.1.
830 #
831 # 2002-01-03 GDR Module-level functions work correctly.
832 #
833 # 2002-01-07 GDR Update sys.path when running a file with the -x option,
834 # so that it matches the value the program would get if it were run on
835 # its own.
836 #
837 # 2004-12-12 NMB Significant code changes.
838 # - Finding executable statements has been rewritten so that docstrings and
839 # other quirks of Python execution aren't mistakenly identified as missing
840 # lines.
841 # - Lines can be excluded from consideration, even entire suites of lines.
842 # - The filesystem cache of covered lines can be disabled programmatically.
843 # - Modernized the code.
844 #
845 # 2004-12-14 NMB Minor tweaks. Return 'analysis' to its original behavior
846 # and add 'analysis2'. Add a global for 'annotate', and factor it, adding
847 # 'annotate_file'.
848 #
849 # 2004-12-31 NMB Allow for keyword arguments in the module global functions.
850 # Thanks, Allen.
851 #
852 # 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
854 # captured to a different destination.
855 #
856 # 2005-12-03 NMB coverage.py can now measure itself.
857 #
858 # 2005-12-04 NMB Adapted Greg Rogers' patch for using relative filenames,
859 # and sorting and omitting files to report on.
860 #
861 # C. COPYRIGHT AND LICENCE
862 #
863 # Copyright 2001 Gareth Rees. All rights reserved.
864 # Copyright 2004-2005 Ned Batchelder. All rights reserved.
865 #
866 # Redistribution and use in source and binary forms, with or without
867 # modification, are permitted provided that the following conditions are
868 # met:
869 #
870 # 1. Redistributions of source code must retain the above copyright
871 # notice, this list of conditions and the following disclaimer.
872 #
873 # 2. Redistributions in binary form must reproduce the above copyright
874 # notice, this list of conditions and the following disclaimer in the
875 # documentation and/or other materials provided with the
876 # distribution.
877 #
878 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
879 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
880 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
881 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
882 # HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
883 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
884 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
885 # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
886 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
887 # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
888 # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
889 # DAMAGE.
890 #
891 # $Id: coverage.py 26 2005-12-04 18:42:44Z ned $