From 6765a72e0882ddf3e9b17ff38e33295df51d5977 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 22 Dec 2019 09:48:17 -0500 Subject: [PATCH] Detect when a 4.x data file is being read. #886 --- CHANGES.rst | 6 ++++++ coverage/sqldata.py | 15 ++++++++++++++- tests/test_api.py | 20 +++++++++++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 37f36080e..bd36d953e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,6 +24,11 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. Unreleased ---------- +- If a 4.x data file is the cause of a "file is not a database" error, then use + a more specific error message, "Looks like a coverage 4.x data file, are you + mixing versions of coverage?" Helps diagnose the problems described in + `issue 886`_. + - Measurement contexts and relative file names didn't work together, as reported in `issue_899`_ and `issue_900`_. This is now fixed, thanks to David Szotten. @@ -37,6 +42,7 @@ Unreleased different drive (`issue 895`_). Thanks, Olivier Grisel. .. _issue 880: https://github.com/nedbat/coveragepy/issues/880 +.. _issue 886: https://github.com/nedbat/coveragepy/issues/886 .. _issue 895: https://github.com/nedbat/coveragepy/issues/895 .. _issue 899: https://github.com/nedbat/coveragepy/issues/899 .. _issue 900: https://github.com/nedbat/coveragepy/issues/900 diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 01f5ce019..613a280c5 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -1031,7 +1031,20 @@ def execute(self, 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)) + msg = str(exc) + try: + # `execute` is the first thing we do with the database, so try + # hard to provide useful hints if something goes wrong now. + with open(self.filename, "rb") as bad_file: + cov4_sig = b"!coverage.py: This is a private format" + if bad_file.read(len(cov4_sig)) == cov4_sig: + msg = ( + "Looks like a coverage 4.x data file. " + "Are you mixing versions of coverage?" + ) + except Exception: + pass + raise CoverageException("Couldn't use data file {!r}: {}".format(self.filename, msg)) def executemany(self, sql, data): """Same as :meth:`python:sqlite3.Connection.executemany`.""" diff --git a/tests/test_api.py b/tests/test_api.py index 8fa81d07a..bab9d8ca5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -295,6 +295,20 @@ def test_empty_reporting(self): with self.assertRaisesRegex(CoverageException, "No data to report."): cov.report() + def test_cov4_data_file(self): + if env.WINDOWS: + # For some reason on Windows this test fails while trying to remove the temp + # directory, because ".coverage" is still in use. Skip it. + self.skipTest("Windows gets tangled up on this test.") + cov4_data = ( + "!coverage.py: This is a private format, don't read it directly!" + '{"lines":{"/private/tmp/foo.py":[1,5,2,3]}}' + ) + self.make_file(".coverage", cov4_data) + cov = coverage.Coverage() + with self.assertRaisesRegex(CoverageException, "Looks like a coverage 4.x data file"): + cov.load() + def make_code1_code2(self): """Create the code1.py and code2.py files.""" self.make_file("code1.py", """\ @@ -384,15 +398,11 @@ def make_good_data_files(self): cov.save() self.assert_file_count(".coverage.*", 2) - def make_bad_data_file(self): - """Make one bad data file.""" - self.make_file(".coverage.foo", """La la la, this isn't coverage data!""") - def test_combining_corrupt_data(self): # If you combine a corrupt data file, then you will get a warning, # and the file will remain. self.make_good_data_files() - self.make_bad_data_file() + self.make_file(".coverage.foo", """La la la, this isn't coverage data!""") cov = coverage.Coverage() warning_regex = ( r"Couldn't use data file '.*\.coverage\.foo': file (is encrypted or )?is not a database"