1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
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 |
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
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 |
8 from coverage.backward import iitems |
9 from coverage.misc import contract, format_lines, SimpleRepr |
9 from coverage.debug import SimpleReprMixin |
|
10 from coverage.misc import contract, CoverageException, nice_pair |
10 |
11 |
11 |
12 |
12 class Analysis(object): |
13 class Analysis(object): |
13 """The results of analyzing a FileReporter.""" |
14 """The results of analyzing a FileReporter.""" |
14 |
15 |
15 def __init__(self, data, file_reporter): |
16 def __init__(self, data, file_reporter, file_mapper): |
16 self.data = data |
17 self.data = data |
17 self.file_reporter = file_reporter |
18 self.file_reporter = file_reporter |
18 self.filename = self.file_reporter.filename |
19 self.filename = file_mapper(self.file_reporter.filename) |
19 self.statements = self.file_reporter.lines() |
20 self.statements = self.file_reporter.lines() |
20 self.excluded = self.file_reporter.excluded_lines() |
21 self.excluded = self.file_reporter.excluded_lines() |
21 |
22 |
22 # Identify missing statements. |
23 # Identify missing statements. |
23 executed = self.data.lines(self.filename) or [] |
24 executed = self.data.lines(self.filename) or [] |
24 executed = self.file_reporter.translate_lines(executed) |
25 executed = self.file_reporter.translate_lines(executed) |
25 self.missing = self.statements - executed |
26 self.executed = executed |
|
27 self.missing = self.statements - self.executed |
26 |
28 |
27 if self.data.has_arcs(): |
29 if self.data.has_arcs(): |
28 self._arc_possibilities = sorted(self.file_reporter.arcs()) |
30 self._arc_possibilities = sorted(self.file_reporter.arcs()) |
29 self.exit_counts = self.file_reporter.exit_counts() |
31 self.exit_counts = self.file_reporter.exit_counts() |
30 self.no_branch = self.file_reporter.no_branch_lines() |
32 self.no_branch = self.file_reporter.no_branch_lines() |
31 n_branches = self.total_branches() |
33 n_branches = self._total_branches() |
32 mba = self.missing_branch_arcs() |
34 mba = self.missing_branch_arcs() |
33 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 iitems(mba) if k not in self.missing) |
34 n_missing_branches = sum(len(v) for k,v in iitems(mba)) |
36 n_missing_branches = sum(len(v) for k,v in iitems(mba)) |
35 else: |
37 else: |
36 self._arc_possibilities = [] |
38 self._arc_possibilities = [] |
46 n_branches=n_branches, |
48 n_branches=n_branches, |
47 n_partial_branches=n_partial_branches, |
49 n_partial_branches=n_partial_branches, |
48 n_missing_branches=n_missing_branches, |
50 n_missing_branches=n_missing_branches, |
49 ) |
51 ) |
50 |
52 |
51 def missing_formatted(self): |
53 def missing_formatted(self, branches=False): |
52 """The missing line numbers, formatted nicely. |
54 """The missing line numbers, formatted nicely. |
53 |
55 |
54 Returns a string like "1-2, 5-11, 13-14". |
56 Returns a string like "1-2, 5-11, 13-14". |
55 |
57 |
|
58 If `branches` is true, includes the missing branch arcs also. |
|
59 |
56 """ |
60 """ |
57 return format_lines(self.statements, self.missing) |
61 if branches and self.has_arcs(): |
|
62 arcs = iitems(self.missing_branch_arcs()) |
|
63 else: |
|
64 arcs = None |
|
65 |
|
66 return format_lines(self.statements, self.missing, arcs=arcs) |
58 |
67 |
59 def has_arcs(self): |
68 def has_arcs(self): |
60 """Were arcs measured in this result?""" |
69 """Were arcs measured in this result?""" |
61 return self.data.has_arcs() |
70 return self.data.has_arcs() |
62 |
71 |
|
72 @contract(returns='list(tuple(int, int))') |
63 def arc_possibilities(self): |
73 def arc_possibilities(self): |
64 """Returns a sorted list of the arcs in the code.""" |
74 """Returns a sorted list of the arcs in the code.""" |
65 return self._arc_possibilities |
75 return self._arc_possibilities |
66 |
76 |
|
77 @contract(returns='list(tuple(int, int))') |
67 def arcs_executed(self): |
78 def arcs_executed(self): |
68 """Returns a sorted list of the arcs actually executed in the code.""" |
79 """Returns a sorted list of the arcs actually executed in the code.""" |
69 executed = self.data.arcs(self.filename) or [] |
80 executed = self.data.arcs(self.filename) or [] |
70 executed = self.file_reporter.translate_arcs(executed) |
81 executed = self.file_reporter.translate_arcs(executed) |
71 return sorted(executed) |
82 return sorted(executed) |
72 |
83 |
|
84 @contract(returns='list(tuple(int, int))') |
73 def arcs_missing(self): |
85 def arcs_missing(self): |
74 """Returns a sorted list of the arcs in the code not executed.""" |
86 """Returns a sorted list of the arcs in the code not executed.""" |
75 possible = self.arc_possibilities() |
87 possible = self.arc_possibilities() |
76 executed = self.arcs_executed() |
88 executed = self.arcs_executed() |
77 missing = ( |
89 missing = ( |
79 if p not in executed |
91 if p not in executed |
80 and p[0] not in self.no_branch |
92 and p[0] not in self.no_branch |
81 ) |
93 ) |
82 return sorted(missing) |
94 return sorted(missing) |
83 |
95 |
84 def arcs_missing_formatted(self): |
96 @contract(returns='list(tuple(int, int))') |
85 """The missing branch arcs, formatted nicely. |
|
86 |
|
87 Returns a string like "1->2, 1->3, 16->20". Omits any mention of |
|
88 branches from missing lines, so if line 17 is missing, then 17->18 |
|
89 won't be included. |
|
90 |
|
91 """ |
|
92 arcs = self.missing_branch_arcs() |
|
93 missing = self.missing |
|
94 line_exits = sorted(iitems(arcs)) |
|
95 pairs = [] |
|
96 for line, exits in line_exits: |
|
97 for ex in sorted(exits): |
|
98 if line not in missing: |
|
99 pairs.append("%d->%s" % (line, (ex if ex > 0 else "exit"))) |
|
100 return ', '.join(pairs) |
|
101 |
|
102 def arcs_unpredicted(self): |
97 def arcs_unpredicted(self): |
103 """Returns a sorted list of the executed arcs missing from the code.""" |
98 """Returns a sorted list of the executed arcs missing from the code.""" |
104 possible = self.arc_possibilities() |
99 possible = self.arc_possibilities() |
105 executed = self.arcs_executed() |
100 executed = self.arcs_executed() |
106 # Exclude arcs here which connect a line to itself. They can occur |
101 # Exclude arcs here which connect a line to itself. They can occur |
114 and e[0] != e[1] |
109 and e[0] != e[1] |
115 and (e[0] > 0 or e[1] > 0) |
110 and (e[0] > 0 or e[1] > 0) |
116 ) |
111 ) |
117 return sorted(unpredicted) |
112 return sorted(unpredicted) |
118 |
113 |
119 def branch_lines(self): |
114 def _branch_lines(self): |
120 """Returns a list of line numbers that have more than one exit.""" |
115 """Returns a list of line numbers that have more than one exit.""" |
121 return [l1 for l1,count in iitems(self.exit_counts) if count > 1] |
116 return [l1 for l1,count in iitems(self.exit_counts) if count > 1] |
122 |
117 |
123 def total_branches(self): |
118 def _total_branches(self): |
124 """How many total branches are there?""" |
119 """How many total branches are there?""" |
125 return sum(count for count in self.exit_counts.values() if count > 1) |
120 return sum(count for count in self.exit_counts.values() if count > 1) |
126 |
121 |
|
122 @contract(returns='dict(int: list(int))') |
127 def missing_branch_arcs(self): |
123 def missing_branch_arcs(self): |
128 """Return arcs that weren't executed from branch lines. |
124 """Return arcs that weren't executed from branch lines. |
129 |
125 |
130 Returns {l1:[l2a,l2b,...], ...} |
126 Returns {l1:[l2a,l2b,...], ...} |
131 |
127 |
132 """ |
128 """ |
133 missing = self.arcs_missing() |
129 missing = self.arcs_missing() |
134 branch_lines = set(self.branch_lines()) |
130 branch_lines = set(self._branch_lines()) |
135 mba = collections.defaultdict(list) |
131 mba = collections.defaultdict(list) |
136 for l1, l2 in missing: |
132 for l1, l2 in missing: |
137 if l1 in branch_lines: |
133 if l1 in branch_lines: |
138 mba[l1].append(l2) |
134 mba[l1].append(l2) |
139 return mba |
135 return mba |
140 |
136 |
|
137 @contract(returns='dict(int: tuple(int, int))') |
141 def branch_stats(self): |
138 def branch_stats(self): |
142 """Get stats about branches. |
139 """Get stats about branches. |
143 |
140 |
144 Returns a dict mapping line numbers to a tuple: |
141 Returns a dict mapping line numbers to a tuple: |
145 (total_exits, taken_exits). |
142 (total_exits, taken_exits). |
146 """ |
143 """ |
147 |
144 |
148 missing_arcs = self.missing_branch_arcs() |
145 missing_arcs = self.missing_branch_arcs() |
149 stats = {} |
146 stats = {} |
150 for lnum in self.branch_lines(): |
147 for lnum in self._branch_lines(): |
151 exits = self.exit_counts[lnum] |
148 exits = self.exit_counts[lnum] |
152 try: |
149 try: |
153 missing = len(missing_arcs[lnum]) |
150 missing = len(missing_arcs[lnum]) |
154 except KeyError: |
151 except KeyError: |
155 missing = 0 |
152 missing = 0 |
156 stats[lnum] = (exits, exits - missing) |
153 stats[lnum] = (exits, exits - missing) |
157 return stats |
154 return stats |
158 |
155 |
159 |
156 |
160 class Numbers(SimpleRepr): |
157 class Numbers(SimpleReprMixin): |
161 """The numerical results of measuring coverage. |
158 """The numerical results of measuring coverage. |
162 |
159 |
163 This holds the basic statistics from `Analysis`, and is used to roll |
160 This holds the basic statistics from `Analysis`, and is used to roll |
164 up statistics across files. |
161 up statistics across files. |
165 |
162 |
269 if other == 0: |
266 if other == 0: |
270 return self |
267 return self |
271 return NotImplemented |
268 return NotImplemented |
272 |
269 |
273 |
270 |
|
271 def _line_ranges(statements, lines): |
|
272 """Produce a list of ranges for `format_lines`.""" |
|
273 statements = sorted(statements) |
|
274 lines = sorted(lines) |
|
275 |
|
276 pairs = [] |
|
277 start = None |
|
278 lidx = 0 |
|
279 for stmt in statements: |
|
280 if lidx >= len(lines): |
|
281 break |
|
282 if stmt == lines[lidx]: |
|
283 lidx += 1 |
|
284 if not start: |
|
285 start = stmt |
|
286 end = stmt |
|
287 elif start: |
|
288 pairs.append((start, end)) |
|
289 start = None |
|
290 if start: |
|
291 pairs.append((start, end)) |
|
292 return pairs |
|
293 |
|
294 |
|
295 def format_lines(statements, lines, arcs=None): |
|
296 """Nicely format a list of line numbers. |
|
297 |
|
298 Format a list of line numbers for printing by coalescing groups of lines as |
|
299 long as the lines represent consecutive statements. This will coalesce |
|
300 even if there are gaps between statements. |
|
301 |
|
302 For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and |
|
303 `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14". |
|
304 |
|
305 Both `lines` and `statements` can be any iterable. All of the elements of |
|
306 `lines` must be in `statements`, and all of the values must be positive |
|
307 integers. |
|
308 |
|
309 If `arcs` is provided, they are (start,[end,end,end]) pairs that will be |
|
310 included in the output as long as start isn't in `lines`. |
|
311 |
|
312 """ |
|
313 line_items = [(pair[0], nice_pair(pair)) for pair in _line_ranges(statements, lines)] |
|
314 if arcs: |
|
315 line_exits = sorted(arcs) |
|
316 for line, exits in line_exits: |
|
317 for ex in sorted(exits): |
|
318 if line not in lines: |
|
319 dest = (ex if ex > 0 else "exit") |
|
320 line_items.append((line, "%d->%s" % (line, dest))) |
|
321 |
|
322 ret = ', '.join(t[-1] for t in sorted(line_items)) |
|
323 return ret |
|
324 |
|
325 |
274 @contract(total='number', fail_under='number', precision=int, returns=bool) |
326 @contract(total='number', fail_under='number', precision=int, returns=bool) |
275 def should_fail_under(total, fail_under, precision): |
327 def should_fail_under(total, fail_under, precision): |
276 """Determine if a total should fail due to fail-under. |
328 """Determine if a total should fail due to fail-under. |
277 |
329 |
278 `total` is a float, the coverage measurement total. `fail_under` is the |
330 `total` is a float, the coverage measurement total. `fail_under` is the |
280 to consider after the decimal point. |
332 to consider after the decimal point. |
281 |
333 |
282 Returns True if the total should fail. |
334 Returns True if the total should fail. |
283 |
335 |
284 """ |
336 """ |
|
337 # We can never achieve higher than 100% coverage, or less than zero. |
|
338 if not (0 <= fail_under <= 100.0): |
|
339 msg = "fail_under={} is invalid. Must be between 0 and 100.".format(fail_under) |
|
340 raise CoverageException(msg) |
|
341 |
285 # Special case for fail_under=100, it must really be 100. |
342 # Special case for fail_under=100, it must really be 100. |
286 if fail_under == 100.0 and total != 100.0: |
343 if fail_under == 100.0 and total != 100.0: |
287 return True |
344 return True |
288 |
345 |
289 return round(total, precision) < fail_under |
346 return round(total, precision) < fail_under |