|
1 """Results of coverage measurement.""" |
|
2 |
|
3 import os |
|
4 |
|
5 from .backward import set, sorted # pylint: disable-msg=W0622 |
|
6 from .misc import format_lines, NoSource |
|
7 from .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_re |
|
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 n_branches = self.total_branches() |
|
39 mba = self.missing_branch_arcs() |
|
40 n_missing_branches = sum([len(v) for v in mba.values()]) |
|
41 else: |
|
42 n_branches = n_missing_branches = 0 |
|
43 |
|
44 self.numbers = Numbers( |
|
45 n_files=1, |
|
46 n_statements=len(self.statements), |
|
47 n_excluded=len(self.excluded), |
|
48 n_missing=len(self.missing), |
|
49 n_branches=n_branches, |
|
50 n_missing_branches=n_missing_branches, |
|
51 ) |
|
52 |
|
53 def missing_formatted(self): |
|
54 """The missing line numbers, formatted nicely. |
|
55 |
|
56 Returns a string like "1-2, 5-11, 13-14". |
|
57 |
|
58 """ |
|
59 return format_lines(self.statements, self.missing) |
|
60 |
|
61 def has_arcs(self): |
|
62 """Were arcs measured in this result?""" |
|
63 return self.coverage.data.has_arcs() |
|
64 |
|
65 def arc_possibilities(self): |
|
66 """Returns a sorted list of the arcs in the code.""" |
|
67 return self.parser.arcs() |
|
68 |
|
69 def arcs_executed(self): |
|
70 """Returns a sorted list of the arcs actually executed in the code.""" |
|
71 executed = self.coverage.data.executed_arcs(self.filename) |
|
72 m2fl = self.parser.first_line |
|
73 executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed] |
|
74 return sorted(executed) |
|
75 |
|
76 def arcs_missing(self): |
|
77 """Returns a sorted list of the arcs in the code not executed.""" |
|
78 possible = self.arc_possibilities() |
|
79 executed = self.arcs_executed() |
|
80 missing = [p for p in possible if p not in executed] |
|
81 return sorted(missing) |
|
82 |
|
83 def arcs_unpredicted(self): |
|
84 """Returns a sorted list of the executed arcs missing from the code.""" |
|
85 possible = self.arc_possibilities() |
|
86 executed = self.arcs_executed() |
|
87 # 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 |
|
89 # trouble, and here is where it's the least burden to remove them. |
|
90 unpredicted = [ |
|
91 e for e in executed |
|
92 if e not in possible and e[0] != e[1] |
|
93 ] |
|
94 return sorted(unpredicted) |
|
95 |
|
96 def branch_lines(self): |
|
97 """Returns lines that have more than one exit.""" |
|
98 exit_counts = self.parser.exit_counts() |
|
99 return [l1 for l1,count in exit_counts.items() if count > 1] |
|
100 |
|
101 def total_branches(self): |
|
102 """How many total branches are there?""" |
|
103 exit_counts = self.parser.exit_counts() |
|
104 return sum([count for count in exit_counts.values() if count > 1]) |
|
105 |
|
106 def missing_branch_arcs(self): |
|
107 """Return arcs that weren't executed from branch lines. |
|
108 |
|
109 Returns {l1:[l2a,l2b,...], ...} |
|
110 |
|
111 """ |
|
112 missing = self.arcs_missing() |
|
113 branch_lines = set(self.branch_lines()) |
|
114 mba = {} |
|
115 for l1, l2 in missing: |
|
116 if l1 in branch_lines: |
|
117 if l1 not in mba: |
|
118 mba[l1] = [] |
|
119 mba[l1].append(l2) |
|
120 return mba |
|
121 |
|
122 |
|
123 class Numbers(object): |
|
124 """The numerical results of measuring coverage. |
|
125 |
|
126 This holds the basic statistics from `Analysis`, and is used to roll |
|
127 up statistics across files. |
|
128 |
|
129 """ |
|
130 def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, |
|
131 n_branches=0, n_missing_branches=0 |
|
132 ): |
|
133 self.n_files = n_files |
|
134 self.n_statements = n_statements |
|
135 self.n_excluded = n_excluded |
|
136 self.n_missing = n_missing |
|
137 self.n_branches = n_branches |
|
138 self.n_missing_branches = n_missing_branches |
|
139 |
|
140 def _get_n_executed(self): |
|
141 """Returns the number of executed statements.""" |
|
142 return self.n_statements - self.n_missing |
|
143 n_executed = property(_get_n_executed) |
|
144 |
|
145 def _get_n_executed_branches(self): |
|
146 """Returns the number of executed branches.""" |
|
147 return self.n_branches - self.n_missing_branches |
|
148 n_executed_branches = property(_get_n_executed_branches) |
|
149 |
|
150 def _get_pc_covered(self): |
|
151 """Returns a single percentage value for coverage.""" |
|
152 if self.n_statements > 0: |
|
153 pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) / |
|
154 (self.n_statements + self.n_branches)) |
|
155 else: |
|
156 pc_cov = 100.0 |
|
157 return pc_cov |
|
158 pc_covered = property(_get_pc_covered) |
|
159 |
|
160 def __add__(self, other): |
|
161 nums = Numbers() |
|
162 nums.n_files = self.n_files + other.n_files |
|
163 nums.n_statements = self.n_statements + other.n_statements |
|
164 nums.n_excluded = self.n_excluded + other.n_excluded |
|
165 nums.n_missing = self.n_missing + other.n_missing |
|
166 nums.n_branches = self.n_branches + other.n_branches |
|
167 nums.n_missing_branches = (self.n_missing_branches + |
|
168 other.n_missing_branches) |
|
169 return nums |
|
170 |
|
171 def __radd__(self, other): |
|
172 # Implementing 0+Numbers allows us to sum() a list of Numbers. |
|
173 if other == 0: |
|
174 return self |
|
175 raise NotImplemented |