| OLD | NEW |
| 1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
| 2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
| 3 |
| 1 """Results of coverage measurement.""" | 4 """Results of coverage measurement.""" |
| 2 | 5 |
| 3 import os | 6 import collections |
| 4 | 7 |
| 5 from coverage.backward import iitems, set, sorted # pylint: disable=W0622 | 8 from coverage.backward import iitems |
| 6 from coverage.misc import format_lines, join_regex, NoSource | 9 from coverage.misc import format_lines |
| 7 from coverage.parser import CodeParser | |
| 8 | 10 |
| 9 | 11 |
| 10 class Analysis(object): | 12 class Analysis(object): |
| 11 """The results of analyzing a code unit.""" | 13 """The results of analyzing a FileReporter.""" |
| 12 | 14 |
| 13 def __init__(self, cov, code_unit): | 15 def __init__(self, data, file_reporter): |
| 14 self.coverage = cov | 16 self.data = data |
| 15 self.code_unit = code_unit | 17 self.file_reporter = file_reporter |
| 16 | 18 self.filename = self.file_reporter.filename |
| 17 self.filename = self.code_unit.filename | 19 self.statements = self.file_reporter.lines() |
| 18 actual_filename, source = self.find_source(self.filename) | 20 self.excluded = self.file_reporter.excluded_lines() |
| 19 | |
| 20 self.parser = CodeParser( | |
| 21 text=source, filename=actual_filename, | |
| 22 exclude=self.coverage._exclude_regex('exclude') | |
| 23 ) | |
| 24 self.statements, self.excluded = self.parser.parse_source() | |
| 25 | 21 |
| 26 # Identify missing statements. | 22 # Identify missing statements. |
| 27 executed = self.coverage.data.executed_lines(self.filename) | 23 executed = self.data.lines(self.filename) or [] |
| 28 exec1 = self.parser.first_lines(executed) | 24 executed = self.file_reporter.translate_lines(executed) |
| 29 self.missing = self.statements - exec1 | 25 self.missing = self.statements - executed |
| 30 | 26 |
| 31 if self.coverage.data.has_arcs(): | 27 if self.data.has_arcs(): |
| 32 self.no_branch = self.parser.lines_matching( | 28 self._arc_possibilities = sorted(self.file_reporter.arcs()) |
| 33 join_regex(self.coverage.config.partial_list), | 29 self.exit_counts = self.file_reporter.exit_counts() |
| 34 join_regex(self.coverage.config.partial_always_list) | 30 self.no_branch = self.file_reporter.no_branch_lines() |
| 35 ) | |
| 36 n_branches = self.total_branches() | 31 n_branches = self.total_branches() |
| 37 mba = self.missing_branch_arcs() | 32 mba = self.missing_branch_arcs() |
| 38 n_partial_branches = sum( | 33 n_partial_branches = sum( |
| 39 [len(v) for k,v in iitems(mba) if k not in self.missing] | 34 len(v) for k,v in iitems(mba) if k not in self.missing |
| 40 ) | 35 ) |
| 41 n_missing_branches = sum([len(v) for k,v in iitems(mba)]) | 36 n_missing_branches = sum(len(v) for k,v in iitems(mba)) |
| 42 else: | 37 else: |
| 38 self._arc_possibilities = [] |
| 39 self.exit_counts = {} |
| 40 self.no_branch = set() |
| 43 n_branches = n_partial_branches = n_missing_branches = 0 | 41 n_branches = n_partial_branches = n_missing_branches = 0 |
| 44 self.no_branch = set() | |
| 45 | 42 |
| 46 self.numbers = Numbers( | 43 self.numbers = Numbers( |
| 47 n_files=1, | 44 n_files=1, |
| 48 n_statements=len(self.statements), | 45 n_statements=len(self.statements), |
| 49 n_excluded=len(self.excluded), | 46 n_excluded=len(self.excluded), |
| 50 n_missing=len(self.missing), | 47 n_missing=len(self.missing), |
| 51 n_branches=n_branches, | 48 n_branches=n_branches, |
| 52 n_partial_branches=n_partial_branches, | 49 n_partial_branches=n_partial_branches, |
| 53 n_missing_branches=n_missing_branches, | 50 n_missing_branches=n_missing_branches, |
| 54 ) | 51 ) |
| 55 | 52 |
| 56 def find_source(self, filename): | |
| 57 """Find the source for `filename`. | |
| 58 | |
| 59 Returns two values: the actual filename, and the source. | |
| 60 | |
| 61 The source returned depends on which of these cases holds: | |
| 62 | |
| 63 * The filename seems to be a non-source file: returns None | |
| 64 | |
| 65 * The filename is a source file, and actually exists: returns None. | |
| 66 | |
| 67 * The filename is a source file, and is in a zip file or egg: | |
| 68 returns the source. | |
| 69 | |
| 70 * The filename is a source file, but couldn't be found: raises | |
| 71 `NoSource`. | |
| 72 | |
| 73 """ | |
| 74 source = None | |
| 75 | |
| 76 base, ext = os.path.splitext(filename) | |
| 77 TRY_EXTS = { | |
| 78 '.py': ['.py', '.pyw'], | |
| 79 '.pyw': ['.pyw'], | |
| 80 } | |
| 81 try_exts = TRY_EXTS.get(ext) | |
| 82 if not try_exts: | |
| 83 return filename, None | |
| 84 | |
| 85 for try_ext in try_exts: | |
| 86 try_filename = base + try_ext | |
| 87 if os.path.exists(try_filename): | |
| 88 return try_filename, None | |
| 89 source = self.coverage.file_locator.get_zip_data(try_filename) | |
| 90 if source: | |
| 91 return try_filename, source | |
| 92 raise NoSource("No source for code: '%s'" % filename) | |
| 93 | |
| 94 def missing_formatted(self): | 53 def missing_formatted(self): |
| 95 """The missing line numbers, formatted nicely. | 54 """The missing line numbers, formatted nicely. |
| 96 | 55 |
| 97 Returns a string like "1-2, 5-11, 13-14". | 56 Returns a string like "1-2, 5-11, 13-14". |
| 98 | 57 |
| 99 """ | 58 """ |
| 100 return format_lines(self.statements, self.missing) | 59 return format_lines(self.statements, self.missing) |
| 101 | 60 |
| 102 def has_arcs(self): | 61 def has_arcs(self): |
| 103 """Were arcs measured in this result?""" | 62 """Were arcs measured in this result?""" |
| 104 return self.coverage.data.has_arcs() | 63 return self.data.has_arcs() |
| 105 | 64 |
| 106 def arc_possibilities(self): | 65 def arc_possibilities(self): |
| 107 """Returns a sorted list of the arcs in the code.""" | 66 """Returns a sorted list of the arcs in the code.""" |
| 108 arcs = self.parser.arcs() | 67 return self._arc_possibilities |
| 109 return arcs | |
| 110 | 68 |
| 111 def arcs_executed(self): | 69 def arcs_executed(self): |
| 112 """Returns a sorted list of the arcs actually executed in the code.""" | 70 """Returns a sorted list of the arcs actually executed in the code.""" |
| 113 executed = self.coverage.data.executed_arcs(self.filename) | 71 executed = self.data.arcs(self.filename) or [] |
| 114 m2fl = self.parser.first_line | 72 executed = self.file_reporter.translate_arcs(executed) |
| 115 executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed] | |
| 116 return sorted(executed) | 73 return sorted(executed) |
| 117 | 74 |
| 118 def arcs_missing(self): | 75 def arcs_missing(self): |
| 119 """Returns a sorted list of the arcs in the code not executed.""" | 76 """Returns a sorted list of the arcs in the code not executed.""" |
| 120 possible = self.arc_possibilities() | 77 possible = self.arc_possibilities() |
| 121 executed = self.arcs_executed() | 78 executed = self.arcs_executed() |
| 122 missing = [ | 79 missing = ( |
| 123 p for p in possible | 80 p for p in possible |
| 124 if p not in executed | 81 if p not in executed |
| 125 and p[0] not in self.no_branch | 82 and p[0] not in self.no_branch |
| 126 ] | 83 ) |
| 127 return sorted(missing) | 84 return sorted(missing) |
| 128 | 85 |
| 86 def arcs_missing_formatted(self): |
| 87 """ The missing branch arcs, formatted nicely. |
| 88 |
| 89 Returns a string like "1->2, 1->3, 16->20". Omits any mention of |
| 90 branches from missing lines, so if line 17 is missing, then 17->18 |
| 91 won't be included. |
| 92 |
| 93 """ |
| 94 arcs = self.missing_branch_arcs() |
| 95 missing = self.missing |
| 96 line_exits = sorted(iitems(arcs)) |
| 97 pairs = [] |
| 98 for line, exits in line_exits: |
| 99 for ex in sorted(exits): |
| 100 if line not in missing: |
| 101 pairs.append('%d->%d' % (line, ex)) |
| 102 return ', '.join(pairs) |
| 103 |
| 129 def arcs_unpredicted(self): | 104 def arcs_unpredicted(self): |
| 130 """Returns a sorted list of the executed arcs missing from the code.""" | 105 """Returns a sorted list of the executed arcs missing from the code.""" |
| 131 possible = self.arc_possibilities() | 106 possible = self.arc_possibilities() |
| 132 executed = self.arcs_executed() | 107 executed = self.arcs_executed() |
| 133 # Exclude arcs here which connect a line to itself. They can occur | 108 # Exclude arcs here which connect a line to itself. They can occur |
| 134 # in executed data in some cases. This is where they can cause | 109 # in executed data in some cases. This is where they can cause |
| 135 # trouble, and here is where it's the least burden to remove them. | 110 # trouble, and here is where it's the least burden to remove them. |
| 136 unpredicted = [ | 111 # Also, generators can somehow cause arcs from "enter" to "exit", so |
| 112 # make sure we have at least one positive value. |
| 113 unpredicted = ( |
| 137 e for e in executed | 114 e for e in executed |
| 138 if e not in possible | 115 if e not in possible |
| 139 and e[0] != e[1] | 116 and e[0] != e[1] |
| 140 ] | 117 and (e[0] > 0 or e[1] > 0) |
| 118 ) |
| 141 return sorted(unpredicted) | 119 return sorted(unpredicted) |
| 142 | 120 |
| 143 def branch_lines(self): | 121 def branch_lines(self): |
| 144 """Returns a list of line numbers that have more than one exit.""" | 122 """Returns a list of line numbers that have more than one exit.""" |
| 145 exit_counts = self.parser.exit_counts() | 123 return [l1 for l1,count in iitems(self.exit_counts) if count > 1] |
| 146 return [l1 for l1,count in iitems(exit_counts) if count > 1] | |
| 147 | 124 |
| 148 def total_branches(self): | 125 def total_branches(self): |
| 149 """How many total branches are there?""" | 126 """How many total branches are there?""" |
| 150 exit_counts = self.parser.exit_counts() | 127 return sum(count for count in self.exit_counts.values() if count > 1) |
| 151 return sum([count for count in exit_counts.values() if count > 1]) | |
| 152 | 128 |
| 153 def missing_branch_arcs(self): | 129 def missing_branch_arcs(self): |
| 154 """Return arcs that weren't executed from branch lines. | 130 """Return arcs that weren't executed from branch lines. |
| 155 | 131 |
| 156 Returns {l1:[l2a,l2b,...], ...} | 132 Returns {l1:[l2a,l2b,...], ...} |
| 157 | 133 |
| 158 """ | 134 """ |
| 159 missing = self.arcs_missing() | 135 missing = self.arcs_missing() |
| 160 branch_lines = set(self.branch_lines()) | 136 branch_lines = set(self.branch_lines()) |
| 161 mba = {} | 137 mba = collections.defaultdict(list) |
| 162 for l1, l2 in missing: | 138 for l1, l2 in missing: |
| 163 if l1 in branch_lines: | 139 if l1 in branch_lines: |
| 164 if l1 not in mba: | |
| 165 mba[l1] = [] | |
| 166 mba[l1].append(l2) | 140 mba[l1].append(l2) |
| 167 return mba | 141 return mba |
| 168 | 142 |
| 169 def branch_stats(self): | 143 def branch_stats(self): |
| 170 """Get stats about branches. | 144 """Get stats about branches. |
| 171 | 145 |
| 172 Returns a dict mapping line numbers to a tuple: | 146 Returns a dict mapping line numbers to a tuple: |
| 173 (total_exits, taken_exits). | 147 (total_exits, taken_exits). |
| 174 """ | 148 """ |
| 175 | 149 |
| 176 exit_counts = self.parser.exit_counts() | |
| 177 missing_arcs = self.missing_branch_arcs() | 150 missing_arcs = self.missing_branch_arcs() |
| 178 stats = {} | 151 stats = {} |
| 179 for lnum in self.branch_lines(): | 152 for lnum in self.branch_lines(): |
| 180 exits = exit_counts[lnum] | 153 exits = self.exit_counts[lnum] |
| 181 try: | 154 try: |
| 182 missing = len(missing_arcs[lnum]) | 155 missing = len(missing_arcs[lnum]) |
| 183 except KeyError: | 156 except KeyError: |
| 184 missing = 0 | 157 missing = 0 |
| 185 stats[lnum] = (exits, exits - missing) | 158 stats[lnum] = (exits, exits - missing) |
| 186 return stats | 159 return stats |
| 187 | 160 |
| 188 | 161 |
| 189 class Numbers(object): | 162 class Numbers(object): |
| 190 """The numerical results of measuring coverage. | 163 """The numerical results of measuring coverage. |
| (...skipping 12 matching lines...) Expand all Loading... |
| 203 n_branches=0, n_partial_branches=0, n_missing_branches=0 | 176 n_branches=0, n_partial_branches=0, n_missing_branches=0 |
| 204 ): | 177 ): |
| 205 self.n_files = n_files | 178 self.n_files = n_files |
| 206 self.n_statements = n_statements | 179 self.n_statements = n_statements |
| 207 self.n_excluded = n_excluded | 180 self.n_excluded = n_excluded |
| 208 self.n_missing = n_missing | 181 self.n_missing = n_missing |
| 209 self.n_branches = n_branches | 182 self.n_branches = n_branches |
| 210 self.n_partial_branches = n_partial_branches | 183 self.n_partial_branches = n_partial_branches |
| 211 self.n_missing_branches = n_missing_branches | 184 self.n_missing_branches = n_missing_branches |
| 212 | 185 |
| 186 def init_args(self): |
| 187 """Return a list for __init__(*args) to recreate this object.""" |
| 188 return [ |
| 189 self.n_files, self.n_statements, self.n_excluded, self.n_missing, |
| 190 self.n_branches, self.n_partial_branches, self.n_missing_branches, |
| 191 ] |
| 192 |
| 193 @classmethod |
| 213 def set_precision(cls, precision): | 194 def set_precision(cls, precision): |
| 214 """Set the number of decimal places used to report percentages.""" | 195 """Set the number of decimal places used to report percentages.""" |
| 215 assert 0 <= precision < 10 | 196 assert 0 <= precision < 10 |
| 216 cls._precision = precision | 197 cls._precision = precision |
| 217 cls._near0 = 1.0 / 10**precision | 198 cls._near0 = 1.0 / 10**precision |
| 218 cls._near100 = 100.0 - cls._near0 | 199 cls._near100 = 100.0 - cls._near0 |
| 219 set_precision = classmethod(set_precision) | |
| 220 | 200 |
| 221 def _get_n_executed(self): | 201 @property |
| 202 def n_executed(self): |
| 222 """Returns the number of executed statements.""" | 203 """Returns the number of executed statements.""" |
| 223 return self.n_statements - self.n_missing | 204 return self.n_statements - self.n_missing |
| 224 n_executed = property(_get_n_executed) | |
| 225 | 205 |
| 226 def _get_n_executed_branches(self): | 206 @property |
| 207 def n_executed_branches(self): |
| 227 """Returns the number of executed branches.""" | 208 """Returns the number of executed branches.""" |
| 228 return self.n_branches - self.n_missing_branches | 209 return self.n_branches - self.n_missing_branches |
| 229 n_executed_branches = property(_get_n_executed_branches) | |
| 230 | 210 |
| 231 def _get_pc_covered(self): | 211 @property |
| 212 def pc_covered(self): |
| 232 """Returns a single percentage value for coverage.""" | 213 """Returns a single percentage value for coverage.""" |
| 233 if self.n_statements > 0: | 214 if self.n_statements > 0: |
| 234 pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) / | 215 numerator, denominator = self.ratio_covered |
| 235 (self.n_statements + self.n_branches)) | 216 pc_cov = (100.0 * numerator) / denominator |
| 236 else: | 217 else: |
| 237 pc_cov = 100.0 | 218 pc_cov = 100.0 |
| 238 return pc_cov | 219 return pc_cov |
| 239 pc_covered = property(_get_pc_covered) | |
| 240 | 220 |
| 241 def _get_pc_covered_str(self): | 221 @property |
| 222 def pc_covered_str(self): |
| 242 """Returns the percent covered, as a string, without a percent sign. | 223 """Returns the percent covered, as a string, without a percent sign. |
| 243 | 224 |
| 244 Note that "0" is only returned when the value is truly zero, and "100" | 225 Note that "0" is only returned when the value is truly zero, and "100" |
| 245 is only returned when the value is truly 100. Rounding can never | 226 is only returned when the value is truly 100. Rounding can never |
| 246 result in either "0" or "100". | 227 result in either "0" or "100". |
| 247 | 228 |
| 248 """ | 229 """ |
| 249 pc = self.pc_covered | 230 pc = self.pc_covered |
| 250 if 0 < pc < self._near0: | 231 if 0 < pc < self._near0: |
| 251 pc = self._near0 | 232 pc = self._near0 |
| 252 elif self._near100 < pc < 100: | 233 elif self._near100 < pc < 100: |
| 253 pc = self._near100 | 234 pc = self._near100 |
| 254 else: | 235 else: |
| 255 pc = round(pc, self._precision) | 236 pc = round(pc, self._precision) |
| 256 return "%.*f" % (self._precision, pc) | 237 return "%.*f" % (self._precision, pc) |
| 257 pc_covered_str = property(_get_pc_covered_str) | |
| 258 | 238 |
| 239 @classmethod |
| 259 def pc_str_width(cls): | 240 def pc_str_width(cls): |
| 260 """How many characters wide can pc_covered_str be?""" | 241 """How many characters wide can pc_covered_str be?""" |
| 261 width = 3 # "100" | 242 width = 3 # "100" |
| 262 if cls._precision > 0: | 243 if cls._precision > 0: |
| 263 width += 1 + cls._precision | 244 width += 1 + cls._precision |
| 264 return width | 245 return width |
| 265 pc_str_width = classmethod(pc_str_width) | 246 |
| 247 @property |
| 248 def ratio_covered(self): |
| 249 """Return a numerator and denominator for the coverage ratio.""" |
| 250 numerator = self.n_executed + self.n_executed_branches |
| 251 denominator = self.n_statements + self.n_branches |
| 252 return numerator, denominator |
| 266 | 253 |
| 267 def __add__(self, other): | 254 def __add__(self, other): |
| 268 nums = Numbers() | 255 nums = Numbers() |
| 269 nums.n_files = self.n_files + other.n_files | 256 nums.n_files = self.n_files + other.n_files |
| 270 nums.n_statements = self.n_statements + other.n_statements | 257 nums.n_statements = self.n_statements + other.n_statements |
| 271 nums.n_excluded = self.n_excluded + other.n_excluded | 258 nums.n_excluded = self.n_excluded + other.n_excluded |
| 272 nums.n_missing = self.n_missing + other.n_missing | 259 nums.n_missing = self.n_missing + other.n_missing |
| 273 nums.n_branches = self.n_branches + other.n_branches | 260 nums.n_branches = self.n_branches + other.n_branches |
| 274 nums.n_partial_branches = ( | 261 nums.n_partial_branches = ( |
| 275 self.n_partial_branches + other.n_partial_branches | 262 self.n_partial_branches + other.n_partial_branches |
| 276 ) | 263 ) |
| 277 nums.n_missing_branches = ( | 264 nums.n_missing_branches = ( |
| 278 self.n_missing_branches + other.n_missing_branches | 265 self.n_missing_branches + other.n_missing_branches |
| 279 ) | 266 ) |
| 280 return nums | 267 return nums |
| 281 | 268 |
| 282 def __radd__(self, other): | 269 def __radd__(self, other): |
| 283 # Implementing 0+Numbers allows us to sum() a list of Numbers. | 270 # Implementing 0+Numbers allows us to sum() a list of Numbers. |
| 284 if other == 0: | 271 if other == 0: |
| 285 return self | 272 return self |
| 286 return NotImplemented | 273 return NotImplemented |
| OLD | NEW |