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