DebugClients/Python/coverage/data.py

changeset 4491
0d8612e24fef
parent 4490
3f58261e7bb1
child 5051
3586ebd9fac8
equal deleted inserted replaced
4487:4ba7a8ab24f2 4491:0d8612e24fef
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

eric ide

mercurial