src/eric7/DebugClients/Python/coverage/results.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9099
0e511e0e94a3
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3
4 """Results of coverage measurement."""
5
6 import collections
7
8 from coverage.debug import SimpleReprMixin
9 from coverage.exceptions import ConfigError
10 from coverage.misc import contract, nice_pair
11
12
13 class Analysis:
14 """The results of analyzing a FileReporter."""
15
16 def __init__(self, data, precision, file_reporter, file_mapper):
17 self.data = data
18 self.file_reporter = file_reporter
19 self.filename = file_mapper(self.file_reporter.filename)
20 self.statements = self.file_reporter.lines()
21 self.excluded = self.file_reporter.excluded_lines()
22
23 # Identify missing statements.
24 executed = self.data.lines(self.filename) or []
25 executed = self.file_reporter.translate_lines(executed)
26 self.executed = executed
27 self.missing = self.statements - self.executed
28
29 if self.data.has_arcs():
30 self._arc_possibilities = sorted(self.file_reporter.arcs())
31 self.exit_counts = self.file_reporter.exit_counts()
32 self.no_branch = self.file_reporter.no_branch_lines()
33 n_branches = self._total_branches()
34 mba = self.missing_branch_arcs()
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 mba.items())
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 precision=precision,
45 n_files=1,
46 n_statements=len(self.statements),
47 n_excluded=len(self.excluded),
48 n_missing=len(self.missing),
49 n_branches=n_branches,
50 n_partial_branches=n_partial_branches,
51 n_missing_branches=n_missing_branches,
52 )
53
54 def missing_formatted(self, branches=False):
55 """The missing line numbers, formatted nicely.
56
57 Returns a string like "1-2, 5-11, 13-14".
58
59 If `branches` is true, includes the missing branch arcs also.
60
61 """
62 if branches and self.has_arcs():
63 arcs = self.missing_branch_arcs().items()
64 else:
65 arcs = None
66
67 return format_lines(self.statements, self.missing, arcs=arcs)
68
69 def has_arcs(self):
70 """Were arcs measured in this result?"""
71 return self.data.has_arcs()
72
73 @contract(returns='list(tuple(int, int))')
74 def arc_possibilities(self):
75 """Returns a sorted list of the arcs in the code."""
76 return self._arc_possibilities
77
78 @contract(returns='list(tuple(int, int))')
79 def arcs_executed(self):
80 """Returns a sorted list of the arcs actually executed in the code."""
81 executed = self.data.arcs(self.filename) or []
82 executed = self.file_reporter.translate_arcs(executed)
83 return sorted(executed)
84
85 @contract(returns='list(tuple(int, int))')
86 def arcs_missing(self):
87 """Returns a sorted list of the unexecuted arcs in the code."""
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 and p[1] not in self.excluded
95 )
96 return sorted(missing)
97
98 @contract(returns='list(tuple(int, int))')
99 def arcs_unpredicted(self):
100 """Returns a sorted list of the executed arcs missing from the code."""
101 possible = self.arc_possibilities()
102 executed = self.arcs_executed()
103 # Exclude arcs here which connect a line to itself. They can occur
104 # in executed data in some cases. This is where they can cause
105 # trouble, and here is where it's the least burden to remove them.
106 # Also, generators can somehow cause arcs from "enter" to "exit", so
107 # make sure we have at least one positive value.
108 unpredicted = (
109 e for e in executed
110 if e not in possible
111 and e[0] != e[1]
112 and (e[0] > 0 or e[1] > 0)
113 )
114 return sorted(unpredicted)
115
116 def _branch_lines(self):
117 """Returns a list of line numbers that have more than one exit."""
118 return [l1 for l1,count in self.exit_counts.items() if count > 1]
119
120 def _total_branches(self):
121 """How many total branches are there?"""
122 return sum(count for count in self.exit_counts.values() if count > 1)
123
124 @contract(returns='dict(int: list(int))')
125 def missing_branch_arcs(self):
126 """Return arcs that weren't executed from branch lines.
127
128 Returns {l1:[l2a,l2b,...], ...}
129
130 """
131 missing = self.arcs_missing()
132 branch_lines = set(self._branch_lines())
133 mba = collections.defaultdict(list)
134 for l1, l2 in missing:
135 if l1 in branch_lines:
136 mba[l1].append(l2)
137 return mba
138
139 @contract(returns='dict(int: list(int))')
140 def executed_branch_arcs(self):
141 """Return arcs that were executed from branch lines.
142
143 Returns {l1:[l2a,l2b,...], ...}
144
145 """
146 executed = self.arcs_executed()
147 branch_lines = set(self._branch_lines())
148 eba = collections.defaultdict(list)
149 for l1, l2 in executed:
150 if l1 in branch_lines:
151 eba[l1].append(l2)
152 return eba
153
154 @contract(returns='dict(int: tuple(int, int))')
155 def branch_stats(self):
156 """Get stats about branches.
157
158 Returns a dict mapping line numbers to a tuple:
159 (total_exits, taken_exits).
160 """
161
162 missing_arcs = self.missing_branch_arcs()
163 stats = {}
164 for lnum in self._branch_lines():
165 exits = self.exit_counts[lnum]
166 missing = len(missing_arcs[lnum])
167 stats[lnum] = (exits, exits - missing)
168 return stats
169
170
171 class Numbers(SimpleReprMixin):
172 """The numerical results of measuring coverage.
173
174 This holds the basic statistics from `Analysis`, and is used to roll
175 up statistics across files.
176
177 """
178
179 def __init__(self,
180 precision=0,
181 n_files=0, n_statements=0, n_excluded=0, n_missing=0,
182 n_branches=0, n_partial_branches=0, n_missing_branches=0
183 ):
184 assert 0 <= precision < 10
185 self._precision = precision
186 self._near0 = 1.0 / 10**precision
187 self._near100 = 100.0 - self._near0
188 self.n_files = n_files
189 self.n_statements = n_statements
190 self.n_excluded = n_excluded
191 self.n_missing = n_missing
192 self.n_branches = n_branches
193 self.n_partial_branches = n_partial_branches
194 self.n_missing_branches = n_missing_branches
195
196 def init_args(self):
197 """Return a list for __init__(*args) to recreate this object."""
198 return [
199 self._precision,
200 self.n_files, self.n_statements, self.n_excluded, self.n_missing,
201 self.n_branches, self.n_partial_branches, self.n_missing_branches,
202 ]
203
204 @property
205 def n_executed(self):
206 """Returns the number of executed statements."""
207 return self.n_statements - self.n_missing
208
209 @property
210 def n_executed_branches(self):
211 """Returns the number of executed branches."""
212 return self.n_branches - self.n_missing_branches
213
214 @property
215 def pc_covered(self):
216 """Returns a single percentage value for coverage."""
217 if self.n_statements > 0:
218 numerator, denominator = self.ratio_covered
219 pc_cov = (100.0 * numerator) / denominator
220 else:
221 pc_cov = 100.0
222 return pc_cov
223
224 @property
225 def pc_covered_str(self):
226 """Returns the percent covered, as a string, without a percent sign.
227
228 Note that "0" is only returned when the value is truly zero, and "100"
229 is only returned when the value is truly 100. Rounding can never
230 result in either "0" or "100".
231
232 """
233 return self.display_covered(self.pc_covered)
234
235 def display_covered(self, pc):
236 """Return a displayable total percentage, as a string.
237
238 Note that "0" is only returned when the value is truly zero, and "100"
239 is only returned when the value is truly 100. Rounding can never
240 result in either "0" or "100".
241
242 """
243 if 0 < pc < self._near0:
244 pc = self._near0
245 elif self._near100 < pc < 100:
246 pc = self._near100
247 else:
248 pc = round(pc, self._precision)
249 return "%.*f" % (self._precision, pc)
250
251 def pc_str_width(self):
252 """How many characters wide can pc_covered_str be?"""
253 width = 3 # "100"
254 if self._precision > 0:
255 width += 1 + self._precision
256 return width
257
258 @property
259 def ratio_covered(self):
260 """Return a numerator and denominator for the coverage ratio."""
261 numerator = self.n_executed + self.n_executed_branches
262 denominator = self.n_statements + self.n_branches
263 return numerator, denominator
264
265 def __add__(self, other):
266 nums = Numbers(precision=self._precision)
267 nums.n_files = self.n_files + other.n_files
268 nums.n_statements = self.n_statements + other.n_statements
269 nums.n_excluded = self.n_excluded + other.n_excluded
270 nums.n_missing = self.n_missing + other.n_missing
271 nums.n_branches = self.n_branches + other.n_branches
272 nums.n_partial_branches = (
273 self.n_partial_branches + other.n_partial_branches
274 )
275 nums.n_missing_branches = (
276 self.n_missing_branches + other.n_missing_branches
277 )
278 return nums
279
280 def __radd__(self, other):
281 # Implementing 0+Numbers allows us to sum() a list of Numbers.
282 assert other == 0 # we only ever call it this way.
283 return self
284
285
286 def _line_ranges(statements, lines):
287 """Produce a list of ranges for `format_lines`."""
288 statements = sorted(statements)
289 lines = sorted(lines)
290
291 pairs = []
292 start = None
293 lidx = 0
294 for stmt in statements:
295 if lidx >= len(lines):
296 break
297 if stmt == lines[lidx]:
298 lidx += 1
299 if not start:
300 start = stmt
301 end = stmt
302 elif start:
303 pairs.append((start, end))
304 start = None
305 if start:
306 pairs.append((start, end))
307 return pairs
308
309
310 def format_lines(statements, lines, arcs=None):
311 """Nicely format a list of line numbers.
312
313 Format a list of line numbers for printing by coalescing groups of lines as
314 long as the lines represent consecutive statements. This will coalesce
315 even if there are gaps between statements.
316
317 For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and
318 `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14".
319
320 Both `lines` and `statements` can be any iterable. All of the elements of
321 `lines` must be in `statements`, and all of the values must be positive
322 integers.
323
324 If `arcs` is provided, they are (start,[end,end,end]) pairs that will be
325 included in the output as long as start isn't in `lines`.
326
327 """
328 line_items = [(pair[0], nice_pair(pair)) for pair in _line_ranges(statements, lines)]
329 if arcs:
330 line_exits = sorted(arcs)
331 for line, exits in line_exits:
332 for ex in sorted(exits):
333 if line not in lines and ex not in lines:
334 dest = (ex if ex > 0 else "exit")
335 line_items.append((line, f"{line}->{dest}"))
336
337 ret = ', '.join(t[-1] for t in sorted(line_items))
338 return ret
339
340
341 @contract(total='number', fail_under='number', precision=int, returns=bool)
342 def should_fail_under(total, fail_under, precision):
343 """Determine if a total should fail due to fail-under.
344
345 `total` is a float, the coverage measurement total. `fail_under` is the
346 fail_under setting to compare with. `precision` is the number of digits
347 to consider after the decimal point.
348
349 Returns True if the total should fail.
350
351 """
352 # We can never achieve higher than 100% coverage, or less than zero.
353 if not (0 <= fail_under <= 100.0):
354 msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100."
355 raise ConfigError(msg)
356
357 # Special case for fail_under=100, it must really be 100.
358 if fail_under == 100.0 and total != 100.0:
359 return True
360
361 return round(total, precision) < fail_under

eric ide

mercurial