1 """Coverage data for Coverage.""" |
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
2 |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
|
3 |
|
4 """Coverage data for coverage.py.""" |
|
5 |
|
6 import glob |
|
7 import json |
|
8 import optparse |
3 import os |
9 import os |
|
10 import os.path |
|
11 import random |
|
12 import re |
|
13 import socket |
4 import sys |
14 import sys |
5 |
15 |
6 from .backward import iitems, pickle, sorted # pylint: disable=W0622 |
16 from coverage import env |
7 from .files import PathAliases |
17 from coverage.backward import iitems, string_class |
8 from .misc import file_be_gone |
18 from coverage.debug import _TEST_NAME_FILE |
|
19 from coverage.files import PathAliases |
|
20 from coverage.misc import CoverageException, file_be_gone |
9 |
21 |
10 |
22 |
11 class CoverageData(object): |
23 class CoverageData(object): |
12 """Manages collected coverage data, including file storage. |
24 """Manages collected coverage data, including file storage. |
13 |
25 |
14 The data file format is a pickled dict, with these keys: |
26 This class is the public supported API to the data coverage.py collects |
15 |
27 during program execution. It includes information about what code was |
16 * collector: a string identifying the collecting software |
28 executed. It does not include information from the analysis phase, to |
17 |
29 determine what lines could have been executed, or what lines were not |
18 * lines: a dict mapping filenames to sorted lists of line numbers |
30 executed. |
19 executed: |
31 |
20 { 'file1': [17,23,45], 'file2': [1,2,3], ... } |
32 .. note:: |
21 |
33 |
22 * arcs: a dict mapping filenames to sorted lists of line number pairs: |
34 The file format is not documented or guaranteed. It will change in |
23 { 'file1': [(17,23), (17,25), (25,26)], ... } |
35 the future, in possibly complicated ways. Do not read coverage.py |
|
36 data files directly. Use this API to avoid disruption. |
|
37 |
|
38 There are a number of kinds of data that can be collected: |
|
39 |
|
40 * **lines**: the line numbers of source lines that were executed. |
|
41 These are always available. |
|
42 |
|
43 * **arcs**: pairs of source and destination line numbers for transitions |
|
44 between source lines. These are only available if branch coverage was |
|
45 used. |
|
46 |
|
47 * **file tracer names**: the module names of the file tracer plugins that |
|
48 handled each file in the data. |
|
49 |
|
50 * **run information**: information about the program execution. This is |
|
51 written during "coverage run", and then accumulated during "coverage |
|
52 combine". |
|
53 |
|
54 Lines, arcs, and file tracer names are stored for each source file. File |
|
55 names in this API are case-sensitive, even on platforms with |
|
56 case-insensitive file systems. |
|
57 |
|
58 To read a coverage.py data file, use :meth:`read_file`, or |
|
59 :meth:`read_fileobj` if you have an already-opened file. You can then |
|
60 access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, |
|
61 or :meth:`file_tracer`. Run information is available with |
|
62 :meth:`run_infos`. |
|
63 |
|
64 The :meth:`has_arcs` method indicates whether arc data is available. You |
|
65 can get a list of the files in the data with :meth:`measured_files`. |
|
66 A summary of the line data is available from :meth:`line_counts`. As with |
|
67 most Python containers, you can determine if there is any data at all by |
|
68 using this object as a boolean value. |
|
69 |
|
70 |
|
71 Most data files will be created by coverage.py itself, but you can use |
|
72 methods here to create data files if you like. The :meth:`add_lines`, |
|
73 :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways |
|
74 that are convenient for coverage.py. The :meth:`add_run_info` method adds |
|
75 key-value pairs to the run information. |
|
76 |
|
77 To add a file without any measured data, use :meth:`touch_file`. |
|
78 |
|
79 You write to a named file with :meth:`write_file`, or to an already opened |
|
80 file with :meth:`write_fileobj`. |
|
81 |
|
82 You can clear the data in memory with :meth:`erase`. Two data collections |
|
83 can be combined by using :meth:`update` on one :class:`CoverageData`, |
|
84 passing it the other. |
24 |
85 |
25 """ |
86 """ |
26 |
87 |
27 def __init__(self, basename=None, collector=None, debug=None): |
88 # The data file format is JSON, with these keys: |
|
89 # |
|
90 # * lines: a dict mapping file names to lists of line numbers |
|
91 # executed:: |
|
92 # |
|
93 # { "file1": [17,23,45], "file2": [1,2,3], ... } |
|
94 # |
|
95 # * arcs: a dict mapping file names to lists of line number pairs:: |
|
96 # |
|
97 # { "file1": [[17,23], [17,25], [25,26]], ... } |
|
98 # |
|
99 # * file_tracers: a dict mapping file names to plugin names:: |
|
100 # |
|
101 # { "file1": "django.coverage", ... } |
|
102 # |
|
103 # * runs: a list of dicts of information about the coverage.py runs |
|
104 # contributing to the data:: |
|
105 # |
|
106 # [ { "brief_sys": "CPython 2.7.10 Darwin" }, ... ] |
|
107 # |
|
108 # Only one of `lines` or `arcs` will be present: with branch coverage, data |
|
109 # is stored as arcs. Without branch coverage, it is stored as lines. The |
|
110 # line data is easily recovered from the arcs: it is all the first elements |
|
111 # of the pairs that are greater than zero. |
|
112 |
|
113 def __init__(self, debug=None): |
28 """Create a CoverageData. |
114 """Create a CoverageData. |
29 |
115 |
30 `basename` is the name of the file to use for storing data. |
|
31 |
|
32 `collector` is a string describing the coverage measurement software. |
|
33 |
|
34 `debug` is a `DebugControl` object for writing debug messages. |
116 `debug` is a `DebugControl` object for writing debug messages. |
35 |
117 |
36 """ |
118 """ |
37 self.collector = collector or 'unknown' |
119 self._debug = debug |
38 self.debug = debug |
|
39 |
|
40 self.use_file = True |
|
41 |
|
42 # Construct the filename that will be used for data file storage, if we |
|
43 # ever do any file storage. |
|
44 self.filename = basename or ".coverage" |
|
45 self.filename = os.path.abspath(self.filename) |
|
46 |
120 |
47 # A map from canonical Python source file name to a dictionary in |
121 # A map from canonical Python source file name to a dictionary in |
48 # which there's an entry for each line number that has been |
122 # which there's an entry for each line number that has been |
49 # executed: |
123 # executed: |
50 # |
124 # |
51 # { |
125 # { 'filename1.py': [12, 47, 1001], ... } |
52 # 'filename1.py': { 12: None, 47: None, ... }, |
|
53 # ... |
|
54 # } |
|
55 # |
126 # |
56 self.lines = {} |
127 self._lines = None |
57 |
128 |
58 # A map from canonical Python source file name to a dictionary with an |
129 # A map from canonical Python source file name to a dictionary with an |
59 # entry for each pair of line numbers forming an arc: |
130 # entry for each pair of line numbers forming an arc: |
60 # |
131 # |
61 # { |
132 # { 'filename1.py': [(12,14), (47,48), ... ], ... } |
62 # 'filename1.py': { (12,14): None, (47,48): None, ... }, |
|
63 # ... |
|
64 # } |
|
65 # |
133 # |
66 self.arcs = {} |
134 self._arcs = None |
67 |
135 |
68 def usefile(self, use_file=True): |
136 # A map from canonical source file name to a plugin module name: |
69 """Set whether or not to use a disk file for data.""" |
137 # |
70 self.use_file = use_file |
138 # { 'filename1.py': 'django.coverage', ... } |
71 |
139 # |
72 def read(self): |
140 self._file_tracers = {} |
73 """Read coverage data from the coverage data file (if it exists).""" |
141 |
74 if self.use_file: |
142 # A list of dicts of information about the coverage.py runs. |
75 self.lines, self.arcs = self._read_file(self.filename) |
143 self._runs = [] |
76 else: |
144 |
77 self.lines, self.arcs = {}, {} |
145 def __repr__(self): |
78 |
146 return "<{klass} lines={lines} arcs={arcs} tracers={tracers} runs={runs}>".format( |
79 def write(self, suffix=None): |
147 klass=self.__class__.__name__, |
80 """Write the collected coverage data to a file. |
148 lines="None" if self._lines is None else "{{{0}}}".format(len(self._lines)), |
81 |
149 arcs="None" if self._arcs is None else "{{{0}}}".format(len(self._arcs)), |
82 `suffix` is a suffix to append to the base file name. This can be used |
150 tracers="{{{0}}}".format(len(self._file_tracers)), |
83 for multiple or parallel execution, so that many coverage data files |
151 runs="[{0}]".format(len(self._runs)), |
84 can exist simultaneously. A dot will be used to join the base name and |
152 ) |
85 the suffix. |
153 |
86 |
154 ## |
87 """ |
155 ## Reading data |
88 if self.use_file: |
156 ## |
89 filename = self.filename |
157 |
90 if suffix: |
158 def has_arcs(self): |
91 filename += "." + suffix |
159 """Does this data have arcs? |
92 self.write_file(filename) |
160 |
93 |
161 Arc data is only available if branch coverage was used during |
94 def erase(self): |
162 collection. |
95 """Erase the data, both in this object, and from its file storage.""" |
163 |
96 if self.use_file: |
164 Returns a boolean. |
97 if self.filename: |
165 |
98 file_be_gone(self.filename) |
166 """ |
99 self.lines = {} |
167 return self._has_arcs() |
100 self.arcs = {} |
168 |
101 |
169 def lines(self, filename): |
102 def line_data(self): |
170 """Get the list of lines executed for a file. |
103 """Return the map from filenames to lists of line numbers executed.""" |
171 |
104 return dict( |
172 If the file was not measured, returns None. A file might be measured, |
105 [(f.decode(sys.getfilesystemencoding()), sorted(lmap.keys())) |
173 and have no lines executed, in which case an empty list is returned. |
106 for f, lmap in iitems(self.lines)] |
174 |
107 ) |
175 If the file was executed, returns a list of integers, the line numbers |
108 |
176 executed in the file. The list is in no particular order. |
109 def arc_data(self): |
177 |
110 """Return the map from filenames to lists of line number pairs.""" |
178 """ |
111 return dict( |
179 if self._arcs is not None: |
112 [(f.decode(sys.getfilesystemencoding()), sorted(amap.keys())) |
180 if filename in self._arcs: |
113 for f, amap in iitems(self.arcs)] |
181 return [s for s, __ in self._arcs[filename] if s > 0] |
114 ) |
182 elif self._lines is not None: |
115 |
183 if filename in self._lines: |
116 def write_file(self, filename): |
184 return self._lines[filename] |
117 """Write the coverage data to `filename`.""" |
185 return None |
118 |
186 |
119 # Create the file data. |
187 def arcs(self, filename): |
120 data = {} |
188 """Get the list of arcs executed for a file. |
121 |
189 |
122 data['lines'] = self.line_data() |
190 If the file was not measured, returns None. A file might be measured, |
123 arcs = self.arc_data() |
191 and have no arcs executed, in which case an empty list is returned. |
124 if arcs: |
192 |
125 data['arcs'] = arcs |
193 If the file was executed, returns a list of 2-tuples of integers. Each |
126 |
194 pair is a starting line number and an ending line number for a |
127 if self.collector: |
195 transition from one line to another. The list is in no particular |
128 data['collector'] = self.collector |
196 order. |
129 |
197 |
130 if self.debug and self.debug.should('dataio'): |
198 Negative numbers have special meaning. If the starting line number is |
131 self.debug.write("Writing data to %r" % (filename,)) |
199 -N, it represents an entry to the code object that starts at line N. |
132 |
200 If the ending ling number is -N, it's an exit from the code object that |
133 # Write the pickle to the file. |
201 starts at line N. |
134 fdata = open(filename, 'wb') |
202 |
135 try: |
203 """ |
136 pickle.dump(data, fdata, 2) |
204 if self._arcs is not None: |
137 finally: |
205 if filename in self._arcs: |
138 fdata.close() |
206 return self._arcs[filename] |
139 |
207 return None |
140 def read_file(self, filename): |
208 |
141 """Read the coverage data from `filename`.""" |
209 def file_tracer(self, filename): |
142 self.lines, self.arcs = self._read_file(filename) |
210 """Get the plugin name of the file tracer for a file. |
143 |
211 |
144 def raw_data(self, filename): |
212 Returns the name of the plugin that handles this file. If the file was |
145 """Return the raw pickled data from `filename`.""" |
213 measured, but didn't use a plugin, then "" is returned. If the file |
146 if self.debug and self.debug.should('dataio'): |
214 was not measured, then None is returned. |
147 self.debug.write("Reading data from %r" % (filename,)) |
215 |
148 fdata = open(filename, 'rb') |
216 """ |
149 try: |
217 # Because the vast majority of files involve no plugin, we don't store |
150 data = pickle.load(fdata) |
218 # them explicitly in self._file_tracers. Check the measured data |
151 finally: |
219 # instead to see if it was a known file with no plugin. |
152 fdata.close() |
220 if filename in (self._arcs or self._lines or {}): |
153 return data |
221 return self._file_tracers.get(filename, "") |
154 |
222 return None |
155 def _read_file(self, filename): |
223 |
156 """Return the stored coverage data from the given file. |
224 def run_infos(self): |
157 |
225 """Return the list of dicts of run information. |
158 Returns two values, suitable for assigning to `self.lines` and |
226 |
159 `self.arcs`. |
227 For data collected during a single run, this will be a one-element |
160 |
228 list. If data has been combined, there will be one element for each |
161 """ |
229 original data file. |
162 lines = {} |
230 |
163 arcs = {} |
231 """ |
164 try: |
232 return self._runs |
165 data = self.raw_data(filename) |
|
166 if isinstance(data, dict): |
|
167 # Unpack the 'lines' item. |
|
168 lines = dict([ |
|
169 (f.encode(sys.getfilesystemencoding()), |
|
170 dict.fromkeys(linenos, None)) |
|
171 for f, linenos in iitems(data.get('lines', {})) |
|
172 ]) |
|
173 # Unpack the 'arcs' item. |
|
174 arcs = dict([ |
|
175 (f.encode(sys.getfilesystemencoding()), |
|
176 dict.fromkeys(arcpairs, None)) |
|
177 for f, arcpairs in iitems(data.get('arcs', {})) |
|
178 ]) |
|
179 except Exception: |
|
180 pass |
|
181 return lines, arcs |
|
182 |
|
183 def combine_parallel_data(self, aliases=None): |
|
184 """Combine a number of data files together. |
|
185 |
|
186 Treat `self.filename` as a file prefix, and combine the data from all |
|
187 of the data files starting with that prefix plus a dot. |
|
188 |
|
189 If `aliases` is provided, it's a `PathAliases` object that is used to |
|
190 re-map paths to match the local machine's. |
|
191 |
|
192 """ |
|
193 aliases = aliases or PathAliases() |
|
194 data_dir, local = os.path.split(self.filename) |
|
195 localdot = local + '.' |
|
196 for f in os.listdir(data_dir or '.'): |
|
197 if f.startswith(localdot): |
|
198 full_path = os.path.join(data_dir, f) |
|
199 new_lines, new_arcs = self._read_file(full_path) |
|
200 for filename, file_data in iitems(new_lines): |
|
201 filename = aliases.map(filename) |
|
202 self.lines.setdefault(filename, {}).update(file_data) |
|
203 for filename, file_data in iitems(new_arcs): |
|
204 filename = aliases.map(filename) |
|
205 self.arcs.setdefault(filename, {}).update(file_data) |
|
206 if f != local: |
|
207 os.remove(full_path) |
|
208 |
|
209 def add_line_data(self, line_data): |
|
210 """Add executed line data. |
|
211 |
|
212 `line_data` is { filename: { lineno: None, ... }, ...} |
|
213 |
|
214 """ |
|
215 for filename, linenos in iitems(line_data): |
|
216 self.lines.setdefault(filename, {}).update(linenos) |
|
217 |
|
218 def add_arc_data(self, arc_data): |
|
219 """Add measured arc data. |
|
220 |
|
221 `arc_data` is { filename: { (l1,l2): None, ... }, ...} |
|
222 |
|
223 """ |
|
224 for filename, arcs in iitems(arc_data): |
|
225 self.arcs.setdefault(filename, {}).update(arcs) |
|
226 |
|
227 def touch_file(self, filename): |
|
228 """Ensure that `filename` appears in the data, empty if needed.""" |
|
229 self.lines.setdefault(filename, {}) |
|
230 |
233 |
231 def measured_files(self): |
234 def measured_files(self): |
232 """A list of all files that had been measured.""" |
235 """A list of all files that had been measured.""" |
233 return list(self.lines.keys()) |
236 return list(self._arcs or self._lines or {}) |
234 |
237 |
235 def executed_lines(self, filename): |
238 def line_counts(self, fullpath=False): |
236 """A map containing all the line numbers executed in `filename`. |
239 """Return a dict summarizing the line coverage data. |
237 |
240 |
238 If `filename` hasn't been collected at all (because it wasn't executed) |
241 Keys are based on the file names, and values are the number of executed |
239 then return an empty map. |
|
240 |
|
241 """ |
|
242 return self.lines.get(filename) or {} |
|
243 |
|
244 def executed_arcs(self, filename): |
|
245 """A map containing all the arcs executed in `filename`.""" |
|
246 return self.arcs.get(filename) or {} |
|
247 |
|
248 def add_to_hash(self, filename, hasher): |
|
249 """Contribute `filename`'s data to the Md5Hash `hasher`.""" |
|
250 hasher.update(self.executed_lines(filename)) |
|
251 hasher.update(self.executed_arcs(filename)) |
|
252 |
|
253 def summary(self, fullpath=False): |
|
254 """Return a dict summarizing the coverage data. |
|
255 |
|
256 Keys are based on the filenames, and values are the number of executed |
|
257 lines. If `fullpath` is true, then the keys are the full pathnames of |
242 lines. If `fullpath` is true, then the keys are the full pathnames of |
258 the files, otherwise they are the basenames of the files. |
243 the files, otherwise they are the basenames of the files. |
|
244 |
|
245 Returns a dict mapping file names to counts of lines. |
259 |
246 |
260 """ |
247 """ |
261 summ = {} |
248 summ = {} |
262 if fullpath: |
249 if fullpath: |
263 filename_fn = lambda f: f |
250 filename_fn = lambda f: f |
264 else: |
251 else: |
265 filename_fn = os.path.basename |
252 filename_fn = os.path.basename |
266 for filename, lines in iitems(self.lines): |
253 for filename in self.measured_files(): |
267 summ[filename_fn(filename)] = len(lines) |
254 summ[filename_fn(filename)] = len(self.lines(filename)) |
268 return summ |
255 return summ |
269 |
256 |
270 def has_arcs(self): |
257 def __nonzero__(self): |
271 """Does this data have arcs?""" |
258 return bool(self._lines or self._arcs) |
272 return bool(self.arcs) |
259 |
|
260 __bool__ = __nonzero__ |
|
261 |
|
262 def read_fileobj(self, file_obj): |
|
263 """Read the coverage data from the given file object. |
|
264 |
|
265 Should only be used on an empty CoverageData object. |
|
266 |
|
267 """ |
|
268 data = self._read_raw_data(file_obj) |
|
269 |
|
270 self._lines = self._arcs = None |
|
271 |
|
272 if 'lines' in data: |
|
273 self._lines = dict( |
|
274 (fname.encode(sys.getfilesystemencoding()), linenos) |
|
275 for fname, linenos in iitems(data['lines']) |
|
276 ) |
|
277 |
|
278 if 'arcs' in data: |
|
279 self._arcs = dict( |
|
280 (fname.encode(sys.getfilesystemencoding()), |
|
281 [tuple(pair) for pair in arcs]) |
|
282 for fname, arcs in iitems(data['arcs']) |
|
283 ) |
|
284 self._file_tracers = data.get('file_tracers', {}) |
|
285 self._runs = data.get('runs', []) |
|
286 |
|
287 self._validate() |
|
288 |
|
289 def read_file(self, filename): |
|
290 """Read the coverage data from `filename` into this object.""" |
|
291 if self._debug and self._debug.should('dataio'): |
|
292 self._debug.write("Reading data from %r" % (filename,)) |
|
293 with self._open_for_reading(filename) as f: |
|
294 self.read_fileobj(f) |
|
295 |
|
296 _GO_AWAY = "!coverage.py: This is a private format, don't read it directly!" |
|
297 |
|
298 @classmethod |
|
299 def _open_for_reading(cls, filename): |
|
300 """Open a file appropriately for reading data.""" |
|
301 return open(filename, "r") |
|
302 |
|
303 @classmethod |
|
304 def _read_raw_data(cls, file_obj): |
|
305 """Read the raw data from a file object.""" |
|
306 go_away = file_obj.read(len(cls._GO_AWAY)) |
|
307 if go_away != cls._GO_AWAY: |
|
308 raise CoverageException("Doesn't seem to be a coverage.py data file") |
|
309 return json.load(file_obj) |
|
310 |
|
311 @classmethod |
|
312 def _read_raw_data_file(cls, filename): |
|
313 """Read the raw data from a file, for debugging.""" |
|
314 with cls._open_for_reading(filename) as f: |
|
315 return cls._read_raw_data(f) |
|
316 |
|
317 ## |
|
318 ## Writing data |
|
319 ## |
|
320 |
|
321 def add_lines(self, line_data): |
|
322 """Add measured line data. |
|
323 |
|
324 `line_data` is a dictionary mapping file names to dictionaries:: |
|
325 |
|
326 { filename: { lineno: None, ... }, ...} |
|
327 |
|
328 """ |
|
329 if self._debug and self._debug.should('dataop'): |
|
330 self._debug.write("Adding lines: %d files, %d lines total" % ( |
|
331 len(line_data), sum(len(lines) for lines in line_data.values()) |
|
332 )) |
|
333 if self._has_arcs(): |
|
334 raise CoverageException("Can't add lines to existing arc data") |
|
335 |
|
336 if self._lines is None: |
|
337 self._lines = {} |
|
338 for filename, linenos in iitems(line_data): |
|
339 if filename in self._lines: |
|
340 new_linenos = set(self._lines[filename]) |
|
341 new_linenos.update(linenos) |
|
342 linenos = new_linenos |
|
343 self._lines[filename] = list(linenos) |
|
344 |
|
345 self._validate() |
|
346 |
|
347 def add_arcs(self, arc_data): |
|
348 """Add measured arc data. |
|
349 |
|
350 `arc_data` is a dictionary mapping file names to dictionaries:: |
|
351 |
|
352 { filename: { (l1,l2): None, ... }, ...} |
|
353 |
|
354 """ |
|
355 if self._debug and self._debug.should('dataop'): |
|
356 self._debug.write("Adding arcs: %d files, %d arcs total" % ( |
|
357 len(arc_data), sum(len(arcs) for arcs in arc_data.values()) |
|
358 )) |
|
359 if self._has_lines(): |
|
360 raise CoverageException("Can't add arcs to existing line data") |
|
361 |
|
362 if self._arcs is None: |
|
363 self._arcs = {} |
|
364 for filename, arcs in iitems(arc_data): |
|
365 if filename in self._arcs: |
|
366 new_arcs = set(self._arcs[filename]) |
|
367 new_arcs.update(arcs) |
|
368 arcs = new_arcs |
|
369 self._arcs[filename] = list(arcs) |
|
370 |
|
371 self._validate() |
|
372 |
|
373 def add_file_tracers(self, file_tracers): |
|
374 """Add per-file plugin information. |
|
375 |
|
376 `file_tracers` is { filename: plugin_name, ... } |
|
377 |
|
378 """ |
|
379 if self._debug and self._debug.should('dataop'): |
|
380 self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) |
|
381 |
|
382 existing_files = self._arcs or self._lines or {} |
|
383 for filename, plugin_name in iitems(file_tracers): |
|
384 if filename not in existing_files: |
|
385 raise CoverageException( |
|
386 "Can't add file tracer data for unmeasured file '%s'" % (filename,) |
|
387 ) |
|
388 existing_plugin = self._file_tracers.get(filename) |
|
389 if existing_plugin is not None and plugin_name != existing_plugin: |
|
390 raise CoverageException( |
|
391 "Conflicting file tracer name for '%s': %r vs %r" % ( |
|
392 filename, existing_plugin, plugin_name, |
|
393 ) |
|
394 ) |
|
395 self._file_tracers[filename] = plugin_name |
|
396 |
|
397 self._validate() |
|
398 |
|
399 def add_run_info(self, **kwargs): |
|
400 """Add information about the run. |
|
401 |
|
402 Keywords are arbitrary, and are stored in the run dictionary. Values |
|
403 must be JSON serializable. You may use this function more than once, |
|
404 but repeated keywords overwrite each other. |
|
405 |
|
406 """ |
|
407 if self._debug and self._debug.should('dataop'): |
|
408 self._debug.write("Adding run info: %r" % (kwargs,)) |
|
409 if not self._runs: |
|
410 self._runs = [{}] |
|
411 self._runs[0].update(kwargs) |
|
412 self._validate() |
|
413 |
|
414 def touch_file(self, filename): |
|
415 """Ensure that `filename` appears in the data, empty if needed.""" |
|
416 if self._debug and self._debug.should('dataop'): |
|
417 self._debug.write("Touching %r" % (filename,)) |
|
418 if not self._has_arcs() and not self._has_lines(): |
|
419 raise CoverageException("Can't touch files in an empty CoverageData") |
|
420 |
|
421 if self._has_arcs(): |
|
422 where = self._arcs |
|
423 else: |
|
424 where = self._lines |
|
425 where.setdefault(filename, []) |
|
426 |
|
427 self._validate() |
|
428 |
|
429 def write_fileobj(self, file_obj): |
|
430 """Write the coverage data to `file_obj`.""" |
|
431 |
|
432 # Create the file data. |
|
433 file_data = {} |
|
434 |
|
435 if self._has_arcs(): |
|
436 file_data['arcs'] = dict( |
|
437 (fname.decode(sys.getfilesystemencoding()), |
|
438 [tuple(pair) for pair in self._arcs]) |
|
439 for fname, arcs in iitems(data['arcs']) |
|
440 ) |
|
441 |
|
442 if self._has_lines(): |
|
443 file_data['lines'] = dict( |
|
444 (fname.decode(sys.getfilesystemencoding()), linenos) |
|
445 for fname, linenos in iitems(self._lines) |
|
446 ) |
|
447 |
|
448 if self._file_tracers: |
|
449 file_data['file_tracers'] = self._file_tracers |
|
450 |
|
451 if self._runs: |
|
452 file_data['runs'] = self._runs |
|
453 |
|
454 # Write the data to the file. |
|
455 file_obj.write(self._GO_AWAY) |
|
456 json.dump(file_data, file_obj) |
|
457 |
|
458 def write_file(self, filename): |
|
459 """Write the coverage data to `filename`.""" |
|
460 if self._debug and self._debug.should('dataio'): |
|
461 self._debug.write("Writing data to %r" % (filename,)) |
|
462 with open(filename, 'w') as fdata: |
|
463 self.write_fileobj(fdata) |
|
464 |
|
465 def erase(self): |
|
466 """Erase the data in this object.""" |
|
467 self._lines = None |
|
468 self._arcs = None |
|
469 self._file_tracers = {} |
|
470 self._runs = [] |
|
471 self._validate() |
|
472 |
|
473 def update(self, other_data, aliases=None): |
|
474 """Update this data with data from another `CoverageData`. |
|
475 |
|
476 If `aliases` is provided, it's a `PathAliases` object that is used to |
|
477 re-map paths to match the local machine's. |
|
478 |
|
479 """ |
|
480 if self._has_lines() and other_data._has_arcs(): |
|
481 raise CoverageException("Can't combine arc data with line data") |
|
482 if self._has_arcs() and other_data._has_lines(): |
|
483 raise CoverageException("Can't combine line data with arc data") |
|
484 |
|
485 aliases = aliases or PathAliases() |
|
486 |
|
487 # _file_tracers: only have a string, so they have to agree. |
|
488 # Have to do these first, so that our examination of self._arcs and |
|
489 # self._lines won't be confused by data updated from other_data. |
|
490 for filename in other_data.measured_files(): |
|
491 other_plugin = other_data.file_tracer(filename) |
|
492 filename = aliases.map(filename) |
|
493 this_plugin = self.file_tracer(filename) |
|
494 if this_plugin is None: |
|
495 if other_plugin: |
|
496 self._file_tracers[filename] = other_plugin |
|
497 elif this_plugin != other_plugin: |
|
498 raise CoverageException( |
|
499 "Conflicting file tracer name for '%s': %r vs %r" % ( |
|
500 filename, this_plugin, other_plugin, |
|
501 ) |
|
502 ) |
|
503 |
|
504 # _runs: add the new runs to these runs. |
|
505 self._runs.extend(other_data._runs) |
|
506 |
|
507 # _lines: merge dicts. |
|
508 if other_data._has_lines(): |
|
509 if self._lines is None: |
|
510 self._lines = {} |
|
511 for filename, file_lines in iitems(other_data._lines): |
|
512 filename = aliases.map(filename) |
|
513 if filename in self._lines: |
|
514 lines = set(self._lines[filename]) |
|
515 lines.update(file_lines) |
|
516 file_lines = list(lines) |
|
517 self._lines[filename] = file_lines |
|
518 |
|
519 # _arcs: merge dicts. |
|
520 if other_data._has_arcs(): |
|
521 if self._arcs is None: |
|
522 self._arcs = {} |
|
523 for filename, file_arcs in iitems(other_data._arcs): |
|
524 filename = aliases.map(filename) |
|
525 if filename in self._arcs: |
|
526 arcs = set(self._arcs[filename]) |
|
527 arcs.update(file_arcs) |
|
528 file_arcs = list(arcs) |
|
529 self._arcs[filename] = file_arcs |
|
530 |
|
531 self._validate() |
|
532 |
|
533 ## |
|
534 ## Miscellaneous |
|
535 ## |
|
536 |
|
537 def _validate(self): |
|
538 """If we are in paranoid mode, validate that everything is right.""" |
|
539 if env.TESTING: |
|
540 self._validate_invariants() |
|
541 |
|
542 def _validate_invariants(self): |
|
543 """Validate internal invariants.""" |
|
544 # Only one of _lines or _arcs should exist. |
|
545 assert not(self._has_lines() and self._has_arcs()), ( |
|
546 "Shouldn't have both _lines and _arcs" |
|
547 ) |
|
548 |
|
549 # _lines should be a dict of lists of ints. |
|
550 if self._has_lines(): |
|
551 for fname, lines in iitems(self._lines): |
|
552 assert isinstance(fname, string_class), "Key in _lines shouldn't be %r" % (fname,) |
|
553 assert all(isinstance(x, int) for x in lines), ( |
|
554 "_lines[%r] shouldn't be %r" % (fname, lines) |
|
555 ) |
|
556 |
|
557 # _arcs should be a dict of lists of pairs of ints. |
|
558 if self._has_arcs(): |
|
559 for fname, arcs in iitems(self._arcs): |
|
560 assert isinstance(fname, string_class), "Key in _arcs shouldn't be %r" % (fname,) |
|
561 assert all(isinstance(x, int) and isinstance(y, int) for x, y in arcs), ( |
|
562 "_arcs[%r] shouldn't be %r" % (fname, arcs) |
|
563 ) |
|
564 |
|
565 # _file_tracers should have only non-empty strings as values. |
|
566 for fname, plugin in iitems(self._file_tracers): |
|
567 assert isinstance(fname, string_class), ( |
|
568 "Key in _file_tracers shouldn't be %r" % (fname,) |
|
569 ) |
|
570 assert plugin and isinstance(plugin, string_class), ( |
|
571 "_file_tracers[%r] shoudn't be %r" % (fname, plugin) |
|
572 ) |
|
573 |
|
574 # _runs should be a list of dicts. |
|
575 for val in self._runs: |
|
576 assert isinstance(val, dict) |
|
577 for key in val: |
|
578 assert isinstance(key, string_class), "Key in _runs shouldn't be %r" % (key,) |
|
579 |
|
580 def add_to_hash(self, filename, hasher): |
|
581 """Contribute `filename`'s data to the `hasher`. |
|
582 |
|
583 `hasher` is a :class:`coverage.misc.Hasher` instance to be updated with |
|
584 the file's data. It should only get the results data, not the run |
|
585 data. |
|
586 |
|
587 """ |
|
588 if self._has_arcs(): |
|
589 hasher.update(sorted(self.arcs(filename) or [])) |
|
590 else: |
|
591 hasher.update(sorted(self.lines(filename) or [])) |
|
592 hasher.update(self.file_tracer(filename)) |
|
593 |
|
594 ## |
|
595 ## Internal |
|
596 ## |
|
597 |
|
598 def _has_lines(self): |
|
599 """Do we have data in self._lines?""" |
|
600 return self._lines is not None |
|
601 |
|
602 def _has_arcs(self): |
|
603 """Do we have data in self._arcs?""" |
|
604 return self._arcs is not None |
|
605 |
|
606 |
|
607 class CoverageDataFiles(object): |
|
608 """Manage the use of coverage data files.""" |
|
609 |
|
610 def __init__(self, basename=None): |
|
611 """Create a CoverageDataFiles to manage data files. |
|
612 |
|
613 `basename` is the name of the file to use for storing data. |
|
614 |
|
615 """ |
|
616 # Construct the file name that will be used for data storage. |
|
617 self.filename = os.path.abspath(basename or ".coverage") |
|
618 |
|
619 def erase(self, parallel=False): |
|
620 """Erase the data from the file storage. |
|
621 |
|
622 If `parallel` is true, then also deletes data files created from the |
|
623 basename by parallel-mode. |
|
624 |
|
625 """ |
|
626 file_be_gone(self.filename) |
|
627 if parallel: |
|
628 data_dir, local = os.path.split(self.filename) |
|
629 localdot = local + '.*' |
|
630 pattern = os.path.join(os.path.abspath(data_dir), localdot) |
|
631 for filename in glob.glob(pattern): |
|
632 file_be_gone(filename) |
|
633 |
|
634 def read(self, data): |
|
635 """Read the coverage data.""" |
|
636 if os.path.exists(self.filename): |
|
637 data.read_file(self.filename) |
|
638 |
|
639 def write(self, data, suffix=None): |
|
640 """Write the collected coverage data to a file. |
|
641 |
|
642 `suffix` is a suffix to append to the base file name. This can be used |
|
643 for multiple or parallel execution, so that many coverage data files |
|
644 can exist simultaneously. A dot will be used to join the base name and |
|
645 the suffix. |
|
646 |
|
647 """ |
|
648 filename = self.filename |
|
649 if suffix is True: |
|
650 # If data_suffix was a simple true value, then make a suffix with |
|
651 # plenty of distinguishing information. We do this here in |
|
652 # `save()` at the last minute so that the pid will be correct even |
|
653 # if the process forks. |
|
654 extra = "" |
|
655 if _TEST_NAME_FILE: # pragma: debugging |
|
656 with open(_TEST_NAME_FILE) as f: |
|
657 test_name = f.read() |
|
658 extra = "." + test_name |
|
659 suffix = "%s%s.%s.%06d" % ( |
|
660 socket.gethostname(), extra, os.getpid(), |
|
661 random.randint(0, 999999) |
|
662 ) |
|
663 |
|
664 if suffix: |
|
665 filename += "." + suffix |
|
666 data.write_file(filename) |
|
667 |
|
668 def combine_parallel_data(self, data, aliases=None, data_paths=None): |
|
669 """Combine a number of data files together. |
|
670 |
|
671 Treat `self.filename` as a file prefix, and combine the data from all |
|
672 of the data files starting with that prefix plus a dot. |
|
673 |
|
674 If `aliases` is provided, it's a `PathAliases` object that is used to |
|
675 re-map paths to match the local machine's. |
|
676 |
|
677 If `data_paths` is provided, it is a list of directories or files to |
|
678 combine. Directories are searched for files that start with |
|
679 `self.filename` plus dot as a prefix, and those files are combined. |
|
680 |
|
681 If `data_paths` is not provided, then the directory portion of |
|
682 `self.filename` is used as the directory to search for data files. |
|
683 |
|
684 Every data file found and combined is then deleted from disk. |
|
685 |
|
686 """ |
|
687 # Because of the os.path.abspath in the constructor, data_dir will |
|
688 # never be an empty string. |
|
689 data_dir, local = os.path.split(self.filename) |
|
690 localdot = local + '.*' |
|
691 |
|
692 data_paths = data_paths or [data_dir] |
|
693 files_to_combine = [] |
|
694 for p in data_paths: |
|
695 if os.path.isfile(p): |
|
696 files_to_combine.append(os.path.abspath(p)) |
|
697 elif os.path.isdir(p): |
|
698 pattern = os.path.join(os.path.abspath(p), localdot) |
|
699 files_to_combine.extend(glob.glob(pattern)) |
|
700 else: |
|
701 raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) |
|
702 |
|
703 for f in files_to_combine: |
|
704 new_data = CoverageData() |
|
705 new_data.read_file(f) |
|
706 data.update(new_data, aliases=aliases) |
|
707 file_be_gone(f) |
|
708 |
|
709 |
|
710 def canonicalize_json_data(data): |
|
711 """Canonicalize our JSON data so it can be compared.""" |
|
712 for fname, lines in iitems(data.get('lines', {})): |
|
713 data['lines'][fname] = sorted(lines) |
|
714 for fname, arcs in iitems(data.get('arcs', {})): |
|
715 data['arcs'][fname] = sorted(arcs) |
|
716 |
|
717 |
|
718 def pretty_data(data): |
|
719 """Format data as JSON, but as nicely as possible. |
|
720 |
|
721 Returns a string. |
|
722 |
|
723 """ |
|
724 # Start with a basic JSON dump. |
|
725 out = json.dumps(data, indent=4, sort_keys=True) |
|
726 # But pairs of numbers shouldn't be split across lines... |
|
727 out = re.sub(r"\[\s+(-?\d+),\s+(-?\d+)\s+]", r"[\1, \2]", out) |
|
728 # Trailing spaces mess with tests, get rid of them. |
|
729 out = re.sub(r"(?m)\s+$", "", out) |
|
730 return out |
|
731 |
|
732 |
|
733 def debug_main(args): |
|
734 """Dump the raw data from data files. |
|
735 |
|
736 Run this as:: |
|
737 |
|
738 $ python -m coverage.data [FILE] |
|
739 |
|
740 """ |
|
741 parser = optparse.OptionParser() |
|
742 parser.add_option( |
|
743 "-c", "--canonical", action="store_true", |
|
744 help="Sort data into a canonical order", |
|
745 ) |
|
746 options, args = parser.parse_args(args) |
|
747 |
|
748 for filename in (args or [".coverage"]): |
|
749 print("--- {0} ------------------------------".format(filename)) |
|
750 data = CoverageData._read_raw_data_file(filename) |
|
751 if options.canonical: |
|
752 canonicalize_json_data(data) |
|
753 print(pretty_data(data)) |
273 |
754 |
274 |
755 |
275 if __name__ == '__main__': |
756 if __name__ == '__main__': |
276 # Ad-hoc: show the raw data in a data file. |
757 debug_main(sys.argv[1:]) |
277 import pprint, sys |
|
278 covdata = CoverageData() |
|
279 if sys.argv[1:]: |
|
280 fname = sys.argv[1] |
|
281 else: |
|
282 fname = covdata.filename |
|
283 pprint.pprint(covdata.raw_data(fname)) |
|
284 |
758 |
285 # |
759 # |
286 # eflag: FileType = Python2 |
760 # eflag: FileType = Python2 |