DebugClients/Python3/coverage/results.py

changeset 4489
d0d6e4ad31bd
parent 3495
fac17a82b431
child 5051
3586ebd9fac8
equal deleted inserted replaced
4481:456c58fc64b0 4489:d0d6e4ad31bd
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".
252 elif self._near100 < pc < 100: 233 elif self._near100 < pc < 100:
253 pc = self._near100 234 pc = self._near100
254 else: 235 else:
255 pc = round(pc, self._precision) 236 pc = round(pc, self._precision)
256 return "%.*f" % (self._precision, pc) 237 return "%.*f" % (self._precision, pc)
257 pc_covered_str = property(_get_pc_covered_str) 238
258 239 @classmethod
259 def pc_str_width(cls): 240 def pc_str_width(cls):
260 """How many characters wide can pc_covered_str be?""" 241 """How many characters wide can pc_covered_str be?"""
261 width = 3 # "100" 242 width = 3 # "100"
262 if cls._precision > 0: 243 if cls._precision > 0:
263 width += 1 + cls._precision 244 width += 1 + cls._precision
264 return width 245 return width
265 pc_str_width = classmethod(pc_str_width) 246
247 @property
248 def ratio_covered(self):
249 """Return a numerator and denominator for the coverage ratio."""
250 numerator = self.n_executed + self.n_executed_branches
251 denominator = self.n_statements + self.n_branches
252 return numerator, denominator
266 253
267 def __add__(self, other): 254 def __add__(self, other):
268 nums = Numbers() 255 nums = Numbers()
269 nums.n_files = self.n_files + other.n_files 256 nums.n_files = self.n_files + other.n_files
270 nums.n_statements = self.n_statements + other.n_statements 257 nums.n_statements = self.n_statements + other.n_statements

eric ide

mercurial