From c24e594796b860531521be0190fc2f922c092c0e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 15 Jul 2018 11:59:33 -0400 Subject: CoverageData now also handles file operations --- coverage/cmdline.py | 2 +- coverage/control.py | 20 ++-- coverage/data.py | 272 +++++++++++++++++++++++--------------------------- tests/test_cmdline.py | 11 +- tests/test_data.py | 73 ++++++-------- 5 files changed, 173 insertions(+), 205 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index fba1112f..4d1d1e72 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -661,7 +661,7 @@ class CoverageScript(object): self.coverage.load() data = self.coverage.get_data() print(info_header("data")) - print("path: %s" % self.coverage._data_files.filename) + print("path: %s" % self.coverage.get_data().filename) if data: print("has_arcs: %r" % data.has_arcs()) summary = data.line_counts(fullpath=True) diff --git a/coverage/control.py b/coverage/control.py index a5943aa8..1760ee78 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -15,7 +15,7 @@ from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems from coverage.collector import Collector from coverage.config import read_coverage_config -from coverage.data import CoverageData, CoverageDataFiles +from coverage.data import CoverageData from coverage.debug import DebugControl, write_formatted_info from coverage.disposition import disposition_debug_msg from coverage.files import PathAliases, set_relative_directory, abs_file @@ -152,7 +152,7 @@ class Coverage(object): self._warnings = [] # Other instance attributes, set later. - self._data = self._data_files = self._collector = None + self._data = self._collector = None self._plugins = None self._inorout = None self._inorout_class = InOrOut @@ -270,8 +270,7 @@ class Coverage(object): # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. - self._data = CoverageData(debug=self._debug) - self._data_files = CoverageDataFiles( + self._data = CoverageData( basename=self.config.data_file, warn=self._warn, debug=self._debug, ) @@ -395,7 +394,7 @@ class Coverage(object): """Load previously-collected coverage data from the data file.""" self._init() self._collector.reset() - self._data_files.read(self._data) + self._data.read() def start(self): """Start measuring code coverage. @@ -449,8 +448,7 @@ class Coverage(object): """ self._init() self._collector.reset() - self._data.erase() - self._data_files.erase(parallel=self.config.parallel) + self._data.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -503,7 +501,7 @@ class Coverage(object): """Save the collected coverage data to the data file.""" self._init() data = self.get_data() - self._data_files.write(data, suffix=self._data_suffix) + data.write(suffix=self._data_suffix) def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -538,9 +536,7 @@ class Coverage(object): for pattern in paths[1:]: aliases.add(pattern, result) - self._data_files.combine_parallel_data( - self._data, aliases=aliases, data_paths=data_paths, strict=strict, - ) + self._data.combine_parallel_data(aliases=aliases, data_paths=data_paths, strict=strict) def get_data(self): """Get the collected data. @@ -827,7 +823,7 @@ class Coverage(object): ('configs_attempted', self.config.attempted_config_files), ('configs_read', self.config.config_files_read), ('config_file', self.config.config_file), - ('data_path', self._data_files.filename), + ('data_path', self._data.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), diff --git a/coverage/data.py b/coverage/data.py index 9f2d1308..6d30e2ba 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -57,8 +57,7 @@ class CoverageData(object): names in this API are case-sensitive, even on platforms with case-insensitive file systems. - To read a coverage.py data file, use :meth:`read_file`, or - :meth:`read_fileobj` if you have an already-opened file. You can then + To read a coverage.py data file, use :meth:`read_file`. You can then access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, or :meth:`file_tracer`. Run information is available with :meth:`run_infos`. @@ -78,8 +77,7 @@ class CoverageData(object): To add a file without any measured data, use :meth:`touch_file`. - You write to a named file with :meth:`write_file`, or to an already opened - file with :meth:`write_fileobj`. + You write to a named file with :meth:`write_file`. You can clear the data in memory with :meth:`erase`. Two data collections can be combined by using :meth:`update` on one :class:`CoverageData`, @@ -112,13 +110,19 @@ class CoverageData(object): # line data is easily recovered from the arcs: it is all the first elements # of the pairs that are greater than zero. - def __init__(self, debug=None): + def __init__(self, basename=None, warn=None, debug=None): """Create a CoverageData. + `warn` is the warning function to use. + + `basename` is the name of the file to use for storing data. + `debug` is a `DebugControl` object for writing debug messages. """ + self._warn = warn self._debug = debug + self.filename = os.path.abspath(basename or ".coverage") # A map from canonical Python source file name to a dictionary in # which there's an entry for each line number that has been @@ -262,7 +266,12 @@ class CoverageData(object): __bool__ = __nonzero__ - def read_fileobj(self, file_obj): + def read(self): + """Read the coverage data.""" + if os.path.exists(self.filename): + self.read_file(self.filename) + + def _read_fileobj(self, file_obj): """Read the coverage data from the given file object. Should only be used on an empty CoverageData object. @@ -290,7 +299,7 @@ class CoverageData(object): self._debug.write("Reading data from %r" % (filename,)) try: with self._open_for_reading(filename) as f: - self.read_fileobj(f) + self._read_fileobj(f) except Exception as exc: raise CoverageException( "Couldn't read data from '%s': %s: %s" % ( @@ -438,7 +447,34 @@ class CoverageData(object): self._validate() - def write_fileobj(self, file_obj): + def write(self, suffix=None): + """Write the collected coverage data to a file. + + `suffix` is a suffix to append to the base file name. This can be used + for multiple or parallel execution, so that many coverage data files + can exist simultaneously. A dot will be used to join the base name and + the suffix. + + """ + filename = self.filename + if suffix is True: + # If data_suffix was a simple true value, then make a suffix with + # plenty of distinguishing information. We do this here in + # `save()` at the last minute so that the pid will be correct even + # if the process forks. + extra = "" + if _TEST_NAME_FILE: # pragma: debugging + with open(_TEST_NAME_FILE) as f: + test_name = f.read() + extra = "." + test_name + dice = random.Random(os.urandom(8)).randint(0, 999999) + suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) + + if suffix: + filename += "." + suffix + self.write_file(filename) + + def _write_fileobj(self, file_obj): """Write the coverage data to `file_obj`.""" # Create the file data. @@ -465,16 +501,33 @@ class CoverageData(object): if self._debug and self._debug.should('dataio'): self._debug.write("Writing data to %r" % (filename,)) with open(filename, 'w') as fdata: - self.write_fileobj(fdata) + self._write_fileobj(fdata) + + def erase(self, parallel=False): + """Erase the data in this object. + + If `parallel` is true, then also deletes data files created from the + basename by parallel-mode. - def erase(self): - """Erase the data in this object.""" + """ self._lines = None self._arcs = None self._file_tracers = {} self._runs = [] self._validate() + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing data file %r" % (self.filename,)) + file_be_gone(self.filename) + if parallel: + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + pattern = os.path.join(os.path.abspath(data_dir), localdot) + for filename in glob.glob(pattern): + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing parallel data file %r" % (filename,)) + file_be_gone(filename) + def update(self, other_data, aliases=None): """Update this data with data from another `CoverageData`. @@ -535,6 +588,69 @@ class CoverageData(object): self._validate() + def combine_parallel_data(self, aliases=None, data_paths=None, strict=False): + """Combine a number of data files together. + + Treat `self.filename` as a file prefix, and combine the data from all + of the data files starting with that prefix plus a dot. + + If `aliases` is provided, it's a `PathAliases` object that is used to + re-map paths to match the local machine's. + + If `data_paths` is provided, it is a list of directories or files to + combine. Directories are searched for files that start with + `self.filename` plus dot as a prefix, and those files are combined. + + If `data_paths` is not provided, then the directory portion of + `self.filename` is used as the directory to search for data files. + + Every data file found and combined is then deleted from disk. If a file + cannot be read, a warning will be issued, and the file will not be + deleted. + + If `strict` is true, and no files are found to combine, an error is + raised. + + """ + # Because of the os.path.abspath in the constructor, data_dir will + # never be an empty string. + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + + data_paths = data_paths or [data_dir] + files_to_combine = [] + for p in data_paths: + if os.path.isfile(p): + files_to_combine.append(os.path.abspath(p)) + elif os.path.isdir(p): + pattern = os.path.join(os.path.abspath(p), localdot) + files_to_combine.extend(glob.glob(pattern)) + else: + raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) + + if strict and not files_to_combine: + raise CoverageException("No data to combine") + + files_combined = 0 + for f in files_to_combine: + new_data = CoverageData(debug=self._debug) + try: + new_data.read_file(f) + except CoverageException as exc: + if self._warn: + # The CoverageException has the file name in it, so just + # use the message as the warning. + self._warn(str(exc)) + else: + self.update(new_data, aliases=aliases) + files_combined += 1 + if self._debug and self._debug.should('dataio'): + self._debug.write("Deleting combined data file %r" % (f,)) + file_be_gone(f) + + if strict and not files_combined: + raise CoverageException("No usable data files") + ## ## Miscellaneous ## @@ -609,140 +725,6 @@ class CoverageData(object): return self._arcs is not None -class CoverageDataFiles(object): - """Manage the use of coverage data files.""" - - def __init__(self, basename=None, warn=None, debug=None): - """Create a CoverageDataFiles to manage data files. - - `warn` is the warning function to use. - - `basename` is the name of the file to use for storing data. - - `debug` is a `DebugControl` object for writing debug messages. - - """ - self.warn = warn - self.debug = debug - - # Construct the file name that will be used for data storage. - self.filename = os.path.abspath(basename or ".coverage") - - def erase(self, parallel=False): - """Erase the data from the file storage. - - If `parallel` is true, then also deletes data files created from the - basename by parallel-mode. - - """ - if self.debug and self.debug.should('dataio'): - self.debug.write("Erasing data file %r" % (self.filename,)) - file_be_gone(self.filename) - if parallel: - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - pattern = os.path.join(os.path.abspath(data_dir), localdot) - for filename in glob.glob(pattern): - if self.debug and self.debug.should('dataio'): - self.debug.write("Erasing parallel data file %r" % (filename,)) - file_be_gone(filename) - - def read(self, data): - """Read the coverage data.""" - if os.path.exists(self.filename): - data.read_file(self.filename) - - def write(self, data, suffix=None): - """Write the collected coverage data to a file. - - `suffix` is a suffix to append to the base file name. This can be used - for multiple or parallel execution, so that many coverage data files - can exist simultaneously. A dot will be used to join the base name and - the suffix. - - """ - filename = self.filename - if suffix is True: - # If data_suffix was a simple true value, then make a suffix with - # plenty of distinguishing information. We do this here in - # `save()` at the last minute so that the pid will be correct even - # if the process forks. - extra = "" - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE) as f: - test_name = f.read() - extra = "." + test_name - dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) - - if suffix: - filename += "." + suffix - data.write_file(filename) - - def combine_parallel_data(self, data, aliases=None, data_paths=None, strict=False): - """Combine a number of data files together. - - Treat `self.filename` as a file prefix, and combine the data from all - of the data files starting with that prefix plus a dot. - - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. - - If `data_paths` is provided, it is a list of directories or files to - combine. Directories are searched for files that start with - `self.filename` plus dot as a prefix, and those files are combined. - - If `data_paths` is not provided, then the directory portion of - `self.filename` is used as the directory to search for data files. - - Every data file found and combined is then deleted from disk. If a file - cannot be read, a warning will be issued, and the file will not be - deleted. - - If `strict` is true, and no files are found to combine, an error is - raised. - - """ - # Because of the os.path.abspath in the constructor, data_dir will - # never be an empty string. - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - - data_paths = data_paths or [data_dir] - files_to_combine = [] - for p in data_paths: - if os.path.isfile(p): - files_to_combine.append(os.path.abspath(p)) - elif os.path.isdir(p): - pattern = os.path.join(os.path.abspath(p), localdot) - files_to_combine.extend(glob.glob(pattern)) - else: - raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) - - if strict and not files_to_combine: - raise CoverageException("No data to combine") - - files_combined = 0 - for f in files_to_combine: - new_data = CoverageData(debug=self.debug) - try: - new_data.read_file(f) - except CoverageException as exc: - if self.warn: - # The CoverageException has the file name in it, so just - # use the message as the warning. - self.warn(str(exc)) - else: - data.update(new_data, aliases=aliases) - files_combined += 1 - if self.debug and self.debug.should('dataio'): - self.debug.write("Deleting combined data file %r" % (f,)) - file_be_gone(f) - - if strict and not files_combined: - raise CoverageException("No usable data files") - - def canonicalize_json_data(data): """Canonicalize our JSON data so it can be compared.""" for fname, lines in iitems(data.get('lines', {})): diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index b6fad76d..7fda7961 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -16,7 +16,7 @@ import coverage import coverage.cmdline from coverage import env from coverage.config import CoverageConfig -from coverage.data import CoverageData, CoverageDataFiles +from coverage.data import CoverageData from coverage.misc import ExceptionDuringRun from tests.coveragetest import CoverageTest, OK, ERR, command_line @@ -605,8 +605,7 @@ class CmdLineWithFilesTest(BaseCmdLineTest): "file2.py": dict.fromkeys(range(1, 24)), }) data.add_file_tracers({"file1.py": "a_plugin"}) - data_files = CoverageDataFiles() - data_files.write(data) + data.write() self.command_line("debug data") self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ @@ -617,16 +616,16 @@ class CmdLineWithFilesTest(BaseCmdLineTest): 2 files: file1.py: 17 lines [a_plugin] file2.py: 23 lines - """).replace("FILENAME", data_files.filename)) + """).replace("FILENAME", data.filename)) def test_debug_data_with_no_data(self): - data_files = CoverageDataFiles() + data = CoverageData() self.command_line("debug data") self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ -- data ------------------------------------------------------ path: FILENAME No data collected - """).replace("FILENAME", data_files.filename)) + """).replace("FILENAME", data.filename)) class CmdLineStdoutTest(BaseCmdLineTest): diff --git a/tests/test_data.py b/tests/test_data.py index 0d3172d4..3c0d602b 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -11,8 +11,7 @@ import re import mock -from coverage.backward import StringIO -from coverage.data import CoverageData, CoverageDataFiles, debug_main, canonicalize_json_data +from coverage.data import CoverageData, debug_main, canonicalize_json_data from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -420,12 +419,10 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): def test_read_and_write_are_opposites(self): covdata1 = CoverageData() covdata1.add_arcs(ARCS_3) - stringio = StringIO() - covdata1.write_fileobj(stringio) + covdata1.write() - stringio.seek(0) covdata2 = CoverageData() - covdata2.read_fileobj(stringio) + covdata2.read() self.assert_arcs3_data(covdata2) @@ -518,27 +515,23 @@ class CoverageDataTestInTempDir(DataTestHelpers, CoverageTest): class CoverageDataFilesTest(DataTestHelpers, CoverageTest): - """Tests of CoverageDataFiles.""" + """Tests of CoverageData file handling.""" no_files_in_temp_dir = True - def setUp(self): - super(CoverageDataFilesTest, self).setUp() - self.data_files = CoverageDataFiles() - def test_reading_missing(self): self.assert_doesnt_exist(".coverage") covdata = CoverageData() - self.data_files.read(covdata) + covdata.read() self.assert_line_counts(covdata, {}) def test_writing_and_reading(self): covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata2 = CoverageData() - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) def test_debug_output_with_debug_option(self): @@ -547,10 +540,10 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): debug = DebugControlString(options=["dataio"]) covdata1 = CoverageData(debug=debug) covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata2 = CoverageData(debug=debug) - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) self.assertRegex( @@ -565,10 +558,10 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): debug = DebugControlString(options=[]) covdata1 = CoverageData(debug=debug) covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata2 = CoverageData(debug=debug) - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) self.assertEqual(debug.get_output(), "") @@ -577,7 +570,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assert_doesnt_exist(".coverage.SUFFIX") covdata = CoverageData() covdata.add_lines(LINES_1) - self.data_files.write(covdata, suffix='SUFFIX') + covdata.write(suffix='SUFFIX') self.assert_exists(".coverage.SUFFIX") self.assert_doesnt_exist(".coverage") @@ -587,7 +580,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): # suffix=True will make a randomly named data file. covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1, suffix=True) + covdata1.write(suffix=True) self.assert_doesnt_exist(".coverage") data_files1 = glob.glob(".coverage.*") self.assertEqual(len(data_files1), 1) @@ -595,7 +588,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): # Another suffix=True will choose a different name. covdata2 = CoverageData() covdata2.add_lines(LINES_1) - self.data_files.write(covdata2, suffix=True) + covdata2.write(suffix=True) self.assert_doesnt_exist(".coverage") data_files2 = glob.glob(".coverage.*") self.assertEqual(len(data_files2), 2) @@ -609,17 +602,17 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1, suffix='1') + covdata1.write(suffix='1') self.assert_exists(".coverage.1") self.assert_doesnt_exist(".coverage.2") covdata2 = CoverageData() covdata2.add_lines(LINES_2) - self.data_files.write(covdata2, suffix='2') + covdata2.write(suffix='2') self.assert_exists(".coverage.2") covdata3 = CoverageData() - self.data_files.combine_parallel_data(covdata3) + covdata3.combine_parallel_data() self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) self.assert_doesnt_exist(".coverage.1") @@ -628,22 +621,21 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): def test_erasing(self): covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata1.erase() self.assert_line_counts(covdata1, {}) - self.data_files.erase() covdata2 = CoverageData() - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, {}) def test_erasing_parallel(self): self.make_file("datafile.1") self.make_file("datafile.2") self.make_file(".coverage") - data_files = CoverageDataFiles("datafile") - data_files.erase(parallel=True) + data = CoverageData("datafile") + data.erase(parallel=True) self.assert_doesnt_exist("datafile.1") self.assert_doesnt_exist("datafile.2") self.assert_exists(".coverage") @@ -659,7 +651,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): # Write with CoverageData, then read the JSON explicitly. covdata = CoverageData() covdata.add_lines(LINES_1) - self.data_files.write(covdata) + covdata.write() data = self.read_json_data_file(".coverage") @@ -676,7 +668,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): # Write with CoverageData, then read the JSON explicitly. covdata = CoverageData() covdata.add_arcs(ARCS_3) - self.data_files.write(covdata) + covdata.write() data = self.read_json_data_file(".coverage") @@ -689,14 +681,13 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assertNotIn('file_tracers', data) def test_writing_to_other_file(self): - data_files = CoverageDataFiles(".otherfile") - covdata = CoverageData() + covdata = CoverageData(".otherfile") covdata.add_lines(LINES_1) - data_files.write(covdata) + covdata.write() self.assert_doesnt_exist(".coverage") self.assert_exists(".otherfile") - data_files.write(covdata, suffix="extra") + covdata.write(suffix="extra") self.assert_exists(".otherfile.extra") self.assert_doesnt_exist(".coverage") @@ -710,20 +701,20 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata1.add_file_tracers({ '/home/ned/proj/src/template.html': 'html.plugin', }) - self.data_files.write(covdata1, suffix='1') + covdata1.write(suffix='1') covdata2 = CoverageData() covdata2.add_lines({ r'c:\ned\test\a.py': {4: None, 5: None}, r'c:\ned\test\sub\b.py': {3: None, 6: None}, }) - self.data_files.write(covdata2, suffix='2') + covdata2.write(suffix='2') covdata3 = CoverageData() aliases = PathAliases() aliases.add("/home/ned/proj/src/", "./") aliases.add(r"c:\ned\test", "./") - self.data_files.combine_parallel_data(covdata3, aliases=aliases) + covdata3.combine_parallel_data(aliases=aliases) apy = canonical_filename('./a.py') sub_bpy = canonical_filename('./sub/b.py') @@ -750,7 +741,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata_xxx.write_file('.coverage.xxx') covdata3 = CoverageData() - self.data_files.combine_parallel_data(covdata3, data_paths=['cov1', 'cov2']) + covdata3.combine_parallel_data(data_paths=['cov1', 'cov2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -776,7 +767,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata_xxx.write_file('cov2/.coverage.xxx') covdata3 = CoverageData() - self.data_files.combine_parallel_data(covdata3, data_paths=['cov1', 'cov2/.coverage.2']) + covdata3.combine_parallel_data(data_paths=['cov1', 'cov2/.coverage.2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -789,4 +780,4 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata = CoverageData() msg = "Couldn't combine from non-existent path 'xyzzy'" with self.assertRaisesRegex(CoverageException, msg): - self.data_files.combine_parallel_data(covdata, data_paths=['xyzzy']) + covdata.combine_parallel_data(data_paths=['xyzzy']) -- cgit v1.2.1 From 7d71b1e052b2adead8c43bbc320582eab4938221 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 15 Jul 2018 16:26:48 -0400 Subject: Make file operations implicit on constructed filename --- coverage/data.py | 26 +++++++++------- tests/test_concurrency.py | 4 +-- tests/test_data.py | 76 ++++++++++++++++++++++++----------------------- tests/test_debug.py | 4 +-- tests/test_process.py | 46 ++++++++++++++-------------- tests/test_summary.py | 2 +- 6 files changed, 83 insertions(+), 75 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 6d30e2ba..23e612a1 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -57,7 +57,10 @@ class CoverageData(object): names in this API are case-sensitive, even on platforms with case-insensitive file systems. - To read a coverage.py data file, use :meth:`read_file`. You can then + A data file is associated with the data when the :class:`CoverageData` + is created. + + To read a coverage.py data file, use :meth:`read`. You can then access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, or :meth:`file_tracer`. Run information is available with :meth:`run_infos`. @@ -68,16 +71,15 @@ class CoverageData(object): most Python containers, you can determine if there is any data at all by using this object as a boolean value. - Most data files will be created by coverage.py itself, but you can use methods here to create data files if you like. The :meth:`add_lines`, :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways that are convenient for coverage.py. The :meth:`add_run_info` method adds key-value pairs to the run information. - To add a file without any measured data, use :meth:`touch_file`. + To add a source file without any measured data, use :meth:`touch_file`. - You write to a named file with :meth:`write_file`. + Write the data to its file with :meth:`write`. You can clear the data in memory with :meth:`erase`. Two data collections can be combined by using :meth:`update` on one :class:`CoverageData`, @@ -267,9 +269,13 @@ class CoverageData(object): __bool__ = __nonzero__ def read(self): - """Read the coverage data.""" + """Read the coverage data. + + It is fine for the file to not exist, in which case no data is read. + + """ if os.path.exists(self.filename): - self.read_file(self.filename) + self._read_file(self.filename) def _read_fileobj(self, file_obj): """Read the coverage data from the given file object. @@ -293,7 +299,7 @@ class CoverageData(object): self._validate() - def read_file(self, filename): + def _read_file(self, filename): """Read the coverage data from `filename` into this object.""" if self._debug and self._debug.should('dataio'): self._debug.write("Reading data from %r" % (filename,)) @@ -472,7 +478,7 @@ class CoverageData(object): if suffix: filename += "." + suffix - self.write_file(filename) + self._write_file(filename) def _write_fileobj(self, file_obj): """Write the coverage data to `file_obj`.""" @@ -496,7 +502,7 @@ class CoverageData(object): file_obj.write(self._GO_AWAY) json.dump(file_data, file_obj, separators=(',', ':')) - def write_file(self, filename): + def _write_file(self, filename): """Write the coverage data to `filename`.""" if self._debug and self._debug.should('dataio'): self._debug.write("Writing data to %r" % (filename,)) @@ -635,7 +641,7 @@ class CoverageData(object): for f in files_to_combine: new_data = CoverageData(debug=self._debug) try: - new_data.read_file(f) + new_data._read_file(f) except CoverageException as exc: if self._warn: # The CoverageException has the file name in it, so just diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 58529ec5..a4f700ed 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -235,8 +235,8 @@ class ConcurrencyTest(CoverageTest): # Read the coverage file and see that try_it.py has all its lines # executed. - data = coverage.CoverageData() - data.read_file(".coverage") + data = coverage.CoverageData(".coverage") + data.read() # If the test fails, it's helpful to see this info: fname = abs_file("try_it.py") diff --git a/tests/test_data.py b/tests/test_data.py index 3c0d602b..5deccef0 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -430,59 +430,58 @@ class CoverageDataTestInTempDir(DataTestHelpers, CoverageTest): """Tests of CoverageData that need a temporary directory to make files.""" def test_read_write_lines(self): - covdata1 = CoverageData() + covdata1 = CoverageData("lines.dat") covdata1.add_lines(LINES_1) - covdata1.write_file("lines.dat") + covdata1.write() - covdata2 = CoverageData() - covdata2.read_file("lines.dat") + covdata2 = CoverageData("lines.dat") + covdata2.read() self.assert_lines1_data(covdata2) def test_read_write_arcs(self): - covdata1 = CoverageData() + covdata1 = CoverageData("arcs.dat") covdata1.add_arcs(ARCS_3) - covdata1.write_file("arcs.dat") + covdata1.write() - covdata2 = CoverageData() - covdata2.read_file("arcs.dat") + covdata2 = CoverageData("arcs.dat") + covdata2.read() self.assert_arcs3_data(covdata2) def test_read_errors(self): - covdata = CoverageData() + msg = r"Couldn't read data from '.*[/\\]{0}': \S+" - msg = r"Couldn't read data from '{0}': \S+" self.make_file("xyzzy.dat", "xyzzy") with self.assertRaisesRegex(CoverageException, msg.format("xyzzy.dat")): - covdata.read_file("xyzzy.dat") + covdata = CoverageData("xyzzy.dat") + covdata.read() + self.assertFalse(covdata) self.make_file("empty.dat", "") with self.assertRaisesRegex(CoverageException, msg.format("empty.dat")): - covdata.read_file("empty.dat") - - with self.assertRaisesRegex(CoverageException, msg.format("nonexistent.dat")): - covdata.read_file("nonexistent.dat") + covdata = CoverageData("empty.dat") + covdata.read() + self.assertFalse(covdata) self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): - covdata.read_file("misleading.dat") - - # After all that, no data should be in our CoverageData. + covdata = CoverageData("misleading.dat") + covdata.read() self.assertFalse(covdata) def test_debug_main(self): - covdata1 = CoverageData() + covdata1 = CoverageData(".coverage") covdata1.add_lines(LINES_1) - covdata1.write_file(".coverage") + covdata1.write() debug_main([]) - covdata2 = CoverageData() + covdata2 = CoverageData("arcs.dat") covdata2.add_arcs(ARCS_3) covdata2.add_file_tracers({"y.py": "magic_plugin"}) covdata2.add_run_info(version="v3.14", chunks=["z", "a"]) - covdata2.write_file("arcs.dat") + covdata2.write() - covdata3 = CoverageData() - covdata3.write_file("empty.dat") + covdata3 = CoverageData("empty.dat") + covdata3.write() debug_main(["arcs.dat", "empty.dat"]) expected = { @@ -725,20 +724,20 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assertEqual(covdata3.file_tracer(template_html), 'html.plugin') def test_combining_from_different_directories(self): - covdata1 = CoverageData() + covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) os.makedirs('cov1') - covdata1.write_file('cov1/.coverage.1') + covdata1.write() - covdata2 = CoverageData() + covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) os.makedirs('cov2') - covdata2.write_file('cov2/.coverage.2') + covdata2.write() # This data won't be included. - covdata_xxx = CoverageData() + covdata_xxx = CoverageData('.coverage.xxx') covdata_xxx.add_arcs(ARCS_3) - covdata_xxx.write_file('.coverage.xxx') + covdata_xxx.write() covdata3 = CoverageData() covdata3.combine_parallel_data(data_paths=['cov1', 'cov2']) @@ -750,21 +749,24 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assert_exists(".coverage.xxx") def test_combining_from_files(self): - covdata1 = CoverageData() + covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) os.makedirs('cov1') - covdata1.write_file('cov1/.coverage.1') + covdata1.write() - covdata2 = CoverageData() + covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) os.makedirs('cov2') - covdata2.write_file('cov2/.coverage.2') + covdata2.write() # This data won't be included. - covdata_xxx = CoverageData() + covdata_xxx = CoverageData('.coverage.xxx') covdata_xxx.add_arcs(ARCS_3) - covdata_xxx.write_file('.coverage.xxx') - covdata_xxx.write_file('cov2/.coverage.xxx') + covdata_xxx.write() + + covdata_2xxx = CoverageData('cov2/.coverage.xxx') + covdata_2xxx.add_arcs(ARCS_3) + covdata_2xxx.write() covdata3 = CoverageData() covdata3.combine_parallel_data(data_paths=['cov1', 'cov2/.coverage.2']) diff --git a/tests/test_debug.py b/tests/test_debug.py index 2699ca61..c46e3dae 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -136,10 +136,10 @@ class DebugTraceTest(CoverageTest): self.assertEqual(len(real_messages), len(frames)) # The last message should be "Writing data", and the last frame should - # be write_file in data.py. + # be _write_file in data.py. self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") last_line = out_lines.splitlines()[-1] - self.assertRegex(last_line, r"\s+write_file : .*coverage[/\\]data.py @\d+$") + self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) diff --git a/tests/test_process.py b/tests/test_process.py index e022e727..341ad37c 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -98,7 +98,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) # Running combine again should fail, because there are no parallel data @@ -109,7 +109,7 @@ class ProcessTest(CoverageTest): # And the originally combined data is still there. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_parallel_data_with_a_corrupt_file(self): @@ -145,7 +145,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_no_usable_files(self): @@ -179,7 +179,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has 6 lines # executed (we only did b, not c). data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 6) def test_combine_parallel_data_in_two_steps(self): @@ -210,7 +210,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_parallel_data_no_append(self): @@ -242,7 +242,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has only 7 lines # because we didn't keep the data from running b. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 7) def test_append_data(self): @@ -261,7 +261,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_append_data_with_different_file(self): @@ -284,8 +284,8 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. - data = coverage.CoverageData() - data.read_file(".mycovdata") + data = coverage.CoverageData(".mycovdata") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_append_can_create_a_data_file(self): @@ -299,7 +299,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has only 6 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 6) def test_combine_with_rc(self): @@ -332,7 +332,7 @@ class ProcessTest(CoverageTest): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) # Reporting should still work even with the .rc file @@ -386,7 +386,7 @@ class ProcessTest(CoverageTest): # Read the coverage data file and see that the two different x.py # files have been combined together. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() summary = data.line_counts(fullpath=True) self.assertEqual(len(summary), 1) actual = os.path.normcase(os.path.abspath(list(summary.keys())[0])) @@ -549,7 +549,7 @@ class ProcessTest(CoverageTest): self.assertEqual(self.number_of_data_files(), 1) data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['fork.py'], 9) def test_warnings_during_reporting(self): @@ -655,8 +655,8 @@ class ProcessTest(CoverageTest): self.make_file("simple.py", """print('hello')""") self.run_command("coverage run simple.py") - data = CoverageData() - data.read_file("mydata.dat") + data = CoverageData("mydata.dat") + data.read() infos = data.run_infos() self.assertEqual(len(infos), 1) expected = u"These are musical notes: ♫𝅗𝅥♩" @@ -686,7 +686,7 @@ class ProcessTest(CoverageTest): out = self.run_command("python -m coverage run -L getenv.py") self.assertEqual(out, "FOOEY == BOO\n") data = coverage.CoverageData() - data.read_file(".coverage") + data.read() # The actual number of executed lines in os.py when it's # imported is 120 or so. Just running os.getenv executes # about 5. @@ -916,7 +916,7 @@ class ExcepthookTest(CoverageTest): # Read the coverage file and see that excepthook.py has 7 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['excepthook.py'], 7) def test_excepthook_exit(self): @@ -1245,9 +1245,9 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): # An existing data file should not be read when a subprocess gets # measured automatically. Create the data file here with bogus data in # it. - data = coverage.CoverageData() + data = coverage.CoverageData(".mycovdata") data.add_lines({os.path.abspath('sub.py'): dict.fromkeys(range(100))}) - data.write_file(".mycovdata") + data.write() self.make_file("coverage.ini", """\ [run] @@ -1261,8 +1261,8 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): # Read the data from .coverage self.assert_exists(".mycovdata") - data = coverage.CoverageData() - data.read_file(".mycovdata") + data = coverage.CoverageData(".mycovdata") + data.read() self.assertEqual(data.line_counts()['sub.py'], 3) def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov @@ -1286,7 +1286,7 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): # assert that the combined .coverage data file is correct self.assert_exists(".coverage") data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['sub.py'], 3) # assert that there are *no* extra data files left over after a combine @@ -1376,7 +1376,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): # Read the data from .coverage self.assert_exists(".coverage") data = coverage.CoverageData() - data.read_file(".coverage") + data.read() summary = data.line_counts() print(summary) self.assertEqual(summary[source + '.py'], 3) diff --git a/tests/test_summary.py b/tests/test_summary.py index b2895370..980fd3d4 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -161,7 +161,7 @@ class SummaryTest(UsingModulesMixin, CoverageTest): # Read the data written, to see that the right files have been omitted from running. covdata = CoverageData() - covdata.read_file(".coverage") + covdata.read() files = [os.path.basename(p) for p in covdata.measured_files()] self.assertIn("covmod1.py", files) self.assertNotIn("covmodzip1.py", files) -- cgit v1.2.1 From e301a01b772cfab9f567724e01df33e862d3b72f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 29 Jul 2018 18:46:02 -0400 Subject: WIP WIP WIP --- coverage/data.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/coverage/data.py b/coverage/data.py index 23e612a1..afb12df4 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -12,6 +12,7 @@ import os.path import random import re import socket +import sqlite3 from coverage import env from coverage.backward import iitems, string_class @@ -731,6 +732,126 @@ class CoverageData(object): return self._arcs is not None +SCHEMA = """ +create table schema ( + version integer +); + +insert into schema (version) values (1); + +create table file ( + id integer primary key, + path text, + tracer text, + unique(path) +); + +create table line ( + file_id integer, + lineno integer, + unique(file_id, lineno) +); +""" + +def _create_db(filename): + con = sqlite3.connect(filename) + with con: + for stmt in SCHEMA.split(';'): + con.execute(stmt.strip()) + con.close() + + +class CoverageDataSqlite(object): + def __init__(self, basename=None, warn=None, debug=None): + self.filename = os.path.abspath(basename or ".coverage") + self._warn = warn + self._debug = debug + + self._file_map = {} + self._db = None + + def _reset(self): + self._file_map = {} + if self._db is not None: + self._db.close() + self._db = None + + def _connect(self): + if self._db is None: + if not os.path.exists(self.filename): + if self._debug and self._debug.should('dataio'): + self._debug.write("Creating data file %r" % (self.filename,)) + _create_db(self.filename) + self._db = sqlite3.connect(self.filename) + for path, id in self._db.execute("select path, id from file"): + self._file_map[path] = id + return self._db + + def _file_id(self, filename): + if filename not in self._file_map: + with self._connect() as con: + cur = con.cursor() + cur.execute("insert into file (path) values (?)", (filename,)) + self._file_map[filename] = cur.lastrowid + return self._file_map[filename] + + def add_lines(self, line_data): + """Add measured line data. + + `line_data` is a dictionary mapping file names to dictionaries:: + + { filename: { lineno: None, ... }, ...} + + """ + with self._connect() as con: + for filename, linenos in iitems(line_data): + file_id = self._file_id(filename) + for lineno in linenos: + con.execute( + "insert or ignore into line (file_id, lineno) values (?, ?)", + (file_id, lineno), + ) + + def add_file_tracers(self, file_tracers): + """Add per-file plugin information. + + `file_tracers` is { filename: plugin_name, ... } + + """ + with self._connect() as con: + for filename, tracer in iitems(file_tracers): + con.execute( + "insert into file (path, tracer) values (?, ?) on duplicate key update", + (filename, tracer), + ) + + def erase(self, parallel=False): + """Erase the data in this object. + + If `parallel` is true, then also deletes data files created from the + basename by parallel-mode. + + """ + self._reset() + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing data file %r" % (self.filename,)) + file_be_gone(self.filename) + if parallel: + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + pattern = os.path.join(os.path.abspath(data_dir), localdot) + for filename in glob.glob(pattern): + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing parallel data file %r" % (filename,)) + file_be_gone(filename) + + def write(self, suffix=None): + """Write the collected coverage data to a file.""" + pass + +CoverageData = CoverageDataSqlite + + def canonicalize_json_data(data): """Canonicalize our JSON data so it can be compared.""" for fname, lines in iitems(data.get('lines', {})): -- cgit v1.2.1 From e7f8cd3804245104657e41b548a431801f6c1cee Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 31 Jul 2018 06:09:00 -0400 Subject: Move sqlite into sqldata.py --- coverage/data.py | 119 +----------------------------------- coverage/sqldata.py | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 118 deletions(-) create mode 100644 coverage/sqldata.py diff --git a/coverage/data.py b/coverage/data.py index afb12df4..eda1a341 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -12,7 +12,6 @@ import os.path import random import re import socket -import sqlite3 from coverage import env from coverage.backward import iitems, string_class @@ -732,123 +731,7 @@ class CoverageData(object): return self._arcs is not None -SCHEMA = """ -create table schema ( - version integer -); - -insert into schema (version) values (1); - -create table file ( - id integer primary key, - path text, - tracer text, - unique(path) -); - -create table line ( - file_id integer, - lineno integer, - unique(file_id, lineno) -); -""" - -def _create_db(filename): - con = sqlite3.connect(filename) - with con: - for stmt in SCHEMA.split(';'): - con.execute(stmt.strip()) - con.close() - - -class CoverageDataSqlite(object): - def __init__(self, basename=None, warn=None, debug=None): - self.filename = os.path.abspath(basename or ".coverage") - self._warn = warn - self._debug = debug - - self._file_map = {} - self._db = None - - def _reset(self): - self._file_map = {} - if self._db is not None: - self._db.close() - self._db = None - - def _connect(self): - if self._db is None: - if not os.path.exists(self.filename): - if self._debug and self._debug.should('dataio'): - self._debug.write("Creating data file %r" % (self.filename,)) - _create_db(self.filename) - self._db = sqlite3.connect(self.filename) - for path, id in self._db.execute("select path, id from file"): - self._file_map[path] = id - return self._db - - def _file_id(self, filename): - if filename not in self._file_map: - with self._connect() as con: - cur = con.cursor() - cur.execute("insert into file (path) values (?)", (filename,)) - self._file_map[filename] = cur.lastrowid - return self._file_map[filename] - - def add_lines(self, line_data): - """Add measured line data. - - `line_data` is a dictionary mapping file names to dictionaries:: - - { filename: { lineno: None, ... }, ...} - - """ - with self._connect() as con: - for filename, linenos in iitems(line_data): - file_id = self._file_id(filename) - for lineno in linenos: - con.execute( - "insert or ignore into line (file_id, lineno) values (?, ?)", - (file_id, lineno), - ) - - def add_file_tracers(self, file_tracers): - """Add per-file plugin information. - - `file_tracers` is { filename: plugin_name, ... } - - """ - with self._connect() as con: - for filename, tracer in iitems(file_tracers): - con.execute( - "insert into file (path, tracer) values (?, ?) on duplicate key update", - (filename, tracer), - ) - - def erase(self, parallel=False): - """Erase the data in this object. - - If `parallel` is true, then also deletes data files created from the - basename by parallel-mode. - - """ - self._reset() - if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing data file %r" % (self.filename,)) - file_be_gone(self.filename) - if parallel: - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - pattern = os.path.join(os.path.abspath(data_dir), localdot) - for filename in glob.glob(pattern): - if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing parallel data file %r" % (filename,)) - file_be_gone(filename) - - def write(self, suffix=None): - """Write the collected coverage data to a file.""" - pass - +from coverage.sqldata import CoverageDataSqlite CoverageData = CoverageDataSqlite diff --git a/coverage/sqldata.py b/coverage/sqldata.py new file mode 100644 index 00000000..ee0798e3 --- /dev/null +++ b/coverage/sqldata.py @@ -0,0 +1,172 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Sqlite coverage data.""" + +import os +import sqlite3 + +from coverage.backward import iitems +from coverage.misc import CoverageException, file_be_gone + + +SCHEMA = """ +create table schema ( + version integer +); + +insert into schema (version) values (1); + +create table meta ( + name text, + value text, + unique(name) +); + +create table file ( + id integer primary key, + path text, + tracer text, + unique(path) +); + +create table line ( + file_id integer, + lineno integer, + unique(file_id, lineno) +); + +create table arc ( + file_id integer, + fromno integer, + tono integer, + unique(file_id, fromno, tono) +); +""" + +def _create_db(filename, schema): + con = sqlite3.connect(filename) + with con: + for stmt in schema.split(';'): + con.execute(stmt.strip()) + con.close() + + +class CoverageDataSqlite(object): + def __init__(self, basename=None, warn=None, debug=None): + self.filename = os.path.abspath(basename or ".coverage") + self._warn = warn + self._debug = debug + + self._file_map = {} + self._db = None + self._have_read = False + + def _reset(self): + self._file_map = {} + if self._db is not None: + self._db.close() + self._db = None + + def _connect(self): + if self._db is None: + if not os.path.exists(self.filename): + if self._debug and self._debug.should('dataio'): + self._debug.write("Creating data file %r" % (self.filename,)) + _create_db(self.filename, SCHEMA) + self._db = sqlite3.connect(self.filename) + for path, id in self._db.execute("select path, id from file"): + self._file_map[path] = id + return self._db + + def _file_id(self, filename): + self._start_writing() + if filename not in self._file_map: + with self._connect() as con: + cur = con.cursor() + cur.execute("insert into file (path) values (?)", (filename,)) + self._file_map[filename] = cur.lastrowid + return self._file_map[filename] + + def add_lines(self, line_data): + """Add measured line data. + + `line_data` is a dictionary mapping file names to dictionaries:: + + { filename: { lineno: None, ... }, ...} + + """ + self._start_writing() + with self._connect() as con: + for filename, linenos in iitems(line_data): + file_id = self._file_id(filename) + for lineno in linenos: + con.execute( + "insert or ignore into line (file_id, lineno) values (?, ?)", + (file_id, lineno), + ) + + def add_file_tracers(self, file_tracers): + """Add per-file plugin information. + + `file_tracers` is { filename: plugin_name, ... } + + """ + self._start_writing() + with self._connect() as con: + for filename, tracer in iitems(file_tracers): + con.execute( + "insert into file (path, tracer) values (?, ?) on duplicate key update", + (filename, tracer), + ) + + def erase(self, parallel=False): + """Erase the data in this object. + + If `parallel` is true, then also deletes data files created from the + basename by parallel-mode. + + """ + self._reset() + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing data file %r" % (self.filename,)) + file_be_gone(self.filename) + if parallel: + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + pattern = os.path.join(os.path.abspath(data_dir), localdot) + for filename in glob.glob(pattern): + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing parallel data file %r" % (filename,)) + file_be_gone(filename) + + def read(self): + self._have_read = True + + def write(self, suffix=None): + """Write the collected coverage data to a file.""" + pass + + def _start_writing(self): + if not self._have_read: + self.erase() + self._have_read = True + + def has_arcs(self): + return False # TODO! + + def measured_files(self): + """A list of all files that had been measured.""" + self._connect() + return list(self._file_map) + + def file_tracer(self, filename): + """Get the plugin name of the file tracer for a file. + + Returns the name of the plugin that handles this file. If the file was + measured, but didn't use a plugin, then "" is returned. If the file + was not measured, then None is returned. + + """ + with self._connect() as con: + pass -- cgit v1.2.1 From 0db04f814339e143422c211fdba351554fcc8f77 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 31 Jul 2018 06:19:05 -0400 Subject: Report works --- coverage/sqldata.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index ee0798e3..db2b0b17 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -141,6 +141,7 @@ class CoverageDataSqlite(object): file_be_gone(filename) def read(self): + self._connect() # TODO: doesn't look right self._have_read = True def write(self, suffix=None): @@ -157,7 +158,6 @@ class CoverageDataSqlite(object): def measured_files(self): """A list of all files that had been measured.""" - self._connect() return list(self._file_map) def file_tracer(self, filename): @@ -168,5 +168,9 @@ class CoverageDataSqlite(object): was not measured, then None is returned. """ + return "" # TODO + + def lines(self, filename): with self._connect() as con: - pass + file_id = self._file_id(filename) + return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] -- cgit v1.2.1 From 48e7984b5c28a6d6a324bdb0fa62ae626be60f8a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 31 Jul 2018 07:23:45 -0400 Subject: SQL debugging --- coverage/sqldata.py | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index db2b0b17..05680043 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -44,12 +44,6 @@ create table arc ( ); """ -def _create_db(filename, schema): - con = sqlite3.connect(filename) - with con: - for stmt in schema.split(';'): - con.execute(stmt.strip()) - con.close() class CoverageDataSqlite(object): @@ -73,8 +67,14 @@ class CoverageDataSqlite(object): if not os.path.exists(self.filename): if self._debug and self._debug.should('dataio'): self._debug.write("Creating data file %r" % (self.filename,)) - _create_db(self.filename, SCHEMA) - self._db = sqlite3.connect(self.filename) + self._db = Sqlite(self.filename, self._debug) + with self._db: + for stmt in SCHEMA.split(';'): + stmt = stmt.strip() + if stmt: + self._db.execute(stmt) + else: + self._db = Sqlite(self.filename, self._debug) for path, id in self._db.execute("select path, id from file"): self._file_map[path] = id return self._db @@ -83,8 +83,7 @@ class CoverageDataSqlite(object): self._start_writing() if filename not in self._file_map: with self._connect() as con: - cur = con.cursor() - cur.execute("insert into file (path) values (?)", (filename,)) + cur = con.execute("insert into file (path) values (?)", (filename,)) self._file_map[filename] = cur.lastrowid return self._file_map[filename] @@ -174,3 +173,28 @@ class CoverageDataSqlite(object): with self._connect() as con: file_id = self._file_id(filename) return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] + + +class Sqlite(object): + def __init__(self, filename, debug): + self.debug = debug if debug.should('sql') else None + if self.debug: + self.debug.write("Connecting to {!r}".format(filename)) + self.con = sqlite3.connect(filename) + + def close(self): + self.con.close() + + def __enter__(self): + self.con.__enter__() + return self + + def __exit__(self, exc_type, exc_value, traceback): + return self.con.__exit__(exc_type, exc_value, traceback) + + def execute(self, sql, parameters=()): + if self.debug: + tail = " with {!r}".format(parameters) if parameters else "" + self.debug.write("Executing {!r}{}".format(sql, tail)) + cur = self.con.execute(sql, parameters) + return cur -- cgit v1.2.1 From b457052020ec90fdba964ff8bd5abe6d92032e6b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 3 Aug 2018 07:44:56 -0400 Subject: Make writing data faster --- coverage/data.py | 6 +++--- coverage/sqldata.py | 27 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index eda1a341..e9243166 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -22,7 +22,7 @@ from coverage.misc import CoverageException, file_be_gone, isolate_module os = isolate_module(os) -class CoverageData(object): +class CoverageJsonData(object): """Manages collected coverage data, including file storage. This class is the public supported API to the data coverage.py collects @@ -731,8 +731,8 @@ class CoverageData(object): return self._arcs is not None -from coverage.sqldata import CoverageDataSqlite -CoverageData = CoverageDataSqlite +from coverage.sqldata import CoverageSqliteData +CoverageData = CoverageSqliteData def canonicalize_json_data(data): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 05680043..39d6268b 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -46,7 +46,7 @@ create table arc ( -class CoverageDataSqlite(object): +class CoverageSqliteData(object): def __init__(self, basename=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") self._warn = warn @@ -99,11 +99,11 @@ class CoverageDataSqlite(object): with self._connect() as con: for filename, linenos in iitems(line_data): file_id = self._file_id(filename) - for lineno in linenos: - con.execute( - "insert or ignore into line (file_id, lineno) values (?, ?)", - (file_id, lineno), - ) + data = [(file_id, lineno) for lineno in linenos] + con.executemany( + "insert or ignore into line (file_id, lineno) values (?, ?)", + data, + ) def add_file_tracers(self, file_tracers): """Add per-file plugin information. @@ -177,11 +177,16 @@ class CoverageDataSqlite(object): class Sqlite(object): def __init__(self, filename, debug): - self.debug = debug if debug.should('sql') else None + self.debug = debug if (debug and debug.should('sql')) else None if self.debug: self.debug.write("Connecting to {!r}".format(filename)) self.con = sqlite3.connect(filename) + # This pragma makes writing faster. It disables rollbacks, but we never need them. + self.con.execute("pragma journal_mode=off") + # This pragma makes writing faster. + self.con.execute("pragma synchronous=off") + def close(self): self.con.close() @@ -196,5 +201,9 @@ class Sqlite(object): if self.debug: tail = " with {!r}".format(parameters) if parameters else "" self.debug.write("Executing {!r}{}".format(sql, tail)) - cur = self.con.execute(sql, parameters) - return cur + return self.con.execute(sql, parameters) + + def executemany(self, sql, data): + if self.debug: + self.debug.write("Executing many {!r}".format(sql)) + return self.con.executemany(sql, data) -- cgit v1.2.1 From 9244c218d282d6e7542487521d9ea0f17bc0c89d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 3 Aug 2018 10:44:19 -0400 Subject: Use a Sqlite application_id to identify the file. --- coverage/sqldata.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 39d6268b..1e5fb570 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -44,6 +44,7 @@ create table arc ( ); """ +APP_ID = 0x0c07ea6e # Kind of looks like "coverage" class CoverageSqliteData(object): @@ -69,12 +70,18 @@ class CoverageSqliteData(object): self._debug.write("Creating data file %r" % (self.filename,)) self._db = Sqlite(self.filename, self._debug) with self._db: + self._db.execute("pragma application_id = {}".format(APP_ID)) for stmt in SCHEMA.split(';'): stmt = stmt.strip() if stmt: self._db.execute(stmt) else: self._db = Sqlite(self.filename, self._debug) + with self._db: + for app_id, in self._db.execute("pragma application_id"): + app_id = int(app_id) + if app_id != APP_ID: + raise Exception("Doesn't look like a coverage data file: 0x{:08x} != 0x{:08x}".format(app_id, APP_ID)) for path, id in self._db.execute("select path, id from file"): self._file_map[path] = id return self._db -- cgit v1.2.1 From 0c5af4612210fa08113ea93372f877ef13aaa007 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 3 Aug 2018 20:57:36 -0400 Subject: Can measure and report branches --- coverage/sqldata.py | 118 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 29 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 1e5fb570..cddece31 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -18,9 +18,8 @@ create table schema ( insert into schema (version) values (1); create table meta ( - name text, - value text, - unique(name) + has_lines boolean, + has_arcs boolean ); create table file ( @@ -44,8 +43,9 @@ create table arc ( ); """ -APP_ID = 0x0c07ea6e # Kind of looks like "coverage" - +# >>> struct.unpack(">i", b"\xc0\x7e\x8a\x6e") # "coverage", kind of. +# (-1065448850,) +APP_ID = -1065448850 class CoverageSqliteData(object): def __init__(self, basename=None, warn=None, debug=None): @@ -55,35 +55,58 @@ class CoverageSqliteData(object): self._file_map = {} self._db = None + # Are we in sync with the data file? self._have_read = False + self._has_lines = False + self._has_arcs = False + def _reset(self): self._file_map = {} if self._db is not None: self._db.close() self._db = None + self._have_read = False + + def _create_db(self): + if self._debug and self._debug.should('dataio'): + self._debug.write("Creating data file {!r}".format(self.filename)) + self._db = Sqlite(self.filename, self._debug) + with self._db: + self._db.execute("pragma application_id = {}".format(APP_ID)) + for stmt in SCHEMA.split(';'): + stmt = stmt.strip() + if stmt: + self._db.execute(stmt) + self._db.execute( + "insert into meta (has_lines, has_arcs) values (?, ?)", + (self._has_lines, self._has_arcs) + ) + + def _open_db(self): + if self._debug and self._debug.should('dataio'): + self._debug.write("Opening data file {!r}".format(self.filename)) + self._db = Sqlite(self.filename, self._debug) + with self._db: + for app_id, in self._db.execute("pragma application_id"): + app_id = int(app_id) + if app_id != APP_ID: + raise CoverageException( + "File {!r} doesn't look like a coverage data file: " + "0x{:08x} != 0x{:08x}".format(self.filename, app_id, APP_ID) + ) + for row in self._db.execute("select has_lines, has_arcs from meta"): + self._has_lines, self._has_arcs = row + + for path, id in self._db.execute("select path, id from file"): + self._file_map[path] = id def _connect(self): if self._db is None: - if not os.path.exists(self.filename): - if self._debug and self._debug.should('dataio'): - self._debug.write("Creating data file %r" % (self.filename,)) - self._db = Sqlite(self.filename, self._debug) - with self._db: - self._db.execute("pragma application_id = {}".format(APP_ID)) - for stmt in SCHEMA.split(';'): - stmt = stmt.strip() - if stmt: - self._db.execute(stmt) + if os.path.exists(self.filename): + self._open_db() else: - self._db = Sqlite(self.filename, self._debug) - with self._db: - for app_id, in self._db.execute("pragma application_id"): - app_id = int(app_id) - if app_id != APP_ID: - raise Exception("Doesn't look like a coverage data file: 0x{:08x} != 0x{:08x}".format(app_id, APP_ID)) - for path, id in self._db.execute("select path, id from file"): - self._file_map[path] = id + self._create_db() return self._db def _file_id(self, filename): @@ -103,6 +126,7 @@ class CoverageSqliteData(object): """ self._start_writing() + self._choose_lines_or_arcs(lines=True) with self._connect() as con: for filename, linenos in iitems(line_data): file_id = self._file_id(filename) @@ -112,6 +136,36 @@ class CoverageSqliteData(object): data, ) + def add_arcs(self, arc_data): + """Add measured arc data. + + `arc_data` is a dictionary mapping file names to dictionaries:: + + { filename: { (l1,l2): None, ... }, ...} + + """ + self._start_writing() + self._choose_lines_or_arcs(arcs=True) + with self._connect() as con: + for filename, arcs in iitems(arc_data): + file_id = self._file_id(filename) + data = [(file_id, fromno, tono) for fromno, tono in arcs] + con.executemany( + "insert or ignore into arc (file_id, fromno, tono) values (?, ?, ?)", + data, + ) + + def _choose_lines_or_arcs(self, lines=False, arcs=False): + if lines and self._has_arcs: + raise CoverageException("Can't add lines to existing arc data") + if arcs and self._has_lines: + raise CoverageException("Can't add arcs to existing line data") + if not self._has_arcs and not self._has_lines: + self._has_lines = lines + self._has_arcs = arcs + with self._connect() as con: + con.execute("update meta set has_lines = ?, has_arcs = ?", (lines, arcs)) + def add_file_tracers(self, file_tracers): """Add per-file plugin information. @@ -120,10 +174,11 @@ class CoverageSqliteData(object): """ self._start_writing() with self._connect() as con: - for filename, tracer in iitems(file_tracers): - con.execute( + data = list(iitems(file_tracers)) + if data: + con.executemany( "insert into file (path, tracer) values (?, ?) on duplicate key update", - (filename, tracer), + data, ) def erase(self, parallel=False): @@ -135,7 +190,7 @@ class CoverageSqliteData(object): """ self._reset() if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing data file %r" % (self.filename,)) + self._debug.write("Erasing data file {!r}".format(self.filename)) file_be_gone(self.filename) if parallel: data_dir, local = os.path.split(self.filename) @@ -143,7 +198,7 @@ class CoverageSqliteData(object): pattern = os.path.join(os.path.abspath(data_dir), localdot) for filename in glob.glob(pattern): if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing parallel data file %r" % (filename,)) + self._debug.write("Erasing parallel data file {!r}".format(filename)) file_be_gone(filename) def read(self): @@ -160,7 +215,7 @@ class CoverageSqliteData(object): self._have_read = True def has_arcs(self): - return False # TODO! + return self._has_arcs def measured_files(self): """A list of all files that had been measured.""" @@ -181,6 +236,11 @@ class CoverageSqliteData(object): file_id = self._file_id(filename) return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] + def arcs(self, filename): + with self._connect() as con: + file_id = self._file_id(filename) + return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] + class Sqlite(object): def __init__(self, filename, debug): -- cgit v1.2.1 From 2f0d57856550ef7ad248e4e6127700bdabb91e7d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 4 Aug 2018 07:36:13 -0400 Subject: Pull combine_parallel_data out of CoverageData --- coverage/control.py | 4 +- coverage/data.py | 134 +++++++++++++++++++++++++++------------------------- tests/test_data.py | 12 ++--- 3 files changed, 77 insertions(+), 73 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 1760ee78..2f084cc2 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -15,7 +15,7 @@ from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems from coverage.collector import Collector from coverage.config import read_coverage_config -from coverage.data import CoverageData +from coverage.data import CoverageData, combine_parallel_data from coverage.debug import DebugControl, write_formatted_info from coverage.disposition import disposition_debug_msg from coverage.files import PathAliases, set_relative_directory, abs_file @@ -536,7 +536,7 @@ class Coverage(object): for pattern in paths[1:]: aliases.add(pattern, result) - self._data.combine_parallel_data(aliases=aliases, data_paths=data_paths, strict=strict) + combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict) def get_data(self): """Get the collected data. diff --git a/coverage/data.py b/coverage/data.py index e9243166..0b3b640b 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -594,69 +594,6 @@ class CoverageJsonData(object): self._validate() - def combine_parallel_data(self, aliases=None, data_paths=None, strict=False): - """Combine a number of data files together. - - Treat `self.filename` as a file prefix, and combine the data from all - of the data files starting with that prefix plus a dot. - - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. - - If `data_paths` is provided, it is a list of directories or files to - combine. Directories are searched for files that start with - `self.filename` plus dot as a prefix, and those files are combined. - - If `data_paths` is not provided, then the directory portion of - `self.filename` is used as the directory to search for data files. - - Every data file found and combined is then deleted from disk. If a file - cannot be read, a warning will be issued, and the file will not be - deleted. - - If `strict` is true, and no files are found to combine, an error is - raised. - - """ - # Because of the os.path.abspath in the constructor, data_dir will - # never be an empty string. - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - - data_paths = data_paths or [data_dir] - files_to_combine = [] - for p in data_paths: - if os.path.isfile(p): - files_to_combine.append(os.path.abspath(p)) - elif os.path.isdir(p): - pattern = os.path.join(os.path.abspath(p), localdot) - files_to_combine.extend(glob.glob(pattern)) - else: - raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) - - if strict and not files_to_combine: - raise CoverageException("No data to combine") - - files_combined = 0 - for f in files_to_combine: - new_data = CoverageData(debug=self._debug) - try: - new_data._read_file(f) - except CoverageException as exc: - if self._warn: - # The CoverageException has the file name in it, so just - # use the message as the warning. - self._warn(str(exc)) - else: - self.update(new_data, aliases=aliases) - files_combined += 1 - if self._debug and self._debug.should('dataio'): - self._debug.write("Deleting combined data file %r" % (f,)) - file_be_gone(f) - - if strict and not files_combined: - raise CoverageException("No usable data files") - ## ## Miscellaneous ## @@ -731,9 +668,76 @@ class CoverageJsonData(object): return self._arcs is not None -from coverage.sqldata import CoverageSqliteData -CoverageData = CoverageSqliteData +which = os.environ.get("COV_STORAGE", "json") +if which == "json": + CoverageData = CoverageJsonData +elif which == "sql": + from coverage.sqldata import CoverageSqliteData + CoverageData = CoverageSqliteData + +def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): + """Combine a number of data files together. + + Treat `data.filename` as a file prefix, and combine the data from all + of the data files starting with that prefix plus a dot. + + If `aliases` is provided, it's a `PathAliases` object that is used to + re-map paths to match the local machine's. + + If `data_paths` is provided, it is a list of directories or files to + combine. Directories are searched for files that start with + `data.filename` plus dot as a prefix, and those files are combined. + + If `data_paths` is not provided, then the directory portion of + `data.filename` is used as the directory to search for data files. + + Every data file found and combined is then deleted from disk. If a file + cannot be read, a warning will be issued, and the file will not be + deleted. + + If `strict` is true, and no files are found to combine, an error is + raised. + + """ + # Because of the os.path.abspath in the constructor, data_dir will + # never be an empty string. + data_dir, local = os.path.split(data.filename) + localdot = local + '.*' + + data_paths = data_paths or [data_dir] + files_to_combine = [] + for p in data_paths: + if os.path.isfile(p): + files_to_combine.append(os.path.abspath(p)) + elif os.path.isdir(p): + pattern = os.path.join(os.path.abspath(p), localdot) + files_to_combine.extend(glob.glob(pattern)) + else: + raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) + + if strict and not files_to_combine: + raise CoverageException("No data to combine") + + files_combined = 0 + for f in files_to_combine: + try: + new_data = CoverageData(f, debug=data._debug) + new_data.read() + except CoverageException as exc: + if data._warn: + # The CoverageException has the file name in it, so just + # use the message as the warning. + data._warn(str(exc)) + else: + data.update(new_data, aliases=aliases) + files_combined += 1 + if data._debug and data._debug.should('dataio'): + data._debug.write("Deleting combined data file %r" % (f,)) + file_be_gone(f) + + if strict and not files_combined: + raise CoverageException("No usable data files") def canonicalize_json_data(data): """Canonicalize our JSON data so it can be compared.""" diff --git a/tests/test_data.py b/tests/test_data.py index 5deccef0..702f4554 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -11,7 +11,7 @@ import re import mock -from coverage.data import CoverageData, debug_main, canonicalize_json_data +from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -611,7 +611,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assert_exists(".coverage.2") covdata3 = CoverageData() - covdata3.combine_parallel_data() + combine_parallel_data(covdata3) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) self.assert_doesnt_exist(".coverage.1") @@ -713,7 +713,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): aliases = PathAliases() aliases.add("/home/ned/proj/src/", "./") aliases.add(r"c:\ned\test", "./") - covdata3.combine_parallel_data(aliases=aliases) + combine_parallel_data(covdata3, aliases=aliases) apy = canonical_filename('./a.py') sub_bpy = canonical_filename('./sub/b.py') @@ -740,7 +740,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata_xxx.write() covdata3 = CoverageData() - covdata3.combine_parallel_data(data_paths=['cov1', 'cov2']) + combine_parallel_data(covdata3, data_paths=['cov1', 'cov2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -769,7 +769,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata_2xxx.write() covdata3 = CoverageData() - covdata3.combine_parallel_data(data_paths=['cov1', 'cov2/.coverage.2']) + combine_parallel_data(covdata3, data_paths=['cov1', 'cov2/.coverage.2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -782,4 +782,4 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata = CoverageData() msg = "Couldn't combine from non-existent path 'xyzzy'" with self.assertRaisesRegex(CoverageException, msg): - covdata.combine_parallel_data(data_paths=['xyzzy']) + combine_parallel_data(covdata, data_paths=['xyzzy']) -- cgit v1.2.1 From f1561b04f58fdd04b86d2ed0dc858a1222752b02 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 4 Aug 2018 10:03:47 -0400 Subject: Improved debugging --- coverage/debug.py | 17 +++++++++++++++++ coverage/misc.py | 10 ---------- coverage/results.py | 3 ++- coverage/sqldata.py | 19 ++++++++++++++----- doc/cmd.rst | 2 ++ 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index d63a9070..fd27c731 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -31,6 +31,8 @@ _TEST_NAME_FILE = "" # "/tmp/covtest.txt" class DebugControl(object): """Control and output for debugging.""" + show_repr_attr = False # For SimpleRepr + def __init__(self, options, output): """Configure the options and output file for debugging.""" self.options = list(options) + FORCED_DEBUG @@ -71,6 +73,10 @@ class DebugControl(object): `msg` is the line to write. A newline will be appended. """ + if self.should('self'): + caller_self = inspect.stack()[1][0].f_locals.get('self') + if caller_self is not None: + msg = "[self: {!r}] {}".format(caller_self, msg) self.output.write(msg+"\n") if self.should('callers'): dump_stack_frames(out=self.output, skip=1) @@ -167,6 +173,17 @@ def add_pid_and_tid(text): return text +class SimpleRepr(object): + """A mixin implementing a simple __repr__.""" + def __repr__(self): + show_attrs = ((k, v) for k, v in self.__dict__.items() if getattr(v, "show_repr_attr", True)) + return "<{klass} @0x{id:x} {attrs}>".format( + klass=self.__class__.__name__, + id=id(self), + attrs=" ".join("{}={!r}".format(k, v) for k, v in show_attrs), + ) + + def filter_text(text, filters): """Run `text` through a series of filters. diff --git a/coverage/misc.py b/coverage/misc.py index fff2a187..78ec027f 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -249,16 +249,6 @@ def _needs_to_implement(that, func_name): ) -class SimpleRepr(object): - """A mixin implementing a simple __repr__.""" - def __repr__(self): - return "<{klass} @{id:x} {attrs}>".format( - klass=self.__class__.__name__, - id=id(self) & 0xFFFFFF, - attrs=" ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()), - ) - - class BaseCoverageException(Exception): """The base of all Coverage exceptions.""" pass diff --git a/coverage/results.py b/coverage/results.py index 7e3bd268..fb919c9b 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -6,7 +6,8 @@ import collections from coverage.backward import iitems -from coverage.misc import contract, format_lines, SimpleRepr +from coverage.debug import SimpleRepr +from coverage.misc import contract, format_lines class Analysis(object): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index cddece31..296e353e 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -7,6 +7,7 @@ import os import sqlite3 from coverage.backward import iitems +from coverage.debug import SimpleRepr from coverage.misc import CoverageException, file_be_gone @@ -47,7 +48,7 @@ create table arc ( # (-1065448850,) APP_ID = -1065448850 -class CoverageSqliteData(object): +class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") self._warn = warn @@ -125,6 +126,10 @@ class CoverageSqliteData(object): { filename: { lineno: None, ... }, ...} """ + if self._debug and self._debug.should('dataop'): + self._debug.write("Adding lines: %d files, %d lines total" % ( + len(line_data), sum(len(lines) for lines in line_data.values()) + )) self._start_writing() self._choose_lines_or_arcs(lines=True) with self._connect() as con: @@ -144,6 +149,10 @@ class CoverageSqliteData(object): { filename: { (l1,l2): None, ... }, ...} """ + if self._debug and self._debug.should('dataop'): + self._debug.write("Adding arcs: %d files, %d arcs total" % ( + len(arc_data), sum(len(arcs) for arcs in arc_data.values()) + )) self._start_writing() self._choose_lines_or_arcs(arcs=True) with self._connect() as con: @@ -242,7 +251,7 @@ class CoverageSqliteData(object): return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] -class Sqlite(object): +class Sqlite(SimpleRepr): def __init__(self, filename, debug): self.debug = debug if (debug and debug.should('sql')) else None if self.debug: @@ -250,9 +259,9 @@ class Sqlite(object): self.con = sqlite3.connect(filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. - self.con.execute("pragma journal_mode=off") + self.execute("pragma journal_mode=off") # This pragma makes writing faster. - self.con.execute("pragma synchronous=off") + self.execute("pragma synchronous=off") def close(self): self.con.close() @@ -272,5 +281,5 @@ class Sqlite(object): def executemany(self, sql, data): if self.debug: - self.debug.write("Executing many {!r}".format(sql)) + self.debug.write("Executing many {!r} with {} rows".format(sql, len(data))) return self.con.executemany(sql, data) diff --git a/doc/cmd.rst b/doc/cmd.rst index d198178f..908b2ee9 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -486,6 +486,8 @@ to log: * ``process``: show process creation information, and changes in the current directory. +* ``self``: annotate each debug message with the object printing the message. + * ``sys``: before starting, dump all the system and environment information, as with :ref:`coverage debug sys `. -- cgit v1.2.1 From b147ea9dafe38e08083842f89502fefd9ba790d7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 5 Aug 2018 21:40:40 -0400 Subject: Simple tool to compare json and sql storage --- lab/gendata.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lab/gendata.py diff --git a/lab/gendata.py b/lab/gendata.py new file mode 100644 index 00000000..0e9c6b6f --- /dev/null +++ b/lab/gendata.py @@ -0,0 +1,40 @@ +import random +import time + +from coverage.data import CoverageJsonData +from coverage.sqldata import CoverageSqliteData + +NUM_FILES = 1000 +NUM_LINES = 1000 + +def gen_data(cdata): + rnd = random.Random() + rnd.seed(17) + + def linenos(num_lines, prob): + return (n for n in range(num_lines) if random.random() < prob) + + start = time.time() + for i in range(NUM_FILES): + filename = f"/src/foo/project/file{i}.py" + line_data = { filename: dict.fromkeys(linenos(NUM_LINES, .6)) } + cdata.add_lines(line_data) + + cdata.write() + end = time.time() + delta = end - start + return delta + +class DummyData: + def add_lines(self, line_data): + return + def write(self): + return + +overhead = gen_data(DummyData()) +jtime = gen_data(CoverageJsonData("gendata.json")) - overhead +stime = gen_data(CoverageSqliteData("gendata.db")) - overhead +print(f"Overhead: {overhead:.3f}s") +print(f"JSON: {jtime:.3f}s") +print(f"SQLite: {stime:.3f}s") +print(f"{stime / jtime:.3f}x slower") -- cgit v1.2.1 From 85725034b429fe46cf26429ce3bad0d53db82f3e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 21:15:24 -0400 Subject: Simplify how run --append works. I don't know why it was using combine after, when .load before seems like the obvious way to do it. --- coverage/cmdline.py | 10 ++++------ tests/test_cmdline.py | 18 +++++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 1b7955d3..14948d1c 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -392,7 +392,7 @@ class CoverageScript(object): def __init__( self, _covpkg=None, _run_python_file=None, - _run_python_module=None, _help_fn=None, _path_exists=None, + _run_python_module=None, _help_fn=None, ): # _covpkg is for dependency injection, so we can test this code. if _covpkg: @@ -405,7 +405,6 @@ class CoverageScript(object): self.run_python_file = _run_python_file or run_python_file self.run_python_module = _run_python_module or run_python_module self.help_fn = _help_fn or self.help - self.path_exists = _path_exists or os.path.exists self.global_option = False self.coverage = None @@ -619,6 +618,9 @@ class CoverageScript(object): ) return ERR + if options.append: + self.coverage.load() + # Run the script. self.coverage.start() code_ran = True @@ -634,10 +636,6 @@ class CoverageScript(object): finally: self.coverage.stop() if code_ran: - if options.append: - data_file = self.coverage.get_option("run:data_file") - if self.path_exists(data_file): - self.coverage.combine(data_paths=[data_file]) self.coverage.save() return OK diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index ecd4d8b3..b12f92ea 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -70,34 +70,31 @@ class BaseCmdLineTest(CoverageTest): return mk - def mock_command_line(self, args, path_exists=None): + def mock_command_line(self, args): """Run `args` through the command line, with a Mock. Returns the Mock it used and the status code returned. """ m = self.model_object() - m.path_exists.return_value = path_exists ret = command_line( args, _covpkg=m, _run_python_file=m.run_python_file, _run_python_module=m.run_python_module, _help_fn=m.help_fn, - _path_exists=m.path_exists, ) return m, ret - def cmd_executes(self, args, code, ret=OK, path_exists=None): + def cmd_executes(self, args, code, ret=OK): """Assert that the `args` end up executing the sequence in `code`.""" - m1, r1 = self.mock_command_line(args, path_exists=path_exists) + m1, r1 = self.mock_command_line(args) self.assertEqual(r1, ret, "Wrong status: got %r, wanted %r" % (r1, ret)) # Remove all indentation, and change ".foo()" to "m2.foo()". code = re.sub(r"(?m)^\s+", "", code) code = re.sub(r"(?m)^\.", "m2.", code) m2 = self.model_object() - m2.path_exists.return_value = path_exists code_obj = compile(code, "", "exec") eval(code_obj, globals(), {'m2': m2}) # pylint: disable=eval-used @@ -366,22 +363,21 @@ class CmdLineTest(BaseCmdLineTest): # run -a combines with an existing data file before saving. self.cmd_executes("run -a foo.py", """\ .Coverage() + .load() .start() .run_python_file('foo.py', ['foo.py']) .stop() - .path_exists('.coverage') - .combine(data_paths=['.coverage']) .save() - """, path_exists=True) + """) # run -a doesn't combine anything if the data file doesn't exist. self.cmd_executes("run -a foo.py", """\ .Coverage() + .load() .start() .run_python_file('foo.py', ['foo.py']) .stop() - .path_exists('.coverage') .save() - """, path_exists=False) + """) # --timid sets a flag, and program arguments get passed through. self.cmd_executes("run --timid foo.py abc 123", """\ .Coverage(timid=True) -- cgit v1.2.1 From 3335bb8df9226fbb3fb71dca65b7f795ee5c9552 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 21:32:59 -0400 Subject: Keep the env var naming scheme --- coverage/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/data.py b/coverage/data.py index 0b3b640b..db9cd526 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -668,7 +668,7 @@ class CoverageJsonData(object): return self._arcs is not None -which = os.environ.get("COV_STORAGE", "json") +which = os.environ.get("COVERAGE_STORAGE", "json") if which == "json": CoverageData = CoverageJsonData elif which == "sql": -- cgit v1.2.1 From 88b20059eb8902b38b76921b42fd7cb4c23346be Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 22:22:38 -0400 Subject: Clean any .coverage files we find --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 29ecff37..4512ad47 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ clean: -rm -f coverage/*,cover -rm -f MANIFEST -rm -f .coverage .coverage.* coverage.xml .metacov* + -rm -f */.coverage */*/.coverage */*/*/.coverage */*/*/*/.coverage */*/*/*/*/.coverage */*/*/*/*/*/.coverage -rm -f tests/zipmods.zip -rm -rf tests/eggsrc/build tests/eggsrc/dist tests/eggsrc/*.egg-info -rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz -- cgit v1.2.1 From 02b2bd8afe3cd171e4bd454ccf244f788ccded3c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 22:22:45 -0400 Subject: Forgot an import --- coverage/sqldata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 296e353e..80188fca 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,6 +3,7 @@ """Sqlite coverage data.""" +import glob import os import sqlite3 -- cgit v1.2.1 From 90bb6a77e02cbac6a23723b5907d5f59d1db1b82 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 10 Aug 2018 16:15:00 -0400 Subject: Move a common method outside the data classes --- coverage/data.py | 29 +++++++++++++++-------------- coverage/html.py | 3 ++- coverage/sqldata.py | 7 +++++++ tests/test_data.py | 9 +++++---- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index db9cd526..9c82ccef 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -641,20 +641,6 @@ class CoverageJsonData(object): for key in val: assert isinstance(key, string_class), "Key in _runs shouldn't be %r" % (key,) - def add_to_hash(self, filename, hasher): - """Contribute `filename`'s data to the `hasher`. - - `hasher` is a `coverage.misc.Hasher` instance to be updated with - the file's data. It should only get the results data, not the run - data. - - """ - if self._has_arcs(): - hasher.update(sorted(self.arcs(filename) or [])) - else: - hasher.update(sorted(self.lines(filename) or [])) - hasher.update(self.file_tracer(filename)) - ## ## Internal ## @@ -676,6 +662,21 @@ elif which == "sql": CoverageData = CoverageSqliteData +def add_data_to_hash(data, filename, hasher): + """Contribute `filename`'s data to the `hasher`. + + `hasher` is a `coverage.misc.Hasher` instance to be updated with + the file's data. It should only get the results data, not the run + data. + + """ + if data.has_arcs(): + hasher.update(sorted(data.arcs(filename) or [])) + else: + hasher.update(sorted(data.lines(filename) or [])) + hasher.update(data.file_tracer(filename)) + + def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): """Combine a number of data files together. diff --git a/coverage/html.py b/coverage/html.py index 186e9d22..2acc2656 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -12,6 +12,7 @@ import shutil import coverage from coverage import env from coverage.backward import iitems +from coverage.data import add_data_to_hash from coverage.files import flat_rootname from coverage.misc import CoverageException, file_be_gone, Hasher, isolate_module from coverage.report import Reporter @@ -169,7 +170,7 @@ class HtmlReporter(Reporter): """Compute a hash that changes if the file needs to be re-reported.""" m = Hasher() m.update(source) - self.data.add_to_hash(fr.filename, m) + add_data_to_hash(self.data, fr.filename, m) return m.hexdigest() def html_file(self, fr, analysis): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 80188fca..25a6d62d 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -242,6 +242,13 @@ class CoverageSqliteData(SimpleRepr): return "" # TODO def lines(self, filename): + if self.has_arcs(): + arcs = self.arcs(filename) + if arcs is not None: + import itertools + all_lines = itertools.chain.from_iterable(arcs) + return list(set(l for l in all_lines if l > 0)) + with self._connect() as con: file_id = self._file_id(filename) return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] diff --git a/tests/test_data.py b/tests/test_data.py index 68b2c375..a450f90b 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -12,6 +12,7 @@ import re import mock from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data +from coverage.data import add_data_to_hash from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -364,7 +365,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata = CoverageData() covdata.add_lines(LINES_1) hasher = mock.Mock() - covdata.add_to_hash("a.py", hasher) + add_data_to_hash(covdata, "a.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([1, 2]), # lines mock.call.update(""), # file_tracer name @@ -375,7 +376,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata.add_arcs(ARCS_3) covdata.add_file_tracers({"y.py": "hologram_plugin"}) hasher = mock.Mock() - covdata.add_to_hash("y.py", hasher) + add_data_to_hash(covdata, "y.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([(-1, 17), (17, 23), (23, -1)]), # arcs mock.call.update("hologram_plugin"), # file_tracer name @@ -386,7 +387,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata = CoverageData() covdata.add_lines(LINES_1) hasher = mock.Mock() - covdata.add_to_hash("missing.py", hasher) + add_data_to_hash(covdata, "missing.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([]), mock.call.update(None), @@ -398,7 +399,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata.add_arcs(ARCS_3) covdata.add_file_tracers({"y.py": "hologram_plugin"}) hasher = mock.Mock() - covdata.add_to_hash("missing.py", hasher) + add_data_to_hash(covdata, "missing.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([]), mock.call.update(None), -- cgit v1.2.1 From 8562aeb29eddf3349f5c363c1842f9822b18a450 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 10 Aug 2018 16:39:22 -0400 Subject: Move line_counts out of the data classes --- coverage/cmdline.py | 3 ++- coverage/data.py | 39 ++++++++++++++++++++------------------- tests/test_api.py | 3 ++- tests/test_concurrency.py | 3 ++- tests/test_data.py | 8 ++++---- tests/test_plugins.py | 9 +++++---- tests/test_process.py | 35 ++++++++++++++++++----------------- 7 files changed, 53 insertions(+), 47 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 14948d1c..23d2aec3 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -14,6 +14,7 @@ import traceback from coverage import env from coverage.collector import CTracer +from coverage.data import line_counts from coverage.debug import info_formatter, info_header from coverage.execfile import run_python_file, run_python_module from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource @@ -660,7 +661,7 @@ class CoverageScript(object): print("path: %s" % self.coverage.get_data().filename) if data: print("has_arcs: %r" % data.has_arcs()) - summary = data.line_counts(fullpath=True) + summary = line_counts(data, fullpath=True) filenames = sorted(summary.keys()) print("\n%d files:" % len(filenames)) for f in filenames: diff --git a/coverage/data.py b/coverage/data.py index 9c82ccef..44b75439 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -244,25 +244,6 @@ class CoverageJsonData(object): """A list of all files that had been measured.""" return list(self._arcs or self._lines or {}) - def line_counts(self, fullpath=False): - """Return a dict summarizing the line coverage data. - - Keys are based on the file names, and values are the number of executed - lines. If `fullpath` is true, then the keys are the full pathnames of - the files, otherwise they are the basenames of the files. - - Returns a dict mapping file names to counts of lines. - - """ - summ = {} - if fullpath: - filename_fn = lambda f: f - else: - filename_fn = os.path.basename - for filename in self.measured_files(): - summ[filename_fn(filename)] = len(self.lines(filename)) - return summ - def __nonzero__(self): return bool(self._lines or self._arcs) @@ -662,6 +643,26 @@ elif which == "sql": CoverageData = CoverageSqliteData +def line_counts(data, fullpath=False): + """Return a dict summarizing the line coverage data. + + Keys are based on the file names, and values are the number of executed + lines. If `fullpath` is true, then the keys are the full pathnames of + the files, otherwise they are the basenames of the files. + + Returns a dict mapping file names to counts of lines. + + """ + summ = {} + if fullpath: + filename_fn = lambda f: f + else: + filename_fn = os.path.basename + for filename in data.measured_files(): + summ[filename_fn(filename)] = len(data.lines(filename)) + return summ + + def add_data_to_hash(data, filename, hasher): """Contribute `filename`'s data to the `hasher`. diff --git a/tests/test_api.py b/tests/test_api.py index a860c7da..3e7e2f06 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,6 +13,7 @@ import warnings import coverage from coverage import env from coverage.backward import StringIO, import_local_file +from coverage.data import line_counts from coverage.misc import CoverageException from coverage.report import Reporter @@ -576,7 +577,7 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest): import usepkgs # pragma: nested # pylint: disable=import-error, unused-variable cov.stop() # pragma: nested data = cov.get_data() - summary = data.line_counts() + summary = line_counts(data) for k, v in list(summary.items()): assert k.endswith(".py") summary[k[:-3]] = v diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index a4f700ed..9e2d73d9 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -14,6 +14,7 @@ from flaky import flaky import coverage from coverage import env from coverage.backward import import_local_file +from coverage.data import line_counts from coverage.files import abs_file from tests.coveragetest import CoverageTest @@ -245,7 +246,7 @@ class ConcurrencyTest(CoverageTest): print_simple_annotation(code, linenos) lines = line_count(code) - self.assertEqual(data.line_counts()['try_it.py'], lines) + self.assertEqual(line_counts(data)['try_it.py'], lines) def test_threads(self): code = (THREAD + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT) diff --git a/tests/test_data.py b/tests/test_data.py index a450f90b..7ca6f655 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -12,7 +12,7 @@ import re import mock from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data -from coverage.data import add_data_to_hash +from coverage.data import add_data_to_hash, line_counts from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -74,9 +74,9 @@ MEASURED_FILES_3_4 = ['x.py', 'y.py', 'z.py'] class DataTestHelpers(CoverageTest): """Test helpers for data tests.""" - def assert_line_counts(self, covdata, line_counts, fullpath=False): - """Check that the line_counts of `covdata` is `line_counts`.""" - self.assertEqual(covdata.line_counts(fullpath), line_counts) + def assert_line_counts(self, covdata, counts, fullpath=False): + """Check that the line_counts of `covdata` is `counts`.""" + self.assertEqual(line_counts(covdata, fullpath), counts) def assert_measured_files(self, covdata, measured): """Check that `covdata`'s measured files are `measured`.""" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 0987e41a..2d0f8426 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -8,6 +8,7 @@ import os.path import coverage from coverage import env from coverage.backward import StringIO +from coverage.data import line_counts from coverage.control import Plugins from coverage.misc import CoverageException @@ -369,19 +370,19 @@ class GoodFileTracerTest(FileTracerTest): _, statements, missing, _ = cov.analysis("foo_7.html") self.assertEqual(statements, [1, 2, 3, 4, 5, 6, 7]) self.assertEqual(missing, [1, 2, 3, 6, 7]) - self.assertIn("foo_7.html", cov.get_data().line_counts()) + self.assertIn("foo_7.html", line_counts(cov.get_data())) _, statements, missing, _ = cov.analysis("bar_4.html") self.assertEqual(statements, [1, 2, 3, 4]) self.assertEqual(missing, [1, 4]) - self.assertIn("bar_4.html", cov.get_data().line_counts()) + self.assertIn("bar_4.html", line_counts(cov.get_data())) - self.assertNotIn("quux_5.html", cov.get_data().line_counts()) + self.assertNotIn("quux_5.html", line_counts(cov.get_data())) _, statements, missing, _ = cov.analysis("uni_3.html") self.assertEqual(statements, [1, 2, 3]) self.assertEqual(missing, [1]) - self.assertIn("uni_3.html", cov.get_data().line_counts()) + self.assertIn("uni_3.html", line_counts(cov.get_data())) def test_plugin2_with_branch(self): self.make_render_and_caller() diff --git a/tests/test_process.py b/tests/test_process.py index ede86691..48083f22 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -16,6 +16,7 @@ import pytest import coverage from coverage import env, CoverageData +from coverage.data import line_counts from coverage.misc import output_encoding from tests.coveragetest import CoverageTest @@ -91,7 +92,7 @@ class ProcessTest(CoverageTest): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) # Running combine again should fail, because there are no parallel data # files to combine. @@ -102,7 +103,7 @@ class ProcessTest(CoverageTest): # And the originally combined data is still there. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_combine_parallel_data_with_a_corrupt_file(self): self.make_b_or_c_py() @@ -138,7 +139,7 @@ class ProcessTest(CoverageTest): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_combine_no_usable_files(self): # https://bitbucket.org/ned/coveragepy/issues/629/multiple-use-of-combine-leads-to-empty @@ -173,7 +174,7 @@ class ProcessTest(CoverageTest): # executed (we only did b, not c). data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 6) + self.assertEqual(line_counts(data)['b_or_c.py'], 6) def test_combine_parallel_data_in_two_steps(self): self.make_b_or_c_py() @@ -204,7 +205,7 @@ class ProcessTest(CoverageTest): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_combine_parallel_data_no_append(self): self.make_b_or_c_py() @@ -236,7 +237,7 @@ class ProcessTest(CoverageTest): # because we didn't keep the data from running b. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(line_counts(data)['b_or_c.py'], 7) def test_append_data(self): self.make_b_or_c_py() @@ -255,7 +256,7 @@ class ProcessTest(CoverageTest): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_append_data_with_different_file(self): self.make_b_or_c_py() @@ -279,7 +280,7 @@ class ProcessTest(CoverageTest): # executed. data = coverage.CoverageData(".mycovdata") data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_append_can_create_a_data_file(self): self.make_b_or_c_py() @@ -293,7 +294,7 @@ class ProcessTest(CoverageTest): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 6) + self.assertEqual(line_counts(data)['b_or_c.py'], 6) def test_combine_with_rc(self): self.make_b_or_c_py() @@ -326,7 +327,7 @@ class ProcessTest(CoverageTest): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) # Reporting should still work even with the .rc file out = self.run_command("coverage report") @@ -380,7 +381,7 @@ class ProcessTest(CoverageTest): # files have been combined together. data = coverage.CoverageData() data.read() - summary = data.line_counts(fullpath=True) + summary = line_counts(data, fullpath=True) self.assertEqual(len(summary), 1) actual = os.path.normcase(os.path.abspath(list(summary.keys())[0])) expected = os.path.normcase(os.path.abspath('src/x.py')) @@ -544,7 +545,7 @@ class ProcessTest(CoverageTest): data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['fork.py'], 9) + self.assertEqual(line_counts(data)['fork.py'], 9) def test_warnings_during_reporting(self): # While fixing issue #224, the warnings were being printed far too @@ -684,7 +685,7 @@ class ProcessTest(CoverageTest): # The actual number of executed lines in os.py when it's # imported is 120 or so. Just running os.getenv executes # about 5. - self.assertGreater(data.line_counts()['os.py'], 50) + self.assertGreater(line_counts(data)['os.py'], 50) def test_lang_c(self): if env.JYTHON: @@ -911,7 +912,7 @@ class ExcepthookTest(CoverageTest): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['excepthook.py'], 7) + self.assertEqual(line_counts(data)['excepthook.py'], 7) def test_excepthook_exit(self): if env.PYPY or env.JYTHON: @@ -1257,7 +1258,7 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): self.assert_exists(".mycovdata") data = coverage.CoverageData(".mycovdata") data.read() - self.assertEqual(data.line_counts()['sub.py'], 3) + self.assertEqual(line_counts(data)['sub.py'], 3) def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov # https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of @@ -1281,7 +1282,7 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): self.assert_exists(".coverage") data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['sub.py'], 3) + self.assertEqual(line_counts(data)['sub.py'], 3) # assert that there are *no* extra data files left over after a combine data_files = glob.glob(os.getcwd() + '/.coverage*') @@ -1371,7 +1372,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): self.assert_exists(".coverage") data = coverage.CoverageData() data.read() - summary = data.line_counts() + summary = line_counts(data) print(summary) self.assertEqual(summary[source + '.py'], 3) self.assertEqual(len(summary), 1) -- cgit v1.2.1 From 420c1b10ddeed1da66a2ffb81d7ac2af32939be5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 07:40:05 -0400 Subject: Implement more --- coverage/data.py | 6 ++--- coverage/sqldata.py | 71 ++++++++++++++++++++++++++++++++++++++++++----------- tests/test_data.py | 19 ++++++++------ 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 44b75439..4b8b7eb2 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -635,10 +635,10 @@ class CoverageJsonData(object): return self._arcs is not None -which = os.environ.get("COVERAGE_STORAGE", "json") -if which == "json": +STORAGE = os.environ.get("COVERAGE_STORAGE", "json") +if STORAGE == "json": CoverageData = CoverageJsonData -elif which == "sql": +elif STORAGE == "sql": from coverage.sqldata import CoverageSqliteData CoverageData = CoverageSqliteData diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 25a6d62d..9d25d92c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -6,6 +6,7 @@ import glob import os import sqlite3 +import struct from coverage.backward import iitems from coverage.debug import SimpleRepr @@ -45,9 +46,13 @@ create table arc ( ); """ -# >>> struct.unpack(">i", b"\xc0\x7e\x8a\x6e") # "coverage", kind of. -# (-1065448850,) -APP_ID = -1065448850 +APP_ID = 0xc07e8a6e # "coverage", kind of. + +def unsigned_to_signed(val): + return struct.unpack('>i', struct.pack('>I', val))[0] + +def signed_to_unsigned(val): + return struct.unpack('>I', struct.pack('>i', val))[0] class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, warn=None, debug=None): @@ -75,7 +80,7 @@ class CoverageSqliteData(SimpleRepr): self._debug.write("Creating data file {!r}".format(self.filename)) self._db = Sqlite(self.filename, self._debug) with self._db: - self._db.execute("pragma application_id = {}".format(APP_ID)) + self._db.execute("pragma application_id = {}".format(unsigned_to_signed(APP_ID))) for stmt in SCHEMA.split(';'): stmt = stmt.strip() if stmt: @@ -91,10 +96,10 @@ class CoverageSqliteData(SimpleRepr): self._db = Sqlite(self.filename, self._debug) with self._db: for app_id, in self._db.execute("pragma application_id"): - app_id = int(app_id) + app_id = signed_to_unsigned(int(app_id)) if app_id != APP_ID: raise CoverageException( - "File {!r} doesn't look like a coverage data file: " + "Couldn't use {!r}: wrong application_id: " "0x{:08x} != 0x{:08x}".format(self.filename, app_id, APP_ID) ) for row in self._db.execute("select has_lines, has_arcs from meta"): @@ -111,6 +116,19 @@ class CoverageSqliteData(SimpleRepr): self._create_db() return self._db + def __nonzero__(self): + try: + with self._connect() as con: + if self.has_arcs(): + rows = con.execute("select * from arc limit 1") + else: + rows = con.execute("select * from line limit 1") + return bool(list(rows)) + except CoverageException: + return False + + __bool__ = __nonzero__ + def _file_id(self, filename): self._start_writing() if filename not in self._file_map: @@ -184,13 +202,28 @@ class CoverageSqliteData(SimpleRepr): """ self._start_writing() with self._connect() as con: - data = list(iitems(file_tracers)) - if data: - con.executemany( - "insert into file (path, tracer) values (?, ?) on duplicate key update", - data, + for filename, plugin_name in iitems(file_tracers): + con.execute( + "update file set tracer = ? where path = ?", + (plugin_name, filename) ) + def touch_file(self, filename, plugin_name=""): + """Ensure that `filename` appears in the data, empty if needed. + + `plugin_name` is the name of the plugin resposible for this file. It is used + to associate the right filereporter, etc. + """ + if self._debug and self._debug.should('dataop'): + self._debug.write("Touching %r" % (filename,)) + if not self._has_arcs and not self._has_lines: + raise CoverageException("Can't touch files in an empty CoverageSqliteData") + + file_id = self._file_id(filename) + if plugin_name: + # Set the tracer for this file + self.add_file_tracers({filename: plugin_name}) + def erase(self, parallel=False): """Erase the data in this object. @@ -239,7 +272,10 @@ class CoverageSqliteData(SimpleRepr): was not measured, then None is returned. """ - return "" # TODO + with self._connect() as con: + for tracer, in con.execute("select tracer from file where path = ?", (filename,)): + return tracer or "" + return None def lines(self, filename): if self.has_arcs(): @@ -258,13 +294,17 @@ class CoverageSqliteData(SimpleRepr): file_id = self._file_id(filename) return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] + def run_infos(self): + return [] # TODO + class Sqlite(SimpleRepr): def __init__(self, filename, debug): self.debug = debug if (debug and debug.should('sql')) else None if self.debug: self.debug.write("Connecting to {!r}".format(filename)) - self.con = sqlite3.connect(filename) + self.filename = filename + self.con = sqlite3.connect(self.filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. self.execute("pragma journal_mode=off") @@ -285,7 +325,10 @@ class Sqlite(SimpleRepr): if self.debug: tail = " with {!r}".format(parameters) if parameters else "" self.debug.write("Executing {!r}{}".format(sql, tail)) - return self.con.execute(sql, parameters) + try: + return self.con.execute(sql, parameters) + except sqlite3.Error as exc: + raise CoverageException("Couldn't use data file {!r}: {}".format(self.filename, exc)) def executemany(self, sql, data): if self.debug: diff --git a/tests/test_data.py b/tests/test_data.py index 7ca6f655..5e75b012 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -12,7 +12,7 @@ import re import mock from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data -from coverage.data import add_data_to_hash, line_counts +from coverage.data import add_data_to_hash, line_counts, STORAGE from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -105,7 +105,7 @@ class DataTestHelpers(CoverageTest): class CoverageDataTest(DataTestHelpers, CoverageTest): """Test cases for CoverageData.""" - run_in_temp_dir = False + run_in_temp_dir = STORAGE == "sql" def test_empty_data_is_false(self): covdata = CoverageData() @@ -449,7 +449,7 @@ class CoverageDataTestInTempDir(DataTestHelpers, CoverageTest): self.assert_arcs3_data(covdata2) def test_read_errors(self): - msg = r"Couldn't read data from '.*[/\\]{0}': \S+" + msg = r"Couldn't .* '.*[/\\]{0}': \S+" self.make_file("xyzzy.dat", "xyzzy") with self.assertRaisesRegex(CoverageException, msg.format("xyzzy.dat")): @@ -463,11 +463,12 @@ class CoverageDataTestInTempDir(DataTestHelpers, CoverageTest): covdata.read() self.assertFalse(covdata) - self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") - with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): - covdata = CoverageData("misleading.dat") - covdata.read() - self.assertFalse(covdata) + if STORAGE == "json": + self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") + with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): + covdata = CoverageData("misleading.dat") + covdata.read() + self.assertFalse(covdata) def test_debug_main(self): covdata1 = CoverageData(".coverage") @@ -640,6 +641,8 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): def read_json_data_file(self, fname): """Read a JSON data file for testing the JSON directly.""" + if STORAGE != "json": + self.skipTest("Not using JSON for data storage") with open(fname, 'r') as fdata: go_away = fdata.read(len(CoverageData._GO_AWAY)) self.assertEqual(go_away, CoverageData._GO_AWAY) -- cgit v1.2.1 From e70a13b69912591a81dfded0261fa3f847232ba1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 08:46:56 -0400 Subject: Don't add data by asking about data --- coverage/sqldata.py | 37 ++++++++++++++++++++++++++----------- tests/test_data.py | 7 +++++++ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 9d25d92c..e84c82fc 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -114,6 +114,7 @@ class CoverageSqliteData(SimpleRepr): self._open_db() else: self._create_db() + self._have_read = True return self._db def __nonzero__(self): @@ -129,13 +130,19 @@ class CoverageSqliteData(SimpleRepr): __bool__ = __nonzero__ - def _file_id(self, filename): - self._start_writing() + def _file_id(self, filename, add=False): + """Get the file id for `filename`. + + If filename is not in the database yet, add if it `add` is True. + If `add` is not True, return None. + """ if filename not in self._file_map: - with self._connect() as con: - cur = con.execute("insert into file (path) values (?)", (filename,)) - self._file_map[filename] = cur.lastrowid - return self._file_map[filename] + if add: + self._start_writing() + with self._connect() as con: + cur = con.execute("insert into file (path) values (?)", (filename,)) + self._file_map[filename] = cur.lastrowid + return self._file_map.get(filename) def add_lines(self, line_data): """Add measured line data. @@ -153,7 +160,7 @@ class CoverageSqliteData(SimpleRepr): self._choose_lines_or_arcs(lines=True) with self._connect() as con: for filename, linenos in iitems(line_data): - file_id = self._file_id(filename) + file_id = self._file_id(filename, add=True) data = [(file_id, lineno) for lineno in linenos] con.executemany( "insert or ignore into line (file_id, lineno) values (?, ?)", @@ -176,7 +183,7 @@ class CoverageSqliteData(SimpleRepr): self._choose_lines_or_arcs(arcs=True) with self._connect() as con: for filename, arcs in iitems(arc_data): - file_id = self._file_id(filename) + file_id = self._file_id(filename, add=True) data = [(file_id, fromno, tono) for fromno, tono in arcs] con.executemany( "insert or ignore into arc (file_id, fromno, tono) values (?, ?, ?)", @@ -219,7 +226,7 @@ class CoverageSqliteData(SimpleRepr): if not self._has_arcs and not self._has_lines: raise CoverageException("Can't touch files in an empty CoverageSqliteData") - file_id = self._file_id(filename) + file_id = self._file_id(filename, add=True) if plugin_name: # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) @@ -287,12 +294,20 @@ class CoverageSqliteData(SimpleRepr): with self._connect() as con: file_id = self._file_id(filename) - return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] + if file_id is None: + return None + else: + linenos = con.execute("select lineno from line where file_id = ?", (file_id,)) + return [lineno for lineno, in linenos] def arcs(self, filename): with self._connect() as con: file_id = self._file_id(filename) - return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] + if file_id is None: + return None + else: + arcs = con.execute("select fromno, tono from arc where file_id = ?", (file_id,)) + return [pair for pair in arcs] def run_infos(self): return [] # TODO diff --git a/tests/test_data.py b/tests/test_data.py index 5e75b012..b2e4644c 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -361,6 +361,13 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): with self.assertRaisesRegex(CoverageException, msg): covdata2.update(covdata1) + def test_asking_isnt_measuring(self): + # Asking about an unmeasured file shouldn't make it seem measured. + covdata = CoverageData() + self.assert_measured_files(covdata, []) + self.assertEqual(covdata.arcs("missing.py"), None) + self.assert_measured_files(covdata, []) + def test_add_to_hash_with_lines(self): covdata = CoverageData() covdata.add_lines(LINES_1) -- cgit v1.2.1 From 0812699cab9226a342dd9b914d3e14ceccdf7691 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 16:42:44 -0400 Subject: A little better --- coverage/sqldata.py | 2 +- tests/test_data.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index e84c82fc..ce78c63b 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -226,7 +226,7 @@ class CoverageSqliteData(SimpleRepr): if not self._has_arcs and not self._has_lines: raise CoverageException("Can't touch files in an empty CoverageSqliteData") - file_id = self._file_id(filename, add=True) + self._file_id(filename, add=True) if plugin_name: # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) diff --git a/tests/test_data.py b/tests/test_data.py index b2e4644c..424e1c15 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -763,14 +763,14 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assert_exists(".coverage.xxx") def test_combining_from_files(self): + os.makedirs('cov1') covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) - os.makedirs('cov1') covdata1.write() + os.makedirs('cov2') covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) - os.makedirs('cov2') covdata2.write() # This data won't be included. -- cgit v1.2.1 From c362e44f3ebeda9929c3537df96eecfa218d83c2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 19:08:19 -0400 Subject: Error handling in add_file_tracers --- coverage/sqldata.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index ce78c63b..dbeced84 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,6 +3,10 @@ """Sqlite coverage data.""" +# TODO: check the schema +# TODO: factor out dataop debugging to a wrapper class? +# TODO: make sure all dataop debugging is in place somehow + import glob import os import sqlite3 @@ -210,6 +214,21 @@ class CoverageSqliteData(SimpleRepr): self._start_writing() with self._connect() as con: for filename, plugin_name in iitems(file_tracers): + file_id = self._file_id(filename) + if file_id is None: + raise CoverageException( + "Can't add file tracer data for unmeasured file '%s'" % (filename,) + ) + + cur = con.execute("select tracer from file where id = ?", (file_id,)) + [existing_plugin] = cur.fetchone() + if existing_plugin is not None and existing_plugin != plugin_name: + raise CoverageException( + "Conflicting file tracer name for '%s': %r vs %r" % ( + filename, existing_plugin, plugin_name, + ) + ) + con.execute( "update file set tracer = ? where path = ?", (plugin_name, filename) -- cgit v1.2.1 From 3d6b9819921f5be15168631452053c63424fa8d8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 19:57:05 -0400 Subject: Sqlite update() method --- coverage/sqldata.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index dbeced84..996a2ae8 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -14,6 +14,7 @@ import struct from coverage.backward import iitems from coverage.debug import SimpleRepr +from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone @@ -250,6 +251,37 @@ class CoverageSqliteData(SimpleRepr): # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) + def update(self, other_data, aliases=None): + if self._has_lines and other_data._has_arcs: + raise CoverageException("Can't combine arc data with line data") + if self._has_arcs and other_data._has_lines: + raise CoverageException("Can't combine line data with arc data") + + aliases = aliases or PathAliases() + + # lines + if other_data._has_lines: + for filename in other_data.measured_files(): + lines = set(other_data.lines(filename)) + filename = aliases.map(filename) + lines.update(self.lines(filename) or ()) + self.add_lines({filename: lines}) + + # arcs + if other_data._has_arcs: + for filename in other_data.measured_files(): + arcs = set(other_data.arcs(filename)) + filename = aliases.map(filename) + arcs.update(self.arcs(filename) or ()) + self.add_arcs({filename: arcs}) + + # file_tracers + for filename in other_data.measured_files(): + other_plugin = other_data.file_tracer(filename) + filename = aliases.map(filename) + self.add_file_tracers({filename: other_plugin}) + + def erase(self, parallel=False): """Erase the data in this object. -- cgit v1.2.1 From 5997b823da8d60d909e776424d4ba488bb3927ec Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 12 Aug 2018 07:05:33 -0400 Subject: Start moving suffix to constructor --- coverage/data.py | 6 ++++-- tests/test_data.py | 29 +++++++++++++++-------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 4b8b7eb2..15d0a273 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -112,7 +112,7 @@ class CoverageJsonData(object): # line data is easily recovered from the arcs: it is all the first elements # of the pairs that are greater than zero. - def __init__(self, basename=None, warn=None, debug=None): + def __init__(self, basename=None, suffix=None, warn=None, debug=None): """Create a CoverageData. `warn` is the warning function to use. @@ -125,6 +125,7 @@ class CoverageJsonData(object): self._warn = warn self._debug = debug self.filename = os.path.abspath(basename or ".coverage") + self.suffix = suffix # A map from canonical Python source file name to a dictionary in # which there's an entry for each line number that has been @@ -434,7 +435,7 @@ class CoverageJsonData(object): self._validate() - def write(self, suffix=None): + def write(self): """Write the collected coverage data to a file. `suffix` is a suffix to append to the base file name. This can be used @@ -444,6 +445,7 @@ class CoverageJsonData(object): """ filename = self.filename + suffix = self.suffix if suffix is True: # If data_suffix was a simple true value, then make a suffix with # plenty of distinguishing information. We do this here in diff --git a/tests/test_data.py b/tests/test_data.py index 424e1c15..ad4dc84a 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -576,9 +576,9 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): def test_explicit_suffix(self): self.assert_doesnt_exist(".coverage.SUFFIX") - covdata = CoverageData() + covdata = CoverageData(suffix='SUFFIX') covdata.add_lines(LINES_1) - covdata.write(suffix='SUFFIX') + covdata.write() self.assert_exists(".coverage.SUFFIX") self.assert_doesnt_exist(".coverage") @@ -586,17 +586,17 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assert_file_count(".coverage.*", 0) # suffix=True will make a randomly named data file. - covdata1 = CoverageData() + covdata1 = CoverageData(suffix=True) covdata1.add_lines(LINES_1) - covdata1.write(suffix=True) + covdata1.write() self.assert_doesnt_exist(".coverage") data_files1 = glob.glob(".coverage.*") self.assertEqual(len(data_files1), 1) # Another suffix=True will choose a different name. - covdata2 = CoverageData() + covdata2 = CoverageData(suffix=True) covdata2.add_lines(LINES_1) - covdata2.write(suffix=True) + covdata2.write() self.assert_doesnt_exist(".coverage") data_files2 = glob.glob(".coverage.*") self.assertEqual(len(data_files2), 2) @@ -607,15 +607,15 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): def test_combining(self): self.assert_file_count(".coverage.*", 0) - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines(LINES_1) - covdata1.write(suffix='1') + covdata1.write() self.assert_exists(".coverage.1") self.assert_file_count(".coverage.*", 1) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_lines(LINES_2) - covdata2.write(suffix='2') + covdata2.write() self.assert_exists(".coverage.2") self.assert_file_count(".coverage.*", 2) @@ -689,6 +689,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assertNotIn('file_tracers', data) def test_writing_to_other_file(self): + self.skipTest("This will be deleted!") # TODO covdata = CoverageData(".otherfile") covdata.add_lines(LINES_1) covdata.write() @@ -700,7 +701,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assert_doesnt_exist(".coverage") def test_combining_with_aliases(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines({ '/home/ned/proj/src/a.py': {1: None, 2: None}, '/home/ned/proj/src/sub/b.py': {3: None}, @@ -709,14 +710,14 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): covdata1.add_file_tracers({ '/home/ned/proj/src/template.html': 'html.plugin', }) - covdata1.write(suffix='1') + covdata1.write() - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_lines({ r'c:\ned\test\a.py': {4: None, 5: None}, r'c:\ned\test\sub\b.py': {3: None, 6: None}, }) - covdata2.write(suffix='2') + covdata2.write() self.assert_file_count(".coverage.*", 2) -- cgit v1.2.1 From 0341a891a22f29466fd525bc5aa010c5d85bed52 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 07:32:08 -0400 Subject: Refactor initialization We need the data file suffix when the data file is created, not when write() is called. This required separating how different pieces were initialized. The old way was dumb anyway, since it (for example) created a Collector when reporting. --- coverage/control.py | 192 ++++++++++++++++++++++++++++---------------------- tests/test_plugins.py | 6 +- 2 files changed, 112 insertions(+), 86 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 46c2ece1..c83432af 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -163,8 +163,11 @@ class Coverage(object): # State machine variables: # Have we initialized everything? self._inited = False + self._inited_for_start = False # Have we started collecting and not stopped it? self._started = False + # Have we written --debug output? + self._wrote_debug = False # If we have sub-process measurement happening automatically, then we # want any explicit creation of a Coverage object to mean, this process @@ -214,73 +217,11 @@ class Coverage(object): # this is a bit childish. :) plugin.configure([self, self.config][int(time.time()) % 2]) - concurrency = self.config.concurrency or [] - if "multiprocessing" in concurrency: - if not patch_multiprocessing: - raise CoverageException( # pragma: only jython - "multiprocessing is not supported on this Python" - ) - patch_multiprocessing(rcfile=self.config.config_file) - # Multi-processing uses parallel for the subprocesses, so also use - # it for the main process. - self.config.parallel = True - - self._collector = Collector( - should_trace=self._should_trace, - check_include=self._check_include_omit_etc, - timid=self.config.timid, - branch=self.config.branch, - warn=self._warn, - concurrency=concurrency, - ) - - # Early warning if we aren't going to be able to support plugins. - if self._plugins.file_tracers and not self._collector.supports_plugins: - self._warn( - "Plugin file tracers (%s) aren't supported with %s" % ( - ", ".join( - plugin._coverage_plugin_name - for plugin in self._plugins.file_tracers - ), - self._collector.tracer_name(), - ) - ) - for plugin in self._plugins.file_tracers: - plugin._coverage_enabled = False - - # Create the file classifying substructure. - self._inorout = self._inorout_class(warn=self._warn) - self._inorout.configure(self.config) - self._inorout.plugins = self._plugins - self._inorout.disp_class = self._collector.file_disposition_class - - # Suffixes are a bit tricky. We want to use the data suffix only when - # collecting data, not when combining data. So we save it as - # `self._run_suffix` now, and promote it to `self._data_suffix` if we - # find that we are collecting data later. - if self._data_suffix_specified or self.config.parallel: - if not isinstance(self._data_suffix_specified, string_class): - # if data_suffix=True, use .machinename.pid.random - self._data_suffix_specified = True - else: - self._data_suffix_specified = None - self._data_suffix = None - self._run_suffix = self._data_suffix_specified - - # Create the data file. We do this at construction time so that the - # data file will be written into the directory where the process - # started rather than wherever the process eventually chdir'd to. - self._data = CoverageData( - basename=self.config.data_file, warn=self._warn, debug=self._debug, - ) - - # Set the reporting precision. - Numbers.set_precision(self.config.precision) - - atexit.register(self._atexit) - - # The user may want to debug things, show info if desired. - self._write_startup_debug() + def _post_init(self): + """Stuff to do after everything is initialized.""" + if not self._wrote_debug: + self._wrote_debug = True + self._write_startup_debug() def _write_startup_debug(self): """Write out debug info at startup if needed.""" @@ -387,9 +328,79 @@ class Coverage(object): def load(self): """Load previously-collected coverage data from the data file.""" self._init() - self._collector.reset() + if self._collector: + self._collector.reset() + self._init_data(suffix=None) + self._post_init() self._data.read() + def _init_for_start(self): + """Initialization for start()""" + concurrency = self.config.concurrency or [] + if "multiprocessing" in concurrency: + if not patch_multiprocessing: + raise CoverageException( # pragma: only jython + "multiprocessing is not supported on this Python" + ) + patch_multiprocessing(rcfile=self.config.config_file) + # Multi-processing uses parallel for the subprocesses, so also use + # it for the main process. + self.config.parallel = True + + self._collector = Collector( + should_trace=self._should_trace, + check_include=self._check_include_omit_etc, + timid=self.config.timid, + branch=self.config.branch, + warn=self._warn, + concurrency=concurrency, + ) + + suffix = self._data_suffix_specified + if suffix or self.config.parallel: + if not isinstance(suffix, string_class): + # if data_suffix=True, use .machinename.pid.random + suffix = True + else: + suffix = None + + self._init_data(suffix) + + # Early warning if we aren't going to be able to support plugins. + if self._plugins.file_tracers and not self._collector.supports_plugins: + self._warn( + "Plugin file tracers (%s) aren't supported with %s" % ( + ", ".join( + plugin._coverage_plugin_name + for plugin in self._plugins.file_tracers + ), + self._collector.tracer_name(), + ) + ) + for plugin in self._plugins.file_tracers: + plugin._coverage_enabled = False + + # Create the file classifying substructure. + self._inorout = self._inorout_class(warn=self._warn) + self._inorout.configure(self.config) + self._inorout.plugins = self._plugins + self._inorout.disp_class = self._collector.file_disposition_class + + atexit.register(self._atexit) + + def _init_data(self, suffix): + """Create a data file if we don't have one yet.""" + if self._data is None: + # Create the data file. We do this at construction time so that the + # data file will be written into the directory where the process + # started rather than wherever the process eventually chdir'd to. + self._data = CoverageData( + basename=self.config.data_file, + suffix=suffix, + warn=self._warn, + debug=self._debug, + ) + def start(self): """Start measuring code coverage. @@ -402,19 +413,22 @@ class Coverage(object): """ self._init() - self._inorout.warn_conflicting_settings() + if not self._inited_for_start: + self._inited_for_start = True + self._init_for_start() + self._post_init() - if self._run_suffix: - # Calling start() means we're running code, so use the run_suffix - # as the data_suffix when we eventually save the data. - self._data_suffix = self._run_suffix - if self._auto_load: - self.load() + # Issue warnings for possible problems. + self._inorout.warn_conflicting_settings() - # See if we think some code that would eventually be measured has already been imported. + # See if we think some code that would eventually be measured has + # already been imported. if self._warn_preimported_source: self._inorout.warn_already_imported_files() + if self._auto_load: + self.load() + self._collector.start() self._started = True @@ -441,7 +455,10 @@ class Coverage(object): """ self._init() - self._collector.reset() + self._post_init() + if self._collector: + self._collector.reset() + self._init_data(suffix=None) self._data.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): @@ -493,9 +510,8 @@ class Coverage(object): def save(self): """Save the collected coverage data to the data file.""" - self._init() data = self.get_data() - data.write(suffix=self._data_suffix) + data.write() def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -520,6 +536,8 @@ class Coverage(object): """ self._init() + self._init_data(suffix=None) + self._post_init() self.get_data() aliases = None @@ -544,7 +562,7 @@ class Coverage(object): """ self._init() - if self._collector.save_data(self._data): + if self._collector and self._collector.save_data(self._data): self._post_save_work() return self._data @@ -595,7 +613,6 @@ class Coverage(object): coverage data. """ - self._init() analysis = self._analyze(morf) return ( analysis.filename, @@ -611,6 +628,11 @@ class Coverage(object): Returns an `Analysis` object. """ + # All reporting comes through here, so do reporting initialization. + self._init() + Numbers.set_precision(self.config.precision) + self._post_init() + data = self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) @@ -797,6 +819,7 @@ class Coverage(object): import coverage as covmod self._init() + self._post_init() def plugin_info(plugins): """Make an entry for the sys_info from a list of plug-ins.""" @@ -811,13 +834,13 @@ class Coverage(object): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('tracer', self._collector.tracer_name()), + ('tracer', self._collector.tracer_name() if self._collector else "-none-"), ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), ('plugins.configurers', plugin_info(self._plugins.configurers)), ('configs_attempted', self.config.attempted_config_files), ('configs_read', self.config.config_files_read), ('config_file', self.config.config_file), - ('data_path', self._data.filename), + ('data_path', self._data.filename if self._data else "-none-"), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), @@ -832,7 +855,8 @@ class Coverage(object): ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] - info.extend(self._inorout.sys_info()) + if self._inorout: + info.extend(self._inorout.sys_info()) return info diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 2d0f8426..04eea3df 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -190,7 +190,8 @@ class PluginTest(CoverageTest): cov = coverage.Coverage(debug=["sys"]) cov._debug_file = debug_out cov.set_option("run:plugins", ["plugin_sys_info"]) - cov.load() + cov.start() + cov.stop() out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] if env.C_TRACER: @@ -219,7 +220,8 @@ class PluginTest(CoverageTest): cov = coverage.Coverage(debug=["sys"]) cov._debug_file = debug_out cov.set_option("run:plugins", ["plugin_no_sys_info"]) - cov.load() + cov.start() + cov.stop() out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] self.assertIn('plugins.file_tracers: -none-', out_lines) -- cgit v1.2.1 From f087c213dbe2ffb1b4a0661c9d25e67915987a99 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 08:05:57 -0400 Subject: Remove an unused debugging thing --- coverage/data.py | 8 +------- coverage/debug.py | 3 --- tests/coveragetest.py | 7 ------- tests/test_farm.py | 5 ----- 4 files changed, 1 insertion(+), 22 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 15d0a273..5e85fc10 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -15,7 +15,6 @@ import socket from coverage import env from coverage.backward import iitems, string_class -from coverage.debug import _TEST_NAME_FILE from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone, isolate_module @@ -451,13 +450,8 @@ class CoverageJsonData(object): # plenty of distinguishing information. We do this here in # `save()` at the last minute so that the pid will be correct even # if the process forks. - extra = "" - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE) as f: - test_name = f.read() - extra = "." + test_name dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) + suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) if suffix: filename += "." + suffix diff --git a/coverage/debug.py b/coverage/debug.py index fd27c731..f491a0f7 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -24,9 +24,6 @@ os = isolate_module(os) # This is a list of forced debugging options. FORCED_DEBUG = [] -# A hack for debugging testing in sub-processes. -_TEST_NAME_FILE = "" # "/tmp/covtest.txt" - class DebugControl(object): """Control and output for debugging.""" diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 94f50852..9814d648 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -24,7 +24,6 @@ from coverage import env from coverage.backunittest import TestCase, unittest from coverage.backward import StringIO, import_local_file, string_class, shlex_quote from coverage.cmdline import CoverageScript -from coverage.debug import _TEST_NAME_FILE from coverage.misc import StopEverything from tests.helpers import run_command, SuperModuleCleaner @@ -91,12 +90,6 @@ class CoverageTest( self.last_command_output = None self.last_module_name = None - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE, "w") as f: - f.write("%s_%s" % ( - self.__class__.__name__, self._testMethodName, - )) - def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_farm.py b/tests/test_farm.py index 942bdd5c..54eeb499 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -20,7 +20,6 @@ from tests.backtest import execfile # pylint: disable=redefined-builtin from coverage import env from coverage.backunittest import unittest -from coverage.debug import _TEST_NAME_FILE # Look for files that become tests. @@ -105,10 +104,6 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): def __call__(self): # pylint: disable=arguments-differ """Execute the test from the runpy file.""" - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE, "w") as f: - f.write(self.description.replace("/", "_")) - # Prepare a dictionary of globals for the run.py files to use. fns = """ copy run clean skip -- cgit v1.2.1 From da37af9a65b144ce6b1f26430bcbc9786e055f8b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 08:37:09 -0400 Subject: Move the suffix parameter, but no implementation yet --- coverage/sqldata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 996a2ae8..5ae5e64d 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -60,7 +60,7 @@ def signed_to_unsigned(val): return struct.unpack('>I', struct.pack('>i', val))[0] class CoverageSqliteData(SimpleRepr): - def __init__(self, basename=None, warn=None, debug=None): + def __init__(self, basename=None, suffix=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") self._warn = warn self._debug = debug @@ -306,7 +306,7 @@ class CoverageSqliteData(SimpleRepr): self._connect() # TODO: doesn't look right self._have_read = True - def write(self, suffix=None): + def write(self): """Write the collected coverage data to a file.""" pass -- cgit v1.2.1 From 067d0a60384b5f12cfee622381cfb5905efb8e13 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 20:38:39 -0400 Subject: Use pid-random suffixes for SQL files --- coverage/data.py | 21 ++++++++++++--------- coverage/sqldata.py | 4 ++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 5e85fc10..aa23e7d4 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -21,6 +21,17 @@ from coverage.misc import CoverageException, file_be_gone, isolate_module os = isolate_module(os) +def filename_suffix(suffix): + if suffix is True: + # If data_suffix was a simple true value, then make a suffix with + # plenty of distinguishing information. We do this here in + # `save()` at the last minute so that the pid will be correct even + # if the process forks. + dice = random.Random(os.urandom(8)).randint(0, 999999) + suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) + return suffix + + class CoverageJsonData(object): """Manages collected coverage data, including file storage. @@ -444,15 +455,7 @@ class CoverageJsonData(object): """ filename = self.filename - suffix = self.suffix - if suffix is True: - # If data_suffix was a simple true value, then make a suffix with - # plenty of distinguishing information. We do this here in - # `save()` at the last minute so that the pid will be correct even - # if the process forks. - dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) - + suffix = filename_suffix(self.suffix) if suffix: filename += "." + suffix self._write_file(filename) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 5ae5e64d..f36a9385 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -13,6 +13,7 @@ import sqlite3 import struct from coverage.backward import iitems +from coverage.data import filename_suffix from coverage.debug import SimpleRepr from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone @@ -62,6 +63,9 @@ def signed_to_unsigned(val): class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, suffix=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") + suffix = filename_suffix(suffix) + if suffix: + self.filename += "." + suffix self._warn = warn self._debug = debug -- cgit v1.2.1 From 9b13a1a7d44d991c4c5dd51d5624f5abe84b77f8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 20:39:31 -0400 Subject: Skip some tests for SQL for now --- coverage/sqldata.py | 3 +++ tests/coveragetest.py | 6 ++++++ tests/test_data.py | 6 ++++-- tests/test_process.py | 4 ++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index f36a9385..c79ad175 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,9 +3,12 @@ """Sqlite coverage data.""" +# TODO: get rid of skip_unless_data_storage_is_json # TODO: check the schema # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow +# TODO: should writes be batched? +# TODO: settle the os.fork question import glob import os diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 9814d648..cd6bb9fc 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -24,6 +24,7 @@ from coverage import env from coverage.backunittest import TestCase, unittest from coverage.backward import StringIO, import_local_file, string_class, shlex_quote from coverage.cmdline import CoverageScript +from coverage.data import STORAGE from coverage.misc import StopEverything from tests.helpers import run_command, SuperModuleCleaner @@ -90,6 +91,11 @@ class CoverageTest( self.last_command_output = None self.last_module_name = None + def skip_unless_data_storage_is_json(self): + if STORAGE != "json": + self.skipTest("Not using JSON for data storage") + + def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_data.py b/tests/test_data.py index ad4dc84a..876357eb 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -187,6 +187,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): self.assertIsNone(covdata.lines('no_such_file.py')) def test_run_info(self): + self.skip_unless_data_storage_is_json() covdata = CoverageData() self.assertEqual(covdata.run_infos(), []) covdata.add_run_info(hello="there") @@ -265,6 +266,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): self.assertEqual(covdata3.run_infos(), []) def test_update_run_info(self): + self.skip_unless_data_storage_is_json() covdata1 = CoverageData() covdata1.add_arcs(ARCS_3) covdata1.add_run_info(hello="there", count=17) @@ -478,6 +480,7 @@ class CoverageDataTestInTempDir(DataTestHelpers, CoverageTest): self.assertFalse(covdata) def test_debug_main(self): + self.skip_unless_data_storage_is_json() covdata1 = CoverageData(".coverage") covdata1.add_lines(LINES_1) covdata1.write() @@ -648,8 +651,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): def read_json_data_file(self, fname): """Read a JSON data file for testing the JSON directly.""" - if STORAGE != "json": - self.skipTest("Not using JSON for data storage") + self.skip_unless_data_storage_is_json() with open(fname, 'r') as fdata: go_away = fdata.read(len(CoverageData._GO_AWAY)) self.assertEqual(go_away, CoverageData._GO_AWAY) diff --git a/tests/test_process.py b/tests/test_process.py index 48083f22..7c705739 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -504,6 +504,8 @@ class ProcessTest(CoverageTest): def test_fork(self): if not hasattr(os, 'fork'): self.skipTest("Can't test os.fork since it doesn't exist.") + # See https://nedbatchelder.com/blog/201808/sqlite_data_storage_for_coveragepy.html + self.skip_unless_data_storage_is_json() self.make_file("fork.py", """\ import os @@ -642,6 +644,8 @@ class ProcessTest(CoverageTest): if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0): # pragma: obscure # https://bitbucket.org/pypy/pypy/issues/2729/pypy3-510-incorrectly-decodes-astral-plane self.skipTest("Avoid incorrect decoding astral plane JSON chars") + self.skip_unless_data_storage_is_json() + self.make_file(".coveragerc", """\ [run] data_file = mydata.dat -- cgit v1.2.1 From 19ec83bde56b6dfecef4ddae275376fdb4262e3a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 20:39:57 -0400 Subject: Be flexible, and accept either json-sourced or sql-source error messages in some tests --- coverage/sqldata.py | 5 ++++- tests/test_api.py | 4 ++++ tests/test_data.py | 10 ++++++++-- tests/test_debug.py | 13 +++++++++---- tests/test_process.py | 13 ++++++++++++- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index c79ad175..3abc3af3 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -4,11 +4,14 @@ """Sqlite coverage data.""" # TODO: get rid of skip_unless_data_storage_is_json +# TODO: get rid of "JSON message" and "SQL message" in the tests # TODO: check the schema +# TODO: get rid of the application_id? # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow # TODO: should writes be batched? # TODO: settle the os.fork question +# TODO: run_info import glob import os @@ -323,7 +326,7 @@ class CoverageSqliteData(SimpleRepr): self._have_read = True def has_arcs(self): - return self._has_arcs + return bool(self._has_arcs) def measured_files(self): """A list of all files that had been measured.""" diff --git a/tests/test_api.py b/tests/test_api.py index 3e7e2f06..854f9cc2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -371,8 +371,12 @@ class ApiTest(CoverageTest): self.make_bad_data_file() cov = coverage.Coverage() warning_regex = ( + r"(" # JSON message: r"Couldn't read data from '.*\.coverage\.foo': " r"CoverageException: Doesn't seem to be a coverage\.py data file" + r"|" # SQL message: + r"Couldn't use data file '.*\.coverage\.foo': file is encrypted or is not a database" + r")" ) with self.assert_warnings(cov, [warning_regex]): cov.combine() diff --git a/tests/test_data.py b/tests/test_data.py index 876357eb..317e04da 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -559,8 +559,14 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assertRegex( debug.get_output(), + r"(" # JSON output: r"^Writing data to '.*\.coverage'\n" r"Reading data from '.*\.coverage'\n$" + r"|" # SQL output: + r"Erasing data file '.*\.coverage'\n" + r"Creating data file '.*\.coverage'\n" + r"Opening data file '.*\.coverage'\n$" + r")" ) def test_debug_output_without_debug_option(self): @@ -741,14 +747,14 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assertEqual(covdata3.file_tracer(template_html), 'html.plugin') def test_combining_from_different_directories(self): + os.makedirs('cov1') covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) - os.makedirs('cov1') covdata1.write() + os.makedirs('cov2') covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) - os.makedirs('cov2') covdata2.write() # This data won't be included. diff --git a/tests/test_debug.py b/tests/test_debug.py index c46e3dae..c47dd343 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -128,8 +128,8 @@ class DebugTraceTest(CoverageTest): def test_debug_callers(self): out_lines = self.f1_debug_output(["pid", "dataop", "dataio", "callers"]) print(out_lines) - # For every real message, there should be a stack - # trace with a line like "f1_debug_output : /Users/ned/coverage/tests/test_debug.py @71" + # For every real message, there should be a stack trace with a line like + # "f1_debug_output : /Users/ned/coverage/tests/test_debug.py @71" real_messages = re_lines(out_lines, r" @\d+", match=False).splitlines() frame_pattern = r"\s+f1_debug_output : .*tests[/\\]test_debug.py @\d+$" frames = re_lines(out_lines, frame_pattern).splitlines() @@ -137,9 +137,14 @@ class DebugTraceTest(CoverageTest): # The last message should be "Writing data", and the last frame should # be _write_file in data.py. - self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") last_line = out_lines.splitlines()[-1] - self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") + from coverage.data import STORAGE + if STORAGE == "json": + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") + self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") + else: + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Creating data file") + self.assertRegex(last_line, r"\s+_create_db : .*coverage[/\\]sqldata.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) diff --git a/tests/test_process.py b/tests/test_process.py index 7c705739..49919b0f 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -127,8 +127,13 @@ class ProcessTest(CoverageTest): self.assert_exists(".coverage") self.assert_exists(".coverage.bad") warning_regex = ( + r"(" # JSON message: r"Coverage.py warning: Couldn't read data from '.*\.coverage\.bad': " r"CoverageException: Doesn't seem to be a coverage\.py data file" + r"|" # SQL message: + r"Coverage.py warning: Couldn't use data file '.*\.coverage\.bad': " + r"file is encrypted or is not a database" + r")" ) self.assertRegex(out, warning_regex) @@ -160,8 +165,14 @@ class ProcessTest(CoverageTest): for n in "12": self.assert_exists(".coverage.bad{0}".format(n)) warning_regex = ( + r"(" # JSON message: r"Coverage.py warning: Couldn't read data from '.*\.coverage\.bad{0}': " - r"CoverageException: Doesn't seem to be a coverage\.py data file".format(n) + r"CoverageException: Doesn't seem to be a coverage\.py data file" + r"|" # SQL message: + r"Coverage.py warning: Couldn't use data file '.*\.coverage.bad{0}': " + r"file is encrypted or is not a database" + r")" + .format(n) ) self.assertRegex(out, warning_regex) self.assertRegex(out, r"No usable data files") -- cgit v1.2.1 From f30f591be04a88dac2080505c241457d45f0314b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 11:12:40 -0400 Subject: Get file_tracer semantics right, whew --- coverage/sqldata.py | 79 ++++++++++++++++++++++++++++++++++------------------- tests/test_data.py | 4 +-- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 3abc3af3..01082a9b 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -40,7 +40,6 @@ create table meta ( create table file ( id integer primary key, path text, - tracer text, unique(path) ); @@ -56,6 +55,11 @@ create table arc ( tono integer, unique(file_id, fromno, tono) ); + +create table tracer ( + file_id integer primary key, + tracer text +); """ APP_ID = 0xc07e8a6e # "coverage", kind of. @@ -78,7 +82,7 @@ class CoverageSqliteData(SimpleRepr): self._file_map = {} self._db = None # Are we in sync with the data file? - self._have_read = False + self._have_used = False self._has_lines = False self._has_arcs = False @@ -88,7 +92,7 @@ class CoverageSqliteData(SimpleRepr): if self._db is not None: self._db.close() self._db = None - self._have_read = False + self._have_used = False def _create_db(self): if self._debug and self._debug.should('dataio'): @@ -129,7 +133,6 @@ class CoverageSqliteData(SimpleRepr): self._open_db() else: self._create_db() - self._have_read = True return self._db def __nonzero__(self): @@ -153,7 +156,6 @@ class CoverageSqliteData(SimpleRepr): """ if filename not in self._file_map: if add: - self._start_writing() with self._connect() as con: cur = con.execute("insert into file (path) values (?)", (filename,)) self._file_map[filename] = cur.lastrowid @@ -171,7 +173,7 @@ class CoverageSqliteData(SimpleRepr): self._debug.write("Adding lines: %d files, %d lines total" % ( len(line_data), sum(len(lines) for lines in line_data.values()) )) - self._start_writing() + self._start_using() self._choose_lines_or_arcs(lines=True) with self._connect() as con: for filename, linenos in iitems(line_data): @@ -194,7 +196,7 @@ class CoverageSqliteData(SimpleRepr): self._debug.write("Adding arcs: %d files, %d arcs total" % ( len(arc_data), sum(len(arcs) for arcs in arc_data.values()) )) - self._start_writing() + self._start_using() self._choose_lines_or_arcs(arcs=True) with self._connect() as con: for filename, arcs in iitems(arc_data): @@ -222,7 +224,7 @@ class CoverageSqliteData(SimpleRepr): `file_tracers` is { filename: plugin_name, ... } """ - self._start_writing() + self._start_using() with self._connect() as con: for filename, plugin_name in iitems(file_tracers): file_id = self._file_id(filename) @@ -231,26 +233,27 @@ class CoverageSqliteData(SimpleRepr): "Can't add file tracer data for unmeasured file '%s'" % (filename,) ) - cur = con.execute("select tracer from file where id = ?", (file_id,)) - [existing_plugin] = cur.fetchone() - if existing_plugin is not None and existing_plugin != plugin_name: - raise CoverageException( - "Conflicting file tracer name for '%s': %r vs %r" % ( - filename, existing_plugin, plugin_name, + existing_plugin = self.file_tracer(filename) + if existing_plugin: + if existing_plugin != plugin_name: + raise CoverageException( + "Conflicting file tracer name for '%s': %r vs %r" % ( + filename, existing_plugin, plugin_name, + ) ) + elif plugin_name: + con.execute( + "insert into tracer (file_id, tracer) values (?, ?)", + (file_id, plugin_name) ) - con.execute( - "update file set tracer = ? where path = ?", - (plugin_name, filename) - ) - def touch_file(self, filename, plugin_name=""): """Ensure that `filename` appears in the data, empty if needed. `plugin_name` is the name of the plugin resposible for this file. It is used to associate the right filereporter, etc. """ + self._start_using() if self._debug and self._debug.should('dataop'): self._debug.write("Touching %r" % (filename,)) if not self._has_arcs and not self._has_lines: @@ -269,6 +272,9 @@ class CoverageSqliteData(SimpleRepr): aliases = aliases or PathAliases() + # See what we had already measured, for accurate conflict reporting. + this_measured = set(self.measured_files()) + # lines if other_data._has_lines: for filename in other_data.measured_files(): @@ -289,8 +295,18 @@ class CoverageSqliteData(SimpleRepr): for filename in other_data.measured_files(): other_plugin = other_data.file_tracer(filename) filename = aliases.map(filename) - self.add_file_tracers({filename: other_plugin}) - + if filename in this_measured: + this_plugin = self.file_tracer(filename) + else: + this_plugin = None + if this_plugin is None: + self.add_file_tracers({filename: other_plugin}) + elif this_plugin != other_plugin: + raise CoverageException( + "Conflicting file tracer name for '%s': %r vs %r" % ( + filename, this_plugin, other_plugin, + ) + ) def erase(self, parallel=False): """Erase the data in this object. @@ -314,16 +330,16 @@ class CoverageSqliteData(SimpleRepr): def read(self): self._connect() # TODO: doesn't look right - self._have_read = True + self._have_used = True def write(self): """Write the collected coverage data to a file.""" pass - def _start_writing(self): - if not self._have_read: + def _start_using(self): + if not self._have_used: self.erase() - self._have_read = True + self._have_used = True def has_arcs(self): return bool(self._has_arcs) @@ -340,12 +356,18 @@ class CoverageSqliteData(SimpleRepr): was not measured, then None is returned. """ + self._start_using() with self._connect() as con: - for tracer, in con.execute("select tracer from file where path = ?", (filename,)): - return tracer or "" - return None + file_id = self._file_id(filename) + if file_id is None: + return None + row = con.execute("select tracer from tracer where file_id = ?", (file_id,)).fetchone() + if row is not None: + return row[0] or "" + return "" # File was measured, but no tracer associated. def lines(self, filename): + self._start_using() if self.has_arcs(): arcs = self.arcs(filename) if arcs is not None: @@ -362,6 +384,7 @@ class CoverageSqliteData(SimpleRepr): return [lineno for lineno, in linenos] def arcs(self, filename): + self._start_using() with self._connect() as con: file_id = self._file_id(filename) if file_id is None: diff --git a/tests/test_data.py b/tests/test_data.py index 317e04da..ad8b805b 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -348,11 +348,11 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata2.update(covdata1) def test_update_file_tracer_vs_no_file_tracer(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix="1") covdata1.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) covdata1.add_file_tracers({"p1.html": "html.plugin"}) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix="2") covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs ''" -- cgit v1.2.1 From 6ee0473a77c8bd8c91681fa86e58acb55a6e44f4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 11:26:14 -0400 Subject: A better more accurate bool(data) --- coverage/sqldata.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 01082a9b..f53561e7 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -138,10 +138,7 @@ class CoverageSqliteData(SimpleRepr): def __nonzero__(self): try: with self._connect() as con: - if self.has_arcs(): - rows = con.execute("select * from arc limit 1") - else: - rows = con.execute("select * from line limit 1") + rows = con.execute("select * from file limit 1") return bool(list(rows)) except CoverageException: return False -- cgit v1.2.1 From fd3dd69cc10026cf6d69925267134c11b281a803 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 12:17:25 -0400 Subject: Cop out for a json/sql difference in data types --- tests/test_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index ad8b805b..48df81fd 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -231,7 +231,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata.add_lines({"p1.foo": dict.fromkeys([1, 2, 3])}) covdata.add_file_tracers({"p1.foo": "p1.plugin"}) - msg = "Conflicting file tracer name for 'p1.foo': 'p1.plugin' vs 'p1.plugin.foo'" + msg = "Conflicting file tracer name for 'p1.foo': u?'p1.plugin' vs u?'p1.plugin.foo'" with self.assertRaisesRegex(CoverageException, msg): covdata.add_file_tracers({"p1.foo": "p1.plugin.foo"}) @@ -339,11 +339,11 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) covdata2.add_file_tracers({"p1.html": "html.other_plugin"}) - msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs 'html.other_plugin'" + msg = "Conflicting file tracer name for 'p1.html': u?'html.plugin' vs u?'html.other_plugin'" with self.assertRaisesRegex(CoverageException, msg): covdata1.update(covdata2) - msg = "Conflicting file tracer name for 'p1.html': 'html.other_plugin' vs 'html.plugin'" + msg = "Conflicting file tracer name for 'p1.html': u?'html.other_plugin' vs u?'html.plugin'" with self.assertRaisesRegex(CoverageException, msg): covdata2.update(covdata1) @@ -355,11 +355,11 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata2 = CoverageData(suffix="2") covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) - msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs ''" + msg = "Conflicting file tracer name for 'p1.html': u?'html.plugin' vs u?''" with self.assertRaisesRegex(CoverageException, msg): covdata1.update(covdata2) - msg = "Conflicting file tracer name for 'p1.html': '' vs 'html.plugin'" + msg = "Conflicting file tracer name for 'p1.html': u?'' vs u?'html.plugin'" with self.assertRaisesRegex(CoverageException, msg): covdata2.update(covdata1) -- cgit v1.2.1 From b282c54ebaaae13aa8b81f2380cdc20acaa9fc69 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 19:57:25 -0400 Subject: Make it run on PyPy for time tests there --- lab/gendata.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lab/gendata.py b/lab/gendata.py index 0e9c6b6f..27ad4fda 100644 --- a/lab/gendata.py +++ b/lab/gendata.py @@ -1,3 +1,5 @@ +# Run some timing tests of JsonData vs SqliteData. + import random import time @@ -16,7 +18,7 @@ def gen_data(cdata): start = time.time() for i in range(NUM_FILES): - filename = f"/src/foo/project/file{i}.py" + filename = "/src/foo/project/file{i}.py".format(i=i) line_data = { filename: dict.fromkeys(linenos(NUM_LINES, .6)) } cdata.add_lines(line_data) @@ -34,7 +36,7 @@ class DummyData: overhead = gen_data(DummyData()) jtime = gen_data(CoverageJsonData("gendata.json")) - overhead stime = gen_data(CoverageSqliteData("gendata.db")) - overhead -print(f"Overhead: {overhead:.3f}s") -print(f"JSON: {jtime:.3f}s") -print(f"SQLite: {stime:.3f}s") -print(f"{stime / jtime:.3f}x slower") +print("Overhead: {overhead:.3f}s".format(overhead=overhead)) +print("JSON: {jtime:.3f}s".format(jtime=jtime)) +print("SQLite: {stime:.3f}s".format(stime=stime)) +print("{slower:.3f}x slower".format(slower=stime/jtime)) -- cgit v1.2.1 From 948c307c89c9f61256bd96b770fa5b14ee4fe383 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 19 Aug 2018 07:47:54 -0400 Subject: PyPy needs to close cursors from pragmas --- coverage/sqldata.py | 7 +++++-- tests/coveragetest.py | 1 - tests/test_data.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index f53561e7..f92e245b 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,6 +3,7 @@ """Sqlite coverage data.""" +# TODO: get sys_info for data class, so we can see sqlite version etc # TODO: get rid of skip_unless_data_storage_is_json # TODO: get rid of "JSON message" and "SQL message" in the tests # TODO: check the schema @@ -403,9 +404,11 @@ class Sqlite(SimpleRepr): self.con = sqlite3.connect(self.filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. - self.execute("pragma journal_mode=off") + # PyPy needs the .close() calls here, or sqlite gets twisted up: + # https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on + self.execute("pragma journal_mode=off").close() # This pragma makes writing faster. - self.execute("pragma synchronous=off") + self.execute("pragma synchronous=off").close() def close(self): self.con.close() diff --git a/tests/coveragetest.py b/tests/coveragetest.py index cd6bb9fc..b804a782 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -95,7 +95,6 @@ class CoverageTest( if STORAGE != "json": self.skipTest("Not using JSON for data storage") - def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_data.py b/tests/test_data.py index 48df81fd..00d5d294 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -105,6 +105,8 @@ class DataTestHelpers(CoverageTest): class CoverageDataTest(DataTestHelpers, CoverageTest): """Test cases for CoverageData.""" + # SQL data storage always has files on disk, even without .write(). + # We need to separate the tests so they don't clobber each other. run_in_temp_dir = STORAGE == "sql" def test_empty_data_is_false(self): -- cgit v1.2.1 From cc8421fa8fb78443c73d3b7d1e7e47ffd0c8d298 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 19 Aug 2018 20:30:01 -0400 Subject: Change XML gold tests to not use a common source directory --- tests/test_xml.py | 114 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 40 deletions(-) diff --git a/tests/test_xml.py b/tests/test_xml.py index acb82a48..3e219a48 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -12,8 +12,7 @@ import coverage from coverage.backward import import_local_file from coverage.files import abs_file -from tests.coveragetest import CoverageTest -from tests.goldtest import CoverageGoldTest +from tests.coveragetest import CoverageTest, TESTS_DIR from tests.goldtest import change_dir, compare from tests.helpers import re_line, re_lines @@ -310,25 +309,34 @@ def clean(text, scrub=None): return text -class XmlGoldTest(CoverageGoldTest): - """Tests of XML reporting that use gold files.""" +def farm_dir(path): + return os.path.join(TESTS_DIR, "farm", path) - # TODO: this should move out of html. - root_dir = 'tests/farm/html' +class XmlGoldTest(CoverageTest): + """Tests of XML reporting that use gold files.""" def test_a_xml_1(self): - self.output_dir("out/xml_1") + self.make_file("a.py", """\ + # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - with change_dir("src"): - # pylint: disable=import-error - cov = coverage.Coverage() - cov.start() - import a # pragma: nested - cov.stop() # pragma: nested - cov.xml_report(a, outfile="../out/xml_1/coverage.xml") - source_path = coverage.files.relative_directory().rstrip(r"\/") + # A test file for HTML reporting by coverage.py. + + if 1 < 2: + # Needed a < to look at HTML entities. + a = 3 + else: + a = 4 + """) + + cov = coverage.Coverage() + cov.start() + import a # pragma: nested # pylint: disable=import-error + cov.stop() # pragma: nested + cov.xml_report(a, outfile="coverage.xml") + source_path = coverage.files.relative_directory().rstrip(r"\/") - compare("gold_x_xml", "out/xml_1", scrubs=[ + compare(".", farm_dir("html/gold_x_xml"), left_extra=True, scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'\s*.*?\s*', '%s' % source_path), @@ -336,18 +344,33 @@ class XmlGoldTest(CoverageGoldTest): ]) def test_a_xml_2(self): - self.output_dir("out/xml_2") - - with change_dir("src"): - # pylint: disable=import-error - cov = coverage.Coverage(config_file="run_a_xml_2.ini") - cov.start() - import a # pragma: nested - cov.stop() # pragma: nested - cov.xml_report(a) - source_path = coverage.files.relative_directory().rstrip(r"\/") - - compare("gold_x_xml", "out/xml_2", scrubs=[ + self.make_file("a.py", """\ + # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + + # A test file for HTML reporting by coverage.py. + + if 1 < 2: + # Needed a < to look at HTML entities. + a = 3 + else: + a = 4 + """) + + self.make_file("run_a_xml_2.ini", """\ + # Put all the XML output in xml_2 + [xml] + output = xml_2/coverage.xml + """) + + cov = coverage.Coverage(config_file="run_a_xml_2.ini") + cov.start() + import a # pragma: nested # pylint: disable=import-error + cov.stop() # pragma: nested + cov.xml_report(a) + source_path = coverage.files.relative_directory().rstrip(r"\/") + + compare("xml_2", farm_dir("html/gold_x_xml"), scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'\s*.*?\s*', '%s' % source_path), @@ -355,18 +378,29 @@ class XmlGoldTest(CoverageGoldTest): ]) def test_y_xml_branch(self): - self.output_dir("out/y_xml_branch") - - with change_dir("src"): - # pylint: disable=import-error - cov = coverage.Coverage(branch=True) - cov.start() - import y # pragma: nested - cov.stop() # pragma: nested - cov.xml_report(y, outfile="../out/y_xml_branch/coverage.xml") - source_path = coverage.files.relative_directory().rstrip(r"\/") - - compare("gold_y_xml_branch", "out/y_xml_branch", scrubs=[ + self.make_file("y.py", """\ + # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + + # A test file for XML reporting by coverage.py. + + def choice(x): + if x < 2: + return 3 + else: + return 4 + + assert choice(1) == 3 + """) + + cov = coverage.Coverage(branch=True) + cov.start() + import y # pragma: nested # pylint: disable=import-error + cov.stop() # pragma: nested + cov.xml_report(y, outfile="y_xml_branch/coverage.xml") + source_path = coverage.files.relative_directory().rstrip(r"\/") + + compare("y_xml_branch", farm_dir("html/gold_y_xml_branch"), scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'\s*.*?\s*', '%s' % source_path), -- cgit v1.2.1 From e248080707eb0d350f2f4bb08b555f4f3670b601 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 20 Aug 2018 07:28:51 -0400 Subject: Stop TempDirTest from complaining that no files were made --- tests/test_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_data.py b/tests/test_data.py index 00d5d294..1e6ce027 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -108,6 +108,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): # SQL data storage always has files on disk, even without .write(). # We need to separate the tests so they don't clobber each other. run_in_temp_dir = STORAGE == "sql" + no_files_in_temp_dir = True def test_empty_data_is_false(self): covdata = CoverageData() -- cgit v1.2.1 From 074d8843c0d7909bbc6692f20cc056725d26041c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 21 Aug 2018 06:56:40 -0400 Subject: Enable keeping test-created temp dirs --- doc/contributing.rst | 11 ++++++++--- tests/coveragetest.py | 6 ++++++ tox.ini | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/doc/contributing.rst b/doc/contributing.rst index 71fa6937..24a2636d 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -122,13 +122,14 @@ To limit tox to just a few versions of Python, use the ``-e`` switch:: To run just a few tests, you can use `pytest test selectors`_:: $ tox tests/test_misc.py - $ tox tests/test_misc.py::SetupPyTest - $ tox tests/test_misc.py::SetupPyTest::test_metadata + $ tox tests/test_misc.py::HasherTest + $ tox tests/test_misc.py::HasherTest::test_string_hashing These command run the tests in one file, one class, and just one test, respectively. -You can also affect the test runs with environment variables: +You can also affect the test runs with environment variables. Define any of +these as 1 to use them: - COVERAGE_NO_PYTRACER disables the Python tracer if you only want to run the CTracer tests. @@ -142,6 +143,10 @@ You can also affect the test runs with environment variables: - COVERAGE_KEEP_OUTPUT will save the output files that were generated by the gold-file tests, ones that compare output files to saved gold files. +- COVERAGE_KEEP_TMP keeps the temporary directories in which tests are run. + This makes debugging tests easier. The temporary directories are at + ``$TMPDIR/coverage_test/*``, and are named for the test that made them. + Of course, run all the tests on every version of Python you have, before submitting a change. diff --git a/tests/coveragetest.py b/tests/coveragetest.py index b804a782..6e308718 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -81,6 +81,12 @@ class CoverageTest( # Let stderr go to stderr, pytest will capture it for us. show_stderr = True + # Temp dirs go to $TMPDIR/coverage_test/* + temp_dir_prefix = "coverage_test/" + + # Keep the temp directories if the env says to. + keep_temp_dir = bool(int(os.getenv("COVERAGE_KEEP_TMP", 0))) + def setUp(self): super(CoverageTest, self).setUp() diff --git a/tox.ini b/tox.ini index 0f81c200..bbc00f3a 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps = setuptools==40.0.0 mock==2.0.0 PyContracts==1.8.3 - unittest-mixins==1.4 + unittest-mixins==1.5 #-e/Users/ned/unittest_mixins # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails py{27,34,35,36}: gevent==1.2.2 -- cgit v1.2.1 From aca3454584e7711a787f7f611837cca9c7d7c996 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 21 Aug 2018 07:05:22 -0400 Subject: Remove some unneeded lines from the code-in-code xml tests --- tests/farm/html/gold_x_xml/coverage.xml | 12 ++++++------ tests/farm/html/gold_y_xml_branch/coverage.xml | 16 ++++++++-------- tests/test_xml.py | 15 --------------- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/tests/farm/html/gold_x_xml/coverage.xml b/tests/farm/html/gold_x_xml/coverage.xml index 162824a0..1030f9f8 100644 --- a/tests/farm/html/gold_x_xml/coverage.xml +++ b/tests/farm/html/gold_x_xml/coverage.xml @@ -1,9 +1,9 @@ - - + + - /Users/ned/coverage/trunk/tests/farm/html/src + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_xml_XmlGoldTest_test_a_xml_1_43316963 @@ -11,9 +11,9 @@ - - - + + + diff --git a/tests/farm/html/gold_y_xml_branch/coverage.xml b/tests/farm/html/gold_y_xml_branch/coverage.xml index bcf1137b..71e08bb0 100644 --- a/tests/farm/html/gold_y_xml_branch/coverage.xml +++ b/tests/farm/html/gold_y_xml_branch/coverage.xml @@ -1,9 +1,9 @@ - - + + - /Users/ned/coverage/trunk/tests/farm/html/src + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_xml_XmlGoldTest_test_y_xml_branch_93378757 @@ -11,11 +11,11 @@ - - - - - + + + + + diff --git a/tests/test_xml.py b/tests/test_xml.py index 3e219a48..dd2de007 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -317,11 +317,6 @@ class XmlGoldTest(CoverageTest): def test_a_xml_1(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -345,11 +340,6 @@ class XmlGoldTest(CoverageTest): def test_a_xml_2(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -379,11 +369,6 @@ class XmlGoldTest(CoverageTest): def test_y_xml_branch(self): self.make_file("y.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for XML reporting by coverage.py. - def choice(x): if x < 2: return 3 -- cgit v1.2.1 From 6df4275aa5e15e0f9033946837c1168a7dec00d5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 21 Aug 2018 07:24:00 -0400 Subject: Smoke should be quiet, and run failed tests first --- Makefile | 6 +++--- setup.cfg | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4512ad47..9ef522a0 100644 --- a/Makefile +++ b/Makefile @@ -48,13 +48,13 @@ pep8: test: tox -e py27,py35 $(ARGS) -TOX_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) +PYTEST_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) smoke: - COVERAGE_NO_PYTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS) + COVERAGE_NO_PYTRACER=1 tox -q -e py27,py34 -- $(PYTEST_SMOKE_ARGS) pysmoke: - COVERAGE_NO_CTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS) + COVERAGE_NO_CTRACER=1 tox -q -e py27,py34 -- $(PYTEST_SMOKE_ARGS) metacov: COVERAGE_COVERAGE=yes tox $(ARGS) diff --git a/setup.cfg b/setup.cfg index 69c64e7e..0ab65b0e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = -q -n3 --strict --no-flaky-report -rfe +addopts = -q -n3 --strict --no-flaky-report -rfe --failed-first markers = expensive: too slow to run during "make smoke" -- cgit v1.2.1 From 7f5fb57e3e264f134c162dfb25c92e2b2d0e79e0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 22 Aug 2018 10:03:33 -0400 Subject: Stop using farm/src for HTML tests. --- coverage/html.py | 2 +- tests/farm/html/gold_other/blah_blah_other_py.html | 4 +- tests/farm/html/gold_other/index.html | 2 +- tests/goldtest.py | 6 +- tests/test_html.py | 484 ++++++++++++++------- tests/test_xml.py | 13 +- tox.ini | 2 +- 7 files changed, 352 insertions(+), 161 deletions(-) diff --git a/coverage/html.py b/coverage/html.py index 2acc2656..5c835684 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -68,7 +68,7 @@ def read_data(fname): def write_html(fname, html): """Write `html` to `fname`, properly encoded.""" - html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) #+ "\n" with open(fname, "wb") as fout: fout.write(html.encode('ascii', 'xmlcharrefreplace')) diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index 4beb2a07..17b7ed3d 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -3,7 +3,7 @@ - Coverage for /Users/ned/coverage/trunk/tests/farm/html/othersrc/other.py: 100% + Coverage for TEST_TMPDIR/othersrc/other.py: 100% @@ -16,7 +16,7 @@ diff --git a/tests/farm/html/gold_b_branch/b_py.html b/tests/farm/html/gold_b_branch/b_py.html index 6c3f75a8..07250e15 100644 --- a/tests/farm/html/gold_b_branch/b_py.html +++ b/tests/farm/html/gold_b_branch/b_py.html @@ -55,72 +55,62 @@
-

1

+

1

2

-

3

-

4

+

3

+

4

5

-

6

+

6

7

-

8

-

9

-

10

-

11

-

12

+

8

+

9

+

10

+

11

+

12

13

14

15

16

-

17

+

17

18

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

def one(x): 

-

# This will be a branch that misses the else. 

-

8 ↛ 11line 8 didn't jump to line 11, because the condition on line 8 was never false if x < 2: 

-

a = 3 

-

else: 

-

a = 4 

-

 

-

one(1) 

+

def one(x): 

+

# This will be a branch that misses the else. 

+

3 ↛ 6line 3 didn't jump to line 6, because the condition on line 3 was never false if x < 2: 

+

a = 3 

+

else: 

+

a = 4 

+

 

+

one(1) 

+

 

+

def two(x): 

+

# A missed else that branches to "exit" 

+

12 ↛ exitline 12 didn't return from function 'two', because the condition on line 12 was never false if x: 

+

a = 5 

 

-

def two(x): 

-

# A missed else that branches to "exit" 

-

17 ↛ exitline 17 didn't return from function 'two', because the condition on line 17 was never false if x: 

-

a = 5 

-

 

-

two(1) 

-

 

-

def three(): 

-

try: 

-

# This if has two branches, *neither* one taken. 

-

25 ↛ 26,   25 ↛ 282 missed branches: 1) line 25 didn't jump to line 26, because the condition on line 25 was never true, 2) line 25 didn't jump to line 28, because the condition on line 25 was never false if name_error_this_variable_doesnt_exist: 

-

a = 1 

-

else: 

-

a = 2 

-

except: 

-

pass 

-

 

-

three() 

+

two(1) 

+

 

+

def three(): 

+

try: 

+

# This if has two branches, *neither* one taken. 

+

20 ↛ 21,   20 ↛ 232 missed branches: 1) line 20 didn't jump to line 21, because the condition on line 20 was never true, 2) line 20 didn't jump to line 23, because the condition on line 20 was never false if name_error_this_variable_doesnt_exist: 

+

a = 1 

+

else: 

+

a = 2 

+

except: 

+

pass 

+

 

+

three() 

@@ -129,7 +119,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_b_branch/index.html b/tests/farm/html/gold_b_branch/index.html index 844f79e1..05f882bf 100644 --- a/tests/farm/html/gold_b_branch/index.html +++ b/tests/farm/html/gold_b_branch/index.html @@ -84,7 +84,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_bom/bom_py.html b/tests/farm/html/gold_bom/bom_py.html index 472f655b..92e609f8 100644 --- a/tests/farm/html/gold_bom/bom_py.html +++ b/tests/farm/html/gold_bom/bom_py.html @@ -55,35 +55,29 @@

1

-

2

+

2

3

-

4

-

5

-

6

+

4

+

5

+

6

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

+

8

+

9

+

10

+

11

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

+

# A Python source file in utf-8, with BOM. 

+

math = "3×4 = 12, ÷2 = 6±0" 

 

-

# A Python source file in utf-8, with BOM. 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

 

-

import sys 

-

 

-

if sys.version_info >= (3, 0): 

-

assert len(math) == 18 

-

assert len(math.encode('utf-8')) == 21 

-

else: 

-

assert len(math) == 21 

-

assert len(math.decode('utf-8')) == 18 

+

import sys 

+

 

+

if sys.version_info >= (3, 0): 

+

assert len(math) == 18 

+

assert len(math.encode('utf-8')) == 21 

+

else: 

+

assert len(math) == 21 

+

assert len(math.decode('utf-8')) == 18 

@@ -92,7 +86,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:44 + created at 2018-08-22 20:00

diff --git a/tests/farm/html/gold_bom/index.html b/tests/farm/html/gold_bom/index.html index 0341c0d0..13c55bf6 100644 --- a/tests/farm/html/gold_bom/index.html +++ b/tests/farm/html/gold_bom/index.html @@ -76,7 +76,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:44 + created at 2018-08-22 20:00

diff --git a/tests/farm/html/gold_isolatin1/index.html b/tests/farm/html/gold_isolatin1/index.html index ec125364..160efcb6 100644 --- a/tests/farm/html/gold_isolatin1/index.html +++ b/tests/farm/html/gold_isolatin1/index.html @@ -76,7 +76,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html index 45d13f42..02e0ac0a 100644 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ b/tests/farm/html/gold_isolatin1/isolatin1_py.html @@ -57,21 +57,15 @@

1

2

3

-

4

-

5

-

6

-

7

-

8

+

4

+

5

# -*- coding: iso8859-1 -*- 

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A Python source file in another encoding. 

-

 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

assert len(math) == 18 

+

# A Python source file in another encoding. 

+

 

+

math = "3×4 = 12, ÷2 = 6±0" 

+

assert len(math) == 18 

@@ -80,7 +74,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/index.html b/tests/farm/html/gold_omit_1/index.html index 9ea591a4..95356e06 100644 --- a/tests/farm/html/gold_omit_1/index.html +++ b/tests/farm/html/gold_omit_1/index.html @@ -97,7 +97,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/farm/html/gold_omit_1/m1_py.html index 1557156c..9ea4648e 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/farm/html/gold_omit_1/m1_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/farm/html/gold_omit_1/m2_py.html index 8f3102d1..d6647ac0 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/farm/html/gold_omit_1/m2_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m2a = 1 

-

m2b = 2 

+

m2a = 1 

+

m2b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/farm/html/gold_omit_1/m3_py.html index 2d1e1d4c..e5a9ebf7 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/farm/html/gold_omit_1/m3_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html index bc93b1a0..cc763094 100644 --- a/tests/farm/html/gold_omit_1/main_py.html +++ b/tests/farm/html/gold_omit_1/main_py.html @@ -54,34 +54,28 @@
-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/index.html b/tests/farm/html/gold_omit_2/index.html index 8c2576f2..5e78cd3a 100644 --- a/tests/farm/html/gold_omit_2/index.html +++ b/tests/farm/html/gold_omit_2/index.html @@ -90,7 +90,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/farm/html/gold_omit_2/m2_py.html index 8f3102d1..d6647ac0 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/farm/html/gold_omit_2/m2_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m2a = 1 

-

m2b = 2 

+

m2a = 1 

+

m2b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/farm/html/gold_omit_2/m3_py.html index 2d1e1d4c..e5a9ebf7 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/farm/html/gold_omit_2/m3_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html index bc93b1a0..cc763094 100644 --- a/tests/farm/html/gold_omit_2/main_py.html +++ b/tests/farm/html/gold_omit_2/main_py.html @@ -54,34 +54,28 @@
-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_3/index.html b/tests/farm/html/gold_omit_3/index.html index f0b32cc4..5c03fb8f 100644 --- a/tests/farm/html/gold_omit_3/index.html +++ b/tests/farm/html/gold_omit_3/index.html @@ -83,7 +83,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/farm/html/gold_omit_3/m3_py.html index 2d1e1d4c..e5a9ebf7 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/farm/html/gold_omit_3/m3_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html index bc93b1a0..cc763094 100644 --- a/tests/farm/html/gold_omit_3/main_py.html +++ b/tests/farm/html/gold_omit_3/main_py.html @@ -54,34 +54,28 @@
-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/index.html b/tests/farm/html/gold_omit_4/index.html index 7dadd229..13c4ab69 100644 --- a/tests/farm/html/gold_omit_4/index.html +++ b/tests/farm/html/gold_omit_4/index.html @@ -90,7 +90,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/farm/html/gold_omit_4/m1_py.html index 1557156c..9ea4648e 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/farm/html/gold_omit_4/m1_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/farm/html/gold_omit_4/m3_py.html index 2d1e1d4c..e5a9ebf7 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/farm/html/gold_omit_4/m3_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html index bc93b1a0..cc763094 100644 --- a/tests/farm/html/gold_omit_4/main_py.html +++ b/tests/farm/html/gold_omit_4/main_py.html @@ -54,34 +54,28 @@
-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_5/index.html b/tests/farm/html/gold_omit_5/index.html index b9912d24..366b1b8b 100644 --- a/tests/farm/html/gold_omit_5/index.html +++ b/tests/farm/html/gold_omit_5/index.html @@ -83,7 +83,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/farm/html/gold_omit_5/m1_py.html index 1557156c..9ea4648e 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/farm/html/gold_omit_5/m1_py.html @@ -54,18 +54,12 @@
-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html index bc93b1a0..cc763094 100644 --- a/tests/farm/html/gold_omit_5/main_py.html +++ b/tests/farm/html/gold_omit_5/main_py.html @@ -54,34 +54,28 @@
-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index fb45d0bf..4d083808 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -3,7 +3,7 @@ - Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_81055852/othersrc/other.py: 100% + Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py: 100% @@ -16,7 +16,7 @@ diff --git a/tests/farm/html/gold_other/index.html b/tests/farm/html/gold_other/index.html index a9c3d4cb..18edab71 100644 --- a/tests/farm/html/gold_other/index.html +++ b/tests/farm/html/gold_other/index.html @@ -60,7 +60,7 @@ - /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_81055852/othersrc/other.py + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py 1 0 0 @@ -83,7 +83,7 @@

coverage.py v5.0a2, - created at 2018-08-22 19:08 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_partial/index.html b/tests/farm/html/gold_partial/index.html index ca8919d7..b0addf9c 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/farm/html/gold_partial/index.html @@ -84,7 +84,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html index 7f4ed31a..c792ff75 100644 --- a/tests/farm/html/gold_partial/partial_py.html +++ b/tests/farm/html/gold_partial/partial_py.html @@ -56,55 +56,47 @@

1

-

2

+

2

3

-

4

-

5

-

6

-

7

+

4

+

5

+

6

+

7

8

-

9

-

10

+

9

+

10

11

-

12

+

12

13

-

14

-

15

+

14

+

15

16

-

17

+

17

18

-

19

-

20

-

21

-

22

-

23

-

24

+

19

+

20

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

+

# partial branches and excluded lines 

+

a = 6 

 

-

# partial branches and excluded lines 

-

 

-

a = 6 

-

 

-

while True: 

-

break 

-

 

-

while 1: 

-

break 

-

 

-

while a: # pragma: no branch 

-

break 

-

 

-

if 0: 

-

never_happen() 

-

 

-

if 1: 

-

a = 21 

-

 

-

if a == 23: 

-

raise AssertionError("Can't") 

+

while True: 

+

break 

+

 

+

while 1: 

+

break 

+

 

+

while a: # pragma: no branch 

+

break 

+

 

+

if 0: 

+

never_happen() 

+

 

+

if 1: 

+

a = 21 

+

 

+

if a == 23: 

+

raise AssertionError("Can't") 

@@ -113,7 +105,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html index 6e6f317c..65f35d6e 100644 --- a/tests/farm/html/gold_styled/a_py.html +++ b/tests/farm/html/gold_styled/a_py.html @@ -55,28 +55,18 @@
-

1

+

1

2

-

3

+

3

4

-

5

-

6

-

7

-

8

-

9

-

10

+

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

if 1 < 2: 

-

# Needed a < to look at HTML entities. 

-

a = 3 

-

else: 

-

a = 4 

+

if 1 < 2: 

+

# Needed a < to look at HTML entities. 

+

a = 3 

+

else: 

+

a = 4 

@@ -85,7 +75,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_styled/index.html b/tests/farm/html/gold_styled/index.html index c0881592..e5c36b95 100644 --- a/tests/farm/html/gold_styled/index.html +++ b/tests/farm/html/gold_styled/index.html @@ -77,7 +77,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_unicode/index.html b/tests/farm/html/gold_unicode/index.html index c1f2fb67..ff722dc9 100644 --- a/tests/farm/html/gold_unicode/index.html +++ b/tests/farm/html/gold_unicode/index.html @@ -76,7 +76,7 @@

coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html index 7708f22e..8207d798 100644 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ b/tests/farm/html/gold_unicode/unicode_py.html @@ -57,21 +57,15 @@

1

2

3

-

4

-

5

-

6

-

7

-

8

+

4

+

5

# -*- coding: utf-8 -*- 

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A Python source file with exotic characters. 

-

 

-

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

-

surrogate = "db40,dd00: x󠄀" 

+

# A Python source file with exotic characters. 

+

 

+

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

+

surrogate = "db40,dd00: x󠄀" 

@@ -80,7 +74,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/test_html.py b/tests/test_html.py index 07ddba82..b4dd4606 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -596,11 +596,6 @@ class HtmlGoldTests(CoverageTest): def test_a(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -632,11 +627,6 @@ class HtmlGoldTests(CoverageTest): def test_b_branch(self): self.make_file("b.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - def one(x): # This will be a branch that misses the else. if x < 2: @@ -680,19 +670,20 @@ class HtmlGoldTests(CoverageTest): (' a = ' '3'), '70%', - ('8 ↛ 11' - 'line 8 didn\'t jump to line 11, ' - 'because the condition on line 8 was never false'), - ('17 ↛ exit' - 'line 17 didn\'t return from function \'two\', ' - 'because the condition on line 17 was never false'), - ('25 ↛ 26,   ' - '25 ↛ 28' + + ('3 ↛ 6' + 'line 3 didn\'t jump to line 6, ' + 'because the condition on line 3 was never false'), + ('12 ↛ exit' + 'line 12 didn\'t return from function \'two\', ' + 'because the condition on line 12 was never false'), + ('20 ↛ 21,   ' + '20 ↛ 23' '2 missed branches: ' - '1) line 25 didn\'t jump to line 26, ' - 'because the condition on line 25 was never true, ' - '2) line 25 didn\'t jump to line 28, ' - 'because the condition on line 25 was never false'), + '1) line 20 didn\'t jump to line 21, ' + 'because the condition on line 20 was never true, ' + '2) line 20 didn\'t jump to line 23, ' + 'because the condition on line 20 was never false'), ) contains( "out/index.html", @@ -703,10 +694,7 @@ class HtmlGoldTests(CoverageTest): def test_bom(self): self.make_file("bom.py", bytes=b"""\ -\xef\xbb\xbf# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -# A Python source file in utf-8, with BOM. +\xef\xbb\xbf# A Python source file in utf-8, with BOM. math = "3\xc3\x974 = 12, \xc3\xb72 = 6\xc2\xb10" import sys @@ -725,7 +713,7 @@ else: with open("bom.py", "rb") as f: data = f.read() assert data[:3] == b"\xef\xbb\xbf" - assert data.count(b"\r\n") == 14 + assert data.count(b"\r\n") == 11 cov = coverage.Coverage() cov.start() @@ -742,9 +730,6 @@ else: def test_isolatin1(self): self.make_file("isolatin1.py", bytes=b"""\ # -*- coding: iso8859-1 -*- -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # A Python source file in another encoding. math = "3\xd74 = 12, \xf72 = 6\xb10" @@ -765,9 +750,6 @@ assert len(math) == 18 def make_main_etc(self): self.make_file("main.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - import m1 import m2 import m3 @@ -780,23 +762,14 @@ assert len(math) == 18 assert m3.m3a == 1 """) self.make_file("m1.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - m1a = 1 m1b = 2 """) self.make_file("m2.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - m2a = 1 m2b = 2 """) self.make_file("m3.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - m3a = 1 m3b = 2 """) @@ -872,11 +845,6 @@ assert len(math) == 18 def test_other(self): self.make_file("src/here.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - import other if 1 < 2: @@ -885,9 +853,6 @@ assert len(math) == 18 h = 4 """) self.make_file("othersrc/other.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # A file in another directory. We're checking that it ends up in the # HTML report. @@ -916,11 +881,7 @@ assert len(math) == 18 def test_partial(self): self.make_file("partial.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # partial branches and excluded lines - a = 6 while True: @@ -942,9 +903,6 @@ assert len(math) == 18 raise AssertionError("Can't") """) self.make_file("partial.ini", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - [run] branch = True @@ -962,13 +920,13 @@ assert len(math) == 18 compare_html("out", gold_path("html/gold_partial")) contains( "out/partial_py.html", - '

', - '

', - '

', + '

', + '

', + '

', # The "if 0" and "if 1" statements are optimized away. - '

', + '

', # The "raise AssertionError" is excluded by regex in the .ini. - '

', + '

', ) contains( "out/index.html", @@ -981,11 +939,6 @@ assert len(math) == 18 def test_styled(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -1058,9 +1011,6 @@ assert len(math) == 18 def test_unicode(self): self.make_file("unicode.py", """\ # -*- coding: utf-8 -*- - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # A Python source file with exotic characters. upside_down = "ʎd˙ǝbɐɹǝʌoɔ" -- cgit v1.2.1 From 9bd3b005d08ee78edbd684ed0706b23843b0460e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 22 Aug 2018 20:11:29 -0400 Subject: When we trimmed trailing whitespace, we lost the last newline. Put it back. --- coverage/html.py | 2 +- tests/farm/html/gold_a/a_py.html | 4 ++-- tests/farm/html/gold_a/index.html | 4 ++-- tests/farm/html/gold_b_branch/b_py.html | 4 ++-- tests/farm/html/gold_b_branch/index.html | 4 ++-- tests/farm/html/gold_bom/bom_py.html | 4 ++-- tests/farm/html/gold_bom/index.html | 4 ++-- tests/farm/html/gold_isolatin1/index.html | 4 ++-- tests/farm/html/gold_isolatin1/isolatin1_py.html | 4 ++-- tests/farm/html/gold_omit_1/index.html | 4 ++-- tests/farm/html/gold_omit_1/m1_py.html | 4 ++-- tests/farm/html/gold_omit_1/m2_py.html | 4 ++-- tests/farm/html/gold_omit_1/m3_py.html | 4 ++-- tests/farm/html/gold_omit_1/main_py.html | 4 ++-- tests/farm/html/gold_omit_2/index.html | 4 ++-- tests/farm/html/gold_omit_2/m2_py.html | 4 ++-- tests/farm/html/gold_omit_2/m3_py.html | 4 ++-- tests/farm/html/gold_omit_2/main_py.html | 4 ++-- tests/farm/html/gold_omit_3/index.html | 4 ++-- tests/farm/html/gold_omit_3/m3_py.html | 4 ++-- tests/farm/html/gold_omit_3/main_py.html | 4 ++-- tests/farm/html/gold_omit_4/index.html | 4 ++-- tests/farm/html/gold_omit_4/m1_py.html | 4 ++-- tests/farm/html/gold_omit_4/m3_py.html | 4 ++-- tests/farm/html/gold_omit_4/main_py.html | 4 ++-- tests/farm/html/gold_omit_5/index.html | 4 ++-- tests/farm/html/gold_omit_5/m1_py.html | 4 ++-- tests/farm/html/gold_omit_5/main_py.html | 4 ++-- tests/farm/html/gold_other/blah_blah_other_py.html | 8 ++++---- tests/farm/html/gold_other/here_py.html | 4 ++-- tests/farm/html/gold_other/index.html | 6 +++--- tests/farm/html/gold_partial/index.html | 4 ++-- tests/farm/html/gold_partial/partial_py.html | 4 ++-- tests/farm/html/gold_styled/a_py.html | 4 ++-- tests/farm/html/gold_styled/index.html | 4 ++-- tests/farm/html/gold_unicode/index.html | 4 ++-- tests/farm/html/gold_unicode/unicode_py.html | 4 ++-- 37 files changed, 76 insertions(+), 76 deletions(-) diff --git a/coverage/html.py b/coverage/html.py index 5c835684..bb519254 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -68,7 +68,7 @@ def read_data(fname): def write_html(fname, html): """Write `html` to `fname`, properly encoded.""" - html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) #+ "\n" + html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n" with open(fname, "wb") as fout: fout.write(html.encode('ascii', 'xmlcharrefreplace')) diff --git a/tests/farm/html/gold_a/a_py.html b/tests/farm/html/gold_a/a_py.html index b90583bc..119ad4a3 100644 --- a/tests/farm/html/gold_a/a_py.html +++ b/tests/farm/html/gold_a/a_py.html @@ -74,9 +74,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_a/index.html b/tests/farm/html/gold_a/index.html index adcda2d6..b839af1e 100644 --- a/tests/farm/html/gold_a/index.html +++ b/tests/farm/html/gold_a/index.html @@ -76,9 +76,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_b_branch/b_py.html b/tests/farm/html/gold_b_branch/b_py.html index 07250e15..a21175eb 100644 --- a/tests/farm/html/gold_b_branch/b_py.html +++ b/tests/farm/html/gold_b_branch/b_py.html @@ -119,9 +119,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_b_branch/index.html b/tests/farm/html/gold_b_branch/index.html index 05f882bf..a0346e86 100644 --- a/tests/farm/html/gold_b_branch/index.html +++ b/tests/farm/html/gold_b_branch/index.html @@ -84,9 +84,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_bom/bom_py.html b/tests/farm/html/gold_bom/bom_py.html index 92e609f8..78d7f7b7 100644 --- a/tests/farm/html/gold_bom/bom_py.html +++ b/tests/farm/html/gold_bom/bom_py.html @@ -86,9 +86,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 20:00 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_bom/index.html b/tests/farm/html/gold_bom/index.html index 13c55bf6..4c4d9897 100644 --- a/tests/farm/html/gold_bom/index.html +++ b/tests/farm/html/gold_bom/index.html @@ -76,9 +76,9 @@

coverage.py v5.0a2, - created at 2018-08-22 20:00 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_isolatin1/index.html b/tests/farm/html/gold_isolatin1/index.html index 160efcb6..c648ae7d 100644 --- a/tests/farm/html/gold_isolatin1/index.html +++ b/tests/farm/html/gold_isolatin1/index.html @@ -76,9 +76,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html index 02e0ac0a..e8ad244b 100644 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ b/tests/farm/html/gold_isolatin1/isolatin1_py.html @@ -74,9 +74,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/index.html b/tests/farm/html/gold_omit_1/index.html index 95356e06..289c6f10 100644 --- a/tests/farm/html/gold_omit_1/index.html +++ b/tests/farm/html/gold_omit_1/index.html @@ -97,9 +97,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/farm/html/gold_omit_1/m1_py.html index 9ea4648e..05b2bd49 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/farm/html/gold_omit_1/m1_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/farm/html/gold_omit_1/m2_py.html index d6647ac0..056e7af1 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/farm/html/gold_omit_1/m2_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/farm/html/gold_omit_1/m3_py.html index e5a9ebf7..428527b2 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/farm/html/gold_omit_1/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html index cc763094..3fbc4af7 100644 --- a/tests/farm/html/gold_omit_1/main_py.html +++ b/tests/farm/html/gold_omit_1/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/index.html b/tests/farm/html/gold_omit_2/index.html index 5e78cd3a..5813c0dc 100644 --- a/tests/farm/html/gold_omit_2/index.html +++ b/tests/farm/html/gold_omit_2/index.html @@ -90,9 +90,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/farm/html/gold_omit_2/m2_py.html index d6647ac0..056e7af1 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/farm/html/gold_omit_2/m2_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/farm/html/gold_omit_2/m3_py.html index e5a9ebf7..428527b2 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/farm/html/gold_omit_2/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html index cc763094..3fbc4af7 100644 --- a/tests/farm/html/gold_omit_2/main_py.html +++ b/tests/farm/html/gold_omit_2/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_3/index.html b/tests/farm/html/gold_omit_3/index.html index 5c03fb8f..4ebcf4a4 100644 --- a/tests/farm/html/gold_omit_3/index.html +++ b/tests/farm/html/gold_omit_3/index.html @@ -83,9 +83,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/farm/html/gold_omit_3/m3_py.html index e5a9ebf7..428527b2 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/farm/html/gold_omit_3/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html index cc763094..3fbc4af7 100644 --- a/tests/farm/html/gold_omit_3/main_py.html +++ b/tests/farm/html/gold_omit_3/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/index.html b/tests/farm/html/gold_omit_4/index.html index 13c4ab69..e7588714 100644 --- a/tests/farm/html/gold_omit_4/index.html +++ b/tests/farm/html/gold_omit_4/index.html @@ -90,9 +90,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/farm/html/gold_omit_4/m1_py.html index 9ea4648e..05b2bd49 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/farm/html/gold_omit_4/m1_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/farm/html/gold_omit_4/m3_py.html index e5a9ebf7..428527b2 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/farm/html/gold_omit_4/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html index cc763094..3fbc4af7 100644 --- a/tests/farm/html/gold_omit_4/main_py.html +++ b/tests/farm/html/gold_omit_4/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_5/index.html b/tests/farm/html/gold_omit_5/index.html index 366b1b8b..e2c1a132 100644 --- a/tests/farm/html/gold_omit_5/index.html +++ b/tests/farm/html/gold_omit_5/index.html @@ -83,9 +83,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/farm/html/gold_omit_5/m1_py.html index 9ea4648e..05b2bd49 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/farm/html/gold_omit_5/m1_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html index cc763094..3fbc4af7 100644 --- a/tests/farm/html/gold_omit_5/main_py.html +++ b/tests/farm/html/gold_omit_5/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index 4d083808..36e3653d 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -3,7 +3,7 @@ - Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py: 100% + Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_95946649/othersrc/other.py: 100% @@ -16,7 +16,7 @@ - \ No newline at end of file + diff --git a/tests/farm/html/gold_other/index.html b/tests/farm/html/gold_other/index.html index 18edab71..10d4ae9a 100644 --- a/tests/farm/html/gold_other/index.html +++ b/tests/farm/html/gold_other/index.html @@ -60,7 +60,7 @@ - /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_95946649/othersrc/other.py 1 0 0 @@ -83,9 +83,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_partial/index.html b/tests/farm/html/gold_partial/index.html index b0addf9c..1948615c 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/farm/html/gold_partial/index.html @@ -84,9 +84,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html index c792ff75..44238f68 100644 --- a/tests/farm/html/gold_partial/partial_py.html +++ b/tests/farm/html/gold_partial/partial_py.html @@ -105,9 +105,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html index 65f35d6e..dd569b1b 100644 --- a/tests/farm/html/gold_styled/a_py.html +++ b/tests/farm/html/gold_styled/a_py.html @@ -75,9 +75,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_styled/index.html b/tests/farm/html/gold_styled/index.html index e5c36b95..1f86b772 100644 --- a/tests/farm/html/gold_styled/index.html +++ b/tests/farm/html/gold_styled/index.html @@ -77,9 +77,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_unicode/index.html b/tests/farm/html/gold_unicode/index.html index ff722dc9..35a98a9e 100644 --- a/tests/farm/html/gold_unicode/index.html +++ b/tests/farm/html/gold_unicode/index.html @@ -76,9 +76,9 @@

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html index 8207d798..174a9a27 100644 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ b/tests/farm/html/gold_unicode/unicode_py.html @@ -74,9 +74,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + -- cgit v1.2.1 From 53d5da251b441c1896be707cf6c8bce2ce7d2cfe Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 06:38:55 -0400 Subject: Py2-specific gold files --- tests/farm/html/gold_bom/2/bom_py.html | 48 +++++++++++++++------------------- tests/farm/html/gold_bom/2/index.html | 4 +-- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/tests/farm/html/gold_bom/2/bom_py.html b/tests/farm/html/gold_bom/2/bom_py.html index 78c498fd..14f25413 100644 --- a/tests/farm/html/gold_bom/2/bom_py.html +++ b/tests/farm/html/gold_bom/2/bom_py.html @@ -55,35 +55,29 @@

1

-

2

+

2

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

+

# A Python source file in utf-8, with BOM. 

+

math = "3×4 = 12, ÷2 = 6±0" 

 

-

# A Python source file in utf-8, with BOM. 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

 

-

import sys 

-

 

-

if sys.version_info >= (3, 0): 

-

assert len(math) == 18 

-

assert len(math.encode('utf-8')) == 21 

-

else: 

-

assert len(math) == 21 

-

assert len(math.decode('utf-8')) == 18 

+

import sys 

+

 

+

if sys.version_info >= (3, 0): 

+

assert len(math) == 18 

+

assert len(math.encode('utf-8')) == 21 

+

else: 

+

assert len(math) == 21 

+

assert len(math.decode('utf-8')) == 18 

@@ -92,9 +86,9 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:46 + created at 2018-08-23 06:35

- \ No newline at end of file + diff --git a/tests/farm/html/gold_bom/2/index.html b/tests/farm/html/gold_bom/2/index.html index 2d285ab1..bde4bb46 100644 --- a/tests/farm/html/gold_bom/2/index.html +++ b/tests/farm/html/gold_bom/2/index.html @@ -76,9 +76,9 @@

coverage.py v5.0a2, - created at 2018-06-29 15:46 + created at 2018-08-23 06:35

- \ No newline at end of file + -- cgit v1.2.1 From b701a0c3088f917e3fc5feb081a5b5166126d4f1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 06:39:29 -0400 Subject: SQL storage means more tests need temp directories --- tests/test_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 854f9cc2..88da3468 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -512,8 +512,6 @@ class NamespaceModuleTest(UsingModulesMixin, CoverageTest): class OmitIncludeTestsMixin(UsingModulesMixin, CoverageTestMethodsMixin): """Test methods for coverage methods taking include and omit.""" - run_in_temp_dir = False - def filenames_in(self, summary, filenames): """Assert the `filenames` are in the keys of `summary`.""" for filename in filenames.split(): -- cgit v1.2.1 From 9f502b230c8c4b48334c0846cc9c50f9783c1a06 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 07:08:21 -0400 Subject: Remove now-unused CoverageGoldTest class --- doc/contributing.rst | 3 --- tests/goldtest.py | 30 +----------------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/doc/contributing.rst b/doc/contributing.rst index 24a2636d..90d73097 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -140,9 +140,6 @@ these as 1 to use them: - COVEGE_AST_DUMP will dump the AST tree as it is being used during code parsing. -- COVERAGE_KEEP_OUTPUT will save the output files that were generated by the - gold-file tests, ones that compare output files to saved gold files. - - COVERAGE_KEEP_TMP keeps the temporary directories in which tests are run. This makes debugging tests easier. The temporary directories are at ``$TMPDIR/coverage_test/*``, and are named for the test that made them. diff --git a/tests/goldtest.py b/tests/goldtest.py index af471a14..48842f0c 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -4,12 +4,10 @@ """A test base class for tests based on gold file comparison.""" import os -import sys from unittest_mixins import change_dir # pylint: disable=unused-import -from tests.coveragetest import CoverageTest, TESTS_DIR -from tests.test_farm import clean +from tests.coveragetest import TESTS_DIR # Import helpers, eventually test_farm.py will go away. from tests.test_farm import ( # pylint: disable=unused-import compare, contains, doesnt_contain, contains_any, @@ -18,29 +16,3 @@ from tests.test_farm import ( # pylint: disable=unused-import def gold_path(path): """Get a path to a gold file for comparison.""" return os.path.join(TESTS_DIR, "farm", path) - - -class CoverageGoldTest(CoverageTest): - """A test based on gold files.""" - - run_in_temp_dir = False - - def setUp(self): - super(CoverageGoldTest, self).setUp() - self.chdir(self.root_dir) - # Modules should be importable from the current directory. - sys.path.insert(0, '') - - def output_dir(self, the_dir): - """Declare where the output directory is. - - The output directory is deleted at the end of the test, unless the - COVERAGE_KEEP_OUTPUT environment variable is set. - - """ - # To make sure tests are isolated, we always clean the directory at the - # beginning of the test. - clean(the_dir) - - if not os.environ.get("COVERAGE_KEEP_OUTPUT"): # pragma: part covered - self.addCleanup(clean, the_dir) -- cgit v1.2.1 From a6097893ac54e6332a7c7b4b3667fc3064d9fb1b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 08:36:47 -0400 Subject: Make SQLite the default storage --- coverage/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/data.py b/coverage/data.py index aa23e7d4..f03e90ca 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -634,7 +634,7 @@ class CoverageJsonData(object): return self._arcs is not None -STORAGE = os.environ.get("COVERAGE_STORAGE", "json") +STORAGE = os.environ.get("COVERAGE_STORAGE", "sql") if STORAGE == "json": CoverageData = CoverageJsonData elif STORAGE == "sql": -- cgit v1.2.1 From 7ef5a0fa170dd96aa257924554473cedfb3ceae7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 20:11:54 -0400 Subject: Add a test emulating pytest-cov --- tests/test_api.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 88da3468..05bde67c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -704,8 +704,6 @@ class TestRunnerPluginTest(CoverageTest): """ def pretend_to_be_nose_with_cover(self, erase): """This is what the nose --with-cover plugin does.""" - cov = coverage.Coverage() - self.make_file("no_biggie.py", """\ a = 1 b = 2 @@ -713,6 +711,7 @@ class TestRunnerPluginTest(CoverageTest): c = 4 """) + cov = coverage.Coverage() if erase: cov.combine() cov.erase() @@ -733,6 +732,34 @@ class TestRunnerPluginTest(CoverageTest): def test_nose_plugin_with_erase(self): self.pretend_to_be_nose_with_cover(erase=True) + def test_pytestcov_parallel(self): + self.make_file("prog.py", """\ + a = 1 + b = 2 + if b == 1: + c = 4 + """) + self.make_file(".coveragerc", """\ + [run] + parallel = True + source = . + """) + + cov = coverage.Coverage(source=None, branch=None, config_file='.coveragerc') + cov.erase() + self.start_import_stop(cov, "prog") + cov.combine() + cov.save() + report = StringIO() + cov.report(show_missing=None, ignore_errors=True, file=report, skip_covered=None) + self.assertEqual(report.getvalue(), textwrap.dedent("""\ + Name Stmts Miss Cover + ----------------------------- + prog.py 4 1 75% + """)) + self.assert_file_count(".coverage", 0) + self.assert_file_count(".coverage.*", 1) + class ReporterDeprecatedAttributeTest(CoverageTest): """Test that Reporter.file_reporters has been deprecated.""" -- cgit v1.2.1 From ad58ff0db4eeb40794e3cf87c2ee9365aedc7bd6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 20:17:50 -0400 Subject: Fix the pytest-cov test --- coverage/control.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coverage/control.py b/coverage/control.py index c83432af..4dd62e10 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -460,6 +460,7 @@ class Coverage(object): self._collector.reset() self._init_data(suffix=None) self._data.erase(parallel=self.config.parallel) + self._data = None def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -561,6 +562,8 @@ class Coverage(object): """ self._init() + self._init_data(suffix=None) + self._post_init() if self._collector and self._collector.save_data(self._data): self._post_save_work() -- cgit v1.2.1 From c315b91653fcedc55f6ef1ff5e199c700b63398e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 24 Aug 2018 06:53:43 -0400 Subject: Disable travis lint until we can clean up the code --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 06782dc9..0eb9f698 100644 --- a/tox.ini +++ b/tox.ini @@ -85,8 +85,9 @@ commands = python -m pylint --notes= {env:LINTABLE} [travis] +#2.7: py27, lint python = - 2.7: py27, lint + 2.7: py27 3.4: py34 3.5: py35 3.6: py36 -- cgit v1.2.1 From dd5b0cc88ebe4528abaa7cdf0b3fd516fb1f7e01 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 24 Aug 2018 07:13:24 -0400 Subject: CHANGES: data format changed to SQLite --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0e80b26c..7ec72811 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,15 @@ Change history for Coverage.py Unreleased ---------- +- Coverage's data storage has changed. In version 4.x, .coverage files were + basically JSON. Now, they are SQLite databases. This means the data file + can be created earlier than it used to. A large amount of code was + refactored to support this change. + +- The old data format is still available (for now) by setting the environment + variable COVERAGE_STORAGE=json. Please tell me if you think you need to keep + the JSON format. + - Development moved from `Bitbucket`_ to `GitHub`_. - HTML files no longer have trailing and extra whitespace. -- cgit v1.2.1