root/gaphor/tags/gaphor-0.11.1/utils/coverage.py

Revision 1121, 32.6 kB (checked in by arjanmol, 2 years ago)

Merged changed from new-canvas branch to trunk

Line 
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