From b82e9fd8766a77c2a275bde7b574f3e8cb529f8f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 13 Nov 2021 19:23:24 -0500 Subject: refactor: specialize exceptions CoverageException is fine as a base class, but not good to use for raising (and catching sometimes). Introduce specialized exceptions that allow third-party tools to integrate better. --- coverage/collector.py | 10 +++++----- coverage/config.py | 18 +++++++++--------- coverage/control.py | 8 ++++---- coverage/data.py | 8 ++++---- coverage/exceptions.py | 19 +++++++++++++++++++ coverage/files.py | 4 ++-- coverage/html.py | 4 ++-- coverage/inorout.py | 4 ++-- coverage/plugin_support.py | 4 ++-- coverage/report.py | 4 ++-- coverage/results.py | 4 ++-- coverage/sqldata.py | 32 ++++++++++++++++---------------- coverage/summary.py | 6 +++--- coverage/tomlconfig.py | 6 +++--- tests/test_api.py | 10 +++++----- tests/test_config.py | 36 ++++++++++++++++++------------------ tests/test_coverage.py | 8 ++++---- tests/test_data.py | 42 +++++++++++++++++++++--------------------- tests/test_files.py | 4 ++-- tests/test_plugins.py | 6 +++--- tests/test_results.py | 4 ++-- tests/test_summary.py | 4 ++-- 22 files changed, 132 insertions(+), 113 deletions(-) diff --git a/coverage/collector.py b/coverage/collector.py index 6f9aa2c1..89ba66ba 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -9,7 +9,7 @@ import sys from coverage import env from coverage.debug import short_stack from coverage.disposition import FileDisposition -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.misc import human_sorted, isolate_module from coverage.pytracer import PyTracer @@ -116,7 +116,7 @@ class Collector: # We can handle a few concurrency options here, but only one at a time. these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) if len(these_concurrencies) > 1: - raise CoverageException(f"Conflicting concurrency settings: {concurrency}") + raise ConfigError(f"Conflicting concurrency settings: {concurrency}") self.concurrency = these_concurrencies.pop() if these_concurrencies else '' try: @@ -136,9 +136,9 @@ class Collector: import threading self.threading = threading else: - raise CoverageException(f"Don't understand concurrency={concurrency}") + raise ConfigError(f"Don't understand concurrency={concurrency}") except ImportError as ex: - raise CoverageException( + raise ConfigError( "Couldn't trace with concurrency={}, the module isn't installed.".format( self.concurrency, ) @@ -245,7 +245,7 @@ class Collector: if hasattr(tracer, 'concur_id_func'): tracer.concur_id_func = self.concur_id_func elif self.concur_id_func: - raise CoverageException( + raise ConfigError( "Can't support concurrency={} with {}, only threads are supported".format( self.concurrency, self.tracer_name(), ) diff --git a/coverage/config.py b/coverage/config.py index 3b873579..d73f69eb 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -10,7 +10,7 @@ import os import os.path import re -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.misc import contract, isolate_module, substitute_variables from coverage.tomlconfig import TomlConfigParser, TomlDecodeError @@ -59,7 +59,7 @@ class HandyConfigParser(configparser.RawConfigParser): real_section = section_prefix + section if configparser.RawConfigParser.has_section(self, real_section): return configparser.RawConfigParser.options(self, real_section) - raise configparser.NoSectionError(section) + raise ConfigError(f"No section: {section!r}") def get_section(self, section): """Get the contents of a section, as a dictionary.""" @@ -83,7 +83,7 @@ class HandyConfigParser(configparser.RawConfigParser): if configparser.RawConfigParser.has_option(self, real_section, option): break else: - raise configparser.NoOptionError(option, section) + raise ConfigError(f"No option {option!r} in section: {section!r}") v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) v = substitute_variables(v, os.environ) @@ -123,7 +123,7 @@ class HandyConfigParser(configparser.RawConfigParser): try: re.compile(value) except re.error as e: - raise CoverageException( + raise ConfigError( f"Invalid [{section}].{option} value {value!r}: {e}" ) from e if value: @@ -272,7 +272,7 @@ class CoverageConfig: try: files_read = cp.read(filename) except (configparser.Error, TomlDecodeError) as err: - raise CoverageException(f"Couldn't read config file {filename}: {err}") from err + raise ConfigError(f"Couldn't read config file {filename}: {err}") from err if not files_read: return False @@ -285,7 +285,7 @@ class CoverageConfig: if was_set: any_set = True except ValueError as err: - raise CoverageException(f"Couldn't read config file {filename}: {err}") from err + raise ConfigError(f"Couldn't read config file {filename}: {err}") from err # Check that there are no unrecognized options. all_options = collections.defaultdict(set) @@ -443,7 +443,7 @@ class CoverageConfig: return # If we get here, we didn't find the option. - raise CoverageException(f"No such option: {option_name!r}") + raise ConfigError(f"No such option: {option_name!r}") def get_option(self, option_name): """Get an option from the configuration. @@ -471,7 +471,7 @@ class CoverageConfig: return self.plugin_options.get(plugin_name, {}).get(key) # If we get here, we didn't find the option. - raise CoverageException(f"No such option: {option_name!r}") + raise ConfigError(f"No such option: {option_name!r}") def post_process_file(self, path): """Make final adjustments to a file path to make it usable.""" @@ -546,7 +546,7 @@ def read_coverage_config(config_file, warn, **kwargs): if config_read: break if specified_file: - raise CoverageException(f"Couldn't read {fname!r} as a config file") + raise ConfigError(f"Couldn't read {fname!r} as a config file") # $set_env.py: COVERAGE_DEBUG - Options for --debug. # 3) from environment variables: diff --git a/coverage/control.py b/coverage/control.py index 47e741aa..c80a74f5 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -21,7 +21,7 @@ from coverage.context import should_start_context_test_function, combine_context from coverage.data import CoverageData, combine_parallel_data from coverage.debug import DebugControl, short_stack, write_formatted_info from coverage.disposition import disposition_debug_msg -from coverage.exceptions import CoverageException, CoverageWarning +from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory from coverage.html import HtmlReporter from coverage.inorout import InOrOut @@ -449,7 +449,7 @@ class Coverage: concurrency = self.config.concurrency or () if "multiprocessing" in concurrency: if not patch_multiprocessing: - raise CoverageException( # pragma: only jython + raise ConfigError( # pragma: only jython "multiprocessing is not supported on this Python" ) patch_multiprocessing(rcfile=self.config.config_file) @@ -460,7 +460,7 @@ class Coverage: elif dycon == "test_function": context_switchers = [should_start_context_test_function] else: - raise CoverageException(f"Don't understand dynamic_context setting: {dycon!r}") + raise ConfigError(f"Don't understand dynamic_context setting: {dycon!r}") context_switchers.extend( plugin.dynamic_context for plugin in self._plugins.context_switchers @@ -835,7 +835,7 @@ class Coverage: if plugin: file_reporter = plugin.file_reporter(mapped_morf) if file_reporter is None: - raise CoverageException( + raise PluginError( "Plugin {!r} did not provide a file reporter for {!r}.".format( plugin._coverage_plugin_name, morf ) diff --git a/coverage/data.py b/coverage/data.py index e7c94b4f..c70993ad 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -13,7 +13,7 @@ imports working. import glob import os.path -from coverage.exceptions import CoverageException +from coverage.exceptions import CoverageException, NoDataError from coverage.misc import file_be_gone from coverage.sqldata import CoverageData @@ -72,7 +72,7 @@ def combinable_files(data_file, data_paths=None): pattern = os.path.join(os.path.abspath(p), f"{local}.*") files_to_combine.extend(glob.glob(pattern)) else: - raise CoverageException(f"Couldn't combine from non-existent path '{p}'") + raise NoDataError(f"Couldn't combine from non-existent path '{p}'") return files_to_combine @@ -107,7 +107,7 @@ def combine_parallel_data( files_to_combine = combinable_files(data.base_filename(), data_paths) if strict and not files_to_combine: - raise CoverageException("No data to combine") + raise NoDataError("No data to combine") files_combined = 0 for f in files_to_combine: @@ -138,4 +138,4 @@ def combine_parallel_data( file_be_gone(f) if strict and not files_combined: - raise CoverageException("No usable data files") + raise NoDataError("No usable data files") diff --git a/coverage/exceptions.py b/coverage/exceptions.py index de2257a1..7352e9fc 100644 --- a/coverage/exceptions.py +++ b/coverage/exceptions.py @@ -14,6 +14,20 @@ class CoverageException(_BaseCoverageException): pass +class ConfigError(_BaseCoverageException): + """A problem with a config file, or a value in one.""" + pass + + +class DataError(CoverageException): + """An error in using a data file.""" + pass + +class NoDataError(CoverageException): + """We didn't have data to work with.""" + pass + + class NoSource(CoverageException): """We couldn't find the source for a module.""" pass @@ -29,6 +43,11 @@ class NotPython(CoverageException): pass +class PluginError(CoverageException): + """A plugin misbehaved.""" + pass + + class _ExceptionDuringRun(CoverageException): """An exception happened while running customer code. diff --git a/coverage/files.py b/coverage/files.py index a721e533..afb0dbf5 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -13,7 +13,7 @@ import re import sys from coverage import env -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.misc import contract, human_sorted, isolate_module, join_regex @@ -356,7 +356,7 @@ class PathAliases: # The pattern can't end with a wildcard component. if pattern.endswith("*"): - raise CoverageException("Pattern must not end with wildcards.") + raise ConfigError("Pattern must not end with wildcards.") # The pattern is meant to match a filepath. Let's make it absolute # unless it already is, or is meant to match any prefix. diff --git a/coverage/html.py b/coverage/html.py index 2acc8aba..ae1d041f 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -12,7 +12,7 @@ import types import coverage from coverage.data import add_data_to_hash -from coverage.exceptions import CoverageException +from coverage.exceptions import NoDataError from coverage.files import flat_rootname from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime from coverage.misc import human_sorted, plural @@ -208,7 +208,7 @@ class HtmlReporter: self.html_file(fr, analysis) if not self.all_files_nums: - raise CoverageException("No data to report.") + raise NoDataError("No data to report.") self.totals = sum(self.all_files_nums) diff --git a/coverage/inorout.py b/coverage/inorout.py index 87afdaaf..69084b26 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -15,7 +15,7 @@ import traceback from coverage import env from coverage.disposition import FileDisposition, disposition_init -from coverage.exceptions import CoverageException +from coverage.exceptions import CoverageException, PluginError from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher from coverage.files import prep_patterns, find_python_files, canonical_filename from coverage.misc import sys_modules_saved @@ -392,7 +392,7 @@ class InOrOut: if not disp.has_dynamic_filename: if not disp.source_filename: - raise CoverageException( + raise PluginError( f"Plugin {plugin!r} didn't set source_filename for '{disp.original_filename}'" ) reason = self.check_include_omit_etc(disp.source_filename, frame) diff --git a/coverage/plugin_support.py b/coverage/plugin_support.py index dfe5a4ec..0b892391 100644 --- a/coverage/plugin_support.py +++ b/coverage/plugin_support.py @@ -7,7 +7,7 @@ import os import os.path import sys -from coverage.exceptions import CoverageException +from coverage.exceptions import PluginError from coverage.misc import isolate_module from coverage.plugin import CoveragePlugin, FileTracer, FileReporter @@ -44,7 +44,7 @@ class Plugins: coverage_init = getattr(mod, "coverage_init", None) if not coverage_init: - raise CoverageException( + raise PluginError( f"Plugin module {module!r} didn't define a coverage_init function" ) diff --git a/coverage/report.py b/coverage/report.py index c1aaa45b..6382eb51 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -5,7 +5,7 @@ import sys -from coverage.exceptions import CoverageException, NotPython +from coverage.exceptions import CoverageException, NoDataError, NotPython from coverage.files import prep_patterns, FnmatchMatcher from coverage.misc import ensure_dir_for_file, file_be_gone @@ -65,7 +65,7 @@ def get_analysis_to_report(coverage, morfs): file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)] if not file_reporters: - raise CoverageException("No data to report.") + raise NoDataError("No data to report.") for fr in sorted(file_reporters): try: diff --git a/coverage/results.py b/coverage/results.py index 64e5130c..7bb4781c 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -6,7 +6,7 @@ import collections from coverage.debug import SimpleReprMixin -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.misc import contract, nice_pair @@ -337,7 +337,7 @@ def should_fail_under(total, fail_under, precision): # We can never achieve higher than 100% coverage, or less than zero. if not (0 <= fail_under <= 100.0): msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100." - raise CoverageException(msg) + raise ConfigError(msg) # Special case for fail_under=100, it must really be 100. if fail_under == 100.0 and total != 100.0: diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 7fca3248..e5fee8ce 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -19,7 +19,7 @@ import threading import zlib from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr -from coverage.exceptions import CoverageException +from coverage.exceptions import CoverageException, DataError from coverage.files import PathAliases from coverage.misc import contract, file_be_gone, filename_suffix, isolate_module from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits @@ -287,14 +287,14 @@ class CoverageData(SimpleReprMixin): try: schema_version, = db.execute_one("select version from coverage_schema") except Exception as exc: - raise CoverageException( + 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 CoverageException( + raise DataError( "Couldn't use data file {!r}: wrong schema: {} instead of {}".format( self._filename, schema_version, SCHEMA_VERSION ) @@ -368,9 +368,9 @@ class CoverageData(SimpleReprMixin): if self._debug.should("dataio"): self._debug.write(f"Loading data into data file {self._filename!r}") if data[:1] != b"z": - raise CoverageException( + raise DataError( f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)" - ) + ) script = zlib.decompress(data[1:]).decode("utf-8") self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug) with db: @@ -511,9 +511,9 @@ class CoverageData(SimpleReprMixin): assert lines or arcs assert not (lines and arcs) if lines and self._has_arcs: - raise CoverageException("Can't add line measurements to existing branch data") + raise DataError("Can't add line measurements to existing branch data") if arcs and self._has_lines: - raise CoverageException("Can't add branch measurements to existing line data") + raise DataError("Can't add branch measurements to existing line data") if not self._has_arcs and not self._has_lines: self._has_lines = lines self._has_arcs = arcs @@ -539,14 +539,14 @@ class CoverageData(SimpleReprMixin): for filename, plugin_name in file_tracers.items(): file_id = self._file_id(filename) if file_id is None: - raise CoverageException( + raise DataError( f"Can't add file tracer data for unmeasured file '{filename}'" ) existing_plugin = self.file_tracer(filename) if existing_plugin: if existing_plugin != plugin_name: - raise CoverageException( + raise DataError( "Conflicting file tracer name for '{}': {!r} vs {!r}".format( filename, existing_plugin, plugin_name, ) @@ -576,7 +576,7 @@ class CoverageData(SimpleReprMixin): self._start_using() with self._connect(): # Use this to get one transaction. if not self._has_arcs and not self._has_lines: - raise CoverageException("Can't touch files in an empty CoverageData") + raise DataError("Can't touch files in an empty CoverageData") for filename in filenames: self._file_id(filename, add=True) @@ -595,9 +595,9 @@ class CoverageData(SimpleReprMixin): getattr(other_data, "_filename", "???"), )) if self._has_lines and other_data._has_arcs: - raise CoverageException("Can't combine arc data with line data") + raise DataError("Can't combine arc data with line data") if self._has_arcs and other_data._has_lines: - raise CoverageException("Can't combine line data with arc data") + raise DataError("Can't combine line data with arc data") aliases = aliases or PathAliases() @@ -690,7 +690,7 @@ class CoverageData(SimpleReprMixin): other_tracer = tracers.get(path, "") # If there is no tracer, there is always the None tracer. if this_tracer is not None and this_tracer != other_tracer: - raise CoverageException( + raise DataError( "Conflicting file tracer name for '{}': {!r} vs {!r}".format( path, this_tracer, other_tracer ) @@ -1033,7 +1033,7 @@ class SqliteDb(SimpleReprMixin): try: self.con = sqlite3.connect(self.filename, check_same_thread=False) except sqlite3.Error as exc: - raise CoverageException(f"Couldn't use data file {self.filename!r}: {exc}") from exc + raise DataError(f"Couldn't use data file {self.filename!r}: {exc}") from exc self.con.create_function("REGEXP", 2, _regexp) @@ -1066,7 +1066,7 @@ class SqliteDb(SimpleReprMixin): except Exception as exc: if self.debug: self.debug.write(f"EXCEPTION from __exit__: {exc}") - raise CoverageException(f"Couldn't end data file {self.filename!r}: {exc}") from exc + raise DataError(f"Couldn't end data file {self.filename!r}: {exc}") from exc def execute(self, sql, parameters=()): """Same as :meth:`python:sqlite3.Connection.execute`.""" @@ -1097,7 +1097,7 @@ class SqliteDb(SimpleReprMixin): pass if self.debug: self.debug.write(f"EXCEPTION from execute: {msg}") - raise CoverageException(f"Couldn't use data file {self.filename!r}: {msg}") from exc + raise DataError(f"Couldn't use data file {self.filename!r}: {msg}") from exc def execute_one(self, sql, parameters=()): """Execute a statement and return the one row that results. diff --git a/coverage/summary.py b/coverage/summary.py index 0b54a05b..467c601d 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -5,7 +5,7 @@ import sys -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError, NoDataError from coverage.misc import human_sorted_items from coverage.report import get_analysis_to_report from coverage.results import Numbers @@ -103,7 +103,7 @@ class SummaryReporter: else: position = column_order.get(sort_option) if position is None: - raise CoverageException(f"Invalid sorting option: {self.config.sort!r}") + raise ConfigError(f"Invalid sorting option: {self.config.sort!r}") lines.sort(key=lambda l: (l[1][position], l[0]), reverse=reverse) for line in lines: @@ -122,7 +122,7 @@ class SummaryReporter: # Write other final lines. if not self.total.n_files and not self.skipped_count: - raise CoverageException("No data to report.") + raise NoDataError("No data to report.") if self.config.skip_covered and self.skipped_count: self.writeout( diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py index 4a1e322c..a06da65f 100644 --- a/coverage/tomlconfig.py +++ b/coverage/tomlconfig.py @@ -7,7 +7,7 @@ import configparser import os import re -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.misc import import_third_party, substitute_variables # TOML support is an install-time extra option. (Import typing is here because @@ -57,7 +57,7 @@ class TomlConfigParser: if self.our_file or has_toml: # Looks like they meant to read TOML, but we can't read it. msg = "Can't read {!r} without TOML support. Install with [toml] extra" - raise CoverageException(msg.format(filename)) + raise ConfigError(msg.format(filename)) return [] def _get_section(self, section): @@ -148,7 +148,7 @@ class TomlConfigParser: try: re.compile(value) except re.error as e: - raise CoverageException(f"Invalid [{name}].{option} value {value!r}: {e}") from e + raise ConfigError(f"Invalid [{name}].{option} value {value!r}: {e}") from e return values def getint(self, section, option): diff --git a/tests/test_api.py b/tests/test_api.py index 9d56b23e..6b065709 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,7 +18,7 @@ import pytest import coverage from coverage import env from coverage.data import line_counts -from coverage.exceptions import CoverageException +from coverage.exceptions import CoverageException, DataError, NoDataError, NoSource from coverage.files import abs_file, relative_filename from coverage.misc import import_local_file @@ -293,7 +293,7 @@ class ApiTest(CoverageTest): # empty summary reports raise exception, just like the xml report cov = coverage.Coverage() cov.erase() - with pytest.raises(CoverageException, match="No data to report."): + with pytest.raises(NoDataError, match="No data to report."): cov.report() def test_completely_zero_reporting(self): @@ -322,7 +322,7 @@ class ApiTest(CoverageTest): ) self.make_file(".coverage", cov4_data) cov = coverage.Coverage() - with pytest.raises(CoverageException, match="Looks like a coverage 4.x data file"): + with pytest.raises(DataError, match="Looks like a coverage 4.x data file"): cov.load() cov.erase() @@ -445,7 +445,7 @@ class ApiTest(CoverageTest): self.assert_exists(".coverage") cov2 = coverage.Coverage() - with pytest.raises(CoverageException, match=r"No data to combine"): + with pytest.raises(NoDataError, match=r"No data to combine"): cov2.combine(strict=True, keep=False) cov3 = coverage.Coverage() @@ -1126,7 +1126,7 @@ class RelativePathTest(CoverageTest): with change_dir("new"): cov = coverage.Coverage() cov.load() - with pytest.raises(CoverageException, match=expected): + with pytest.raises(NoSource, match=expected): cov.report() def test_moving_stuff_with_relative(self): diff --git a/tests/test_config.py b/tests/test_config.py index 411a456f..9a64d5b1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,7 +10,7 @@ import pytest import coverage from coverage.config import HandyConfigParser -from coverage.exceptions import CoverageException, CoverageWarning +from coverage.exceptions import ConfigError, CoverageWarning from tests.coveragetest import CoverageTest, UsingModulesMixin from tests.helpers import without_module @@ -159,7 +159,7 @@ class ConfigTest(CoverageTest): def test_missing_rcfile_from_environment(self): self.set_environ("COVERAGE_RCFILE", "nowhere.ini") msg = "Couldn't read 'nowhere.ini' as a config file" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): coverage.Coverage() @pytest.mark.parametrize("bad_config, msg", [ @@ -178,9 +178,9 @@ class ConfigTest(CoverageTest): r"multiple repeat"), ]) def test_parse_errors(self, bad_config, msg): - # Im-parsable values raise CoverageException, with details. + # Im-parsable values raise ConfigError, with details. self.make_file(".coveragerc", bad_config) - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): coverage.Coverage() @pytest.mark.parametrize("bad_config, msg", [ @@ -201,9 +201,9 @@ class ConfigTest(CoverageTest): ('[tool.coverage.report]\nfail_under="s"', "not a float"), ]) def test_toml_parse_errors(self, bad_config, msg): - # Im-parsable values raise CoverageException, with details. + # Im-parsable values raise ConfigError, with details. self.make_file("pyproject.toml", bad_config) - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): coverage.Coverage() def test_environment_vars_in_config(self): @@ -360,13 +360,13 @@ class ConfigTest(CoverageTest): def test_tweak_error_checking(self): # Trying to set an unknown config value raises an error. cov = coverage.Coverage() - with pytest.raises(CoverageException, match="No such option: 'run:xyzzy'"): + with pytest.raises(ConfigError, match="No such option: 'run:xyzzy'"): cov.set_option("run:xyzzy", 12) - with pytest.raises(CoverageException, match="No such option: 'xyzzy:foo'"): + with pytest.raises(ConfigError, match="No such option: 'xyzzy:foo'"): cov.set_option("xyzzy:foo", 12) - with pytest.raises(CoverageException, match="No such option: 'run:xyzzy'"): + with pytest.raises(ConfigError, match="No such option: 'run:xyzzy'"): _ = cov.get_option("run:xyzzy") - with pytest.raises(CoverageException, match="No such option: 'xyzzy:foo'"): + with pytest.raises(ConfigError, match="No such option: 'xyzzy:foo'"): _ = cov.get_option("xyzzy:foo") def test_tweak_plugin_options(self): @@ -375,12 +375,12 @@ class ConfigTest(CoverageTest): cov.set_option("run:plugins", ["fooey.plugin", "xyzzy.coverage.plugin"]) cov.set_option("fooey.plugin:xyzzy", 17) cov.set_option("xyzzy.coverage.plugin:plugh", ["a", "b"]) - with pytest.raises(CoverageException, match="No such option: 'no_such.plugin:foo'"): + with pytest.raises(ConfigError, match="No such option: 'no_such.plugin:foo'"): cov.set_option("no_such.plugin:foo", 23) assert cov.get_option("fooey.plugin:xyzzy") == 17 assert cov.get_option("xyzzy.coverage.plugin:plugh") == ["a", "b"] - with pytest.raises(CoverageException, match="No such option: 'no_such.plugin:foo'"): + with pytest.raises(ConfigError, match="No such option: 'no_such.plugin:foo'"): _ = cov.get_option("no_such.plugin:foo") def test_unknown_option(self): @@ -425,9 +425,9 @@ class ConfigTest(CoverageTest): branch = True """) config = HandyConfigParser("config.ini") - with pytest.raises(Exception, match="No section: 'xyzzy'"): + with pytest.raises(ConfigError, match="No section: 'xyzzy'"): config.options("xyzzy") - with pytest.raises(Exception, match="No option 'foo' in section: 'xyzzy'"): + with pytest.raises(ConfigError, match="No option 'foo' in section: 'xyzzy'"): config.get("xyzzy", "foo") @@ -683,7 +683,7 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): # If a config file is explicitly specified, then it is an error for it # to not be readable. msg = f"Couldn't read {bad_file!r} as a config file" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): coverage.Coverage(config_file=bad_file) def test_nocoveragerc_file_when_specified(self): @@ -707,7 +707,7 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): # Can't read a toml file that doesn't exist. with without_module(coverage.tomlconfig, 'tomli'): msg = "Couldn't read 'cov.toml' as a config file" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): coverage.Coverage(config_file="cov.toml") def test_no_toml_installed_explicit_toml(self): @@ -715,7 +715,7 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): self.make_file("cov.toml", "# A toml file!") with without_module(coverage.tomlconfig, 'tomli'): msg = "Can't read 'cov.toml' without TOML support" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): coverage.Coverage(config_file="cov.toml") def test_no_toml_installed_pyproject_toml(self): @@ -727,7 +727,7 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): """) with without_module(coverage.tomlconfig, 'tomli'): msg = "Can't read 'pyproject.toml' without TOML support" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): coverage.Coverage() def test_no_toml_installed_pyproject_no_coverage(self): diff --git a/tests/test_coverage.py b/tests/test_coverage.py index d69a0418..9fc7a001 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -7,7 +7,7 @@ import pytest import coverage from coverage import env -from coverage.exceptions import CoverageException +from coverage.exceptions import NoDataError from tests.coveragetest import CoverageTest @@ -1850,19 +1850,19 @@ class ReportingTest(CoverageTest): def test_no_data_to_report_on_annotate(self): # Reporting with no data produces a nice message and no output # directory. - with pytest.raises(CoverageException, match="No data to report."): + with pytest.raises(NoDataError, match="No data to report."): self.command_line("annotate -d ann") self.assert_doesnt_exist("ann") def test_no_data_to_report_on_html(self): # Reporting with no data produces a nice message and no output # directory. - with pytest.raises(CoverageException, match="No data to report."): + with pytest.raises(NoDataError, match="No data to report."): self.command_line("html -d htmlcov") self.assert_doesnt_exist("htmlcov") def test_no_data_to_report_on_xml(self): # Reporting with no data produces a nice message. - with pytest.raises(CoverageException, match="No data to report."): + with pytest.raises(NoDataError, match="No data to report."): self.command_line("xml") self.assert_doesnt_exist("coverage.xml") diff --git a/tests/test_data.py b/tests/test_data.py index 2e20618b..bc06eb23 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -16,7 +16,7 @@ import pytest from coverage.data import CoverageData, combine_parallel_data from coverage.data import add_data_to_hash, line_counts from coverage.debug import DebugControlString -from coverage.exceptions import CoverageException +from coverage.exceptions import DataError, NoDataError from coverage.files import PathAliases, canonical_filename from tests.coveragetest import CoverageTest @@ -164,14 +164,14 @@ class CoverageDataTest(CoverageTest): covdata = DebugCoverageData() covdata.add_lines(LINES_1) msg = "Can't add branch measurements to existing line data" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata.add_arcs(ARCS_3) def test_cant_add_lines_with_arcs(self): covdata = DebugCoverageData() covdata.add_arcs(ARCS_3) msg = "Can't add line measurements to existing branch data" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata.add_lines(LINES_1) def test_touch_file_with_lines(self): @@ -324,11 +324,11 @@ class CoverageDataTest(CoverageTest): def test_cant_file_tracer_unmeasured_files(self): covdata = DebugCoverageData() msg = "Can't add file tracer data for unmeasured file 'p1.foo'" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata.add_file_tracers({"p1.foo": "p1.plugin"}) covdata.add_lines({"p2.html": [10, 11, 12]}) - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata.add_file_tracers({"p1.foo": "p1.plugin"}) def test_cant_change_file_tracer_name(self): @@ -337,7 +337,7 @@ class CoverageDataTest(CoverageTest): covdata.add_file_tracers({"p1.foo": "p1.plugin"}) msg = "Conflicting file tracer name for 'p1.foo': 'p1.plugin' vs 'p1.plugin.foo'" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata.add_file_tracers({"p1.foo": "p1.plugin.foo"}) def test_update_lines(self): @@ -375,10 +375,10 @@ class CoverageDataTest(CoverageTest): covdata2 = DebugCoverageData(suffix='2') covdata2.add_arcs(ARCS_3) - with pytest.raises(CoverageException, match="Can't combine arc data with line data"): + with pytest.raises(DataError, match="Can't combine arc data with line data"): covdata1.update(covdata2) - with pytest.raises(CoverageException, match="Can't combine line data with arc data"): + with pytest.raises(DataError, match="Can't combine line data with arc data"): covdata2.update(covdata1) def test_update_file_tracers(self): @@ -424,11 +424,11 @@ class CoverageDataTest(CoverageTest): covdata2.add_file_tracers({"p1.html": "html.other_plugin"}) msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs 'html.other_plugin'" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata1.update(covdata2) msg = "Conflicting file tracer name for 'p1.html': 'html.other_plugin' vs 'html.plugin'" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata2.update(covdata1) def test_update_file_tracer_vs_no_file_tracer(self): @@ -440,11 +440,11 @@ class CoverageDataTest(CoverageTest): covdata2.add_lines({"p1.html": [1, 2, 3]}) msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs ''" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata1.update(covdata2) msg = "Conflicting file tracer name for 'p1.html': '' vs 'html.plugin'" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata2.update(covdata1) def test_update_lines_empty(self): @@ -529,7 +529,7 @@ class CoverageDataTest(CoverageTest): def test_cant_touch_in_empty_data(self): covdata = DebugCoverageData() msg = "Can't touch files in an empty CoverageData" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata.touch_file("abc.py") def test_read_and_write_are_opposites(self): @@ -587,13 +587,13 @@ class CoverageDataInTempDirTest(CoverageTest): msg = r"Couldn't .* '.*[/\\]{}': \S+" self.make_file("xyzzy.dat", "xyzzy") - with pytest.raises(CoverageException, match=msg.format("xyzzy.dat")): + with pytest.raises(DataError, match=msg.format("xyzzy.dat")): covdata = DebugCoverageData("xyzzy.dat") covdata.read() assert not covdata self.make_file("empty.dat", "") - with pytest.raises(CoverageException, match=msg.format("empty.dat")): + with pytest.raises(DataError, match=msg.format("empty.dat")): covdata = DebugCoverageData("empty.dat") covdata.read() assert not covdata @@ -601,14 +601,14 @@ class CoverageDataInTempDirTest(CoverageTest): def test_hard_read_error(self): self.make_file("noperms.dat", "go away") os.chmod("noperms.dat", 0) - with pytest.raises(CoverageException, match=r"Couldn't .* '.*[/\\]noperms.dat': "): + with pytest.raises(DataError, match=r"Couldn't .* '.*[/\\]noperms.dat': "): covdata = DebugCoverageData("noperms.dat") covdata.read() @pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData]) def test_error_when_closing(self, klass): msg = r"Couldn't .* '.*[/\\]flaked.dat': \S+" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata = klass("flaked.dat") covdata.add_lines(LINES_1) # I don't know how to make a real error, so let's fake one. @@ -621,7 +621,7 @@ class CoverageDataInTempDirTest(CoverageTest): con.execute("create table coverage_schema (version integer)") con.execute("insert into coverage_schema (version) values (99)") msg = r"Couldn't .* '.*[/\\]wrong_schema.db': wrong schema: 99 instead of \d+" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata = DebugCoverageData("wrong_schema.db") covdata.read() assert not covdata @@ -629,7 +629,7 @@ class CoverageDataInTempDirTest(CoverageTest): 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(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata = DebugCoverageData("no_schema.db") covdata.read() assert not covdata @@ -854,7 +854,7 @@ class CoverageDataFilesTest(CoverageTest): def test_combining_from_nonexistent_directories(self): covdata = DebugCoverageData() msg = "Couldn't combine from non-existent path 'xyzzy'" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(NoDataError, match=msg): combine_parallel_data(covdata, data_paths=['xyzzy']) def test_interleaved_erasing_bug716(self): @@ -894,5 +894,5 @@ class DumpsLoadsTest(CoverageTest): re.escape(repr(bad_data[:40])), len(bad_data), ) - with pytest.raises(CoverageException, match=msg): + with pytest.raises(DataError, match=msg): covdata.loads(bad_data) diff --git a/tests/test_files.py b/tests/test_files.py index de0dbbd5..0780fdb3 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -10,7 +10,7 @@ import pytest from coverage import env from coverage import files -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.files import ( TreeMatcher, FnmatchMatcher, ModuleMatcher, PathAliases, find_python_files, abs_file, actual_path, flat_rootname, fnmatches_to_regex, @@ -291,7 +291,7 @@ class PathAliasesTest(CoverageTest): def test_cant_have_wildcard_at_end(self, badpat): aliases = PathAliases() msg = "Pattern must not end with wildcards." - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): aliases.add(badpat, "fooey") def test_no_accidental_munging(self): diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 5d3b5322..2140f00c 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -14,7 +14,7 @@ import coverage from coverage import env from coverage.control import Plugins from coverage.data import line_counts -from coverage.exceptions import CoverageException, CoverageWarning +from coverage.exceptions import CoverageWarning, NoSource, PluginError from coverage.misc import import_local_file import coverage.plugin @@ -133,7 +133,7 @@ class LoadPluginsTest(CoverageTest): Nothing = 0 """) msg_pat = "Plugin module 'no_plugin' didn't define a coverage_init function" - with pytest.raises(CoverageException, match=msg_pat): + with pytest.raises(PluginError, match=msg_pat): list(Plugins.load_plugins(["no_plugin"], None)) @@ -576,7 +576,7 @@ class GoodFileTracerTest(FileTracerTest): # But completely new filenames are not in the results. assert len(cov.get_data().measured_files()) == 3 - with pytest.raises(CoverageException): + with pytest.raises(NoSource): cov.analysis("fictional.py") diff --git a/tests/test_results.py b/tests/test_results.py index 83968956..8b2a737b 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -5,7 +5,7 @@ import pytest -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.results import format_lines, Numbers, should_fail_under from tests.coveragetest import CoverageTest @@ -114,7 +114,7 @@ def test_should_fail_under(total, fail_under, precision, result): def test_should_fail_under_invalid_value(): - with pytest.raises(CoverageException, match=r"fail_under=101"): + with pytest.raises(ConfigError, match=r"fail_under=101"): should_fail_under(100.0, 101, 0) diff --git a/tests/test_summary.py b/tests/test_summary.py index 6fbb034d..769c6ec7 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -16,7 +16,7 @@ import coverage from coverage import env from coverage.control import Coverage from coverage.data import CoverageData -from coverage.exceptions import CoverageException +from coverage.exceptions import ConfigError from coverage.summary import SummaryReporter from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin @@ -921,5 +921,5 @@ class SummaryReporterConfigurationTest(CoverageTest): def test_sort_report_by_invalid_option(self): # Sort the text report by a nonsense column. msg = "Invalid sorting option: 'Xyzzy'" - with pytest.raises(CoverageException, match=msg): + with pytest.raises(ConfigError, match=msg): self.get_summary_text(('report:sort', 'Xyzzy')) -- cgit v1.2.1