19 * arcs: a dict mapping filenames to sorted lists of line number pairs: |
21 * arcs: a dict mapping filenames to sorted lists of line number pairs: |
20 { 'file1': [(17,23), (17,25), (25,26)], ... } |
22 { 'file1': [(17,23), (17,25), (25,26)], ... } |
21 |
23 |
22 """ |
24 """ |
23 |
25 |
24 # Name of the data file (unless environment variable is set). |
26 def __init__(self, basename=None, collector=None, debug=None): |
25 filename_default = ".coverage" |
|
26 |
|
27 # Environment variable naming the data file. |
|
28 filename_env = "COVERAGE_FILE" |
|
29 |
|
30 def __init__(self, basename=None, suffix=None, collector=None): |
|
31 """Create a CoverageData. |
27 """Create a CoverageData. |
32 |
28 |
33 `basename` is the name of the file to use for storing data. |
29 `basename` is the name of the file to use for storing data. |
34 |
30 |
35 `suffix` is a suffix to append to the base file name. This can be used |
|
36 for multiple or parallel execution, so that many coverage data files |
|
37 can exist simultaneously. |
|
38 |
|
39 `collector` is a string describing the coverage measurement software. |
31 `collector` is a string describing the coverage measurement software. |
40 |
32 |
41 """ |
33 `debug` is a `DebugControl` object for writing debug messages. |
42 self.collector = collector |
34 |
|
35 """ |
|
36 self.collector = collector or 'unknown' |
|
37 self.debug = debug |
43 |
38 |
44 self.use_file = True |
39 self.use_file = True |
45 |
40 |
46 # Construct the filename that will be used for data file storage, if we |
41 # Construct the filename that will be used for data file storage, if we |
47 # ever do any file storage. |
42 # ever do any file storage. |
48 self.filename = (basename or |
43 self.filename = basename or ".coverage" |
49 os.environ.get(self.filename_env, self.filename_default)) |
|
50 if suffix: |
|
51 self.filename += suffix |
|
52 self.filename = os.path.abspath(self.filename) |
44 self.filename = os.path.abspath(self.filename) |
53 |
45 |
54 # A map from canonical Python source file name to a dictionary in |
46 # A map from canonical Python source file name to a dictionary in |
55 # which there's an entry for each line number that has been |
47 # which there's an entry for each line number that has been |
56 # executed: |
48 # executed: |
78 if self.use_file: |
73 if self.use_file: |
79 self.lines, self.arcs = self._read_file(self.filename) |
74 self.lines, self.arcs = self._read_file(self.filename) |
80 else: |
75 else: |
81 self.lines, self.arcs = {}, {} |
76 self.lines, self.arcs = {}, {} |
82 |
77 |
83 def write(self): |
78 def write(self, suffix=None): |
84 """Write the collected coverage data to a file.""" |
79 """Write the collected coverage data to a file. |
|
80 |
|
81 `suffix` is a suffix to append to the base file name. This can be used |
|
82 for multiple or parallel execution, so that many coverage data files |
|
83 can exist simultaneously. A dot will be used to join the base name and |
|
84 the suffix. |
|
85 |
|
86 """ |
85 if self.use_file: |
87 if self.use_file: |
86 self.write_file(self.filename) |
88 filename = self.filename |
|
89 if suffix: |
|
90 filename += "." + suffix |
|
91 self.write_file(filename) |
87 |
92 |
88 def erase(self): |
93 def erase(self): |
89 """Erase the data, both in this object, and from its file storage.""" |
94 """Erase the data, both in this object, and from its file storage.""" |
90 if self.use_file: |
95 if self.use_file: |
91 if self.filename and os.path.exists(self.filename): |
96 if self.filename: |
92 os.remove(self.filename) |
97 file_be_gone(self.filename) |
93 self.lines = {} |
98 self.lines = {} |
94 self.arcs = {} |
99 self.arcs = {} |
95 |
100 |
96 def line_data(self): |
101 def line_data(self): |
97 """Return the map from filenames to lists of line numbers executed.""" |
102 """Return the map from filenames to lists of line numbers executed.""" |
98 return dict( |
103 return dict( |
99 [(f, sorted(lmap.keys())) for f, lmap in self.lines.items()] |
104 [(f, sorted(lmap.keys())) for f, lmap in iitems(self.lines)] |
100 ) |
105 ) |
101 |
106 |
102 def arc_data(self): |
107 def arc_data(self): |
103 """Return the map from filenames to lists of line number pairs.""" |
108 """Return the map from filenames to lists of line number pairs.""" |
104 return dict( |
109 return dict( |
105 [(f, sorted(amap.keys())) for f, amap in self.arcs.items()] |
110 [(f, sorted(amap.keys())) for f, amap in iitems(self.arcs)] |
106 ) |
111 ) |
107 |
112 |
108 def write_file(self, filename): |
113 def write_file(self, filename): |
109 """Write the coverage data to `filename`.""" |
114 """Write the coverage data to `filename`.""" |
110 |
115 |
152 data = self.raw_data(filename) |
162 data = self.raw_data(filename) |
153 if isinstance(data, dict): |
163 if isinstance(data, dict): |
154 # Unpack the 'lines' item. |
164 # Unpack the 'lines' item. |
155 lines = dict([ |
165 lines = dict([ |
156 (f, dict.fromkeys(linenos, None)) |
166 (f, dict.fromkeys(linenos, None)) |
157 for f, linenos in data.get('lines', {}).items() |
167 for f, linenos in iitems(data.get('lines', {})) |
158 ]) |
168 ]) |
159 # Unpack the 'arcs' item. |
169 # Unpack the 'arcs' item. |
160 arcs = dict([ |
170 arcs = dict([ |
161 (f, dict.fromkeys(arcpairs, None)) |
171 (f, dict.fromkeys(arcpairs, None)) |
162 for f, arcpairs in data.get('arcs', {}).items() |
172 for f, arcpairs in iitems(data.get('arcs', {})) |
163 ]) |
173 ]) |
164 except Exception: |
174 except Exception: |
165 pass |
175 pass |
166 return lines, arcs |
176 return lines, arcs |
167 |
177 |
168 def combine_parallel_data(self): |
178 def combine_parallel_data(self, aliases=None): |
169 """Combine a number of data files together. |
179 """Combine a number of data files together. |
170 |
180 |
171 Treat `self.filename` as a file prefix, and combine the data from all |
181 Treat `self.filename` as a file prefix, and combine the data from all |
172 of the data files starting with that prefix. |
182 of the data files starting with that prefix plus a dot. |
173 |
183 |
174 """ |
184 If `aliases` is provided, it's a `PathAliases` object that is used to |
|
185 re-map paths to match the local machine's. |
|
186 |
|
187 """ |
|
188 aliases = aliases or PathAliases() |
175 data_dir, local = os.path.split(self.filename) |
189 data_dir, local = os.path.split(self.filename) |
|
190 localdot = local + '.' |
176 for f in os.listdir(data_dir or '.'): |
191 for f in os.listdir(data_dir or '.'): |
177 if f.startswith(local): |
192 if f.startswith(localdot): |
178 full_path = os.path.join(data_dir, f) |
193 full_path = os.path.join(data_dir, f) |
179 new_lines, new_arcs = self._read_file(full_path) |
194 new_lines, new_arcs = self._read_file(full_path) |
180 for filename, file_data in new_lines.items(): |
195 for filename, file_data in iitems(new_lines): |
|
196 filename = aliases.map(filename) |
181 self.lines.setdefault(filename, {}).update(file_data) |
197 self.lines.setdefault(filename, {}).update(file_data) |
182 for filename, file_data in new_arcs.items(): |
198 for filename, file_data in iitems(new_arcs): |
|
199 filename = aliases.map(filename) |
183 self.arcs.setdefault(filename, {}).update(file_data) |
200 self.arcs.setdefault(filename, {}).update(file_data) |
|
201 if f != local: |
|
202 os.remove(full_path) |
184 |
203 |
185 def add_line_data(self, line_data): |
204 def add_line_data(self, line_data): |
186 """Add executed line data. |
205 """Add executed line data. |
187 |
206 |
188 `line_data` is { filename: { lineno: None, ... }, ...} |
207 `line_data` is { filename: { lineno: None, ... }, ...} |
189 |
208 |
190 """ |
209 """ |
191 for filename, linenos in line_data.items(): |
210 for filename, linenos in iitems(line_data): |
192 self.lines.setdefault(filename, {}).update(linenos) |
211 self.lines.setdefault(filename, {}).update(linenos) |
193 |
212 |
194 def add_arc_data(self, arc_data): |
213 def add_arc_data(self, arc_data): |
195 """Add measured arc data. |
214 """Add measured arc data. |
196 |
215 |
197 `arc_data` is { filename: { (l1,l2): None, ... }, ...} |
216 `arc_data` is { filename: { (l1,l2): None, ... }, ...} |
198 |
217 |
199 """ |
218 """ |
200 for filename, arcs in arc_data.items(): |
219 for filename, arcs in iitems(arc_data): |
201 self.arcs.setdefault(filename, {}).update(arcs) |
220 self.arcs.setdefault(filename, {}).update(arcs) |
202 |
221 |
203 def executed_files(self): |
222 def touch_file(self, filename): |
204 """A list of all files that had been measured as executed.""" |
223 """Ensure that `filename` appears in the data, empty if needed.""" |
|
224 self.lines.setdefault(filename, {}) |
|
225 |
|
226 def measured_files(self): |
|
227 """A list of all files that had been measured.""" |
205 return list(self.lines.keys()) |
228 return list(self.lines.keys()) |
206 |
229 |
207 def executed_lines(self, filename): |
230 def executed_lines(self, filename): |
208 """A map containing all the line numbers executed in `filename`. |
231 """A map containing all the line numbers executed in `filename`. |
209 |
232 |
214 return self.lines.get(filename) or {} |
237 return self.lines.get(filename) or {} |
215 |
238 |
216 def executed_arcs(self, filename): |
239 def executed_arcs(self, filename): |
217 """A map containing all the arcs executed in `filename`.""" |
240 """A map containing all the arcs executed in `filename`.""" |
218 return self.arcs.get(filename) or {} |
241 return self.arcs.get(filename) or {} |
|
242 |
|
243 def add_to_hash(self, filename, hasher): |
|
244 """Contribute `filename`'s data to the Md5Hash `hasher`.""" |
|
245 hasher.update(self.executed_lines(filename)) |
|
246 hasher.update(self.executed_arcs(filename)) |
219 |
247 |
220 def summary(self, fullpath=False): |
248 def summary(self, fullpath=False): |
221 """Return a dict summarizing the coverage data. |
249 """Return a dict summarizing the coverage data. |
222 |
250 |
223 Keys are based on the filenames, and values are the number of executed |
251 Keys are based on the filenames, and values are the number of executed |