~ubuntu-branches/ubuntu/quantal/virtinst/quantal-proposed

« back to all changes in this revision

Viewing changes to tests/coverage.py

  • Committer: Bazaar Package Importer
  • Author(s): Soren Hansen
  • Date: 2007-11-20 13:40:28 UTC
  • Revision ID: james.westby@ubuntu.com-20071120134028-rg0pjby0jc4mycks
Tags: upstream-0.300.1+hg20071120
ImportĀ upstreamĀ versionĀ 0.300.1+hg20071120

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
#
 
10
#
 
11
# 1. INTRODUCTION
 
12
#
 
13
# This module provides coverage testing for Python code.
 
14
#
 
15
# The intended readership is all Python developers.
 
16
#
 
17
# This document is not confidential.
 
18
#
 
19
# See [GDR 2001-12-04a] for the command-line interface, programmatic
 
20
# interface and limitations.  See [GDR 2001-12-04b] for requirements and
 
21
# design.
 
22
 
 
23
"""Usage:
 
24
 
 
25
coverage.py -x MODULE.py [ARG1 ARG2 ...]
 
26
    Execute module, passing the given command-line arguments, collecting
 
27
    coverage data.
 
28
 
 
29
coverage.py -e
 
30
    Erase collected coverage data.
 
31
 
 
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.
 
35
 
 
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.
 
41
 
 
42
Coverage data is saved in the file .coverage by default.  Set the
 
43
COVERAGE_FILE environment variable to save it somewhere else."""
 
44
 
 
45
import os
 
46
import re
 
47
import string
 
48
import sys
 
49
import types
 
50
 
 
51
 
 
52
# 2. IMPLEMENTATION
 
53
#
 
54
# This uses the "singleton" pattern.
 
55
#
 
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
 
58
# filename.
 
59
#
 
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.
 
65
#
 
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.
 
71
 
 
72
# A dictionary with an entry for (Python source file name, line number
 
73
# in that file) if that line has been executed.
 
74
c = {}
 
75
 
 
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
 
80
# objects.
 
81
 
 
82
def t(f, x, y):
 
83
    c[(f.f_code.co_filename, f.f_lineno)] = 1
 
84
    return t
 
85
 
 
86
the_coverage = None
 
87
 
 
88
class coverage:
 
89
    error = "coverage error"
 
90
 
 
91
    # Name of the cache file (unless environment variable is set).
 
92
    cache_default = ".coverage"
 
93
 
 
94
    # Environment variable naming the cache file.
 
95
    cache_env = "COVERAGE_FILE"
 
96
 
 
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
 
99
    # executed.
 
100
    cexecuted = {}
 
101
 
 
102
    # Cache of results of calling the analysis() method, so that you can
 
103
    # specify both -r and -a without doing double work.
 
104
    analysis_cache = {}
 
105
 
 
106
    # Cache of results of calling the canonical_filename() method, to
 
107
    # avoid duplicating work.
 
108
    canonical_filename_cache = {}
 
109
 
 
110
    def __init__(self):
 
111
        global the_coverage
 
112
        if the_coverage:
 
113
            raise self.error, "Only one coverage object allowed."
 
114
        self.cache = os.environ.get(self.cache_env, self.cache_default)
 
115
        self.restore()
 
116
        self.analysis_cache = {}
 
117
 
 
118
    def help(self, error=None):
 
119
        if error:
 
120
            print error
 
121
            print
 
122
        print __doc__
 
123
        sys.exit(1)
 
124
 
 
125
    def command_line(self):
 
126
        import getopt
 
127
        settings = {}
 
128
        optmap = {
 
129
            '-a': 'annotate',
 
130
            '-d:': 'directory=',
 
131
            '-e': 'erase',
 
132
            '-h': 'help',
 
133
            '-i': 'ignore-errors',
 
134
            '-m': 'show-missing',
 
135
            '-r': 'report',
 
136
            '-x': 'execute',
 
137
            }
 
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,
 
141
                                      long_opts)
 
142
        for o, a in options:
 
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:
 
148
                settings[o[2:]] = 1
 
149
            elif o[2:] + '=' in long_opts:
 
150
                settings[o[2:]] = a
 
151
            else:
 
152
                self.help("Unknown option: '%s'." % o)
 
153
        if settings.get('help'):
 
154
            self.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
 
164
        if not action:
 
165
            self.help("You must specify at least one of -e, -x, -r, "
 
166
                      "or -a.")
 
167
        if not args_needed and args:
 
168
            self.help("Unexpected arguments %s." % args)
 
169
        if settings.get('erase'):
 
170
            self.erase()
 
171
        if settings.get('execute'):
 
172
            if not args:
 
173
                self.help("Nothing to do.")
 
174
            sys.argv = args
 
175
            self.start()
 
176
            import __main__
 
177
            sys.path[0] = os.path.dirname(sys.argv[0])
 
178
            execfile(sys.argv[0], __main__.__dict__)
 
179
        if not args:
 
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)
 
188
 
 
189
    def start(self):
 
190
        sys.settrace(t)
 
191
 
 
192
    def stop(self):
 
193
        sys.settrace(None)
 
194
 
 
195
    def erase(self):
 
196
        global c
 
197
        c = {}
 
198
        self.analysis_cache = {}
 
199
        self.cexecuted = {}
 
200
        if os.path.exists(self.cache):
 
201
            os.remove(self.cache)
 
202
 
 
203
    # save().  Save coverage data to the coverage cache.
 
204
 
 
205
    def save(self):
 
206
        self.canonicalize_filenames()
 
207
        cache = open(self.cache, 'wb')
 
208
        import marshal
 
209
        marshal.dump(self.cexecuted, cache)
 
210
        cache.close()
 
211
 
 
212
    # restore().  Restore coverage data from the coverage cache (if it
 
213
    # exists).
 
214
 
 
215
    def restore(self):
 
216
        global c
 
217
        c = {}
 
218
        self.cexecuted = {}
 
219
        if not os.path.exists(self.cache):
 
220
            return
 
221
        try:
 
222
            cache = open(self.cache, 'rb')
 
223
            import marshal
 
224
            cexecuted = marshal.load(cache)
 
225
            cache.close()
 
226
            if isinstance(cexecuted, types.DictType):
 
227
                self.cexecuted = cexecuted
 
228
        except:
 
229
            pass
 
230
 
 
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].
 
234
 
 
235
    def canonical_filename(self, filename):
 
236
        if not self.canonical_filename_cache.has_key(filename):
 
237
            f = 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):
 
244
                        f = g
 
245
                        break
 
246
            cf = os.path.normcase(os.path.abspath(f))
 
247
            self.canonical_filename_cache[filename] = cf
 
248
        return self.canonical_filename_cache[filename]
 
249
 
 
250
    # canonicalize_filenames().  Copy results from "executed" to
 
251
    # "cexecuted", canonicalizing filenames on the way.  Clear the
 
252
    # "executed" map.
 
253
 
 
254
    def canonicalize_filenames(self):
 
255
        global c
 
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
 
261
        c = {}
 
262
 
 
263
    # morf_filename(morf).  Return the filename for a module or file.
 
264
 
 
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."
 
269
            file = morf.__file__
 
270
        else:
 
271
            file = morf
 
272
        return self.canonical_filename(file)
 
273
 
 
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.
 
279
 
 
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]
 
285
        if ext == '.pyc':
 
286
            if not os.path.exists(filename[0:-1]):
 
287
                raise self.error, ("No source for compiled code '%s'."
 
288
                                   % filename)
 
289
            filename = filename[0:-1]
 
290
        elif ext != '.py':
 
291
            raise self.error, "File '%s' not Python source." % filename
 
292
        source = open(filename, 'r')
 
293
        import parser
 
294
        tree = parser.suite(source.read()).totuple(1)
 
295
        source.close()
 
296
        statements = {}
 
297
        self.find_statements(tree, statements)
 
298
        lines = statements.keys()
 
299
        lines.sort()
 
300
        result = filename, lines
 
301
        self.analysis_cache[morf] = result
 
302
        return result
 
303
 
 
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).
 
307
    #
 
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
 
313
    # was found.
 
314
    #
 
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
 
318
    # dictionary.
 
319
    #
 
320
    # See also [GDR 2001-12-04b, 3.2].
 
321
 
 
322
    def find_statements(self, tree, dict):
 
323
        import symbol, token
 
324
        if token.ISNONTERMINAL(tree[0]):
 
325
            for t in tree[1:]:
 
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']):
 
331
            dict[tree[2]] = 1
 
332
 
 
333
    def find_statement(self, tree, dict):
 
334
        import token
 
335
        while token.ISNONTERMINAL(tree[0]):
 
336
            tree = tree[1]
 
337
        dict[tree[2]] = 1
 
338
 
 
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".
 
345
 
 
346
    def format_lines(self, statements, lines):
 
347
        pairs = []
 
348
        i = 0
 
349
        j = 0
 
350
        start = None
 
351
        pairs = []
 
352
        while i < len(statements) and j < len(lines):
 
353
            if statements[i] == lines[j]:
 
354
                if start == None:
 
355
                    start = lines[j]
 
356
                end = lines[j]
 
357
                j = j + 1
 
358
            elif start:
 
359
                pairs.append((start, end))
 
360
                start = None
 
361
            i = i + 1
 
362
        if start:
 
363
            pairs.append((start, end))
 
364
        def stringify(pair):
 
365
            start, end = pair
 
366
            if start == end:
 
367
                return "%d" % start
 
368
            else:
 
369
                return "%d-%d" % (start, end)
 
370
        import string
 
371
        return string.join(map(stringify, pairs), ", ")
 
372
 
 
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] = {}
 
378
        missing = []
 
379
        for line in statements:
 
380
            if not self.cexecuted[filename].has_key(line):
 
381
                missing.append(line)
 
382
        return (filename, statements, missing,
 
383
                self.format_lines(statements, missing))
 
384
 
 
385
    def morf_name(self, morf):
 
386
        if isinstance(morf, types.ModuleType):
 
387
            return morf.__name__
 
388
        else:
 
389
            return os.path.splitext(os.path.basename(morf))[0]
 
390
 
 
391
    def report(self, morfs, show_missing=1, ignore_errors=0):
 
392
        if not isinstance(morfs, types.ListType):
 
393
            morfs = [morfs]
 
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%%"
 
399
        if show_missing:
 
400
            header = header + "   Missing"
 
401
            fmt_coverage = fmt_coverage + "   %s"
 
402
        print header
 
403
        print "-" * len(header)
 
404
        total_statements = 0
 
405
        total_executed = 0
 
406
        for morf in morfs:
 
407
            name = self.morf_name(morf)
 
408
            try:
 
409
                _, statements, missing, readable  = self.analysis(morf)
 
410
                n = len(statements)
 
411
                m = n - len(missing)
 
412
                if n > 0:
 
413
                    pc = 100.0 * m / n
 
414
                else:
 
415
                    pc = 100.0
 
416
                args = (name, n, m, pc)
 
417
                if show_missing:
 
418
                    args = args + (readable,)
 
419
                print fmt_coverage % args
 
420
                total_statements = total_statements + n
 
421
                total_executed = total_executed + m
 
422
            except KeyboardInterrupt:
 
423
                raise
 
424
            except:
 
425
                if not ignore_errors:
 
426
                    type, msg = sys.exc_info()[0:2]
 
427
                    print fmt_err % (name, type, msg)
 
428
        if len(morfs) > 1:
 
429
            print "-" * len(header)
 
430
            if total_statements > 0:
 
431
                pc = 100.0 * total_executed / total_statements
 
432
            else:
 
433
                pc = 100.0
 
434
            args = ("TOTAL", total_statements, total_executed, pc)
 
435
            if show_missing:
 
436
                args = args + ("",)
 
437
            print fmt_coverage % args
 
438
 
 
439
    # annotate(morfs, ignore_errors).
 
440
 
 
441
    blank_re = re.compile("\\s*(#|$)")
 
442
    else_re = re.compile("\\s*else\\s*:\\s*(#|$)")
 
443
 
 
444
    def annotate(self, morfs, directory=None, ignore_errors=0):
 
445
        for morf in morfs:
 
446
            try:
 
447
                filename, statements, missing, _ = self.analysis(morf)
 
448
                source = open(filename, 'r')
 
449
                if directory:
 
450
                    dest_file = os.path.join(directory,
 
451
                                             os.path.basename(filename)
 
452
                                             + ',cover')
 
453
                else:
 
454
                    dest_file = filename + ',cover'
 
455
                dest = open(dest_file, 'w')
 
456
                lineno = 0
 
457
                i = 0
 
458
                j = 0
 
459
                covered = 1
 
460
                while 1:
 
461
                    line = source.readline()
 
462
                    if line == '':
 
463
                        break
 
464
                    lineno = lineno + 1
 
465
                    while i < len(statements) and statements[i] < lineno:
 
466
                        i = i + 1
 
467
                    while j < len(missing) and missing[j] < lineno:
 
468
                        j = j + 1
 
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):
 
472
                        dest.write('  ')
 
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):
 
477
                            dest.write('! ')
 
478
                        elif i >= len(statements) or j >= len(missing):
 
479
                            dest.write('> ')
 
480
                        elif statements[i] == missing[j]:
 
481
                            dest.write('! ')
 
482
                        else:
 
483
                            dest.write('> ')
 
484
                    elif covered:
 
485
                        dest.write('> ')
 
486
                    else:
 
487
                        dest.write('! ')
 
488
                    dest.write(line)
 
489
                source.close()
 
490
                dest.close()
 
491
            except KeyboardInterrupt:
 
492
                raise
 
493
            except:
 
494
                if not ignore_errors:
 
495
                    raise
 
496
 
 
497
 
 
498
# Singleton object.
 
499
the_coverage = coverage()
 
500
 
 
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)
 
507
 
 
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
 
510
# available.)
 
511
try:
 
512
    import atexit
 
513
    atexit.register(the_coverage.save)
 
514
except ImportError:
 
515
    sys.exitfunc = the_coverage.save
 
516
 
 
517
# Command-line interface.
 
518
if __name__ == '__main__':
 
519
    the_coverage.command_line()
 
520
 
 
521
 
 
522
# A. REFERENCES
 
523
#
 
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/>.
 
527
#
 
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>.
 
531
#
 
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>.
 
535
#
 
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>.
 
538
#
 
539
#
 
540
# B. DOCUMENT HISTORY
 
541
#
 
542
# 2001-12-04 GDR Created.
 
543
#
 
544
# 2001-12-06 GDR Added command-line interface and source code
 
545
# annotation.
 
546
#
 
547
# 2001-12-09 GDR Moved design and interface to separate documents.
 
548
#
 
549
# 2001-12-10 GDR Open cache file as binary on Windows.  Allow
 
550
# simultaneous -e and -x, or -a and -r.
 
551
#
 
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.
 
554
#
 
555
# 2001-12-13 GDR Improved speed while recording.  Portable between
 
556
# Python 1.5.2 and 2.1.1.
 
557
#
 
558
# 2002-01-03 GDR Module-level functions work correctly.
 
559
#
 
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
 
562
# its own.
 
563
#
 
564
#
 
565
# C. COPYRIGHT AND LICENCE
 
566
#
 
567
# Copyright 2001 Gareth Rees.  All rights reserved.
 
568
#
 
569
# Redistribution and use in source and binary forms, with or without
 
570
# modification, are permitted provided that the following conditions are
 
571
# met:
 
572
#
 
573
# 1. Redistributions of source code must retain the above copyright
 
574
#    notice, this list of conditions and the following disclaimer.
 
575
#
 
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
 
579
#    distribution.
 
580
#
 
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
 
592
# DAMAGE.
 
593
#
 
594
#
 
595
#
 
596
# $Id: //info.ravenbrook.com/user/gdr/www.garethrees.org/2001/12/04/python-coverage/coverage.py#8 $