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