DebugClients/Python/coverage/data.py

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

eric ide

mercurial