| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame^] | 1 | """Results of coverage measurement.""" |
| 2 | |
| 3 | import os |
| 4 | |
| 5 | from coverage.backward import set, sorted # pylint: disable=W0622 |
| 6 | from coverage.misc import format_lines, join_regex, NoSource |
| 7 | from coverage.parser import CodeParser |
| 8 | |
| 9 | |
| 10 | class Analysis(object): |
| 11 | """The results of analyzing a code unit.""" |
| 12 | |
| 13 | def __init__(self, cov, code_unit): |
| 14 | self.coverage = cov |
| 15 | self.code_unit = code_unit |
| 16 | |
| 17 | self.filename = self.code_unit.filename |
| 18 | ext = os.path.splitext(self.filename)[1] |
| 19 | source = None |
| 20 | if ext == '.py': |
| 21 | if not os.path.exists(self.filename): |
| 22 | source = self.coverage.file_locator.get_zip_data(self.filename) |
| 23 | if not source: |
| 24 | raise NoSource("No source for code: %r" % self.filename) |
| 25 | |
| 26 | self.parser = CodeParser( |
| 27 | text=source, filename=self.filename, |
| 28 | exclude=self.coverage._exclude_regex('exclude') |
| 29 | ) |
| 30 | self.statements, self.excluded = self.parser.parse_source() |
| 31 | |
| 32 | # Identify missing statements. |
| 33 | executed = self.coverage.data.executed_lines(self.filename) |
| 34 | exec1 = self.parser.first_lines(executed) |
| 35 | self.missing = sorted(set(self.statements) - set(exec1)) |
| 36 | |
| 37 | if self.coverage.data.has_arcs(): |
| 38 | self.no_branch = self.parser.lines_matching( |
| 39 | join_regex(self.coverage.config.partial_list), |
| 40 | join_regex(self.coverage.config.partial_always_list) |
| 41 | ) |
| 42 | n_branches = self.total_branches() |
| 43 | mba = self.missing_branch_arcs() |
| 44 | n_missing_branches = sum([len(v) for v in mba.values()]) |
| 45 | else: |
| 46 | n_branches = n_missing_branches = 0 |
| 47 | self.no_branch = set() |
| 48 | |
| 49 | self.numbers = Numbers( |
| 50 | n_files=1, |
| 51 | n_statements=len(self.statements), |
| 52 | n_excluded=len(self.excluded), |
| 53 | n_missing=len(self.missing), |
| 54 | n_branches=n_branches, |
| 55 | n_missing_branches=n_missing_branches, |
| 56 | ) |
| 57 | |
| 58 | def missing_formatted(self): |
| 59 | """The missing line numbers, formatted nicely. |
| 60 | |
| 61 | Returns a string like "1-2, 5-11, 13-14". |
| 62 | |
| 63 | """ |
| 64 | return format_lines(self.statements, self.missing) |
| 65 | |
| 66 | def has_arcs(self): |
| 67 | """Were arcs measured in this result?""" |
| 68 | return self.coverage.data.has_arcs() |
| 69 | |
| 70 | def arc_possibilities(self): |
| 71 | """Returns a sorted list of the arcs in the code.""" |
| 72 | arcs = self.parser.arcs() |
| 73 | return arcs |
| 74 | |
| 75 | def arcs_executed(self): |
| 76 | """Returns a sorted list of the arcs actually executed in the code.""" |
| 77 | executed = self.coverage.data.executed_arcs(self.filename) |
| 78 | m2fl = self.parser.first_line |
| 79 | executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed] |
| 80 | return sorted(executed) |
| 81 | |
| 82 | def arcs_missing(self): |
| 83 | """Returns a sorted list of the arcs in the code not executed.""" |
| 84 | possible = self.arc_possibilities() |
| 85 | executed = self.arcs_executed() |
| 86 | missing = [ |
| 87 | p for p in possible |
| 88 | if p not in executed |
| 89 | and p[0] not in self.no_branch |
| 90 | ] |
| 91 | return sorted(missing) |
| 92 | |
| 93 | def arcs_unpredicted(self): |
| 94 | """Returns a sorted list of the executed arcs missing from the code.""" |
| 95 | possible = self.arc_possibilities() |
| 96 | executed = self.arcs_executed() |
| 97 | # Exclude arcs here which connect a line to itself. They can occur |
| 98 | # in executed data in some cases. This is where they can cause |
| 99 | # trouble, and here is where it's the least burden to remove them. |
| 100 | unpredicted = [ |
| 101 | e for e in executed |
| 102 | if e not in possible |
| 103 | and e[0] != e[1] |
| 104 | ] |
| 105 | return sorted(unpredicted) |
| 106 | |
| 107 | def branch_lines(self): |
| 108 | """Returns a list of line numbers that have more than one exit.""" |
| 109 | exit_counts = self.parser.exit_counts() |
| 110 | return [l1 for l1,count in exit_counts.items() if count > 1] |
| 111 | |
| 112 | def total_branches(self): |
| 113 | """How many total branches are there?""" |
| 114 | exit_counts = self.parser.exit_counts() |
| 115 | return sum([count for count in exit_counts.values() if count > 1]) |
| 116 | |
| 117 | def missing_branch_arcs(self): |
| 118 | """Return arcs that weren't executed from branch lines. |
| 119 | |
| 120 | Returns {l1:[l2a,l2b,...], ...} |
| 121 | |
| 122 | """ |
| 123 | missing = self.arcs_missing() |
| 124 | branch_lines = set(self.branch_lines()) |
| 125 | mba = {} |
| 126 | for l1, l2 in missing: |
| 127 | if l1 in branch_lines: |
| 128 | if l1 not in mba: |
| 129 | mba[l1] = [] |
| 130 | mba[l1].append(l2) |
| 131 | return mba |
| 132 | |
| 133 | def branch_stats(self): |
| 134 | """Get stats about branches. |
| 135 | |
| 136 | Returns a dict mapping line numbers to a tuple: |
| 137 | (total_exits, taken_exits). |
| 138 | """ |
| 139 | |
| 140 | exit_counts = self.parser.exit_counts() |
| 141 | missing_arcs = self.missing_branch_arcs() |
| 142 | stats = {} |
| 143 | for lnum in self.branch_lines(): |
| 144 | exits = exit_counts[lnum] |
| 145 | try: |
| 146 | missing = len(missing_arcs[lnum]) |
| 147 | except KeyError: |
| 148 | missing = 0 |
| 149 | stats[lnum] = (exits, exits - missing) |
| 150 | return stats |
| 151 | |
| 152 | |
| 153 | class Numbers(object): |
| 154 | """The numerical results of measuring coverage. |
| 155 | |
| 156 | This holds the basic statistics from `Analysis`, and is used to roll |
| 157 | up statistics across files. |
| 158 | |
| 159 | """ |
| 160 | # A global to determine the precision on coverage percentages, the number |
| 161 | # of decimal places. |
| 162 | _precision = 0 |
| 163 | _near0 = 1.0 # These will change when _precision is changed. |
| 164 | _near100 = 99.0 |
| 165 | |
| 166 | def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, |
| 167 | n_branches=0, n_missing_branches=0 |
| 168 | ): |
| 169 | self.n_files = n_files |
| 170 | self.n_statements = n_statements |
| 171 | self.n_excluded = n_excluded |
| 172 | self.n_missing = n_missing |
| 173 | self.n_branches = n_branches |
| 174 | self.n_missing_branches = n_missing_branches |
| 175 | |
| 176 | def set_precision(cls, precision): |
| 177 | """Set the number of decimal places used to report percentages.""" |
| 178 | assert 0 <= precision < 10 |
| 179 | cls._precision = precision |
| 180 | cls._near0 = 1.0 / 10**precision |
| 181 | cls._near100 = 100.0 - cls._near0 |
| 182 | set_precision = classmethod(set_precision) |
| 183 | |
| 184 | def _get_n_executed(self): |
| 185 | """Returns the number of executed statements.""" |
| 186 | return self.n_statements - self.n_missing |
| 187 | n_executed = property(_get_n_executed) |
| 188 | |
| 189 | def _get_n_executed_branches(self): |
| 190 | """Returns the number of executed branches.""" |
| 191 | return self.n_branches - self.n_missing_branches |
| 192 | n_executed_branches = property(_get_n_executed_branches) |
| 193 | |
| 194 | def _get_pc_covered(self): |
| 195 | """Returns a single percentage value for coverage.""" |
| 196 | if self.n_statements > 0: |
| 197 | pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) / |
| 198 | (self.n_statements + self.n_branches)) |
| 199 | else: |
| 200 | pc_cov = 100.0 |
| 201 | return pc_cov |
| 202 | pc_covered = property(_get_pc_covered) |
| 203 | |
| 204 | def _get_pc_covered_str(self): |
| 205 | """Returns the percent covered, as a string, without a percent sign. |
| 206 | |
| 207 | Note that "0" is only returned when the value is truly zero, and "100" |
| 208 | is only returned when the value is truly 100. Rounding can never |
| 209 | result in either "0" or "100". |
| 210 | |
| 211 | """ |
| 212 | pc = self.pc_covered |
| 213 | if 0 < pc < self._near0: |
| 214 | pc = self._near0 |
| 215 | elif self._near100 < pc < 100: |
| 216 | pc = self._near100 |
| 217 | else: |
| 218 | pc = round(pc, self._precision) |
| 219 | return "%.*f" % (self._precision, pc) |
| 220 | pc_covered_str = property(_get_pc_covered_str) |
| 221 | |
| 222 | def pc_str_width(cls): |
| 223 | """How many characters wide can pc_covered_str be?""" |
| 224 | width = 3 # "100" |
| 225 | if cls._precision > 0: |
| 226 | width += 1 + cls._precision |
| 227 | return width |
| 228 | pc_str_width = classmethod(pc_str_width) |
| 229 | |
| 230 | def __add__(self, other): |
| 231 | nums = Numbers() |
| 232 | nums.n_files = self.n_files + other.n_files |
| 233 | nums.n_statements = self.n_statements + other.n_statements |
| 234 | nums.n_excluded = self.n_excluded + other.n_excluded |
| 235 | nums.n_missing = self.n_missing + other.n_missing |
| 236 | nums.n_branches = self.n_branches + other.n_branches |
| 237 | nums.n_missing_branches = (self.n_missing_branches + |
| 238 | other.n_missing_branches) |
| 239 | return nums |
| 240 | |
| 241 | def __radd__(self, other): |
| 242 | # Implementing 0+Numbers allows us to sum() a list of Numbers. |
| 243 | if other == 0: |
| 244 | return self |
| 245 | return NotImplemented |