| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame^] | 1 | """Source file annotation for Coverage.""" |
| 2 | |
| 3 | import os, re |
| 4 | |
| 5 | from coverage.report import Reporter |
| 6 | |
| 7 | class AnnotateReporter(Reporter): |
| 8 | """Generate annotated source files showing line coverage. |
| 9 | |
| 10 | This reporter creates annotated copies of the measured source files. Each |
| 11 | .py file is copied as a .py,cover file, with a left-hand margin annotating |
| 12 | each line:: |
| 13 | |
| 14 | > def h(x): |
| 15 | - if 0: #pragma: no cover |
| 16 | - pass |
| 17 | > if x == 1: |
| 18 | ! a = 1 |
| 19 | > else: |
| 20 | > a = 2 |
| 21 | |
| 22 | > h(2) |
| 23 | |
| 24 | Executed lines use '>', lines not executed use '!', lines excluded from |
| 25 | consideration use '-'. |
| 26 | |
| 27 | """ |
| 28 | |
| 29 | def __init__(self, coverage, ignore_errors=False): |
| 30 | super(AnnotateReporter, self).__init__(coverage, ignore_errors) |
| 31 | self.directory = None |
| 32 | |
| 33 | blank_re = re.compile(r"\s*(#|$)") |
| 34 | else_re = re.compile(r"\s*else\s*:\s*(#|$)") |
| 35 | |
| 36 | def report(self, morfs, config, directory=None): |
| 37 | """Run the report. |
| 38 | |
| 39 | See `coverage.report()` for arguments. |
| 40 | |
| 41 | """ |
| 42 | self.report_files(self.annotate_file, morfs, config, directory) |
| 43 | |
| 44 | def annotate_file(self, cu, analysis): |
| 45 | """Annotate a single file. |
| 46 | |
| 47 | `cu` is the CodeUnit for the file to annotate. |
| 48 | |
| 49 | """ |
| 50 | if not cu.relative: |
| 51 | return |
| 52 | |
| 53 | filename = cu.filename |
| 54 | source = cu.source_file() |
| 55 | if self.directory: |
| 56 | dest_file = os.path.join(self.directory, cu.flat_rootname()) |
| 57 | dest_file += ".py,cover" |
| 58 | else: |
| 59 | dest_file = filename + ",cover" |
| 60 | dest = open(dest_file, 'w') |
| 61 | |
| 62 | statements = analysis.statements |
| 63 | missing = analysis.missing |
| 64 | excluded = analysis.excluded |
| 65 | |
| 66 | lineno = 0 |
| 67 | i = 0 |
| 68 | j = 0 |
| 69 | covered = True |
| 70 | while True: |
| 71 | line = source.readline() |
| 72 | if line == '': |
| 73 | break |
| 74 | lineno += 1 |
| 75 | while i < len(statements) and statements[i] < lineno: |
| 76 | i += 1 |
| 77 | while j < len(missing) and missing[j] < lineno: |
| 78 | j += 1 |
| 79 | if i < len(statements) and statements[i] == lineno: |
| 80 | covered = j >= len(missing) or missing[j] > lineno |
| 81 | if self.blank_re.match(line): |
| 82 | dest.write(' ') |
| 83 | elif self.else_re.match(line): |
| 84 | # Special logic for lines containing only 'else:'. |
| 85 | if i >= len(statements) and j >= len(missing): |
| 86 | dest.write('! ') |
| 87 | elif i >= len(statements) or j >= len(missing): |
| 88 | dest.write('> ') |
| 89 | elif statements[i] == missing[j]: |
| 90 | dest.write('! ') |
| 91 | else: |
| 92 | dest.write('> ') |
| 93 | elif lineno in excluded: |
| 94 | dest.write('- ') |
| 95 | elif covered: |
| 96 | dest.write('> ') |
| 97 | else: |
| 98 | dest.write('! ') |
| 99 | dest.write(line) |
| 100 | source.close() |
| 101 | dest.close() |