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