Generate ConfigValidationError message from English translations (#113844)

* Fetch ConfigValidationError message from translation cache

* Sync error logmessages with translation cache

* More sync

* Cleanup

* Remove unrelated change

* Follow up comments

* Rebase and improve contructor

* Improve name

* Rename to MULTIPLE_INTEGRATION_CONFIG_ERRORS
This commit is contained in:
Jan Bouwhuis 2024-03-25 10:39:30 +01:00 committed by GitHub
parent a58049554d
commit 19fa39d556
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 200 additions and 178 deletions

View file

@ -163,31 +163,31 @@
"message": "Error importing config platform {domain}: {error}" "message": "Error importing config platform {domain}: {error}"
}, },
"config_validation_err": { "config_validation_err": {
"message": "Invalid config for integration {domain} at {config_file}, line {line}: {error}. Check the logs for more information." "message": "Invalid config for integration {domain} at {config_file}, line {line}: {error}."
}, },
"config_validator_unknown_err": { "config_validator_unknown_err": {
"message": "Unknown error calling {domain} config validator. Check the logs for more information." "message": "Unknown error calling {domain} config validator - {error}."
}, },
"config_schema_unknown_err": { "config_schema_unknown_err": {
"message": "Unknown error calling {domain} CONFIG_SCHEMA. Check the logs for more information." "message": "Unknown error calling {domain} CONFIG_SCHEMA - {error}."
}, },
"integration_config_error": { "multiple_integration_config_errors": {
"message": "Failed to process config for integration {domain} due to multiple ({errors}) errors. Check the logs for more information." "message": "Failed to process config for integration {domain} due to multiple ({errors}) errors. Check the logs for more information."
}, },
"max_length_exceeded": { "max_length_exceeded": {
"message": "Value {value} for property {property_name} has a maximum length of {max_length} characters." "message": "Value {value} for property {property_name} has a maximum length of {max_length} characters."
}, },
"platform_component_load_err": { "platform_component_load_err": {
"message": "Platform error: {domain} - {error}. Check the logs for more information." "message": "Platform error: {domain} - {error}."
}, },
"platform_component_load_exc": { "platform_component_load_exc": {
"message": "Platform error: {domain} - {error}. Check the logs for more information." "message": "[%key:component::homeassistant::exceptions::platform_component_load_err::message%]"
}, },
"platform_config_validation_err": { "platform_config_validation_err": {
"message": "Invalid config for {domain} from integration {p_name} at file {config_file}, line {line}: {error}. Check the logs for more information." "message": "Invalid config for {domain} from integration {p_name} at file {config_file}, line {line}: {error}. Check the logs for more information."
}, },
"platform_schema_validator_err": { "platform_schema_validator_err": {
"message": "Unknown error when validating config for {domain} from integration {p_name}" "message": "Unknown error when validating config for {domain} from integration {p_name} - {error}."
}, },
"service_not_found": { "service_not_found": {
"message": "Service {domain}.{service} not found." "message": "Service {domain}.{service} not found."

View file

@ -63,6 +63,7 @@ from .exceptions import ConfigValidationError, HomeAssistantError
from .generated.currencies import HISTORIC_CURRENCIES from .generated.currencies import HISTORIC_CURRENCIES
from .helpers import config_validation as cv, issue_registry as ir from .helpers import config_validation as cv, issue_registry as ir
from .helpers.entity_values import EntityValues from .helpers.entity_values import EntityValues
from .helpers.translation import async_get_exception_message
from .helpers.typing import ConfigType from .helpers.typing import ConfigType
from .loader import ComponentProtocol, Integration, IntegrationNotFound from .loader import ComponentProtocol, Integration, IntegrationNotFound
from .requirements import RequirementsNotFound, async_get_integration_with_requirements from .requirements import RequirementsNotFound, async_get_integration_with_requirements
@ -130,13 +131,23 @@ class ConfigErrorTranslationKey(StrEnum):
CONFIG_PLATFORM_IMPORT_ERR = "config_platform_import_err" CONFIG_PLATFORM_IMPORT_ERR = "config_platform_import_err"
CONFIG_VALIDATOR_UNKNOWN_ERR = "config_validator_unknown_err" CONFIG_VALIDATOR_UNKNOWN_ERR = "config_validator_unknown_err"
CONFIG_SCHEMA_UNKNOWN_ERR = "config_schema_unknown_err" CONFIG_SCHEMA_UNKNOWN_ERR = "config_schema_unknown_err"
PLATFORM_VALIDATOR_UNKNOWN_ERR = "platform_validator_unknown_err"
PLATFORM_COMPONENT_LOAD_ERR = "platform_component_load_err" PLATFORM_COMPONENT_LOAD_ERR = "platform_component_load_err"
PLATFORM_COMPONENT_LOAD_EXC = "platform_component_load_exc" PLATFORM_COMPONENT_LOAD_EXC = "platform_component_load_exc"
PLATFORM_SCHEMA_VALIDATOR_ERR = "platform_schema_validator_err" PLATFORM_SCHEMA_VALIDATOR_ERR = "platform_schema_validator_err"
# translation key in case multiple errors occurred # translation key in case multiple errors occurred
INTEGRATION_CONFIG_ERROR = "integration_config_error" MULTIPLE_INTEGRATION_CONFIG_ERRORS = "multiple_integration_config_errors"
_CONFIG_LOG_SHOW_STACK_TRACE: dict[ConfigErrorTranslationKey, bool] = {
ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR: False,
ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR: False,
ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR: True,
ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR: True,
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR: False,
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC: True,
ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR: True,
}
@dataclass @dataclass
@ -1183,48 +1194,16 @@ def _get_log_message_and_stack_print_pref(
platform_config = platform_exception.config platform_config = platform_exception.config
link = platform_exception.integration_link link = platform_exception.integration_link
placeholders: dict[str, str] = {"domain": domain, "error": str(exception)} placeholders: dict[str, str] = {
"domain": domain,
log_message_mapping: dict[ConfigErrorTranslationKey, tuple[str, bool]] = { "error": str(exception),
ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR: ( "p_name": platform_path,
f"Unable to import {domain}: {exception}",
False,
),
ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR: (
f"Error importing config platform {domain}: {exception}",
False,
),
ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR: (
f"Unknown error calling {domain} config validator",
True,
),
ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR: (
f"Unknown error calling {domain} CONFIG_SCHEMA",
True,
),
ConfigErrorTranslationKey.PLATFORM_VALIDATOR_UNKNOWN_ERR: (
f"Unknown error validating {platform_path} platform config with {domain} "
"component platform schema",
True,
),
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR: (
f"Platform error: {domain} - {exception}",
False,
),
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC: (
f"Platform error: {domain} - {exception}",
True,
),
ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR: (
f"Unknown error validating config for {platform_path} platform "
f"for {domain} component with PLATFORM_SCHEMA",
True,
),
} }
log_message_show_stack_trace = log_message_mapping.get(
show_stack_trace: bool | None = _CONFIG_LOG_SHOW_STACK_TRACE.get(
platform_exception.translation_key platform_exception.translation_key
) )
if log_message_show_stack_trace is None: if show_stack_trace is None:
# If no pre defined log_message is set, we generate an enriched error # If no pre defined log_message is set, we generate an enriched error
# message, so we can notify about it during setup # message, so we can notify about it during setup
show_stack_trace = False show_stack_trace = False
@ -1247,9 +1226,14 @@ def _get_log_message_and_stack_print_pref(
show_stack_trace = True show_stack_trace = True
return (log_message, show_stack_trace, placeholders) return (log_message, show_stack_trace, placeholders)
assert isinstance(log_message_show_stack_trace, tuple) # Generate the log message from the English translations
log_message = async_get_exception_message(
HA_DOMAIN,
platform_exception.translation_key,
translation_placeholders=placeholders,
)
return (*log_message_show_stack_trace, placeholders) return (log_message, show_stack_trace, placeholders)
async def async_process_component_and_handle_errors( async def async_process_component_and_handle_errors(
@ -1348,21 +1332,16 @@ def async_handle_component_errors(
if len(config_exception_info) == 1: if len(config_exception_info) == 1:
translation_key = platform_exception.translation_key translation_key = platform_exception.translation_key
else: else:
translation_key = ConfigErrorTranslationKey.INTEGRATION_CONFIG_ERROR translation_key = ConfigErrorTranslationKey.MULTIPLE_INTEGRATION_CONFIG_ERRORS
errors = str(len(config_exception_info)) errors = str(len(config_exception_info))
log_message = (
f"Failed to process component config for integration {domain} "
f"due to multiple errors ({errors}), check the logs for more information."
)
placeholders = { placeholders = {
"domain": domain, "domain": domain,
"errors": errors, "errors": errors,
} }
raise ConfigValidationError( raise ConfigValidationError(
str(log_message), translation_key,
[platform_exception.exception for platform_exception in config_exception_info], [platform_exception.exception for platform_exception in config_exception_info],
translation_domain="homeassistant", translation_domain=HA_DOMAIN,
translation_key=translation_key,
translation_placeholders=placeholders, translation_placeholders=placeholders,
) )

View file

@ -87,20 +87,19 @@ class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]):
def __init__( def __init__(
self, self,
message: str, message_translation_key: str,
exceptions: list[Exception], exceptions: list[Exception],
translation_domain: str | None = None, translation_domain: str | None = None,
translation_key: str | None = None,
translation_placeholders: dict[str, str] | None = None, translation_placeholders: dict[str, str] | None = None,
) -> None: ) -> None:
"""Initialize exception.""" """Initialize exception."""
super().__init__( super().__init__(
*(message, exceptions), *(message_translation_key, exceptions),
translation_domain=translation_domain, translation_domain=translation_domain,
translation_key=translation_key, translation_key=message_translation_key,
translation_placeholders=translation_placeholders, translation_placeholders=translation_placeholders,
) )
self._message = message self.generate_message = True
class ServiceValidationError(HomeAssistantError): class ServiceValidationError(HomeAssistantError):

View file

@ -1,4 +1,19 @@
# serializer version: 1 # serializer version: 1
# name: test_component_config_error_processing[exception_info_list0-bla-messages0-False-component_import_err]
'Unable to import test_domain: bla'
# ---
# name: test_component_config_error_processing[exception_info_list1-bla-messages1-True-config_validation_err]
'Invalid config for integration test_domain at configuration.yaml, line 140: bla'
# ---
# name: test_component_config_error_processing[exception_info_list2-bla @ data['path']-messages2-False-config_validation_err]
"Invalid config for integration test_domain at configuration.yaml, line 140: bla @ data['path']"
# ---
# name: test_component_config_error_processing[exception_info_list3-bla @ data['path']-messages3-False-platform_config_validation_err]
"Invalid config for test_domain from integration test_domain at file configuration.yaml, line 140: bla @ data['path']. Check the logs for more information"
# ---
# name: test_component_config_error_processing[exception_info_list4-bla-messages4-False-platform_component_load_err]
'Platform error: test_domain - bla'
# ---
# name: test_component_config_validation_error[basic] # name: test_component_config_validation_error[basic]
list([ list([
dict({ dict({
@ -63,7 +78,7 @@
}), }),
dict({ dict({
'has_exc_info': True, 'has_exc_info': True,
'message': 'Unknown error calling custom_validator_bad_2 config validator', 'message': 'config_validator_unknown_err',
}), }),
]) ])
# --- # ---
@ -131,7 +146,7 @@
}), }),
dict({ dict({
'has_exc_info': True, 'has_exc_info': True,
'message': 'Unknown error calling custom_validator_bad_2 config validator', 'message': 'config_validator_unknown_err',
}), }),
]) ])
# --- # ---
@ -247,7 +262,7 @@
}), }),
dict({ dict({
'has_exc_info': True, 'has_exc_info': True,
'message': 'Unknown error calling custom_validator_bad_2 config validator', 'message': 'config_validator_unknown_err',
}), }),
]) ])
# --- # ---
@ -291,7 +306,7 @@
}), }),
dict({ dict({
'has_exc_info': True, 'has_exc_info': True,
'message': 'Unknown error calling custom_validator_bad_2 config validator', 'message': 'config_validator_unknown_err',
}), }),
dict({ dict({
'has_exc_info': False, 'has_exc_info': False,
@ -342,7 +357,27 @@
''', ''',
"Invalid config for 'custom_validator_ok_2' at configuration.yaml, line 52: required key 'host' not provided, please check the docs at https://www.home-assistant.io/integrations/custom_validator_ok_2", "Invalid config for 'custom_validator_ok_2' at configuration.yaml, line 52: required key 'host' not provided, please check the docs at https://www.home-assistant.io/integrations/custom_validator_ok_2",
"Invalid config for 'custom_validator_bad_1' at configuration.yaml, line 55: broken, please check the docs at https://www.home-assistant.io/integrations/custom_validator_bad_1", "Invalid config for 'custom_validator_bad_1' at configuration.yaml, line 55: broken, please check the docs at https://www.home-assistant.io/integrations/custom_validator_bad_1",
'Unknown error calling custom_validator_bad_2 config validator', 'config_validator_unknown_err',
])
# ---
# name: test_individual_packages_schema_validation_errors[packages_dict]
list([
"Setup of package 'should_be_a_dict' at configuration.yaml, line 3 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized",
])
# ---
# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_dict]
list([
"Setup of package 'should_be_a_dict' at packages/expected_dict.yaml, line 1 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized",
])
# ---
# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_slug]
list([
"Setup of package 'this is not a slug but it should be one' at packages/expected_slug.yaml, line 1 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized",
])
# ---
# name: test_individual_packages_schema_validation_errors[packages_slug]
list([
"Setup of package 'this is not a slug but it should be one' at configuration.yaml, line 3 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized",
]) ])
# --- # ---
# name: test_package_merge_error[packages] # name: test_package_merge_error[packages]
@ -381,6 +416,21 @@
"Setup of package 'unknown_integration' at integrations/unknown_integration.yaml, line 1 failed: Integration test_domain caused error: ModuleNotFoundError: No module named 'not_installed_something'", "Setup of package 'unknown_integration' at integrations/unknown_integration.yaml, line 1 failed: Integration test_domain caused error: ModuleNotFoundError: No module named 'not_installed_something'",
]) ])
# --- # ---
# name: test_packages_schema_validation_error[packages_is_a_list]
list([
"Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary",
])
# ---
# name: test_packages_schema_validation_error[packages_is_a_value]
list([
"Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary",
])
# ---
# name: test_packages_schema_validation_error[packages_is_null]
list([
"Invalid package configuration 'packages' at configuration.yaml, line 3: expected a dictionary",
])
# ---
# name: test_yaml_error[basic] # name: test_yaml_error[basic]
''' '''
mapping values are not allowed here mapping values are not allowed here
@ -451,38 +501,3 @@
''', ''',
]) ])
# --- # ---
# name: test_individual_packages_schema_validation_errors[packages_dict]
list([
"Setup of package 'should_be_a_dict' at configuration.yaml, line 3 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized",
])
# ---
# name: test_individual_packages_schema_validation_errors[packages_slug]
list([
"Setup of package 'this is not a slug but it should be one' at configuration.yaml, line 3 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized",
])
# ---
# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_dict]
list([
"Setup of package 'should_be_a_dict' at packages/expected_dict.yaml, line 1 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized",
])
# ---
# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_slug]
list([
"Setup of package 'this is not a slug but it should be one' at packages/expected_slug.yaml, line 1 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized",
])
# ---
# name: test_packages_schema_validation_error[packages_is_a_list]
list([
"Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary",
])
# ---
# name: test_packages_schema_validation_error[packages_is_a_value]
list([
"Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary",
])
# ---
# name: test_packages_schema_validation_error[packages_is_null]
list([
"Invalid package configuration 'packages' at configuration.yaml, line 3: expected a dictionary",
])
# ---

View file

@ -44,6 +44,7 @@ import homeassistant.helpers.check_config as check_config
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import Integration, async_get_integration from homeassistant.loader import Integration, async_get_integration
from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import ( from homeassistant.util.unit_system import (
_CONF_UNIT_SYSTEM_US_CUSTOMARY, _CONF_UNIT_SYSTEM_US_CUSTOMARY,
METRIC_SYSTEM, METRIC_SYSTEM,
@ -51,6 +52,7 @@ from homeassistant.util.unit_system import (
UnitSystem, UnitSystem,
) )
from homeassistant.util.yaml import SECRET_YAML from homeassistant.util.yaml import SECRET_YAML
from homeassistant.util.yaml.objects import NodeDictClass
from .common import ( from .common import (
MockModule, MockModule,
@ -373,6 +375,14 @@ async def mock_custom_validator_integrations_with_docs(
) )
class ConfigTestClass(NodeDictClass):
"""Test class for config with wrapper."""
__dict__ = {"__config_file__": "configuration.yaml", "__line__": 140}
__line__ = 140
__config_file__ = "configuration.yaml"
async def test_create_default_config(hass: HomeAssistant) -> None: async def test_create_default_config(hass: HomeAssistant) -> None:
"""Test creation of default config.""" """Test creation of default config."""
assert not os.path.isfile(YAML_PATH) assert not os.path.isfile(YAML_PATH)
@ -1435,7 +1445,24 @@ async def test_component_config_exceptions(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test unexpected exceptions validating component config.""" """Test unexpected exceptions validating component config."""
# Config validator
# Create test config with embedded info
test_config = ConfigTestClass({"test_domain": {}})
test_platform_config = ConfigTestClass(
{"test_domain": {"platform": "test_platform"}}
)
test_multi_platform_config = ConfigTestClass(
{
"test_domain": [
{"platform": "test_platform1"},
{"platform": "test_platform2"},
]
},
)
# Make sure the exception translation cache is loaded
await async_setup_component(hass, "homeassistant", {})
test_integration = Mock( test_integration = Mock(
domain="test_domain", domain="test_domain",
async_get_component=AsyncMock(), async_get_component=AsyncMock(),
@ -1447,7 +1474,7 @@ async def test_component_config_exceptions(
) )
assert ( assert (
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, {}, integration=test_integration hass, test_config, integration=test_integration
) )
is None is None
) )
@ -1456,12 +1483,13 @@ async def test_component_config_exceptions(
caplog.clear() caplog.clear()
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, {}, integration=test_integration, raise_on_failure=True hass, test_config, integration=test_integration, raise_on_failure=True
) )
assert "ValueError: broken" in caplog.text assert "ValueError: broken" in caplog.text
assert "Unknown error calling test_domain config validator" in caplog.text assert "Unknown error calling test_domain config validator" in caplog.text
assert str(ex.value) == "Unknown error calling test_domain config validator" assert (
str(ex.value) == "Unknown error calling test_domain config validator - broken"
)
test_integration = Mock( test_integration = Mock(
domain="test_domain", domain="test_domain",
async_get_platform=AsyncMock( async_get_platform=AsyncMock(
@ -1476,17 +1504,23 @@ async def test_component_config_exceptions(
caplog.clear() caplog.clear()
assert ( assert (
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, {}, integration=test_integration, raise_on_failure=False hass, test_config, integration=test_integration, raise_on_failure=False
) )
is None is None
) )
assert "Invalid config for 'test_domain': broken" in caplog.text assert (
"Invalid config for 'test_domain' at ../../configuration.yaml, "
"line 140: broken, please check the docs at" in caplog.text
)
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, {}, integration=test_integration, raise_on_failure=True hass, test_config, integration=test_integration, raise_on_failure=True
) )
assert "Invalid config for 'test_domain': broken" in str(ex.value) assert (
str(ex.value)
== "Invalid config for integration test_domain at configuration.yaml, "
"line 140: broken"
)
# component.CONFIG_SCHEMA # component.CONFIG_SCHEMA
caplog.clear() caplog.clear()
test_integration = Mock( test_integration = Mock(
@ -1499,23 +1533,23 @@ async def test_component_config_exceptions(
assert ( assert (
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, hass,
{}, test_config,
integration=test_integration, integration=test_integration,
raise_on_failure=False, raise_on_failure=False,
) )
is None is None
) )
assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
caplog.clear()
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, hass,
{}, test_config,
integration=test_integration, integration=test_integration,
raise_on_failure=True, raise_on_failure=True,
) )
assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
assert str(ex.value) == "Unknown error calling test_domain CONFIG_SCHEMA" assert str(ex.value) == "Unknown error calling test_domain CONFIG_SCHEMA - broken"
# component.PLATFORM_SCHEMA # component.PLATFORM_SCHEMA
caplog.clear() caplog.clear()
test_integration = Mock( test_integration = Mock(
@ -1530,30 +1564,30 @@ async def test_component_config_exceptions(
) )
assert await config_util.async_process_component_and_handle_errors( assert await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {"platform": "test_platform"}}, test_platform_config,
integration=test_integration, integration=test_integration,
raise_on_failure=False, raise_on_failure=False,
) == {"test_domain": []} ) == {"test_domain": []}
assert "ValueError: broken" in caplog.text assert "ValueError: broken" in caplog.text
assert ( assert (
"Unknown error validating config for test_platform platform " "Unknown error when validating config for test_domain "
"for test_domain component with PLATFORM_SCHEMA" "from integration test_platform - broken"
) in caplog.text ) in caplog.text
caplog.clear() caplog.clear()
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {"platform": "test_platform"}}, test_platform_config,
integration=test_integration, integration=test_integration,
raise_on_failure=True, raise_on_failure=True,
) )
assert ( assert (
"Unknown error validating config for test_platform platform " "Unknown error when validating config for test_domain "
"for test_domain component with PLATFORM_SCHEMA" "from integration test_platform - broken"
) in caplog.text ) in caplog.text
assert str(ex.value) == ( assert str(ex.value) == (
"Unknown error validating config for test_platform platform " "Unknown error when validating config for test_domain "
"for test_domain component with PLATFORM_SCHEMA" "from integration test_platform - broken"
) )
# platform.PLATFORM_SCHEMA # platform.PLATFORM_SCHEMA
@ -1575,78 +1609,65 @@ async def test_component_config_exceptions(
): ):
assert await config_util.async_process_component_and_handle_errors( assert await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {"platform": "test_platform"}}, test_platform_config,
integration=test_integration, integration=test_integration,
raise_on_failure=False, raise_on_failure=False,
) == {"test_domain": []} ) == {"test_domain": []}
assert "ValueError: broken" in caplog.text assert "ValueError: broken" in caplog.text
assert ( assert (
"Unknown error validating config for test_platform platform for test_domain" "Unknown error when validating config for test_domain "
" component with PLATFORM_SCHEMA" "from integration test_platform - broken"
) in caplog.text ) in caplog.text
caplog.clear() caplog.clear()
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
assert await config_util.async_process_component_and_handle_errors( assert await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {"platform": "test_platform"}}, test_platform_config,
integration=test_integration, integration=test_integration,
raise_on_failure=True, raise_on_failure=True,
) )
assert ( assert (
"Unknown error validating config for test_platform platform for test_domain" "Unknown error when validating config for test_domain "
" component with PLATFORM_SCHEMA" "from integration test_platform - broken" in str(ex.value)
) in str(ex.value) )
assert "ValueError: broken" in caplog.text assert "ValueError: broken" in caplog.text
assert ( assert (
"Unknown error validating config for test_platform platform for test_domain" "Unknown error when validating config for test_domain "
" component with PLATFORM_SCHEMA" in caplog.text "from integration test_platform - broken" in caplog.text
) )
# Test multiple platform failures # Test multiple platform failures
assert await config_util.async_process_component_and_handle_errors( assert await config_util.async_process_component_and_handle_errors(
hass, hass,
{ test_multi_platform_config,
"test_domain": [
{"platform": "test_platform1"},
{"platform": "test_platform2"},
]
},
integration=test_integration, integration=test_integration,
raise_on_failure=False, raise_on_failure=False,
) == {"test_domain": []} ) == {"test_domain": []}
assert "ValueError: broken" in caplog.text assert "ValueError: broken" in caplog.text
assert ( assert (
"Unknown error validating config for test_platform1 platform " "Unknown error when validating config for test_domain "
"for test_domain component with PLATFORM_SCHEMA" "from integration test_platform - broken"
) in caplog.text
assert (
"Unknown error validating config for test_platform2 platform "
"for test_domain component with PLATFORM_SCHEMA"
) in caplog.text ) in caplog.text
caplog.clear() caplog.clear()
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
assert await config_util.async_process_component_and_handle_errors( assert await config_util.async_process_component_and_handle_errors(
hass, hass,
{ test_multi_platform_config,
"test_domain": [
{"platform": "test_platform1"},
{"platform": "test_platform2"},
]
},
integration=test_integration, integration=test_integration,
raise_on_failure=True, raise_on_failure=True,
) )
assert ( assert (
"Failed to process component config for integration test_domain" "Failed to process config for integration test_domain "
" due to multiple errors (2), check the logs for more information." "due to multiple (2) errors. Check the logs for more information"
) in str(ex.value) in str(ex.value)
)
assert "ValueError: broken" in caplog.text assert "ValueError: broken" in caplog.text
assert ( assert (
"Unknown error validating config for test_platform1 platform " "Unknown error when validating config for test_domain "
"for test_domain component with PLATFORM_SCHEMA" "from integration test_platform1 - broken"
) in caplog.text ) in caplog.text
assert ( assert (
"Unknown error validating config for test_platform2 platform " "Unknown error when validating config for test_domain "
"for test_domain component with PLATFORM_SCHEMA" "from integration test_platform2 - broken"
) in caplog.text ) in caplog.text
# async_get_platform("domain") raising on ImportError # async_get_platform("domain") raising on ImportError
@ -1668,7 +1689,7 @@ async def test_component_config_exceptions(
): ):
assert await config_util.async_process_component_and_handle_errors( assert await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {"platform": "test_platform"}}, test_platform_config,
integration=test_integration, integration=test_integration,
raise_on_failure=False, raise_on_failure=False,
) == {"test_domain": []} ) == {"test_domain": []}
@ -1680,7 +1701,7 @@ async def test_component_config_exceptions(
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
assert await config_util.async_process_component_and_handle_errors( assert await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {"platform": "test_platform"}}, test_platform_config,
integration=test_integration, integration=test_integration,
raise_on_failure=True, raise_on_failure=True,
) )
@ -1713,7 +1734,7 @@ async def test_component_config_exceptions(
assert ( assert (
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {}}, test_config,
integration=test_integration, integration=test_integration,
raise_on_failure=False, raise_on_failure=False,
) )
@ -1726,7 +1747,7 @@ async def test_component_config_exceptions(
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {}}, test_config,
integration=test_integration, integration=test_integration,
raise_on_failure=True, raise_on_failure=True,
) )
@ -1751,7 +1772,7 @@ async def test_component_config_exceptions(
assert ( assert (
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {}}, test_config,
integration=test_integration, integration=test_integration,
raise_on_failure=False, raise_on_failure=False,
) )
@ -1761,7 +1782,7 @@ async def test_component_config_exceptions(
with pytest.raises(HomeAssistantError) as ex: with pytest.raises(HomeAssistantError) as ex:
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, hass,
{"test_domain": {}}, test_config,
integration=test_integration, integration=test_integration,
raise_on_failure=True, raise_on_failure=True,
) )
@ -1778,7 +1799,7 @@ async def test_component_config_exceptions(
ImportError("bla"), ImportError("bla"),
"component_import_err", "component_import_err",
"test_domain", "test_domain",
{"test_domain": []}, ConfigTestClass({"test_domain": []}),
"https://example.com", "https://example.com",
) )
], ],
@ -1793,13 +1814,14 @@ async def test_component_config_exceptions(
HomeAssistantError("bla"), HomeAssistantError("bla"),
"config_validation_err", "config_validation_err",
"test_domain", "test_domain",
{"test_domain": []}, ConfigTestClass({"test_domain": []}),
"https://example.com", "https://example.com",
) )
], ],
"bla", "bla",
[ [
"Invalid config for 'test_domain': bla, " "Invalid config for 'test_domain' at "
"../../configuration.yaml, line 140: bla, "
"please check the docs at https://example.com", "please check the docs at https://example.com",
"bla", "bla",
], ],
@ -1812,14 +1834,15 @@ async def test_component_config_exceptions(
vol.Invalid("bla", ["path"]), vol.Invalid("bla", ["path"]),
"config_validation_err", "config_validation_err",
"test_domain", "test_domain",
{"test_domain": []}, ConfigTestClass({"test_domain": []}),
"https://example.com", "https://example.com",
) )
], ],
"bla @ data['path']", "bla @ data['path']",
[ [
"Invalid config for 'test_domain': bla 'path', got None, " "Invalid config for 'test_domain' at "
"please check the docs at https://example.com", "../../configuration.yaml, line 140: bla 'path', "
"got None, please check the docs at https://example.com",
"bla", "bla",
], ],
False, False,
@ -1831,14 +1854,15 @@ async def test_component_config_exceptions(
vol.Invalid("bla", ["path"]), vol.Invalid("bla", ["path"]),
"platform_config_validation_err", "platform_config_validation_err",
"test_domain", "test_domain",
{"test_domain": []}, ConfigTestClass({"test_domain": []}),
"https://alt.example.com", "https://alt.example.com",
) )
], ],
"bla @ data['path']", "bla @ data['path']",
[ [
"Invalid config for 'test_domain': bla 'path', got None, " "Invalid config for 'test_domain' at "
"please check the docs at https://alt.example.com", "../../configuration.yaml, line 140: bla 'path', "
"got None, please check the docs at https://alt.example.com",
"bla", "bla",
], ],
False, False,
@ -1850,7 +1874,7 @@ async def test_component_config_exceptions(
ImportError("bla"), ImportError("bla"),
"platform_component_load_err", "platform_component_load_err",
"test_domain", "test_domain",
{"test_domain": []}, ConfigTestClass({"test_domain": []}),
"https://example.com", "https://example.com",
) )
], ],
@ -1864,13 +1888,18 @@ async def test_component_config_exceptions(
async def test_component_config_error_processing( async def test_component_config_error_processing(
hass: HomeAssistant, hass: HomeAssistant,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
error: str,
exception_info_list: list[config_util.ConfigExceptionInfo], exception_info_list: list[config_util.ConfigExceptionInfo],
snapshot: SnapshotAssertion,
error: str,
messages: list[str], messages: list[str],
show_stack_trace: bool, show_stack_trace: bool,
translation_key: str, translation_key: str,
) -> None: ) -> None:
"""Test component config error processing.""" """Test component config error processing."""
# Make sure the exception translation cache is loaded
await async_setup_component(hass, "homeassistant", {})
test_integration = Mock( test_integration = Mock(
domain="test_domain", domain="test_domain",
documentation="https://example.com", documentation="https://example.com",
@ -1890,7 +1919,7 @@ async def test_component_config_error_processing(
records = [record for record in caplog.records if record.msg == messages[0]] records = [record for record in caplog.records if record.msg == messages[0]]
assert len(records) == 1 assert len(records) == 1
assert (records[0].exc_info is not None) == show_stack_trace assert (records[0].exc_info is not None) == show_stack_trace
assert str(ex.value) == messages[0] assert str(ex.value) == snapshot
assert ex.value.translation_key == translation_key assert ex.value.translation_key == translation_key
assert ex.value.translation_domain == "homeassistant" assert ex.value.translation_domain == "homeassistant"
assert ex.value.translation_placeholders["domain"] == "test_domain" assert ex.value.translation_placeholders["domain"] == "test_domain"
@ -1902,7 +1931,7 @@ async def test_component_config_error_processing(
return_value=config_util.IntegrationConfigInfo(None, exception_info_list), return_value=config_util.IntegrationConfigInfo(None, exception_info_list),
): ):
await config_util.async_process_component_and_handle_errors( await config_util.async_process_component_and_handle_errors(
hass, {}, test_integration hass, ConfigTestClass({}), test_integration
) )
assert all(message in caplog.text for message in messages) assert all(message in caplog.text for message in messages)