From 511bea7ee8350bbb8b83fa465253936dd08fdf55 Mon Sep 17 00:00:00 2001 From: Stephan Deibel Date: Mon, 30 Jan 2023 17:37:17 -0500 Subject: [PATCH 1/4] Add purge_files method to CoverageData, to allow for selective removal and update of coverage data. --- coverage/sqldata.py | 36 +++++++++++++++ tests/test_api.py | 106 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 77577437e..8bc86608c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -615,6 +615,42 @@ def touch_files(self, filenames: Collection[str], plugin_name: Optional[str] = N # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) + def purge_files(self, filenames, context=None): + """Purge any existing coverage data for the given `filenames`. + + If `context` is given, purge only data associated with that measurement context. + """ + + if self._debug.should("dataop"): + self._debug.write(f"Purging {filenames!r} for context {context}") + 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' + elif self._has_arcs: + table = 'arcs' + else: + return + + 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) + def update(self, other_data: CoverageData, aliases: Optional[PathAliases] = None) -> None: """Update this data with data from several other :class:`CoverageData` instances. diff --git a/tests/test_api.py b/tests/test_api.py index 1c5654216..9d5239315 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -754,6 +754,112 @@ def test_run_debug_sys(self) -> None: 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(): + 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().""" From da1b6b90ca0eb8b59946277a5f2c24c3b5bdd8f2 Mon Sep 17 00:00:00 2001 From: Stephan Deibel Date: Mon, 30 Jan 2023 18:08:18 -0500 Subject: [PATCH 2/4] Fix assert syntax so it's not true; this code isn't reached in the test unless it fails and then it would have failed to fail. --- tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 9d5239315..94168b41a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -829,7 +829,7 @@ def dummy_function(): except coverage.sqldata.DataError: pass else: - assert(0, "exception expected") + assert 0, "exception expected" assert len(data.measured_files()) == 3 assert [1, 2] == sorted_lines(data, fn1) assert [1,] == sorted_lines(data, fn2) From 249d4e33184cf08e70bcd0d81109662fa6850b8e Mon Sep 17 00:00:00 2001 From: Stephan Deibel Date: Mon, 30 Jan 2023 18:18:22 -0500 Subject: [PATCH 3/4] Remove trailing whitespace; did not expect this would matter on a blank line. --- coverage/sqldata.py | 10 +++++----- tests/test_api.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 8bc86608c..267d83b4c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -617,29 +617,29 @@ def touch_files(self, filenames: Collection[str], plugin_name: Optional[str] = N def purge_files(self, filenames, context=None): """Purge any existing coverage data for the given `filenames`. - + If `context` is given, purge only data associated with that measurement context. """ - + if self._debug.should("dataop"): self._debug.write(f"Purging {filenames!r} for context {context}") 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' elif self._has_arcs: table = 'arcs' else: return - + for filename in filenames: file_id = self._file_id(filename, add=False) if file_id is None: diff --git a/tests/test_api.py b/tests/test_api.py index 94168b41a..f784ef3c5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -755,7 +755,7 @@ def test_run_debug_sys(self) -> None: assert cast(str, d['data_file']).endswith(".coverage") def test_purge_filenames(self) -> None: - + fn1 = self.make_file("mymain.py", """\ import mymod a = 1 @@ -771,7 +771,7 @@ def test_purge_filenames(self) -> None: 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) @@ -788,9 +788,9 @@ def test_purge_filenames(self) -> None: 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 @@ -804,7 +804,7 @@ def test_purge_filenames_context(self) -> None: def dummy_function(): unused = 42 - + # Start/stop since otherwise cantext cov = coverage.Coverage() cov.start() @@ -815,7 +815,7 @@ def dummy_function(): 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) @@ -851,7 +851,7 @@ def dummy_function(): 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 @@ -859,7 +859,7 @@ def dummy_function(): 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().""" From 91f8d80f0d9070e2ca63c362e3e889d5d9304fe7 Mon Sep 17 00:00:00 2001 From: Stephan Deibel Date: Mon, 30 Jan 2023 23:45:01 -0500 Subject: [PATCH 4/4] Add type annotations required by mypy --- coverage/sqldata.py | 2 +- tests/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 267d83b4c..12676d0bb 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -615,7 +615,7 @@ def touch_files(self, filenames: Collection[str], plugin_name: Optional[str] = N # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) - def purge_files(self, filenames, context=None): + def purge_files(self, filenames: Iterable[str], context: Optional[str] = None) -> None: """Purge any existing coverage data for the given `filenames`. If `context` is given, purge only data associated with that measurement context. diff --git a/tests/test_api.py b/tests/test_api.py index f784ef3c5..885831550 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -802,7 +802,7 @@ def test_purge_filenames_context(self) -> None: """) fn2 = os.path.join(self.temp_dir, fn2) - def dummy_function(): + def dummy_function() -> None: unused = 42 # Start/stop since otherwise cantext