|
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 |
1 """Results of coverage measurement.""" |
4 """Results of coverage measurement.""" |
2 |
5 |
3 import os |
6 import collections |
4 |
7 |
5 from .backward import iitems, set, sorted # pylint: disable=W0622 |
8 from coverage.backward import iitems |
6 from .misc import format_lines, join_regex, NoSource |
9 from coverage.misc import format_lines |
7 from .parser import CodeParser |
|
8 |
10 |
9 |
11 |
10 class Analysis(object): |
12 class Analysis(object): |
11 """The results of analyzing a code unit.""" |
13 """The results of analyzing a FileReporter.""" |
12 |
14 |
13 def __init__(self, cov, code_unit): |
15 def __init__(self, data, file_reporter): |
14 self.coverage = cov |
16 self.data = data |
15 self.code_unit = code_unit |
17 self.file_reporter = file_reporter |
16 |
18 self.filename = self.file_reporter.filename |
17 self.filename = self.code_unit.filename |
19 self.statements = self.file_reporter.lines() |
18 actual_filename, source = self.find_source(self.filename) |
20 self.excluded = self.file_reporter.excluded_lines() |
19 |
|
20 self.parser = CodeParser( |
|
21 text=source, filename=actual_filename, |
|
22 exclude=self.coverage._exclude_regex('exclude') |
|
23 ) |
|
24 self.statements, self.excluded = self.parser.parse_source() |
|
25 |
21 |
26 # Identify missing statements. |
22 # Identify missing statements. |
27 executed = self.coverage.data.executed_lines(self.filename) |
23 executed = self.data.lines(self.filename) or [] |
28 exec1 = self.parser.first_lines(executed) |
24 executed = self.file_reporter.translate_lines(executed) |
29 self.missing = self.statements - exec1 |
25 self.missing = self.statements - executed |
30 |
26 |
31 if self.coverage.data.has_arcs(): |
27 if self.data.has_arcs(): |
32 self.no_branch = self.parser.lines_matching( |
28 self._arc_possibilities = sorted(self.file_reporter.arcs()) |
33 join_regex(self.coverage.config.partial_list), |
29 self.exit_counts = self.file_reporter.exit_counts() |
34 join_regex(self.coverage.config.partial_always_list) |
30 self.no_branch = self.file_reporter.no_branch_lines() |
35 ) |
|
36 n_branches = self.total_branches() |
31 n_branches = self.total_branches() |
37 mba = self.missing_branch_arcs() |
32 mba = self.missing_branch_arcs() |
38 n_partial_branches = sum( |
33 n_partial_branches = sum( |
39 [len(v) for k,v in iitems(mba) if k not in self.missing] |
34 len(v) for k,v in iitems(mba) if k not in self.missing |
40 ) |
35 ) |
41 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)) |
42 else: |
37 else: |
|
38 self._arc_possibilities = [] |
|
39 self.exit_counts = {} |
|
40 self.no_branch = set() |
43 n_branches = n_partial_branches = n_missing_branches = 0 |
41 n_branches = n_partial_branches = n_missing_branches = 0 |
44 self.no_branch = set() |
|
45 |
42 |
46 self.numbers = Numbers( |
43 self.numbers = Numbers( |
47 n_files=1, |
44 n_files=1, |
48 n_statements=len(self.statements), |
45 n_statements=len(self.statements), |
49 n_excluded=len(self.excluded), |
46 n_excluded=len(self.excluded), |
51 n_branches=n_branches, |
48 n_branches=n_branches, |
52 n_partial_branches=n_partial_branches, |
49 n_partial_branches=n_partial_branches, |
53 n_missing_branches=n_missing_branches, |
50 n_missing_branches=n_missing_branches, |
54 ) |
51 ) |
55 |
52 |
56 def find_source(self, filename): |
|
57 """Find the source for `filename`. |
|
58 |
|
59 Returns two values: the actual filename, and the source. |
|
60 |
|
61 The source returned depends on which of these cases holds: |
|
62 |
|
63 * The filename seems to be a non-source file: returns None |
|
64 |
|
65 * The filename is a source file, and actually exists: returns None. |
|
66 |
|
67 * The filename is a source file, and is in a zip file or egg: |
|
68 returns the source. |
|
69 |
|
70 * The filename is a source file, but couldn't be found: raises |
|
71 `NoSource`. |
|
72 |
|
73 """ |
|
74 source = None |
|
75 |
|
76 base, ext = os.path.splitext(filename) |
|
77 TRY_EXTS = { |
|
78 '.py': ['.py', '.pyw'], |
|
79 '.pyw': ['.pyw'], |
|
80 } |
|
81 try_exts = TRY_EXTS.get(ext) |
|
82 if not try_exts: |
|
83 return filename, None |
|
84 |
|
85 for try_ext in try_exts: |
|
86 try_filename = base + try_ext |
|
87 if os.path.exists(try_filename): |
|
88 return try_filename, None |
|
89 source = self.coverage.file_locator.get_zip_data(try_filename) |
|
90 if source: |
|
91 return try_filename, source |
|
92 raise NoSource("No source for code: '%s'" % filename) |
|
93 |
|
94 def missing_formatted(self): |
53 def missing_formatted(self): |
95 """The missing line numbers, formatted nicely. |
54 """The missing line numbers, formatted nicely. |
96 |
55 |
97 Returns a string like "1-2, 5-11, 13-14". |
56 Returns a string like "1-2, 5-11, 13-14". |
98 |
57 |
99 """ |
58 """ |
100 return format_lines(self.statements, self.missing) |
59 return format_lines(self.statements, self.missing) |
101 |
60 |
102 def has_arcs(self): |
61 def has_arcs(self): |
103 """Were arcs measured in this result?""" |
62 """Were arcs measured in this result?""" |
104 return self.coverage.data.has_arcs() |
63 return self.data.has_arcs() |
105 |
64 |
106 def arc_possibilities(self): |
65 def arc_possibilities(self): |
107 """Returns a sorted list of the arcs in the code.""" |
66 """Returns a sorted list of the arcs in the code.""" |
108 arcs = self.parser.arcs() |
67 return self._arc_possibilities |
109 return arcs |
|
110 |
68 |
111 def arcs_executed(self): |
69 def arcs_executed(self): |
112 """Returns a sorted list of the arcs actually executed in the code.""" |
70 """Returns a sorted list of the arcs actually executed in the code.""" |
113 executed = self.coverage.data.executed_arcs(self.filename) |
71 executed = self.data.arcs(self.filename) or [] |
114 m2fl = self.parser.first_line |
72 executed = self.file_reporter.translate_arcs(executed) |
115 executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed] |
|
116 return sorted(executed) |
73 return sorted(executed) |
117 |
74 |
118 def arcs_missing(self): |
75 def arcs_missing(self): |
119 """Returns a sorted list of the arcs in the code not executed.""" |
76 """Returns a sorted list of the arcs in the code not executed.""" |
120 possible = self.arc_possibilities() |
77 possible = self.arc_possibilities() |
121 executed = self.arcs_executed() |
78 executed = self.arcs_executed() |
122 missing = [ |
79 missing = ( |
123 p for p in possible |
80 p for p in possible |
124 if p not in executed |
81 if p not in executed |
125 and p[0] not in self.no_branch |
82 and p[0] not in self.no_branch |
126 ] |
83 ) |
127 return sorted(missing) |
84 return sorted(missing) |
|
85 |
|
86 def arcs_missing_formatted(self): |
|
87 """ The missing branch arcs, formatted nicely. |
|
88 |
|
89 Returns a string like "1->2, 1->3, 16->20". Omits any mention of |
|
90 branches from missing lines, so if line 17 is missing, then 17->18 |
|
91 won't be included. |
|
92 |
|
93 """ |
|
94 arcs = self.missing_branch_arcs() |
|
95 missing = self.missing |
|
96 line_exits = sorted(iitems(arcs)) |
|
97 pairs = [] |
|
98 for line, exits in line_exits: |
|
99 for ex in sorted(exits): |
|
100 if line not in missing: |
|
101 pairs.append('%d->%d' % (line, ex)) |
|
102 return ', '.join(pairs) |
128 |
103 |
129 def arcs_unpredicted(self): |
104 def arcs_unpredicted(self): |
130 """Returns a sorted list of the executed arcs missing from the code.""" |
105 """Returns a sorted list of the executed arcs missing from the code.""" |
131 possible = self.arc_possibilities() |
106 possible = self.arc_possibilities() |
132 executed = self.arcs_executed() |
107 executed = self.arcs_executed() |
133 # Exclude arcs here which connect a line to itself. They can occur |
108 # Exclude arcs here which connect a line to itself. They can occur |
134 # in executed data in some cases. This is where they can cause |
109 # in executed data in some cases. This is where they can cause |
135 # trouble, and here is where it's the least burden to remove them. |
110 # trouble, and here is where it's the least burden to remove them. |
136 unpredicted = [ |
111 # Also, generators can somehow cause arcs from "enter" to "exit", so |
|
112 # make sure we have at least one positive value. |
|
113 unpredicted = ( |
137 e for e in executed |
114 e for e in executed |
138 if e not in possible |
115 if e not in possible |
139 and e[0] != e[1] |
116 and e[0] != e[1] |
140 ] |
117 and (e[0] > 0 or e[1] > 0) |
|
118 ) |
141 return sorted(unpredicted) |
119 return sorted(unpredicted) |
142 |
120 |
143 def branch_lines(self): |
121 def branch_lines(self): |
144 """Returns a list of line numbers that have more than one exit.""" |
122 """Returns a list of line numbers that have more than one exit.""" |
145 exit_counts = self.parser.exit_counts() |
123 return [l1 for l1,count in iitems(self.exit_counts) if count > 1] |
146 return [l1 for l1,count in iitems(exit_counts) if count > 1] |
|
147 |
124 |
148 def total_branches(self): |
125 def total_branches(self): |
149 """How many total branches are there?""" |
126 """How many total branches are there?""" |
150 exit_counts = self.parser.exit_counts() |
127 return sum(count for count in self.exit_counts.values() if count > 1) |
151 return sum([count for count in exit_counts.values() if count > 1]) |
|
152 |
128 |
153 def missing_branch_arcs(self): |
129 def missing_branch_arcs(self): |
154 """Return arcs that weren't executed from branch lines. |
130 """Return arcs that weren't executed from branch lines. |
155 |
131 |
156 Returns {l1:[l2a,l2b,...], ...} |
132 Returns {l1:[l2a,l2b,...], ...} |
157 |
133 |
158 """ |
134 """ |
159 missing = self.arcs_missing() |
135 missing = self.arcs_missing() |
160 branch_lines = set(self.branch_lines()) |
136 branch_lines = set(self.branch_lines()) |
161 mba = {} |
137 mba = collections.defaultdict(list) |
162 for l1, l2 in missing: |
138 for l1, l2 in missing: |
163 if l1 in branch_lines: |
139 if l1 in branch_lines: |
164 if l1 not in mba: |
|
165 mba[l1] = [] |
|
166 mba[l1].append(l2) |
140 mba[l1].append(l2) |
167 return mba |
141 return mba |
168 |
142 |
169 def branch_stats(self): |
143 def branch_stats(self): |
170 """Get stats about branches. |
144 """Get stats about branches. |
171 |
145 |
172 Returns a dict mapping line numbers to a tuple: |
146 Returns a dict mapping line numbers to a tuple: |
173 (total_exits, taken_exits). |
147 (total_exits, taken_exits). |
174 """ |
148 """ |
175 |
149 |
176 exit_counts = self.parser.exit_counts() |
|
177 missing_arcs = self.missing_branch_arcs() |
150 missing_arcs = self.missing_branch_arcs() |
178 stats = {} |
151 stats = {} |
179 for lnum in self.branch_lines(): |
152 for lnum in self.branch_lines(): |
180 exits = exit_counts[lnum] |
153 exits = self.exit_counts[lnum] |
181 try: |
154 try: |
182 missing = len(missing_arcs[lnum]) |
155 missing = len(missing_arcs[lnum]) |
183 except KeyError: |
156 except KeyError: |
184 missing = 0 |
157 missing = 0 |
185 stats[lnum] = (exits, exits - missing) |
158 stats[lnum] = (exits, exits - missing) |
208 self.n_missing = n_missing |
181 self.n_missing = n_missing |
209 self.n_branches = n_branches |
182 self.n_branches = n_branches |
210 self.n_partial_branches = n_partial_branches |
183 self.n_partial_branches = n_partial_branches |
211 self.n_missing_branches = n_missing_branches |
184 self.n_missing_branches = n_missing_branches |
212 |
185 |
|
186 def init_args(self): |
|
187 """Return a list for __init__(*args) to recreate this object.""" |
|
188 return [ |
|
189 self.n_files, self.n_statements, self.n_excluded, self.n_missing, |
|
190 self.n_branches, self.n_partial_branches, self.n_missing_branches, |
|
191 ] |
|
192 |
|
193 @classmethod |
213 def set_precision(cls, precision): |
194 def set_precision(cls, precision): |
214 """Set the number of decimal places used to report percentages.""" |
195 """Set the number of decimal places used to report percentages.""" |
215 assert 0 <= precision < 10 |
196 assert 0 <= precision < 10 |
216 cls._precision = precision |
197 cls._precision = precision |
217 cls._near0 = 1.0 / 10**precision |
198 cls._near0 = 1.0 / 10**precision |
218 cls._near100 = 100.0 - cls._near0 |
199 cls._near100 = 100.0 - cls._near0 |
219 set_precision = classmethod(set_precision) |
200 |
220 |
201 @property |
221 def _get_n_executed(self): |
202 def n_executed(self): |
222 """Returns the number of executed statements.""" |
203 """Returns the number of executed statements.""" |
223 return self.n_statements - self.n_missing |
204 return self.n_statements - self.n_missing |
224 n_executed = property(_get_n_executed) |
205 |
225 |
206 @property |
226 def _get_n_executed_branches(self): |
207 def n_executed_branches(self): |
227 """Returns the number of executed branches.""" |
208 """Returns the number of executed branches.""" |
228 return self.n_branches - self.n_missing_branches |
209 return self.n_branches - self.n_missing_branches |
229 n_executed_branches = property(_get_n_executed_branches) |
210 |
230 |
211 @property |
231 def _get_pc_covered(self): |
212 def pc_covered(self): |
232 """Returns a single percentage value for coverage.""" |
213 """Returns a single percentage value for coverage.""" |
233 if self.n_statements > 0: |
214 if self.n_statements > 0: |
234 pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) / |
215 numerator, denominator = self.ratio_covered |
235 (self.n_statements + self.n_branches)) |
216 pc_cov = (100.0 * numerator) / denominator |
236 else: |
217 else: |
237 pc_cov = 100.0 |
218 pc_cov = 100.0 |
238 return pc_cov |
219 return pc_cov |
239 pc_covered = property(_get_pc_covered) |
220 |
240 |
221 @property |
241 def _get_pc_covered_str(self): |
222 def pc_covered_str(self): |
242 """Returns the percent covered, as a string, without a percent sign. |
223 """Returns the percent covered, as a string, without a percent sign. |
243 |
224 |
244 Note that "0" is only returned when the value is truly zero, and "100" |
225 Note that "0" is only returned when the value is truly zero, and "100" |
245 is only returned when the value is truly 100. Rounding can never |
226 is only returned when the value is truly 100. Rounding can never |
246 result in either "0" or "100". |
227 result in either "0" or "100". |