2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
3 |
3 |
4 """Coverage data for coverage.py.""" |
4 """Coverage data for coverage.py.""" |
5 |
5 |
6 import glob |
6 import glob |
|
7 import itertools |
7 import json |
8 import json |
8 import optparse |
9 import optparse |
9 import os |
10 import os |
10 import os.path |
11 import os.path |
11 import random |
12 import random |
12 import re |
13 import re |
13 import socket |
14 import socket |
14 import sys |
|
15 |
15 |
16 from coverage import env |
16 from coverage import env |
17 from coverage.backward import iitems, string_class |
17 from coverage.backward import iitems, string_class |
18 from coverage.debug import _TEST_NAME_FILE |
18 from coverage.debug import _TEST_NAME_FILE |
19 from coverage.files import PathAliases |
19 from coverage.files import PathAliases |
20 from coverage.misc import CoverageException, file_be_gone |
20 from coverage.misc import CoverageException, file_be_gone, isolate_module |
|
21 |
|
22 os = isolate_module(os) |
21 |
23 |
22 |
24 |
23 class CoverageData(object): |
25 class CoverageData(object): |
24 """Manages collected coverage data, including file storage. |
26 """Manages collected coverage data, including file storage. |
25 |
27 |
175 If the file was executed, returns a list of integers, the line numbers |
177 If the file was executed, returns a list of integers, the line numbers |
176 executed in the file. The list is in no particular order. |
178 executed in the file. The list is in no particular order. |
177 |
179 |
178 """ |
180 """ |
179 if self._arcs is not None: |
181 if self._arcs is not None: |
180 if filename in self._arcs: |
182 arcs = self._arcs.get(filename) |
181 return [s for s, __ in self._arcs[filename] if s > 0] |
183 if arcs is not None: |
|
184 all_lines = itertools.chain.from_iterable(arcs) |
|
185 return list(set(l for l in all_lines if l > 0)) |
182 elif self._lines is not None: |
186 elif self._lines is not None: |
183 if filename in self._lines: |
187 return self._lines.get(filename) |
184 return self._lines[filename] |
|
185 return None |
188 return None |
186 |
189 |
187 def arcs(self, filename): |
190 def arcs(self, filename): |
188 """Get the list of arcs executed for a file. |
191 """Get the list of arcs executed for a file. |
189 |
192 |
268 data = self._read_raw_data(file_obj) |
271 data = self._read_raw_data(file_obj) |
269 |
272 |
270 self._lines = self._arcs = None |
273 self._lines = self._arcs = None |
271 |
274 |
272 if 'lines' in data: |
275 if 'lines' in data: |
273 self._lines = dict( |
276 self._lines = data['lines'] |
274 (fname.encode(sys.getfilesystemencoding()), linenos) |
|
275 for fname, linenos in iitems(data['lines']) |
|
276 ) |
|
277 |
|
278 if 'arcs' in data: |
277 if 'arcs' in data: |
279 self._arcs = dict( |
278 self._arcs = dict( |
280 (fname.encode(sys.getfilesystemencoding()), |
279 (fname, [tuple(pair) for pair in arcs]) |
281 [tuple(pair) for pair in arcs]) |
|
282 for fname, arcs in iitems(data['arcs']) |
280 for fname, arcs in iitems(data['arcs']) |
283 ) |
281 ) |
284 self._file_tracers = data.get('file_tracers', {}) |
282 self._file_tracers = data.get('file_tracers', {}) |
285 self._runs = data.get('runs', []) |
283 self._runs = data.get('runs', []) |
286 |
284 |
288 |
286 |
289 def read_file(self, filename): |
287 def read_file(self, filename): |
290 """Read the coverage data from `filename` into this object.""" |
288 """Read the coverage data from `filename` into this object.""" |
291 if self._debug and self._debug.should('dataio'): |
289 if self._debug and self._debug.should('dataio'): |
292 self._debug.write("Reading data from %r" % (filename,)) |
290 self._debug.write("Reading data from %r" % (filename,)) |
293 with self._open_for_reading(filename) as f: |
291 try: |
294 self.read_fileobj(f) |
292 with self._open_for_reading(filename) as f: |
|
293 self.read_fileobj(f) |
|
294 except Exception as exc: |
|
295 raise CoverageException( |
|
296 "Couldn't read data from '%s': %s: %s" % ( |
|
297 filename, exc.__class__.__name__, exc, |
|
298 ) |
|
299 ) |
295 |
300 |
296 _GO_AWAY = "!coverage.py: This is a private format, don't read it directly!" |
301 _GO_AWAY = "!coverage.py: This is a private format, don't read it directly!" |
297 |
302 |
298 @classmethod |
303 @classmethod |
299 def _open_for_reading(cls, filename): |
304 def _open_for_reading(cls, filename): |
431 |
436 |
432 # Create the file data. |
437 # Create the file data. |
433 file_data = {} |
438 file_data = {} |
434 |
439 |
435 if self._has_arcs(): |
440 if self._has_arcs(): |
436 file_data['arcs'] = dict( |
441 file_data['arcs'] = self._arcs |
437 (fname.decode(sys.getfilesystemencoding()), |
|
438 [tuple(pair) for pair in self._arcs]) |
|
439 for fname, arcs in iitems(data['arcs']) |
|
440 ) |
|
441 |
442 |
442 if self._has_lines(): |
443 if self._has_lines(): |
443 file_data['lines'] = dict( |
444 file_data['lines'] = self._lines |
444 (fname.decode(sys.getfilesystemencoding()), linenos) |
|
445 for fname, linenos in iitems(self._lines) |
|
446 ) |
|
447 |
445 |
448 if self._file_tracers: |
446 if self._file_tracers: |
449 file_data['file_tracers'] = self._file_tracers |
447 file_data['file_tracers'] = self._file_tracers |
450 |
448 |
451 if self._runs: |
449 if self._runs: |
578 assert isinstance(key, string_class), "Key in _runs shouldn't be %r" % (key,) |
576 assert isinstance(key, string_class), "Key in _runs shouldn't be %r" % (key,) |
579 |
577 |
580 def add_to_hash(self, filename, hasher): |
578 def add_to_hash(self, filename, hasher): |
581 """Contribute `filename`'s data to the `hasher`. |
579 """Contribute `filename`'s data to the `hasher`. |
582 |
580 |
583 `hasher` is a :class:`coverage.misc.Hasher` instance to be updated with |
581 `hasher` is a `coverage.misc.Hasher` instance to be updated with |
584 the file's data. It should only get the results data, not the run |
582 the file's data. It should only get the results data, not the run |
585 data. |
583 data. |
586 |
584 |
587 """ |
585 """ |
588 if self._has_arcs(): |
586 if self._has_arcs(): |
605 |
603 |
606 |
604 |
607 class CoverageDataFiles(object): |
605 class CoverageDataFiles(object): |
608 """Manage the use of coverage data files.""" |
606 """Manage the use of coverage data files.""" |
609 |
607 |
610 def __init__(self, basename=None): |
608 def __init__(self, basename=None, warn=None): |
611 """Create a CoverageDataFiles to manage data files. |
609 """Create a CoverageDataFiles to manage data files. |
612 |
610 |
|
611 `warn` is the warning function to use. |
|
612 |
613 `basename` is the name of the file to use for storing data. |
613 `basename` is the name of the file to use for storing data. |
614 |
614 |
615 """ |
615 """ |
|
616 self.warn = warn |
616 # Construct the file name that will be used for data storage. |
617 # Construct the file name that will be used for data storage. |
617 self.filename = os.path.abspath(basename or ".coverage") |
618 self.filename = os.path.abspath(basename or ".coverage") |
618 |
619 |
619 def erase(self, parallel=False): |
620 def erase(self, parallel=False): |
620 """Erase the data from the file storage. |
621 """Erase the data from the file storage. |
679 `self.filename` plus dot as a prefix, and those files are combined. |
680 `self.filename` plus dot as a prefix, and those files are combined. |
680 |
681 |
681 If `data_paths` is not provided, then the directory portion of |
682 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 `self.filename` is used as the directory to search for data files. |
683 |
684 |
684 Every data file found and combined is then deleted from disk. |
685 Every data file found and combined is then deleted from disk. If a file |
|
686 cannot be read, a warning will be issued, and the file will not be |
|
687 deleted. |
685 |
688 |
686 """ |
689 """ |
687 # Because of the os.path.abspath in the constructor, data_dir will |
690 # Because of the os.path.abspath in the constructor, data_dir will |
688 # never be an empty string. |
691 # never be an empty string. |
689 data_dir, local = os.path.split(self.filename) |
692 data_dir, local = os.path.split(self.filename) |
700 else: |
703 else: |
701 raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) |
704 raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) |
702 |
705 |
703 for f in files_to_combine: |
706 for f in files_to_combine: |
704 new_data = CoverageData() |
707 new_data = CoverageData() |
705 new_data.read_file(f) |
708 try: |
706 data.update(new_data, aliases=aliases) |
709 new_data.read_file(f) |
707 file_be_gone(f) |
710 except CoverageException as exc: |
|
711 if self.warn: |
|
712 # The CoverageException has the file name in it, so just |
|
713 # use the message as the warning. |
|
714 self.warn(str(exc)) |
|
715 else: |
|
716 data.update(new_data, aliases=aliases) |
|
717 file_be_gone(f) |
708 |
718 |
709 |
719 |
710 def canonicalize_json_data(data): |
720 def canonicalize_json_data(data): |
711 """Canonicalize our JSON data so it can be compared.""" |
721 """Canonicalize our JSON data so it can be compared.""" |
712 for fname, lines in iitems(data.get('lines', {})): |
722 for fname, lines in iitems(data.get('lines', {})): |