13 def __init__(self, cov, code_unit): |
13 def __init__(self, cov, code_unit): |
14 self.coverage = cov |
14 self.coverage = cov |
15 self.code_unit = code_unit |
15 self.code_unit = code_unit |
16 |
16 |
17 self.filename = self.code_unit.filename |
17 self.filename = self.code_unit.filename |
18 ext = os.path.splitext(self.filename)[1] |
18 actual_filename, source = self.find_source(self.filename) |
19 source = None |
|
20 if ext == '.py': |
|
21 if not os.path.exists(self.filename): |
|
22 source = self.coverage.file_locator.get_zip_data(self.filename) |
|
23 if not source: |
|
24 raise NoSource("No source for code: %r" % self.filename) |
|
25 |
19 |
26 self.parser = CodeParser( |
20 self.parser = CodeParser( |
27 text=source, filename=self.filename, |
21 text=source, filename=actual_filename, |
28 exclude=self.coverage.exclude_re |
22 exclude=self.coverage._exclude_regex('exclude') |
29 ) |
23 ) |
30 self.statements, self.excluded = self.parser.parse_source() |
24 self.statements, self.excluded = self.parser.parse_source() |
31 |
25 |
32 # Identify missing statements. |
26 # Identify missing statements. |
33 executed = self.coverage.data.executed_lines(self.filename) |
27 executed = self.coverage.data.executed_lines(self.filename) |
34 exec1 = self.parser.first_lines(executed) |
28 exec1 = self.parser.first_lines(executed) |
35 self.missing = sorted(set(self.statements) - set(exec1)) |
29 self.missing = self.statements - exec1 |
36 |
30 |
37 if self.coverage.data.has_arcs(): |
31 if self.coverage.data.has_arcs(): |
|
32 self.no_branch = self.parser.lines_matching( |
|
33 join_regex(self.coverage.config.partial_list), |
|
34 join_regex(self.coverage.config.partial_always_list) |
|
35 ) |
38 n_branches = self.total_branches() |
36 n_branches = self.total_branches() |
39 mba = self.missing_branch_arcs() |
37 mba = self.missing_branch_arcs() |
40 n_missing_branches = sum([len(v) for v in mba.values()]) |
38 n_partial_branches = sum( |
|
39 [len(v) for k,v in iitems(mba) if k not in self.missing] |
|
40 ) |
|
41 n_missing_branches = sum([len(v) for k,v in iitems(mba)]) |
41 else: |
42 else: |
42 n_branches = n_missing_branches = 0 |
43 n_branches = n_partial_branches = n_missing_branches = 0 |
|
44 self.no_branch = set() |
43 |
45 |
44 self.numbers = Numbers( |
46 self.numbers = Numbers( |
45 n_files=1, |
47 n_files=1, |
46 n_statements=len(self.statements), |
48 n_statements=len(self.statements), |
47 n_excluded=len(self.excluded), |
49 n_excluded=len(self.excluded), |
48 n_missing=len(self.missing), |
50 n_missing=len(self.missing), |
49 n_branches=n_branches, |
51 n_branches=n_branches, |
|
52 n_partial_branches=n_partial_branches, |
50 n_missing_branches=n_missing_branches, |
53 n_missing_branches=n_missing_branches, |
51 ) |
54 ) |
|
55 |
|
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) |
52 |
93 |
53 def missing_formatted(self): |
94 def missing_formatted(self): |
54 """The missing line numbers, formatted nicely. |
95 """The missing line numbers, formatted nicely. |
55 |
96 |
56 Returns a string like "1-2, 5-11, 13-14". |
97 Returns a string like "1-2, 5-11, 13-14". |
87 # Exclude arcs here which connect a line to itself. They can occur |
133 # Exclude arcs here which connect a line to itself. They can occur |
88 # in executed data in some cases. This is where they can cause |
134 # in executed data in some cases. This is where they can cause |
89 # trouble, and here is where it's the least burden to remove them. |
135 # trouble, and here is where it's the least burden to remove them. |
90 unpredicted = [ |
136 unpredicted = [ |
91 e for e in executed |
137 e for e in executed |
92 if e not in possible and e[0] != e[1] |
138 if e not in possible |
|
139 and e[0] != e[1] |
93 ] |
140 ] |
94 return sorted(unpredicted) |
141 return sorted(unpredicted) |
95 |
142 |
96 def branch_lines(self): |
143 def branch_lines(self): |
97 """Returns lines that have more than one exit.""" |
144 """Returns a list of line numbers that have more than one exit.""" |
98 exit_counts = self.parser.exit_counts() |
145 exit_counts = self.parser.exit_counts() |
99 return [l1 for l1,count in exit_counts.items() if count > 1] |
146 return [l1 for l1,count in iitems(exit_counts) if count > 1] |
100 |
147 |
101 def total_branches(self): |
148 def total_branches(self): |
102 """How many total branches are there?""" |
149 """How many total branches are there?""" |
103 exit_counts = self.parser.exit_counts() |
150 exit_counts = self.parser.exit_counts() |
104 return sum([count for count in exit_counts.values() if count > 1]) |
151 return sum([count for count in exit_counts.values() if count > 1]) |
117 if l1 not in mba: |
164 if l1 not in mba: |
118 mba[l1] = [] |
165 mba[l1] = [] |
119 mba[l1].append(l2) |
166 mba[l1].append(l2) |
120 return mba |
167 return mba |
121 |
168 |
|
169 def branch_stats(self): |
|
170 """Get stats about branches. |
|
171 |
|
172 Returns a dict mapping line numbers to a tuple: |
|
173 (total_exits, taken_exits). |
|
174 """ |
|
175 |
|
176 exit_counts = self.parser.exit_counts() |
|
177 missing_arcs = self.missing_branch_arcs() |
|
178 stats = {} |
|
179 for lnum in self.branch_lines(): |
|
180 exits = exit_counts[lnum] |
|
181 try: |
|
182 missing = len(missing_arcs[lnum]) |
|
183 except KeyError: |
|
184 missing = 0 |
|
185 stats[lnum] = (exits, exits - missing) |
|
186 return stats |
|
187 |
122 |
188 |
123 class Numbers(object): |
189 class Numbers(object): |
124 """The numerical results of measuring coverage. |
190 """The numerical results of measuring coverage. |
125 |
191 |
126 This holds the basic statistics from `Analysis`, and is used to roll |
192 This holds the basic statistics from `Analysis`, and is used to roll |
127 up statistics across files. |
193 up statistics across files. |
128 |
194 |
129 """ |
195 """ |
|
196 # A global to determine the precision on coverage percentages, the number |
|
197 # of decimal places. |
|
198 _precision = 0 |
|
199 _near0 = 1.0 # These will change when _precision is changed. |
|
200 _near100 = 99.0 |
|
201 |
130 def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, |
202 def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, |
131 n_branches=0, n_missing_branches=0 |
203 n_branches=0, n_partial_branches=0, n_missing_branches=0 |
132 ): |
204 ): |
133 self.n_files = n_files |
205 self.n_files = n_files |
134 self.n_statements = n_statements |
206 self.n_statements = n_statements |
135 self.n_excluded = n_excluded |
207 self.n_excluded = n_excluded |
136 self.n_missing = n_missing |
208 self.n_missing = n_missing |
137 self.n_branches = n_branches |
209 self.n_branches = n_branches |
|
210 self.n_partial_branches = n_partial_branches |
138 self.n_missing_branches = n_missing_branches |
211 self.n_missing_branches = n_missing_branches |
|
212 |
|
213 def set_precision(cls, precision): |
|
214 """Set the number of decimal places used to report percentages.""" |
|
215 assert 0 <= precision < 10 |
|
216 cls._precision = precision |
|
217 cls._near0 = 1.0 / 10**precision |
|
218 cls._near100 = 100.0 - cls._near0 |
|
219 set_precision = classmethod(set_precision) |
139 |
220 |
140 def _get_n_executed(self): |
221 def _get_n_executed(self): |
141 """Returns the number of executed statements.""" |
222 """Returns the number of executed statements.""" |
142 return self.n_statements - self.n_missing |
223 return self.n_statements - self.n_missing |
143 n_executed = property(_get_n_executed) |
224 n_executed = property(_get_n_executed) |
155 else: |
236 else: |
156 pc_cov = 100.0 |
237 pc_cov = 100.0 |
157 return pc_cov |
238 return pc_cov |
158 pc_covered = property(_get_pc_covered) |
239 pc_covered = property(_get_pc_covered) |
159 |
240 |
|
241 def _get_pc_covered_str(self): |
|
242 """Returns the percent covered, as a string, without a percent sign. |
|
243 |
|
244 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 |
|
246 result in either "0" or "100". |
|
247 |
|
248 """ |
|
249 pc = self.pc_covered |
|
250 if 0 < pc < self._near0: |
|
251 pc = self._near0 |
|
252 elif self._near100 < pc < 100: |
|
253 pc = self._near100 |
|
254 else: |
|
255 pc = round(pc, self._precision) |
|
256 return "%.*f" % (self._precision, pc) |
|
257 pc_covered_str = property(_get_pc_covered_str) |
|
258 |
|
259 def pc_str_width(cls): |
|
260 """How many characters wide can pc_covered_str be?""" |
|
261 width = 3 # "100" |
|
262 if cls._precision > 0: |
|
263 width += 1 + cls._precision |
|
264 return width |
|
265 pc_str_width = classmethod(pc_str_width) |
|
266 |
160 def __add__(self, other): |
267 def __add__(self, other): |
161 nums = Numbers() |
268 nums = Numbers() |
162 nums.n_files = self.n_files + other.n_files |
269 nums.n_files = self.n_files + other.n_files |
163 nums.n_statements = self.n_statements + other.n_statements |
270 nums.n_statements = self.n_statements + other.n_statements |
164 nums.n_excluded = self.n_excluded + other.n_excluded |
271 nums.n_excluded = self.n_excluded + other.n_excluded |
165 nums.n_missing = self.n_missing + other.n_missing |
272 nums.n_missing = self.n_missing + other.n_missing |
166 nums.n_branches = self.n_branches + other.n_branches |
273 nums.n_branches = self.n_branches + other.n_branches |
167 nums.n_missing_branches = (self.n_missing_branches + |
274 nums.n_partial_branches = ( |
168 other.n_missing_branches) |
275 self.n_partial_branches + other.n_partial_branches |
|
276 ) |
|
277 nums.n_missing_branches = ( |
|
278 self.n_missing_branches + other.n_missing_branches |
|
279 ) |
169 return nums |
280 return nums |
170 |
281 |
171 def __radd__(self, other): |
282 def __radd__(self, other): |
172 # Implementing 0+Numbers allows us to sum() a list of Numbers. |
283 # Implementing 0+Numbers allows us to sum() a list of Numbers. |
173 if other == 0: |
284 if other == 0: |
174 return self |
285 return self |
175 raise NotImplemented |
286 return NotImplemented |