414 if not self._runs: |
414 if not self._runs: |
415 self._runs = [{}] |
415 self._runs = [{}] |
416 self._runs[0].update(kwargs) |
416 self._runs[0].update(kwargs) |
417 self._validate() |
417 self._validate() |
418 |
418 |
419 def touch_file(self, filename): |
419 def touch_file(self, filename, plugin_name=""): |
420 """Ensure that `filename` appears in the data, empty if needed.""" |
420 """Ensure that `filename` appears in the data, empty if needed. |
|
421 |
|
422 `plugin_name` is the name of the plugin resposible for this file. It is used |
|
423 to associate the right filereporter, etc. |
|
424 """ |
421 if self._debug and self._debug.should('dataop'): |
425 if self._debug and self._debug.should('dataop'): |
422 self._debug.write("Touching %r" % (filename,)) |
426 self._debug.write("Touching %r" % (filename,)) |
423 if not self._has_arcs() and not self._has_lines(): |
427 if not self._has_arcs() and not self._has_lines(): |
424 raise CoverageException("Can't touch files in an empty CoverageData") |
428 raise CoverageException("Can't touch files in an empty CoverageData") |
425 |
429 |
426 if self._has_arcs(): |
430 if self._has_arcs(): |
427 where = self._arcs |
431 where = self._arcs |
428 else: |
432 else: |
429 where = self._lines |
433 where = self._lines |
430 where.setdefault(filename, []) |
434 where.setdefault(filename, []) |
|
435 if plugin_name: |
|
436 # Set the tracer for this file |
|
437 self._file_tracers[filename] = plugin_name |
431 |
438 |
432 self._validate() |
439 self._validate() |
433 |
440 |
434 def write_fileobj(self, file_obj): |
441 def write_fileobj(self, file_obj): |
435 """Write the coverage data to `file_obj`.""" |
442 """Write the coverage data to `file_obj`.""" |
449 if self._runs: |
456 if self._runs: |
450 file_data['runs'] = self._runs |
457 file_data['runs'] = self._runs |
451 |
458 |
452 # Write the data to the file. |
459 # Write the data to the file. |
453 file_obj.write(self._GO_AWAY) |
460 file_obj.write(self._GO_AWAY) |
454 json.dump(file_data, file_obj) |
461 json.dump(file_data, file_obj, separators=(',', ':')) |
455 |
462 |
456 def write_file(self, filename): |
463 def write_file(self, filename): |
457 """Write the coverage data to `filename`.""" |
464 """Write the coverage data to `filename`.""" |
458 if self._debug and self._debug.should('dataio'): |
465 if self._debug and self._debug.should('dataio'): |
459 self._debug.write("Writing data to %r" % (filename,)) |
466 self._debug.write("Writing data to %r" % (filename,)) |
603 |
610 |
604 |
611 |
605 class CoverageDataFiles(object): |
612 class CoverageDataFiles(object): |
606 """Manage the use of coverage data files.""" |
613 """Manage the use of coverage data files.""" |
607 |
614 |
608 def __init__(self, basename=None, warn=None): |
615 def __init__(self, basename=None, warn=None, debug=None): |
609 """Create a CoverageDataFiles to manage data files. |
616 """Create a CoverageDataFiles to manage data files. |
610 |
617 |
611 `warn` is the warning function to use. |
618 `warn` is the warning function to use. |
612 |
619 |
613 `basename` is the name of the file to use for storing data. |
620 `basename` is the name of the file to use for storing data. |
614 |
621 |
|
622 `debug` is a `DebugControl` object for writing debug messages. |
|
623 |
615 """ |
624 """ |
616 self.warn = warn |
625 self.warn = warn |
|
626 self.debug = debug |
|
627 |
617 # Construct the file name that will be used for data storage. |
628 # Construct the file name that will be used for data storage. |
618 self.filename = os.path.abspath(basename or ".coverage") |
629 self.filename = os.path.abspath(basename or ".coverage") |
619 |
630 |
620 def erase(self, parallel=False): |
631 def erase(self, parallel=False): |
621 """Erase the data from the file storage. |
632 """Erase the data from the file storage. |
622 |
633 |
623 If `parallel` is true, then also deletes data files created from the |
634 If `parallel` is true, then also deletes data files created from the |
624 basename by parallel-mode. |
635 basename by parallel-mode. |
625 |
636 |
626 """ |
637 """ |
|
638 if self.debug and self.debug.should('dataio'): |
|
639 self.debug.write("Erasing data file %r" % (self.filename,)) |
627 file_be_gone(self.filename) |
640 file_be_gone(self.filename) |
628 if parallel: |
641 if parallel: |
629 data_dir, local = os.path.split(self.filename) |
642 data_dir, local = os.path.split(self.filename) |
630 localdot = local + '.*' |
643 localdot = local + '.*' |
631 pattern = os.path.join(os.path.abspath(data_dir), localdot) |
644 pattern = os.path.join(os.path.abspath(data_dir), localdot) |
632 for filename in glob.glob(pattern): |
645 for filename in glob.glob(pattern): |
|
646 if self.debug and self.debug.should('dataio'): |
|
647 self.debug.write("Erasing parallel data file %r" % (filename,)) |
633 file_be_gone(filename) |
648 file_be_gone(filename) |
634 |
649 |
635 def read(self, data): |
650 def read(self, data): |
636 """Read the coverage data.""" |
651 """Read the coverage data.""" |
637 if os.path.exists(self.filename): |
652 if os.path.exists(self.filename): |
655 extra = "" |
670 extra = "" |
656 if _TEST_NAME_FILE: # pragma: debugging |
671 if _TEST_NAME_FILE: # pragma: debugging |
657 with open(_TEST_NAME_FILE) as f: |
672 with open(_TEST_NAME_FILE) as f: |
658 test_name = f.read() |
673 test_name = f.read() |
659 extra = "." + test_name |
674 extra = "." + test_name |
660 suffix = "%s%s.%s.%06d" % ( |
675 dice = random.Random(os.urandom(8)).randint(0, 999999) |
661 socket.gethostname(), extra, os.getpid(), |
676 suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) |
662 random.randint(0, 999999) |
|
663 ) |
|
664 |
677 |
665 if suffix: |
678 if suffix: |
666 filename += "." + suffix |
679 filename += "." + suffix |
667 data.write_file(filename) |
680 data.write_file(filename) |
668 |
681 |
669 def combine_parallel_data(self, data, aliases=None, data_paths=None): |
682 def combine_parallel_data(self, data, aliases=None, data_paths=None, strict=False): |
670 """Combine a number of data files together. |
683 """Combine a number of data files together. |
671 |
684 |
672 Treat `self.filename` as a file prefix, and combine the data from all |
685 Treat `self.filename` as a file prefix, and combine the data from all |
673 of the data files starting with that prefix plus a dot. |
686 of the data files starting with that prefix plus a dot. |
674 |
687 |
683 `self.filename` is used as the directory to search for data files. |
696 `self.filename` is used as the directory to search for data files. |
684 |
697 |
685 Every data file found and combined is then deleted from disk. If a file |
698 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 |
699 cannot be read, a warning will be issued, and the file will not be |
687 deleted. |
700 deleted. |
|
701 |
|
702 If `strict` is true, and no files are found to combine, an error is |
|
703 raised. |
688 |
704 |
689 """ |
705 """ |
690 # Because of the os.path.abspath in the constructor, data_dir will |
706 # Because of the os.path.abspath in the constructor, data_dir will |
691 # never be an empty string. |
707 # never be an empty string. |
692 data_dir, local = os.path.split(self.filename) |
708 data_dir, local = os.path.split(self.filename) |
701 pattern = os.path.join(os.path.abspath(p), localdot) |
717 pattern = os.path.join(os.path.abspath(p), localdot) |
702 files_to_combine.extend(glob.glob(pattern)) |
718 files_to_combine.extend(glob.glob(pattern)) |
703 else: |
719 else: |
704 raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) |
720 raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) |
705 |
721 |
|
722 if strict and not files_to_combine: |
|
723 raise CoverageException("No data to combine") |
|
724 |
|
725 files_combined = 0 |
706 for f in files_to_combine: |
726 for f in files_to_combine: |
707 new_data = CoverageData() |
727 new_data = CoverageData(debug=self.debug) |
708 try: |
728 try: |
709 new_data.read_file(f) |
729 new_data.read_file(f) |
710 except CoverageException as exc: |
730 except CoverageException as exc: |
711 if self.warn: |
731 if self.warn: |
712 # The CoverageException has the file name in it, so just |
732 # The CoverageException has the file name in it, so just |
713 # use the message as the warning. |
733 # use the message as the warning. |
714 self.warn(str(exc)) |
734 self.warn(str(exc)) |
715 else: |
735 else: |
716 data.update(new_data, aliases=aliases) |
736 data.update(new_data, aliases=aliases) |
|
737 files_combined += 1 |
|
738 if self.debug and self.debug.should('dataio'): |
|
739 self.debug.write("Deleting combined data file %r" % (f,)) |
717 file_be_gone(f) |
740 file_be_gone(f) |
|
741 |
|
742 if strict and not files_combined: |
|
743 raise CoverageException("No usable data files") |
718 |
744 |
719 |
745 |
720 def canonicalize_json_data(data): |
746 def canonicalize_json_data(data): |
721 """Canonicalize our JSON data so it can be compared.""" |
747 """Canonicalize our JSON data so it can be compared.""" |
722 for fname, lines in iitems(data.get('lines', {})): |
748 for fname, lines in iitems(data.get('lines', {})): |