mirror of
https://github.com/home-assistant/core.git
synced 2025-11-09 10:59:40 +00:00
Raise and suppress stack trace when reloading yaml fails (#102410)
* Allow async_integration_yaml_config to raise * Docstr - split check * Implement as wrapper, return dataclass * Fix setup error handling * Fix reload test mock * Move log_messages to error handler * Remove unreachable code * Remove config test helper * Refactor and ensure notifications during setup * Remove redundat error, adjust tests notifications * Fix patch * Apply suggestions from code review Co-authored-by: Erik Montnemery <erik@montnemery.com> * Follow up comments * Add call_back decorator * Split long lines * Update exception abbreviations --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
@@ -30,6 +30,7 @@ from homeassistant.const import (
|
||||
__version__,
|
||||
)
|
||||
from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigValidationError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
import homeassistant.helpers.check_config as check_config
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -1427,71 +1428,132 @@ async def test_component_config_exceptions(
|
||||
) -> None:
|
||||
"""Test unexpected exceptions validating component config."""
|
||||
# Config validator
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(
|
||||
return_value=Mock(
|
||||
async_validate_config=AsyncMock(side_effect=ValueError("broken"))
|
||||
)
|
||||
),
|
||||
)
|
||||
assert (
|
||||
await config_util.async_process_component_config(
|
||||
hass,
|
||||
{},
|
||||
integration=Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(
|
||||
return_value=Mock(
|
||||
async_validate_config=AsyncMock(
|
||||
side_effect=ValueError("broken")
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, integration=test_integration
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert "Unknown error calling test_domain config validator" in caplog.text
|
||||
caplog.clear()
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, integration=test_integration, raise_on_failure=True
|
||||
)
|
||||
assert "ValueError: broken" 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"
|
||||
|
||||
# component.CONFIG_SCHEMA
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(
|
||||
return_value=Mock(
|
||||
async_validate_config=AsyncMock(
|
||||
side_effect=HomeAssistantError("broken")
|
||||
)
|
||||
)
|
||||
),
|
||||
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||
)
|
||||
caplog.clear()
|
||||
assert (
|
||||
await config_util.async_process_component_config(
|
||||
hass,
|
||||
{},
|
||||
integration=Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(
|
||||
return_value=Mock(
|
||||
CONFIG_SCHEMA=Mock(side_effect=ValueError("broken"))
|
||||
)
|
||||
),
|
||||
),
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, integration=test_integration, raise_on_failure=False
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert "Invalid config for 'test_domain': broken" in caplog.text
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, integration=test_integration, raise_on_failure=True
|
||||
)
|
||||
assert "Invalid config for 'test_domain': broken" in str(ex.value)
|
||||
|
||||
# component.CONFIG_SCHEMA
|
||||
caplog.clear()
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(
|
||||
return_value=Mock(CONFIG_SCHEMA=Mock(side_effect=ValueError("broken")))
|
||||
),
|
||||
)
|
||||
assert (
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{},
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{},
|
||||
integration=test_integration,
|
||||
raise_on_failure=True,
|
||||
)
|
||||
assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
|
||||
assert str(ex.value) == "Unknown error calling test_domain CONFIG_SCHEMA"
|
||||
|
||||
# component.PLATFORM_SCHEMA
|
||||
caplog.clear()
|
||||
assert await config_util.async_process_component_config(
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(
|
||||
return_value=Mock(
|
||||
spec=["PLATFORM_SCHEMA_BASE"],
|
||||
PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")),
|
||||
)
|
||||
),
|
||||
)
|
||||
assert await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
integration=Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(
|
||||
return_value=Mock(
|
||||
spec=["PLATFORM_SCHEMA_BASE"],
|
||||
PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")),
|
||||
)
|
||||
),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
) == {"test_domain": []}
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert (
|
||||
"Unknown error validating test_platform platform config "
|
||||
"with test_domain component platform schema"
|
||||
"Unknown error validating config for test_platform platform "
|
||||
"for test_domain component with PLATFORM_SCHEMA"
|
||||
) in caplog.text
|
||||
caplog.clear()
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
integration=test_integration,
|
||||
raise_on_failure=True,
|
||||
)
|
||||
assert (
|
||||
"Unknown error validating config for test_platform platform "
|
||||
"for test_domain component with PLATFORM_SCHEMA"
|
||||
) in caplog.text
|
||||
assert str(ex.value) == (
|
||||
"Unknown error validating config for test_platform platform "
|
||||
"for test_domain component with PLATFORM_SCHEMA"
|
||||
)
|
||||
|
||||
# platform.PLATFORM_SCHEMA
|
||||
caplog.clear()
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.config.async_get_integration_with_requirements",
|
||||
return_value=Mock( # integration that owns platform
|
||||
@@ -1502,67 +1564,337 @@ async def test_component_config_exceptions(
|
||||
)
|
||||
),
|
||||
):
|
||||
assert await config_util.async_process_component_config(
|
||||
assert await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
integration=Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
) == {"test_domain": []}
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert (
|
||||
"Unknown error validating config for test_platform platform for test_domain"
|
||||
" component with PLATFORM_SCHEMA"
|
||||
) in caplog.text
|
||||
caplog.clear()
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
assert await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
integration=test_integration,
|
||||
raise_on_failure=True,
|
||||
)
|
||||
assert (
|
||||
"Unknown error validating config for test_platform platform for test_domain"
|
||||
" component with PLATFORM_SCHEMA"
|
||||
) in str(ex.value)
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert (
|
||||
"Unknown error validating config for test_platform platform for test_domain"
|
||||
" component with PLATFORM_SCHEMA" in caplog.text
|
||||
)
|
||||
# Test multiple platform failures
|
||||
assert await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{
|
||||
"test_domain": [
|
||||
{"platform": "test_platform1"},
|
||||
{"platform": "test_platform2"},
|
||||
]
|
||||
},
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
) == {"test_domain": []}
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert (
|
||||
"Unknown error validating config for test_platform1 platform "
|
||||
"for test_domain component with PLATFORM_SCHEMA"
|
||||
) in caplog.text
|
||||
assert (
|
||||
"Unknown error validating config for test_platform2 platform "
|
||||
"for test_domain component with PLATFORM_SCHEMA"
|
||||
) in caplog.text
|
||||
caplog.clear()
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
assert await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{
|
||||
"test_domain": [
|
||||
{"platform": "test_platform1"},
|
||||
{"platform": "test_platform2"},
|
||||
]
|
||||
},
|
||||
integration=test_integration,
|
||||
raise_on_failure=True,
|
||||
)
|
||||
assert (
|
||||
"Failed to process component config for integration test_domain"
|
||||
" due to multiple errors (2), check the logs for more information."
|
||||
) in str(ex.value)
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert (
|
||||
"Unknown error validating config for test_platform1 platform "
|
||||
"for test_domain component with PLATFORM_SCHEMA"
|
||||
) in caplog.text
|
||||
assert (
|
||||
"Unknown error validating config for test_platform2 platform "
|
||||
"for test_domain component with PLATFORM_SCHEMA"
|
||||
) in caplog.text
|
||||
|
||||
# get_platform("domain") raising on ImportError
|
||||
caplog.clear()
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||
)
|
||||
import_error = ImportError(
|
||||
("ModuleNotFoundError: No module named 'not_installed_something'"),
|
||||
name="not_installed_something",
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.config.async_get_integration_with_requirements",
|
||||
return_value=Mock( # integration that owns platform
|
||||
get_platform=Mock(side_effect=import_error)
|
||||
),
|
||||
):
|
||||
assert await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
) == {"test_domain": []}
|
||||
assert (
|
||||
"ImportError: ModuleNotFoundError: No module named "
|
||||
"'not_installed_something'" in caplog.text
|
||||
)
|
||||
caplog.clear()
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
assert await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
integration=test_integration,
|
||||
raise_on_failure=True,
|
||||
)
|
||||
assert (
|
||||
"ImportError: ModuleNotFoundError: No module named "
|
||||
"'not_installed_something'" in caplog.text
|
||||
)
|
||||
assert (
|
||||
"Platform error: test_domain - ModuleNotFoundError: "
|
||||
"No module named 'not_installed_something'"
|
||||
) in caplog.text
|
||||
assert (
|
||||
"Platform error: test_domain - ModuleNotFoundError: "
|
||||
"No module named 'not_installed_something'"
|
||||
) in str(ex.value)
|
||||
|
||||
# get_platform("config") raising
|
||||
caplog.clear()
|
||||
test_integration = Mock(
|
||||
pkg_path="homeassistant.components.test_domain",
|
||||
domain="test_domain",
|
||||
get_platform=Mock(
|
||||
side_effect=ImportError(
|
||||
("ModuleNotFoundError: No module named 'not_installed_something'"),
|
||||
name="not_installed_something",
|
||||
)
|
||||
),
|
||||
)
|
||||
assert (
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {}},
|
||||
integration=Mock(
|
||||
pkg_path="homeassistant.components.test_domain",
|
||||
domain="test_domain",
|
||||
get_platform=Mock(
|
||||
side_effect=ImportError(
|
||||
(
|
||||
"ModuleNotFoundError: No module named"
|
||||
" 'not_installed_something'"
|
||||
),
|
||||
name="not_installed_something",
|
||||
)
|
||||
),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
"Error importing config platform test_domain: ModuleNotFoundError: No module"
|
||||
" named 'not_installed_something'" in caplog.text
|
||||
"Error importing config platform test_domain: ModuleNotFoundError: "
|
||||
"No module named 'not_installed_something'" in caplog.text
|
||||
)
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {}},
|
||||
integration=test_integration,
|
||||
raise_on_failure=True,
|
||||
)
|
||||
assert (
|
||||
"Error importing config platform test_domain: ModuleNotFoundError: "
|
||||
"No module named 'not_installed_something'" in caplog.text
|
||||
)
|
||||
assert (
|
||||
"Error importing config platform test_domain: ModuleNotFoundError: "
|
||||
"No module named 'not_installed_something'" in str(ex.value)
|
||||
)
|
||||
|
||||
# get_component raising
|
||||
caplog.clear()
|
||||
test_integration = Mock(
|
||||
pkg_path="homeassistant.components.test_domain",
|
||||
domain="test_domain",
|
||||
get_component=Mock(
|
||||
side_effect=FileNotFoundError("No such file or directory: b'liblibc.a'")
|
||||
),
|
||||
)
|
||||
assert (
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {}},
|
||||
integration=Mock(
|
||||
pkg_path="homeassistant.components.test_domain",
|
||||
domain="test_domain",
|
||||
get_component=Mock(
|
||||
side_effect=FileNotFoundError(
|
||||
"No such file or directory: b'liblibc.a'"
|
||||
)
|
||||
),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert "Unable to import test_domain: No such file or directory" in caplog.text
|
||||
with pytest.raises(HomeAssistantError) as ex:
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {}},
|
||||
integration=test_integration,
|
||||
raise_on_failure=True,
|
||||
)
|
||||
assert "Unable to import test_domain: No such file or directory" in caplog.text
|
||||
assert "Unable to import test_domain: No such file or directory" in str(ex.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception_info_list", "error", "messages", "show_stack_trace", "translation_key"),
|
||||
[
|
||||
(
|
||||
[
|
||||
config_util.ConfigExceptionInfo(
|
||||
ImportError("bla"),
|
||||
"component_import_err",
|
||||
"test_domain",
|
||||
{"test_domain": []},
|
||||
"https://example.com",
|
||||
)
|
||||
],
|
||||
"bla",
|
||||
["Unable to import test_domain: bla", "bla"],
|
||||
False,
|
||||
"component_import_err",
|
||||
),
|
||||
(
|
||||
[
|
||||
config_util.ConfigExceptionInfo(
|
||||
HomeAssistantError("bla"),
|
||||
"config_validation_err",
|
||||
"test_domain",
|
||||
{"test_domain": []},
|
||||
"https://example.com",
|
||||
)
|
||||
],
|
||||
"bla",
|
||||
[
|
||||
"Invalid config for 'test_domain': bla, "
|
||||
"please check the docs at https://example.com",
|
||||
"bla",
|
||||
],
|
||||
True,
|
||||
"config_validation_err",
|
||||
),
|
||||
(
|
||||
[
|
||||
config_util.ConfigExceptionInfo(
|
||||
vol.Invalid("bla", ["path"]),
|
||||
"config_validation_err",
|
||||
"test_domain",
|
||||
{"test_domain": []},
|
||||
"https://example.com",
|
||||
)
|
||||
],
|
||||
"bla @ data['path']",
|
||||
[
|
||||
"Invalid config for 'test_domain': bla 'path', got None, "
|
||||
"please check the docs at https://example.com",
|
||||
"bla",
|
||||
],
|
||||
False,
|
||||
"config_validation_err",
|
||||
),
|
||||
(
|
||||
[
|
||||
config_util.ConfigExceptionInfo(
|
||||
vol.Invalid("bla", ["path"]),
|
||||
"platform_config_validation_err",
|
||||
"test_domain",
|
||||
{"test_domain": []},
|
||||
"https://alt.example.com",
|
||||
)
|
||||
],
|
||||
"bla @ data['path']",
|
||||
[
|
||||
"Invalid config for 'test_domain': bla 'path', got None, "
|
||||
"please check the docs at https://alt.example.com",
|
||||
"bla",
|
||||
],
|
||||
False,
|
||||
"platform_config_validation_err",
|
||||
),
|
||||
(
|
||||
[
|
||||
config_util.ConfigExceptionInfo(
|
||||
ImportError("bla"),
|
||||
"platform_component_load_err",
|
||||
"test_domain",
|
||||
{"test_domain": []},
|
||||
"https://example.com",
|
||||
)
|
||||
],
|
||||
"bla",
|
||||
["Platform error: test_domain - bla", "bla"],
|
||||
False,
|
||||
"platform_component_load_err",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_component_config_error_processing(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
error: str,
|
||||
exception_info_list: list[config_util.ConfigExceptionInfo],
|
||||
messages: list[str],
|
||||
show_stack_trace: bool,
|
||||
translation_key: str,
|
||||
) -> None:
|
||||
"""Test component config error processing."""
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
documentation="https://example.com",
|
||||
get_platform=Mock(
|
||||
return_value=Mock(
|
||||
async_validate_config=AsyncMock(side_effect=ValueError("broken"))
|
||||
)
|
||||
),
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.config.async_process_component_config",
|
||||
return_value=config_util.IntegrationConfigInfo(None, exception_info_list),
|
||||
), pytest.raises(ConfigValidationError) as ex:
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, test_integration, raise_on_failure=True
|
||||
)
|
||||
records = [record for record in caplog.records if record.msg == messages[0]]
|
||||
assert len(records) == 1
|
||||
assert (records[0].exc_info is not None) == show_stack_trace
|
||||
assert str(ex.value) == messages[0]
|
||||
assert ex.value.translation_key == translation_key
|
||||
assert ex.value.translation_domain == "homeassistant"
|
||||
assert ex.value.translation_placeholders["domain"] == "test_domain"
|
||||
assert all(message in caplog.text for message in messages)
|
||||
|
||||
caplog.clear()
|
||||
with patch(
|
||||
"homeassistant.config.async_process_component_config",
|
||||
return_value=config_util.IntegrationConfigInfo(None, exception_info_list),
|
||||
):
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, test_integration
|
||||
)
|
||||
assert all(message in caplog.text for message in messages)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1713,7 +2045,7 @@ async def test_component_config_validation_error(
|
||||
integration = await async_get_integration(
|
||||
hass, domain_with_label.partition(" ")[0]
|
||||
)
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
config,
|
||||
integration=integration,
|
||||
@@ -1758,7 +2090,7 @@ async def test_component_config_validation_error_with_docs(
|
||||
integration = await async_get_integration(
|
||||
hass, domain_with_label.partition(" ")[0]
|
||||
)
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
config,
|
||||
integration=integration,
|
||||
|
||||
Reference in New Issue
Block a user