3 |
3 |
4 """Results of coverage measurement.""" |
4 """Results of coverage measurement.""" |
5 |
5 |
6 import collections |
6 import collections |
7 |
7 |
8 from coverage.backward import iitems |
|
9 from coverage.debug import SimpleReprMixin |
8 from coverage.debug import SimpleReprMixin |
10 from coverage.misc import contract, CoverageException, nice_pair |
9 from coverage.exceptions import CoverageException |
11 |
10 from coverage.misc import contract, nice_pair |
12 |
11 |
13 class Analysis(object): |
12 |
|
13 class Analysis: |
14 """The results of analyzing a FileReporter.""" |
14 """The results of analyzing a FileReporter.""" |
15 |
15 |
16 def __init__(self, data, file_reporter, file_mapper): |
16 def __init__(self, data, precision, file_reporter, file_mapper): |
17 self.data = data |
17 self.data = data |
18 self.file_reporter = file_reporter |
18 self.file_reporter = file_reporter |
19 self.filename = file_mapper(self.file_reporter.filename) |
19 self.filename = file_mapper(self.file_reporter.filename) |
20 self.statements = self.file_reporter.lines() |
20 self.statements = self.file_reporter.lines() |
21 self.excluded = self.file_reporter.excluded_lines() |
21 self.excluded = self.file_reporter.excluded_lines() |
30 self._arc_possibilities = sorted(self.file_reporter.arcs()) |
30 self._arc_possibilities = sorted(self.file_reporter.arcs()) |
31 self.exit_counts = self.file_reporter.exit_counts() |
31 self.exit_counts = self.file_reporter.exit_counts() |
32 self.no_branch = self.file_reporter.no_branch_lines() |
32 self.no_branch = self.file_reporter.no_branch_lines() |
33 n_branches = self._total_branches() |
33 n_branches = self._total_branches() |
34 mba = self.missing_branch_arcs() |
34 mba = self.missing_branch_arcs() |
35 n_partial_branches = sum(len(v) for k,v in iitems(mba) if k not in self.missing) |
35 n_partial_branches = sum(len(v) for k,v in mba.items() if k not in self.missing) |
36 n_missing_branches = sum(len(v) for k,v in iitems(mba)) |
36 n_missing_branches = sum(len(v) for k,v in mba.items()) |
37 else: |
37 else: |
38 self._arc_possibilities = [] |
38 self._arc_possibilities = [] |
39 self.exit_counts = {} |
39 self.exit_counts = {} |
40 self.no_branch = set() |
40 self.no_branch = set() |
41 n_branches = n_partial_branches = n_missing_branches = 0 |
41 n_branches = n_partial_branches = n_missing_branches = 0 |
42 |
42 |
43 self.numbers = Numbers( |
43 self.numbers = Numbers( |
|
44 precision=precision, |
44 n_files=1, |
45 n_files=1, |
45 n_statements=len(self.statements), |
46 n_statements=len(self.statements), |
46 n_excluded=len(self.excluded), |
47 n_excluded=len(self.excluded), |
47 n_missing=len(self.missing), |
48 n_missing=len(self.missing), |
48 n_branches=n_branches, |
49 n_branches=n_branches, |
81 executed = self.file_reporter.translate_arcs(executed) |
82 executed = self.file_reporter.translate_arcs(executed) |
82 return sorted(executed) |
83 return sorted(executed) |
83 |
84 |
84 @contract(returns='list(tuple(int, int))') |
85 @contract(returns='list(tuple(int, int))') |
85 def arcs_missing(self): |
86 def arcs_missing(self): |
86 """Returns a sorted list of the arcs in the code not executed.""" |
87 """Returns a sorted list of the unexecuted arcs in the code.""" |
87 possible = self.arc_possibilities() |
88 possible = self.arc_possibilities() |
88 executed = self.arcs_executed() |
89 executed = self.arcs_executed() |
89 missing = ( |
90 missing = ( |
90 p for p in possible |
91 p for p in possible |
91 if p not in executed |
92 if p not in executed |
92 and p[0] not in self.no_branch |
93 and p[0] not in self.no_branch |
|
94 and p[1] not in self.excluded |
93 ) |
95 ) |
94 return sorted(missing) |
96 return sorted(missing) |
95 |
97 |
96 @contract(returns='list(tuple(int, int))') |
98 @contract(returns='list(tuple(int, int))') |
97 def arcs_unpredicted(self): |
99 def arcs_unpredicted(self): |
111 ) |
113 ) |
112 return sorted(unpredicted) |
114 return sorted(unpredicted) |
113 |
115 |
114 def _branch_lines(self): |
116 def _branch_lines(self): |
115 """Returns a list of line numbers that have more than one exit.""" |
117 """Returns a list of line numbers that have more than one exit.""" |
116 return [l1 for l1,count in iitems(self.exit_counts) if count > 1] |
118 return [l1 for l1,count in self.exit_counts.items() if count > 1] |
117 |
119 |
118 def _total_branches(self): |
120 def _total_branches(self): |
119 """How many total branches are there?""" |
121 """How many total branches are there?""" |
120 return sum(count for count in self.exit_counts.values() if count > 1) |
122 return sum(count for count in self.exit_counts.values() if count > 1) |
121 |
123 |
156 |
158 |
157 This holds the basic statistics from `Analysis`, and is used to roll |
159 This holds the basic statistics from `Analysis`, and is used to roll |
158 up statistics across files. |
160 up statistics across files. |
159 |
161 |
160 """ |
162 """ |
161 # A global to determine the precision on coverage percentages, the number |
163 |
162 # of decimal places. |
164 def __init__(self, |
163 _precision = 0 |
165 precision=0, |
164 _near0 = 1.0 # These will change when _precision is changed. |
166 n_files=0, n_statements=0, n_excluded=0, n_missing=0, |
165 _near100 = 99.0 |
167 n_branches=0, n_partial_branches=0, n_missing_branches=0 |
166 |
168 ): |
167 def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, |
169 assert 0 <= precision < 10 |
168 n_branches=0, n_partial_branches=0, n_missing_branches=0 |
170 self._precision = precision |
169 ): |
171 self._near0 = 1.0 / 10**precision |
|
172 self._near100 = 100.0 - self._near0 |
170 self.n_files = n_files |
173 self.n_files = n_files |
171 self.n_statements = n_statements |
174 self.n_statements = n_statements |
172 self.n_excluded = n_excluded |
175 self.n_excluded = n_excluded |
173 self.n_missing = n_missing |
176 self.n_missing = n_missing |
174 self.n_branches = n_branches |
177 self.n_branches = n_branches |
176 self.n_missing_branches = n_missing_branches |
179 self.n_missing_branches = n_missing_branches |
177 |
180 |
178 def init_args(self): |
181 def init_args(self): |
179 """Return a list for __init__(*args) to recreate this object.""" |
182 """Return a list for __init__(*args) to recreate this object.""" |
180 return [ |
183 return [ |
|
184 self._precision, |
181 self.n_files, self.n_statements, self.n_excluded, self.n_missing, |
185 self.n_files, self.n_statements, self.n_excluded, self.n_missing, |
182 self.n_branches, self.n_partial_branches, self.n_missing_branches, |
186 self.n_branches, self.n_partial_branches, self.n_missing_branches, |
183 ] |
187 ] |
184 |
|
185 @classmethod |
|
186 def set_precision(cls, precision): |
|
187 """Set the number of decimal places used to report percentages.""" |
|
188 assert 0 <= precision < 10 |
|
189 cls._precision = precision |
|
190 cls._near0 = 1.0 / 10**precision |
|
191 cls._near100 = 100.0 - cls._near0 |
|
192 |
188 |
193 @property |
189 @property |
194 def n_executed(self): |
190 def n_executed(self): |
195 """Returns the number of executed statements.""" |
191 """Returns the number of executed statements.""" |
196 return self.n_statements - self.n_missing |
192 return self.n_statements - self.n_missing |
217 Note that "0" is only returned when the value is truly zero, and "100" |
213 Note that "0" is only returned when the value is truly zero, and "100" |
218 is only returned when the value is truly 100. Rounding can never |
214 is only returned when the value is truly 100. Rounding can never |
219 result in either "0" or "100". |
215 result in either "0" or "100". |
220 |
216 |
221 """ |
217 """ |
222 pc = self.pc_covered |
218 return self.display_covered(self.pc_covered) |
|
219 |
|
220 def display_covered(self, pc): |
|
221 """Return a displayable total percentage, as a string. |
|
222 |
|
223 Note that "0" is only returned when the value is truly zero, and "100" |
|
224 is only returned when the value is truly 100. Rounding can never |
|
225 result in either "0" or "100". |
|
226 |
|
227 """ |
223 if 0 < pc < self._near0: |
228 if 0 < pc < self._near0: |
224 pc = self._near0 |
229 pc = self._near0 |
225 elif self._near100 < pc < 100: |
230 elif self._near100 < pc < 100: |
226 pc = self._near100 |
231 pc = self._near100 |
227 else: |
232 else: |
228 pc = round(pc, self._precision) |
233 pc = round(pc, self._precision) |
229 return "%.*f" % (self._precision, pc) |
234 return "%.*f" % (self._precision, pc) |
230 |
235 |
231 @classmethod |
236 def pc_str_width(self): |
232 def pc_str_width(cls): |
|
233 """How many characters wide can pc_covered_str be?""" |
237 """How many characters wide can pc_covered_str be?""" |
234 width = 3 # "100" |
238 width = 3 # "100" |
235 if cls._precision > 0: |
239 if self._precision > 0: |
236 width += 1 + cls._precision |
240 width += 1 + self._precision |
237 return width |
241 return width |
238 |
242 |
239 @property |
243 @property |
240 def ratio_covered(self): |
244 def ratio_covered(self): |
241 """Return a numerator and denominator for the coverage ratio.""" |
245 """Return a numerator and denominator for the coverage ratio.""" |
242 numerator = self.n_executed + self.n_executed_branches |
246 numerator = self.n_executed + self.n_executed_branches |
243 denominator = self.n_statements + self.n_branches |
247 denominator = self.n_statements + self.n_branches |
244 return numerator, denominator |
248 return numerator, denominator |
245 |
249 |
246 def __add__(self, other): |
250 def __add__(self, other): |
247 nums = Numbers() |
251 nums = Numbers(precision=self._precision) |
248 nums.n_files = self.n_files + other.n_files |
252 nums.n_files = self.n_files + other.n_files |
249 nums.n_statements = self.n_statements + other.n_statements |
253 nums.n_statements = self.n_statements + other.n_statements |
250 nums.n_excluded = self.n_excluded + other.n_excluded |
254 nums.n_excluded = self.n_excluded + other.n_excluded |
251 nums.n_missing = self.n_missing + other.n_missing |
255 nums.n_missing = self.n_missing + other.n_missing |
252 nums.n_branches = self.n_branches + other.n_branches |
256 nums.n_branches = self.n_branches + other.n_branches |
258 ) |
262 ) |
259 return nums |
263 return nums |
260 |
264 |
261 def __radd__(self, other): |
265 def __radd__(self, other): |
262 # Implementing 0+Numbers allows us to sum() a list of Numbers. |
266 # Implementing 0+Numbers allows us to sum() a list of Numbers. |
263 if other == 0: |
267 assert other == 0 # we only ever call it this way. |
264 return self |
268 return self |
265 return NotImplemented # pragma: not covered (we never call it this way) |
|
266 |
269 |
267 |
270 |
268 def _line_ranges(statements, lines): |
271 def _line_ranges(statements, lines): |
269 """Produce a list of ranges for `format_lines`.""" |
272 """Produce a list of ranges for `format_lines`.""" |
270 statements = sorted(statements) |
273 statements = sorted(statements) |
331 Returns True if the total should fail. |
334 Returns True if the total should fail. |
332 |
335 |
333 """ |
336 """ |
334 # We can never achieve higher than 100% coverage, or less than zero. |
337 # We can never achieve higher than 100% coverage, or less than zero. |
335 if not (0 <= fail_under <= 100.0): |
338 if not (0 <= fail_under <= 100.0): |
336 msg = "fail_under={} is invalid. Must be between 0 and 100.".format(fail_under) |
339 msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100." |
337 raise CoverageException(msg) |
340 raise CoverageException(msg) |
338 |
341 |
339 # Special case for fail_under=100, it must really be 100. |
342 # Special case for fail_under=100, it must really be 100. |
340 if fail_under == 100.0 and total != 100.0: |
343 if fail_under == 100.0 and total != 100.0: |
341 return True |
344 return True |