eric6/DebugClients/Python/coverage/results.py

changeset 6942
2602857055c5
parent 6219
d6c795b5ce33
child 7427
362cd1b6f81a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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
6 import collections
7
8 from coverage.backward import iitems
9 from coverage.misc import contract, format_lines, SimpleRepr
10
11
12 class 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(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))
35 else:
36 self._arc_possibilities = []
37 self.exit_counts = {}
38 self.no_branch = set()
39 n_branches = n_partial_branches = n_missing_branches = 0
40
41 self.numbers = Numbers(
42 n_files=1,
43 n_statements=len(self.statements),
44 n_excluded=len(self.excluded),
45 n_missing=len(self.missing),
46 n_branches=n_branches,
47 n_partial_branches=n_partial_branches,
48 n_missing_branches=n_missing_branches,
49 )
50
51 def missing_formatted(self):
52 """The missing line numbers, formatted nicely.
53
54 Returns a string like "1-2, 5-11, 13-14".
55
56 """
57 return format_lines(self.statements, self.missing)
58
59 def has_arcs(self):
60 """Were arcs measured in this result?"""
61 return self.data.has_arcs()
62
63 def arc_possibilities(self):
64 """Returns a sorted list of the arcs in the code."""
65 return self._arc_possibilities
66
67 def arcs_executed(self):
68 """Returns a sorted list of the arcs actually executed in the code."""
69 executed = self.data.arcs(self.filename) or []
70 executed = self.file_reporter.translate_arcs(executed)
71 return sorted(executed)
72
73 def arcs_missing(self):
74 """Returns a sorted list of the arcs in the code not executed."""
75 possible = self.arc_possibilities()
76 executed = self.arcs_executed()
77 missing = (
78 p for p in possible
79 if p not in executed
80 and p[0] not in self.no_branch
81 )
82 return sorted(missing)
83
84 def arcs_missing_formatted(self):
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):
103 """Returns a sorted list of the executed arcs missing from the code."""
104 possible = self.arc_possibilities()
105 executed = self.arcs_executed()
106 # Exclude arcs here which connect a line to itself. They can occur
107 # in executed data in some cases. This is where they can cause
108 # trouble, and here is where it's the least burden to remove them.
109 # Also, generators can somehow cause arcs from "enter" to "exit", so
110 # make sure we have at least one positive value.
111 unpredicted = (
112 e for e in executed
113 if e not in possible
114 and e[0] != e[1]
115 and (e[0] > 0 or e[1] > 0)
116 )
117 return sorted(unpredicted)
118
119 def branch_lines(self):
120 """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]
122
123 def total_branches(self):
124 """How many total branches are there?"""
125 return sum(count for count in self.exit_counts.values() if count > 1)
126
127 def missing_branch_arcs(self):
128 """Return arcs that weren't executed from branch lines.
129
130 Returns {l1:[l2a,l2b,...], ...}
131
132 """
133 missing = self.arcs_missing()
134 branch_lines = set(self.branch_lines())
135 mba = collections.defaultdict(list)
136 for l1, l2 in missing:
137 if l1 in branch_lines:
138 mba[l1].append(l2)
139 return mba
140
141 def branch_stats(self):
142 """Get stats about branches.
143
144 Returns a dict mapping line numbers to a tuple:
145 (total_exits, taken_exits).
146 """
147
148 missing_arcs = self.missing_branch_arcs()
149 stats = {}
150 for lnum in self.branch_lines():
151 exits = self.exit_counts[lnum]
152 try:
153 missing = len(missing_arcs[lnum])
154 except KeyError:
155 missing = 0
156 stats[lnum] = (exits, exits - missing)
157 return stats
158
159
160 class Numbers(SimpleRepr):
161 """The numerical results of measuring coverage.
162
163 This holds the basic statistics from `Analysis`, and is used to roll
164 up statistics across files.
165
166 """
167 # A global to determine the precision on coverage percentages, the number
168 # of decimal places.
169 _precision = 0
170 _near0 = 1.0 # These will change when _precision is changed.
171 _near100 = 99.0
172
173 def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0,
174 n_branches=0, n_partial_branches=0, n_missing_branches=0
175 ):
176 self.n_files = n_files
177 self.n_statements = n_statements
178 self.n_excluded = n_excluded
179 self.n_missing = n_missing
180 self.n_branches = n_branches
181 self.n_partial_branches = n_partial_branches
182 self.n_missing_branches = n_missing_branches
183
184 def init_args(self):
185 """Return a list for __init__(*args) to recreate this object."""
186 return [
187 self.n_files, self.n_statements, self.n_excluded, self.n_missing,
188 self.n_branches, self.n_partial_branches, self.n_missing_branches,
189 ]
190
191 @classmethod
192 def set_precision(cls, precision):
193 """Set the number of decimal places used to report percentages."""
194 assert 0 <= precision < 10
195 cls._precision = precision
196 cls._near0 = 1.0 / 10**precision
197 cls._near100 = 100.0 - cls._near0
198
199 @property
200 def n_executed(self):
201 """Returns the number of executed statements."""
202 return self.n_statements - self.n_missing
203
204 @property
205 def n_executed_branches(self):
206 """Returns the number of executed branches."""
207 return self.n_branches - self.n_missing_branches
208
209 @property
210 def pc_covered(self):
211 """Returns a single percentage value for coverage."""
212 if self.n_statements > 0:
213 numerator, denominator = self.ratio_covered
214 pc_cov = (100.0 * numerator) / denominator
215 else:
216 pc_cov = 100.0
217 return pc_cov
218
219 @property
220 def pc_covered_str(self):
221 """Returns the percent covered, as a string, without a percent sign.
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 """
228 pc = self.pc_covered
229 if 0 < pc < self._near0:
230 pc = self._near0
231 elif self._near100 < pc < 100:
232 pc = self._near100
233 else:
234 pc = round(pc, self._precision)
235 return "%.*f" % (self._precision, pc)
236
237 @classmethod
238 def pc_str_width(cls):
239 """How many characters wide can pc_covered_str be?"""
240 width = 3 # "100"
241 if cls._precision > 0:
242 width += 1 + cls._precision
243 return width
244
245 @property
246 def ratio_covered(self):
247 """Return a numerator and denominator for the coverage ratio."""
248 numerator = self.n_executed + self.n_executed_branches
249 denominator = self.n_statements + self.n_branches
250 return numerator, denominator
251
252 def __add__(self, other):
253 nums = Numbers()
254 nums.n_files = self.n_files + other.n_files
255 nums.n_statements = self.n_statements + other.n_statements
256 nums.n_excluded = self.n_excluded + other.n_excluded
257 nums.n_missing = self.n_missing + other.n_missing
258 nums.n_branches = self.n_branches + other.n_branches
259 nums.n_partial_branches = (
260 self.n_partial_branches + other.n_partial_branches
261 )
262 nums.n_missing_branches = (
263 self.n_missing_branches + other.n_missing_branches
264 )
265 return nums
266
267 def __radd__(self, other):
268 # Implementing 0+Numbers allows us to sum() a list of Numbers.
269 if other == 0:
270 return self
271 return NotImplemented
272
273
274 @contract(total='number', fail_under='number', precision=int, returns=bool)
275 def should_fail_under(total, fail_under, precision):
276 """Determine if a total should fail due to fail-under.
277
278 `total` is a float, the coverage measurement total. `fail_under` is the
279 fail_under setting to compare with. `precision` is the number of digits
280 to consider after the decimal point.
281
282 Returns True if the total should fail.
283
284 """
285 # Special case for fail_under=100, it must really be 100.
286 if fail_under == 100.0 and total != 100.0:
287 return True
288
289 return round(total, precision) < fail_under

eric ide

mercurial