diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 9f9c06559..1f254e004 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -13,6 +13,7 @@ import sys import textwrap import traceback +from contextlib import suppress from typing import cast, Any, NoReturn @@ -24,7 +25,8 @@ from coverage.control import DEFAULT_DATAFILE from coverage.data import combinable_files, debug_data_file from coverage.debug import info_header, short_stack, write_formatted_info -from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource +from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource, \ + NoDataFilesFoundError from coverage.execfile import PyRunner from coverage.results import display_covered, should_fail_under from coverage.version import __url__ @@ -882,9 +884,10 @@ def do_debug(self, args: list[str]) -> int: print(info_header("data")) data_file = self.coverage.config.data_file debug_data_file(data_file) - for filename in combinable_files(data_file): - print("-----") - debug_data_file(filename) + with suppress(NoDataFilesFoundError): + for filename in combinable_files(data_file): + print("-----") + debug_data_file(filename) elif args[0] == "config": write_formatted_info(print, "config", self.coverage.config.debug_info()) elif args[0] == "premain": diff --git a/coverage/data.py b/coverage/data.py index 9513adfca..2bd5c01f2 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -18,7 +18,8 @@ from typing import Callable, Iterable -from coverage.exceptions import CoverageException, NoDataError +from coverage.exceptions import CoverageException, NoDataError, DataFileNotFoundError, \ + NoDataFilesFoundError from coverage.files import PathAliases from coverage.misc import Hasher, file_be_gone, human_sorted, plural from coverage.sqldata import CoverageData @@ -82,12 +83,15 @@ def combinable_files(data_file: str, data_paths: Iterable[str] | None = None) -> pattern = glob.escape(os.path.join(os.path.abspath(p), local)) +".*" files_to_combine.extend(glob.glob(pattern)) else: - raise NoDataError(f"Couldn't combine from non-existent path '{p}'") + raise DataFileNotFoundError.new_for_data_file(p, True) # SQLite might have made journal files alongside our database files. # We never want to combine those. files_to_combine = [fnm for fnm in files_to_combine if not fnm.endswith("-journal")] + if not files_to_combine: + raise NoDataFilesFoundError.new_for_data_directory(data_dir) + # Sorting isn't usually needed, since it shouldn't matter what order files # are combined, but sorting makes tests more predictable, and makes # debugging more understandable when things go wrong. @@ -129,10 +133,12 @@ def combine_parallel_data( `message` is a function to use for printing messages to the user. """ - files_to_combine = combinable_files(data.base_filename(), data_paths) - - if strict and not files_to_combine: - raise NoDataError("No data to combine") + try: + files_to_combine = combinable_files(data.base_filename(), data_paths) + except NoDataFilesFoundError: + if strict: + raise + return None file_hashes = set() combined_any = False @@ -190,6 +196,7 @@ def combine_parallel_data( file_be_gone(f) if strict and not combined_any: + # @todo Files found but no usable data in them. Parse error? raise NoDataError("No usable data files") diff --git a/coverage/exceptions.py b/coverage/exceptions.py index ecd1b5e64..6c76d819c 100644 --- a/coverage/exceptions.py +++ b/coverage/exceptions.py @@ -5,6 +5,9 @@ from __future__ import annotations +import os.path + + class _BaseCoverageException(Exception): """The base-base of all Coverage exceptions.""" pass @@ -24,11 +27,29 @@ class DataError(CoverageException): """An error in using a data file.""" pass + class NoDataError(CoverageException): """We didn't have data to work with.""" pass +class DataFileNotFoundError(NoDataError): + """A data file or data directory could be found.""" + @classmethod + def new_for_data_file(cls, data_file_path: str, combined: bool = False) -> 'Self': + message = f"The data file or directory `{os.path.abspath(data_file_path)}` could not be found." + if not combined: + message += " Perhaps `coverage combine` must be run first." + return cls(message) + + +class NoDataFilesFoundError(NoDataError): + """No data files could be found in a data directory.""" + @classmethod + def new_for_data_directory(cls, data_directory_path: str) -> 'Self': + return cls(f"The data directory `{os.path.abspath(data_directory_path)}` does not contain any data files.") + + class NoSource(CoverageException): """We couldn't find the source for a module.""" pass diff --git a/tests/test_api.py b/tests/test_api.py index 9f65166b9..c763364f0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -446,7 +446,7 @@ def test_combining_twice(self) -> None: self.assert_exists(".coverage") cov2 = coverage.Coverage() - with pytest.raises(NoDataError, match=r"No data to combine"): + with pytest.raises(NoDataError, match=r"^The data directory `(.+?)` does not contain any data files.$"): cov2.combine(strict=True, keep=False) cov3 = coverage.Coverage() @@ -1326,7 +1326,7 @@ def test_combine_parallel_data(self) -> None: # Running combine again should fail, because there are no parallel data # files to combine. cov = coverage.Coverage() - with pytest.raises(NoDataError, match=r"No data to combine"): + with pytest.raises(NoDataError): cov.combine(strict=True) # And the originally combined data is still there. diff --git a/tests/test_data.py b/tests/test_data.py index 1f0bb20dc..67e1045d8 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -915,7 +915,7 @@ def test_combining_from_files(self) -> None: def test_combining_from_nonexistent_directories(self) -> None: covdata = DebugCoverageData() - msg = "Couldn't combine from non-existent path 'xyzzy'" + msg = r"^The data file or directory `(.+?)` could not be found.$" with pytest.raises(NoDataError, match=msg): combine_parallel_data(covdata, data_paths=['xyzzy'])