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