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 4"""Results of coverage measurement.""" 5 6import collections 7 8from coverage.backward import iitems 9from coverage.misc import format_lines 10 11 12class Analysis(object): 13 """The results of analyzing a FileReporter.""" 14 15 def __init__(self, data, file_reporter): 16 self.data = data 17 self.file_reporter = file_reporter 18 self.filename = self.file_reporter.filename 19 self.statements = self.file_reporter.lines() 20 self.excluded = self.file_reporter.excluded_lines() 21 22 # Identify missing statements. 23 executed = self.data.lines(self.filename) or [] 24 executed = self.file_reporter.translate_lines(executed) 25 self.missing = self.statements - executed 26 27 if self.data.has_arcs(): 28 self._arc_possibilities = sorted(self.file_reporter.arcs()) 29 self.exit_counts = self.file_reporter.exit_counts() 30 self.no_branch = self.file_reporter.no_branch_lines() 31 n_branches = self.total_branches() 32 mba = self.missing_branch_arcs() 33 n_partial_branches = sum( 34 len(v) for k,v in iitems(mba) if k not in self.missing 35 ) 36 n_missing_branches = sum(len(v) for k,v in iitems(mba)) 37 else: 38 self._arc_possibilities = [] 39 self.exit_counts = {} 40 self.no_branch = set() 41 n_branches = n_partial_branches = n_missing_branches = 0 42 43 self.numbers = Numbers( 44 n_files=1, 45 n_statements=len(self.statements), 46 n_excluded=len(self.excluded), 47 n_missing=len(self.missing), 48 n_branches=n_branches, 49 n_partial_branches=n_partial_branches, 50 n_missing_branches=n_missing_branches, 51 ) 52 53 def missing_formatted(self): 54 """The missing line numbers, formatted nicely. 55 56 Returns a string like "1-2, 5-11, 13-14". 57 58 """ 59 return format_lines(self.statements, self.missing) 60 61 def has_arcs(self): 62 """Were arcs measured in this result?""" 63 return self.data.has_arcs() 64 65 def arc_possibilities(self): 66 """Returns a sorted list of the arcs in the code.""" 67 return self._arc_possibilities 68 69 def arcs_executed(self): 70 """Returns a sorted list of the arcs actually executed in the code.""" 71 executed = self.data.arcs(self.filename) or [] 72 executed = self.file_reporter.translate_arcs(executed) 73 return sorted(executed) 74 75 def arcs_missing(self): 76 """Returns a sorted list of the arcs in the code not executed.""" 77 possible = self.arc_possibilities() 78 executed = self.arcs_executed() 79 missing = ( 80 p for p in possible 81 if p not in executed 82 and p[0] not in self.no_branch 83 ) 84 return sorted(missing) 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 104 def arcs_unpredicted(self): 105 """Returns a sorted list of the executed arcs missing from the code.""" 106 possible = self.arc_possibilities() 107 executed = self.arcs_executed() 108 # Exclude arcs here which connect a line to itself. They can occur 109 # in executed data in some cases. This is where they can cause 110 # trouble, and here is where it's the least burden to remove them. 111 # Also, generators can somehow cause arcs from "enter" to "exit", so 112 # make sure we have at least one positive value. 113 unpredicted = ( 114 e for e in executed 115 if e not in possible 116 and e[0] != e[1] 117 and (e[0] > 0 or e[1] > 0) 118 ) 119 return sorted(unpredicted) 120 121 def branch_lines(self): 122 """Returns a list of line numbers that have more than one exit.""" 123 return [l1 for l1,count in iitems(self.exit_counts) if count > 1] 124 125 def total_branches(self): 126 """How many total branches are there?""" 127 return sum(count for count in self.exit_counts.values() if count > 1) 128 129 def missing_branch_arcs(self): 130 """Return arcs that weren't executed from branch lines. 131 132 Returns {l1:[l2a,l2b,...], ...} 133 134 """ 135 missing = self.arcs_missing() 136 branch_lines = set(self.branch_lines()) 137 mba = collections.defaultdict(list) 138 for l1, l2 in missing: 139 if l1 in branch_lines: 140 mba[l1].append(l2) 141 return mba 142 143 def branch_stats(self): 144 """Get stats about branches. 145 146 Returns a dict mapping line numbers to a tuple: 147 (total_exits, taken_exits). 148 """ 149 150 missing_arcs = self.missing_branch_arcs() 151 stats = {} 152 for lnum in self.branch_lines(): 153 exits = self.exit_counts[lnum] 154 try: 155 missing = len(missing_arcs[lnum]) 156 except KeyError: 157 missing = 0 158 stats[lnum] = (exits, exits - missing) 159 return stats 160 161 162class Numbers(object): 163 """The numerical results of measuring coverage. 164 165 This holds the basic statistics from `Analysis`, and is used to roll 166 up statistics across files. 167 168 """ 169 # A global to determine the precision on coverage percentages, the number 170 # of decimal places. 171 _precision = 0 172 _near0 = 1.0 # These will change when _precision is changed. 173 _near100 = 99.0 174 175 def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, 176 n_branches=0, n_partial_branches=0, n_missing_branches=0 177 ): 178 self.n_files = n_files 179 self.n_statements = n_statements 180 self.n_excluded = n_excluded 181 self.n_missing = n_missing 182 self.n_branches = n_branches 183 self.n_partial_branches = n_partial_branches 184 self.n_missing_branches = n_missing_branches 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 194 def set_precision(cls, precision): 195 """Set the number of decimal places used to report percentages.""" 196 assert 0 <= precision < 10 197 cls._precision = precision 198 cls._near0 = 1.0 / 10**precision 199 cls._near100 = 100.0 - cls._near0 200 201 @property 202 def n_executed(self): 203 """Returns the number of executed statements.""" 204 return self.n_statements - self.n_missing 205 206 @property 207 def n_executed_branches(self): 208 """Returns the number of executed branches.""" 209 return self.n_branches - self.n_missing_branches 210 211 @property 212 def pc_covered(self): 213 """Returns a single percentage value for coverage.""" 214 if self.n_statements > 0: 215 numerator, denominator = self.ratio_covered 216 pc_cov = (100.0 * numerator) / denominator 217 else: 218 pc_cov = 100.0 219 return pc_cov 220 221 @property 222 def pc_covered_str(self): 223 """Returns the percent covered, as a string, without a percent sign. 224 225 Note that "0" is only returned when the value is truly zero, and "100" 226 is only returned when the value is truly 100. Rounding can never 227 result in either "0" or "100". 228 229 """ 230 pc = self.pc_covered 231 if 0 < pc < self._near0: 232 pc = self._near0 233 elif self._near100 < pc < 100: 234 pc = self._near100 235 else: 236 pc = round(pc, self._precision) 237 return "%.*f" % (self._precision, pc) 238 239 @classmethod 240 def pc_str_width(cls): 241 """How many characters wide can pc_covered_str be?""" 242 width = 3 # "100" 243 if cls._precision > 0: 244 width += 1 + cls._precision 245 return 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 253 254 def __add__(self, other): 255 nums = Numbers() 256 nums.n_files = self.n_files + other.n_files 257 nums.n_statements = self.n_statements + other.n_statements 258 nums.n_excluded = self.n_excluded + other.n_excluded 259 nums.n_missing = self.n_missing + other.n_missing 260 nums.n_branches = self.n_branches + other.n_branches 261 nums.n_partial_branches = ( 262 self.n_partial_branches + other.n_partial_branches 263 ) 264 nums.n_missing_branches = ( 265 self.n_missing_branches + other.n_missing_branches 266 ) 267 return nums 268 269 def __radd__(self, other): 270 # Implementing 0+Numbers allows us to sum() a list of Numbers. 271 if other == 0: 272 return self 273 return NotImplemented 274