diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2023-02-08 07:11:45 -0700 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2023-02-08 13:16:05 -0700 |
commit | 423fa596325acb8f6bcb37a3502cf7853e5d395a (patch) | |
tree | c8429927f19470c342b2f6440562c2083ddd54fd | |
parent | cb7d67962ca8ed9eb176e144b9cfe96373803bf4 (diff) | |
download | python-coveragepy-git-423fa596325acb8f6bcb37a3502cf7853e5d395a.tar.gz |
feat: simplify purges_files
Also, move tests to test_data.py, and finish covering the code.
-rw-r--r-- | CHANGES.rst | 4 | ||||
-rw-r--r-- | CONTRIBUTORS.txt | 1 | ||||
-rw-r--r-- | coverage/sqldata.py | 43 | ||||
-rw-r--r-- | tests/test_api.py | 106 | ||||
-rw-r--r-- | tests/test_data.py | 29 |
5 files changed, 53 insertions, 130 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 0023f31b..b4883728 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,9 @@ development at the same time, such as 4.5.x and 5.0. Unreleased ---------- +- Added a :meth:`.CoverageData.purge_files` method to remove recorded data for + a particular file. Contributed by `Stephan Deibel <pull 1547_>`_. + - Fix: In some embedded environments, an IndexError could occur on stop() when the originating thread exits before completion. This is now fixed, thanks to `Russell Keith-Magee <pull 1543_>`_, closing `issue 1542`_. @@ -29,6 +32,7 @@ Unreleased .. _issue 1542: https://github.com/nedbat/coveragepy/issues/1542 .. _pull 1543: https://github.com/nedbat/coveragepy/pull/1543 +.. _pull 1547: https://github.com/nedbat/coveragepy/pull/1547 .. _pull 1550: https://github.com/nedbat/coveragepy/pull/1550 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e06acd07..8889ed61 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -149,6 +149,7 @@ Sigve Tjora Simon Willison Stan Hu Stefan Behnel +Stephan Deibel Stephan Richter Stephen Finucane Steve Dower diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 12676d0b..9aa2b129 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -197,9 +197,11 @@ class CoverageData(AutoReprMixin): 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`, - passing it the other. + You can clear the data in memory with :meth:`erase`. Data for specific + files can be removed from the database with :meth:`purge_files`. + + Two data collections can be combined by using :meth:`update` on one + :class:`CoverageData`, passing it the other. Data in a :class:`CoverageData` can be serialized and deserialized with :meth:`dumps` and :meth:`loads`. @@ -615,41 +617,29 @@ class CoverageData(AutoReprMixin): # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) - def purge_files(self, filenames: Iterable[str], context: Optional[str] = None) -> None: + def purge_files(self, filenames: Collection[str]) -> None: """Purge any existing coverage data for the given `filenames`. - If `context` is given, purge only data associated with that measurement context. - """ + .. versionadded:: 7.2 + """ if self._debug.should("dataop"): - self._debug.write(f"Purging {filenames!r} for context {context}") + self._debug.write(f"Purging data for {filenames!r}") self._start_using() with self._connect() as con: - if context is not None: - context_id = self._context_id(context) - if context_id is None: - raise DataError("Unknown context {context}") - else: - context_id = None - if self._has_lines: - table = 'line_bits' + sql = "delete from line_bits where file_id=?" elif self._has_arcs: - table = 'arcs' + sql = "delete from arc where file_id=?" else: - return + raise DataError("Can't purge files in an empty CoverageData") for filename in filenames: file_id = self._file_id(filename, add=False) if file_id is None: continue - self._file_map.pop(filename, None) - if context_id is None: - q = f'delete from {table} where file_id={file_id}' - else: - q = f'delete from {table} where file_id={file_id} and context_id={context_id}' - con.execute(q) + con.execute_void(sql, (file_id,)) def update(self, other_data: CoverageData, aliases: Optional[PathAliases] = None) -> None: """Update this data with data from several other :class:`CoverageData` instances. @@ -868,7 +858,12 @@ class CoverageData(AutoReprMixin): return bool(self._has_arcs) def measured_files(self) -> Set[str]: - """A set of all files that had been measured.""" + """A set of all files that have been measured. + + Note that a file may be mentioned as measured even though no lines or + arcs for that file are present in the data. + + """ return set(self._file_map) def measured_contexts(self) -> Set[str]: diff --git a/tests/test_api.py b/tests/test_api.py index 88583155..1c565421 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -754,112 +754,6 @@ class ApiTest(CoverageTest): cov.stop() # pragma: nested assert cast(str, d['data_file']).endswith(".coverage") - def test_purge_filenames(self) -> None: - - fn1 = self.make_file("mymain.py", """\ - import mymod - a = 1 - """) - fn1 = os.path.join(self.temp_dir, fn1) - - fn2 = self.make_file("mymod.py", """\ - fooey = 17 - """) - fn2 = os.path.join(self.temp_dir, fn2) - - cov = coverage.Coverage() - self.start_import_stop(cov, "mymain") - - data = cov.get_data() - - # Initial measurement was for two files - assert len(data.measured_files()) == 2 - assert [1, 2] == sorted_lines(data, fn1) - assert [1,] == sorted_lines(data, fn2) - - # Purge one file's data and one should remain - data.purge_files([fn1]) - assert len(data.measured_files()) == 1 - assert [] == sorted_lines(data, fn1) - assert [1,] == sorted_lines(data, fn2) - - # Purge second file's data and none should remain - data.purge_files([fn2]) - assert len(data.measured_files()) == 0 - assert [] == sorted_lines(data, fn1) - assert [] == sorted_lines(data, fn2) - - def test_purge_filenames_context(self) -> None: - - fn1 = self.make_file("mymain.py", """\ - import mymod - a = 1 - """) - fn1 = os.path.join(self.temp_dir, fn1) - - fn2 = self.make_file("mymod.py", """\ - fooey = 17 - """) - fn2 = os.path.join(self.temp_dir, fn2) - - def dummy_function() -> None: - unused = 42 - - # Start/stop since otherwise cantext - cov = coverage.Coverage() - cov.start() - cov.switch_context('initialcontext') - dummy_function() - cov.switch_context('testcontext') - cov.stop() - self.start_import_stop(cov, "mymain") - - data = cov.get_data() - - # Initial measurement was for three files and two contexts - assert len(data.measured_files()) == 3 - assert [1, 2] == sorted_lines(data, fn1) - assert [1,] == sorted_lines(data, fn2) - assert len(sorted_lines(data, __file__)) == 1 - assert len(data.measured_contexts()) == 2 - - # Remove specifying wrong context should raise exception and not remove anything - try: - data.purge_files([fn1], 'wrongcontext') - except coverage.sqldata.DataError: - pass - else: - assert 0, "exception expected" - assert len(data.measured_files()) == 3 - assert [1, 2] == sorted_lines(data, fn1) - assert [1,] == sorted_lines(data, fn2) - assert len(sorted_lines(data, __file__)) == 1 - assert len(data.measured_contexts()) == 2 - - # Remove one file specifying correct context - data.purge_files([fn1], 'testcontext') - assert len(data.measured_files()) == 2 - assert [] == sorted_lines(data, fn1) - assert [1,] == sorted_lines(data, fn2) - assert len(sorted_lines(data, __file__)) == 1 - assert len(data.measured_contexts()) == 2 - - # Remove second file with other correct context - data.purge_files([__file__], 'initialcontext') - assert len(data.measured_files()) == 1 - assert [] == sorted_lines(data, fn1) - assert [1,] == sorted_lines(data, fn2) - assert len(sorted_lines(data, __file__)) == 0 - assert len(data.measured_contexts()) == 2 - - # Remove last file specifying correct context - data.purge_files([fn2], 'testcontext') - assert len(data.measured_files()) == 0 - assert [] == sorted_lines(data, fn1) - assert [] == sorted_lines(data, fn2) - assert len(sorted_lines(data, __file__)) == 0 - assert len(data.measured_contexts()) == 2 - class CurrentInstanceTest(CoverageTest): """Tests of Coverage.current().""" diff --git a/tests/test_data.py b/tests/test_data.py index 5953ba36..1cc64572 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -588,6 +588,35 @@ class CoverageDataTest(CoverageTest): assert_lines1_data(covdata) assert not exceptions + def test_purge_files_lines(self) -> None: + covdata = DebugCoverageData() + covdata.add_lines(LINES_1) + covdata.add_lines(LINES_2) + assert_line_counts(covdata, SUMMARY_1_2) + covdata.purge_files(["a.py", "b.py"]) + assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 1}) + covdata.purge_files(["c.py"]) + assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0}) + # It's OK to "purge" a file that wasn't measured. + covdata.purge_files(["xyz.py"]) + assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0}) + + def test_purge_files_arcs(self) -> None: + covdata = CoverageData() + covdata.add_arcs(ARCS_3) + covdata.add_arcs(ARCS_4) + assert_line_counts(covdata, SUMMARY_3_4) + covdata.purge_files(["x.py", "y.py"]) + assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 1}) + covdata.purge_files(["z.py"]) + assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 0}) + + def test_cant_purge_in_empty_data(self) -> None: + covdata = DebugCoverageData() + msg = "Can't purge files in an empty CoverageData" + with pytest.raises(DataError, match=msg): + covdata.purge_files(["abc.py"]) + class CoverageDataInTempDirTest(CoverageTest): """Tests of CoverageData that need a temporary directory to make files.""" |