diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2022-01-16 17:47:28 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2022-01-20 06:48:14 -0500 |
commit | b41be3f9580dbae2434df14185c4dfe42cd05cf2 (patch) | |
tree | 7f5398bfcc3ca063ef8bbede826359c5d8c1114f | |
parent | f4ee9a2995f53be3b6108dd9e562f5546dfebab8 (diff) | |
download | python-coveragepy-git-b41be3f9580dbae2434df14185c4dfe42cd05cf2.tar.gz |
fix: create the db as needed when accessed
-rw-r--r-- | CHANGES.rst | 6 | ||||
-rw-r--r-- | coverage/sqldata.py | 55 | ||||
-rw-r--r-- | tests/test_data.py | 17 |
3 files changed, 33 insertions, 45 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 57719698..b3f398a7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,14 +26,20 @@ Unreleased - Updated Python 3.11 support to 3.11.0a4, fixing `issue 1294`_. +- Fix: the coverage data file is now created in a more robust way, to avoid + problems when multiple processes are trying to write data at once. Fixes + `issue 1303`_ and `issue 883`_. + - Fix: a .gitignore file will only be written into the HTML report output directory if the directory is empty. This should prevent certain unfortunate accidents of writing the file where it is not wanted. - Releases now have MacOS arm64 wheels for Apple Silicon (fixes `issue 1288`_). +.. _issue 883: https://github.com/nedbat/coveragepy/issues/883 .. _issue 1288: https://github.com/nedbat/coveragepy/issues/1288 .. _issue 1294: https://github.com/nedbat/coveragepy/issues/1294 +.. _issue 1303: https://github.com/nedbat/coveragepy/issues/1303 .. _changes_62: diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 05450999..e136c7f6 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -256,26 +256,6 @@ class CoverageData(SimpleReprMixin): self._have_used = False self._current_context_id = None - def _create_db(self): - """Create a db file that doesn't exist yet. - - Initializes the schema and certain metadata. - """ - if self._debug.should("dataio"): - self._debug.write(f"Creating data file {self._filename!r}") - self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug) - with db: - db.executescript(SCHEMA) - db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) - db.executemany( - "insert into meta (key, value) values (?, ?)", - [ - ("sys_argv", str(getattr(sys, "argv", None))), - ("version", __version__), - ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - ] - ) - def _open_db(self): """Open an existing db file, and read its metadata.""" if self._debug.should("dataio"): @@ -289,11 +269,14 @@ class CoverageData(SimpleReprMixin): try: schema_version, = db.execute_one("select version from coverage_schema") except Exception as exc: - raise DataError( - "Data file {!r} doesn't seem to be a coverage data file: {}".format( - self._filename, exc - ) - ) from exc + if "no such table: coverage_schema" in str(exc): + self._init_db(db) + else: + raise DataError( + "Data file {!r} doesn't seem to be a coverage data file: {}".format( + self._filename, exc + ) + ) from exc else: if schema_version != SCHEMA_VERSION: raise DataError( @@ -309,13 +292,25 @@ class CoverageData(SimpleReprMixin): for path, file_id in db.execute("select path, id from file"): self._file_map[path] = file_id + def _init_db(self, db): + """Write the initial contents of the database.""" + if self._debug.should("dataio"): + self._debug.write(f"Initing data file {self._filename!r}") + db.executescript(SCHEMA) + db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) + db.executemany( + "insert or ignore into meta (key, value) values (?, ?)", + [ + ("sys_argv", str(getattr(sys, "argv", None))), + ("version", __version__), + ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), + ] + ) + def _connect(self): """Get the SqliteDb object to use.""" if threading.get_ident() not in self._dbs: - if os.path.exists(self._filename): - self._open_db() - else: - self._create_db() + self._open_db() return self._dbs[threading.get_ident()] def __bool__(self): @@ -522,7 +517,7 @@ class CoverageData(SimpleReprMixin): self._has_arcs = arcs with self._connect() as con: con.execute( - "insert into meta (key, value) values (?, ?)", + "insert or ignore into meta (key, value) values (?, ?)", ("has_arcs", str(int(arcs))) ) diff --git a/tests/test_data.py b/tests/test_data.py index 527ac932..acdca7d8 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -592,12 +592,6 @@ class CoverageDataInTempDirTest(CoverageTest): covdata.read() assert not covdata - self.make_file("empty.dat", "") - with pytest.raises(DataError, match=msg.format("empty.dat")): - covdata = DebugCoverageData("empty.dat") - covdata.read() - assert not covdata - def test_hard_read_error(self): self.make_file("noperms.dat", "go away") os.chmod("noperms.dat", 0) @@ -626,14 +620,6 @@ class CoverageDataInTempDirTest(CoverageTest): covdata.read() assert not covdata - with sqlite3.connect("no_schema.db") as con: - con.execute("create table foobar (baz text)") - msg = r"Couldn't .* '.*[/\\]no_schema.db': \S+" - with pytest.raises(DataError, match=msg): - covdata = DebugCoverageData("no_schema.db") - covdata.read() - assert not covdata - class CoverageDataFilesTest(CoverageTest): """Tests of CoverageData file handling.""" @@ -667,7 +653,8 @@ class CoverageDataFilesTest(CoverageTest): assert re.search( r"^Erasing data file '.*\.coverage'\n" + - r"Creating data file '.*\.coverage'\n" + + r"Opening data file '.*\.coverage'\n" + + r"Initing data file '.*\.coverage'\n" + r"Opening data file '.*\.coverage'\n$", debug.get_output() ) |