3
# Perforce Defect Tracking Integration Project
4
# <http://www.ravenbrook.com/project/p4dti/>
6
# COVERAGE.PY -- COVERAGE TESTING
8
# Gareth Rees, Ravenbrook Limited, 2001-12-04
13
# This module provides coverage testing for Python code.
15
# The intended readership is all Python developers.
17
# This document is not confidential.
19
# See [GDR 2001-12-04a] for the command-line interface, programmatic
20
# interface and limitations. See [GDR 2001-12-04b] for requirements and
25
coverage.py -x MODULE.py [ARG1 ARG2 ...]
26
Execute module, passing the given command-line arguments, collecting
30
Erase collected coverage data.
32
coverage.py -r [-m] FILE1 FILE2 ...
33
Report on the statement coverage for the given files. With the -m
34
option, show line numbers of the statements that weren't executed.
36
coverage.py -a [-d dir] FILE1 FILE2 ...
37
Make annotated copies of the given files, marking statements that
38
are executed with > and statements that are missed with !. With
39
the -d option, make the copies in that directory. Without the -d
40
option, make each copy in the same directory as the original.
42
Coverage data is saved in the file .coverage by default. Set the
43
COVERAGE_FILE environment variable to save it somewhere else."""
54
# This uses the "singleton" pattern.
56
# The word "morf" means a module object (from which the source file can
57
# be deduced by suitable manipulation of the __file__ attribute) or a
60
# When we generate a coverage report we have to canonicalize every
61
# filename in the coverage dictionary just in case it refers to the
62
# module we are reporting on. It seems a shame to throw away this
63
# information so the data in the coverage dictionary is transferred to
64
# the 'cexecuted' dictionary under the canonical filenames.
66
# The coverage dictionary is called "c" and the trace function "t". The
67
# reason for these short names is that Python looks up variables by name
68
# at runtime and so execution time depends on the length of variables!
69
# In the bottleneck of this application it's appropriate to abbreviate
70
# names to increase speed.
72
# A dictionary with an entry for (Python source file name, line number
73
# in that file) if that line has been executed.
76
# t(f, x, y). This method is passed to sys.settrace as a trace
77
# function. See [van Rossum 2001-07-20b, 9.2] for an explanation of
78
# sys.settrace and the arguments and return value of the trace function.
79
# See [van Rossum 2001-07-20a, 3.2] for a description of frame and code
83
c[(f.f_code.co_filename, f.f_lineno)] = 1
89
error = "coverage error"
91
# Name of the cache file (unless environment variable is set).
92
cache_default = ".coverage"
94
# Environment variable naming the cache file.
95
cache_env = "COVERAGE_FILE"
97
# A map from canonical Python source file name to a dictionary in
98
# which there's an entry for each line number that has been
102
# Cache of results of calling the analysis() method, so that you can
103
# specify both -r and -a without doing double work.
106
# Cache of results of calling the canonical_filename() method, to
107
# avoid duplicating work.
108
canonical_filename_cache = {}
113
raise self.error, "Only one coverage object allowed."
114
self.cache = os.environ.get(self.cache_env, self.cache_default)
116
self.analysis_cache = {}
118
def help(self, error=None):
125
def command_line(self):
133
'-i': 'ignore-errors',
134
'-m': 'show-missing',
138
short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
139
long_opts = optmap.values()
140
options, args = getopt.getopt(sys.argv[1:], short_opts,
143
if optmap.has_key(o):
144
settings[optmap[o]] = 1
145
elif optmap.has_key(o + ':'):
146
settings[optmap[o + ':']] = a
147
elif o[2:] in long_opts:
149
elif o[2:] + '=' in long_opts:
152
self.help("Unknown option: '%s'." % o)
153
if settings.get('help'):
155
for i in ['erase', 'execute']:
156
for j in ['annotate', 'report']:
157
if settings.get(i) and settings.get(j):
158
self.help("You can't specify the '%s' and '%s' "
159
"options at the same time." % (i, j))
160
args_needed = (settings.get('execute')
161
or settings.get('annotate')
162
or settings.get('report'))
163
action = settings.get('erase') or args_needed
165
self.help("You must specify at least one of -e, -x, -r, "
167
if not args_needed and args:
168
self.help("Unexpected arguments %s." % args)
169
if settings.get('erase'):
171
if settings.get('execute'):
173
self.help("Nothing to do.")
177
sys.path[0] = os.path.dirname(sys.argv[0])
178
execfile(sys.argv[0], __main__.__dict__)
180
args = self.cexecuted.keys()
181
ignore_errors = settings.get('ignore-errors')
182
show_missing = settings.get('show-missing')
183
directory = settings.get('directory=')
184
if settings.get('report'):
185
self.report(args, show_missing, ignore_errors)
186
if settings.get('annotate'):
187
self.annotate(args, directory, ignore_errors)
198
self.analysis_cache = {}
200
if os.path.exists(self.cache):
201
os.remove(self.cache)
203
# save(). Save coverage data to the coverage cache.
206
self.canonicalize_filenames()
207
cache = open(self.cache, 'wb')
209
marshal.dump(self.cexecuted, cache)
212
# restore(). Restore coverage data from the coverage cache (if it
219
if not os.path.exists(self.cache):
222
cache = open(self.cache, 'rb')
224
cexecuted = marshal.load(cache)
226
if isinstance(cexecuted, types.DictType):
227
self.cexecuted = cexecuted
231
# canonical_filename(filename). Return a canonical filename for the
232
# file (that is, an absolute path with no redundant components and
233
# normalized case). See [GDR 2001-12-04b, 3.3].
235
def canonical_filename(self, filename):
236
if not self.canonical_filename_cache.has_key(filename):
238
if os.path.isabs(f) and not os.path.exists(f):
239
f = os.path.basename(f)
240
if not os.path.isabs(f):
241
for path in [os.curdir] + sys.path:
242
g = os.path.join(path, f)
243
if os.path.exists(g):
246
cf = os.path.normcase(os.path.abspath(f))
247
self.canonical_filename_cache[filename] = cf
248
return self.canonical_filename_cache[filename]
250
# canonicalize_filenames(). Copy results from "executed" to
251
# "cexecuted", canonicalizing filenames on the way. Clear the
254
def canonicalize_filenames(self):
256
for filename, lineno in c.keys():
257
f = self.canonical_filename(filename)
258
if not self.cexecuted.has_key(f):
259
self.cexecuted[f] = {}
260
self.cexecuted[f][lineno] = 1
263
# morf_filename(morf). Return the filename for a module or file.
265
def morf_filename(self, morf):
266
if isinstance(morf, types.ModuleType):
267
if not hasattr(morf, '__file__'):
268
raise self.error, "Module has no __file__ attribute."
272
return self.canonical_filename(file)
274
# analyze_morf(morf). Analyze the module or filename passed as
275
# the argument. If the source code can't be found, raise an error.
276
# Otherwise, return a pair of (1) the canonical filename of the
277
# source code for the module, and (2) a list of lines of statements
278
# in the source code.
280
def analyze_morf(self, morf):
281
if self.analysis_cache.has_key(morf):
282
return self.analysis_cache[morf]
283
filename = self.morf_filename(morf)
284
ext = os.path.splitext(filename)[1]
286
if not os.path.exists(filename[0:-1]):
287
raise self.error, ("No source for compiled code '%s'."
289
filename = filename[0:-1]
291
raise self.error, "File '%s' not Python source." % filename
292
source = open(filename, 'r')
294
tree = parser.suite(source.read()).totuple(1)
297
self.find_statements(tree, statements)
298
lines = statements.keys()
300
result = filename, lines
301
self.analysis_cache[morf] = result
304
# find_statements(tree, dict). Find each statement in the parse
305
# tree and record the line on which the statement starts in the
306
# dictionary (by assigning it to 1).
308
# It works by walking the whole tree depth-first. Every time it
309
# comes across a statement (symbol.stmt -- this includes compound
310
# statements like 'if' and 'while') it calls find_statement, which
311
# descends the tree below the statement to find the first terminal
312
# token in that statement and record the lines on which that token
315
# This algorithm may find some lines several times (because of the
316
# grammar production statement -> compound statement -> statement),
317
# but that doesn't matter because we record lines as the keys of the
320
# See also [GDR 2001-12-04b, 3.2].
322
def find_statements(self, tree, dict):
324
if token.ISNONTERMINAL(tree[0]):
326
self.find_statements(t, dict)
327
if tree[0] == symbol.stmt:
328
self.find_statement(tree[1], dict)
329
elif (tree[0] == token.NAME
330
and tree[1] in ['elif', 'except', 'finally']):
333
def find_statement(self, tree, dict):
335
while token.ISNONTERMINAL(tree[0]):
339
# format_lines(statements, lines). Format a list of line numbers
340
# for printing by coalescing groups of lines as long as the lines
341
# represent consecutive statements. This will coalesce even if
342
# there are gaps between statements, so if statements =
343
# [1,2,3,4,5,10,11,12,13,14] and lines = [1,2,5,10,11,13,14] then
344
# format_lines will return "1-2, 5-11, 13-14".
346
def format_lines(self, statements, lines):
352
while i < len(statements) and j < len(lines):
353
if statements[i] == lines[j]:
359
pairs.append((start, end))
363
pairs.append((start, end))
369
return "%d-%d" % (start, end)
371
return string.join(map(stringify, pairs), ", ")
373
def analysis(self, morf):
374
filename, statements = self.analyze_morf(morf)
375
self.canonicalize_filenames()
376
if not self.cexecuted.has_key(filename):
377
self.cexecuted[filename] = {}
379
for line in statements:
380
if not self.cexecuted[filename].has_key(line):
382
return (filename, statements, missing,
383
self.format_lines(statements, missing))
385
def morf_name(self, morf):
386
if isinstance(morf, types.ModuleType):
389
return os.path.splitext(os.path.basename(morf))[0]
391
def report(self, morfs, show_missing=1, ignore_errors=0):
392
if not isinstance(morfs, types.ListType):
394
max_name = max([5,] + map(len, map(self.morf_name, morfs)))
395
fmt_name = "%%- %ds " % max_name
396
fmt_err = fmt_name + "%s: %s"
397
header = fmt_name % "Name" + " Stmts Exec Cover"
398
fmt_coverage = fmt_name + "% 6d % 6d % 5d%%"
400
header = header + " Missing"
401
fmt_coverage = fmt_coverage + " %s"
403
print "-" * len(header)
407
name = self.morf_name(morf)
409
_, statements, missing, readable = self.analysis(morf)
416
args = (name, n, m, pc)
418
args = args + (readable,)
419
print fmt_coverage % args
420
total_statements = total_statements + n
421
total_executed = total_executed + m
422
except KeyboardInterrupt:
425
if not ignore_errors:
426
type, msg = sys.exc_info()[0:2]
427
print fmt_err % (name, type, msg)
429
print "-" * len(header)
430
if total_statements > 0:
431
pc = 100.0 * total_executed / total_statements
434
args = ("TOTAL", total_statements, total_executed, pc)
437
print fmt_coverage % args
439
# annotate(morfs, ignore_errors).
441
blank_re = re.compile("\\s*(#|$)")
442
else_re = re.compile("\\s*else\\s*:\\s*(#|$)")
444
def annotate(self, morfs, directory=None, ignore_errors=0):
447
filename, statements, missing, _ = self.analysis(morf)
448
source = open(filename, 'r')
450
dest_file = os.path.join(directory,
451
os.path.basename(filename)
454
dest_file = filename + ',cover'
455
dest = open(dest_file, 'w')
461
line = source.readline()
465
while i < len(statements) and statements[i] < lineno:
467
while j < len(missing) and missing[j] < lineno:
469
if i < len(statements) and statements[i] == lineno:
470
covered = j >= len(missing) or missing[j] > lineno
471
if self.blank_re.match(line):
473
elif self.else_re.match(line):
474
# Special logic for lines containing only
475
# 'else:'. See [GDR 2001-12-04b, 3.2].
476
if i >= len(statements) and j >= len(missing):
478
elif i >= len(statements) or j >= len(missing):
480
elif statements[i] == missing[j]:
491
except KeyboardInterrupt:
494
if not ignore_errors:
499
the_coverage = coverage()
501
# Module functions call methods in the singleton object.
502
def start(*args): return apply(the_coverage.start, args)
503
def stop(*args): return apply(the_coverage.stop, args)
504
def erase(*args): return apply(the_coverage.erase, args)
505
def analysis(*args): return apply(the_coverage.analysis, args)
506
def report(*args): return apply(the_coverage.report, args)
508
# Save coverage data when Python exits. (The atexit module wasn't
509
# introduced until Python 2.0, so use sys.exitfunc when it's not
513
atexit.register(the_coverage.save)
515
sys.exitfunc = the_coverage.save
517
# Command-line interface.
518
if __name__ == '__main__':
519
the_coverage.command_line()
524
# [GDR 2001-12-04a] "Statement coverage for Python"; Gareth Rees;
525
# Ravenbrook Limited; 2001-12-04;
526
# <http://www.garethrees.org/2001/12/04/python-coverage/>.
528
# [GDR 2001-12-04b] "Statement coverage for Python: design and
529
# analysis"; Gareth Rees; Ravenbrook Limited; 2001-12-04;
530
# <http://www.garethrees.org/2001/12/04/python-coverage/design.html>.
532
# [van Rossum 2001-07-20a] "Python Reference Manual (releae 2.1.1)";
533
# Guide van Rossum; 2001-07-20;
534
# <http://www.python.org/doc/2.1.1/ref/ref.html>.
536
# [van Rossum 2001-07-20b] "Python Library Reference"; Guido van Rossum;
537
# 2001-07-20; <http://www.python.org/doc/2.1.1/lib/lib.html>.
540
# B. DOCUMENT HISTORY
542
# 2001-12-04 GDR Created.
544
# 2001-12-06 GDR Added command-line interface and source code
547
# 2001-12-09 GDR Moved design and interface to separate documents.
549
# 2001-12-10 GDR Open cache file as binary on Windows. Allow
550
# simultaneous -e and -x, or -a and -r.
552
# 2001-12-12 GDR Added command-line help. Cache analysis so that it
553
# only needs to be done once when you specify -a and -r.
555
# 2001-12-13 GDR Improved speed while recording. Portable between
556
# Python 1.5.2 and 2.1.1.
558
# 2002-01-03 GDR Module-level functions work correctly.
560
# 2002-01-07 GDR Update sys.path when running a file with the -x option,
561
# so that it matches the value the program would get if it were run on
565
# C. COPYRIGHT AND LICENCE
567
# Copyright 2001 Gareth Rees. All rights reserved.
569
# Redistribution and use in source and binary forms, with or without
570
# modification, are permitted provided that the following conditions are
573
# 1. Redistributions of source code must retain the above copyright
574
# notice, this list of conditions and the following disclaimer.
576
# 2. Redistributions in binary form must reproduce the above copyright
577
# notice, this list of conditions and the following disclaimer in the
578
# documentation and/or other materials provided with the
581
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
582
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
583
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
584
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
585
# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
586
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
587
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
588
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
589
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
590
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
591
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
596
# $Id: //info.ravenbrook.com/user/gdr/www.garethrees.org/2001/12/04/python-coverage/coverage.py#8 $