tests/coverage.py
changeset 2066 a5d072f38a4a
child 2067 3094becfd8e8
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 $