hass-core/tests/test_setup.py
J. Nick Koston def6c5c21c
Refactor integration startup time tracking to reduce overhead (#110136)
* Refactor integration startup time tracking to reduce overhead

- Use monotonic time for watching integration startup time as it avoids incorrect values if time moves backwards because of ntp during startup and reduces many time conversions since we want durations in seconds and not local time

- Use loop scheduling instead of a task

- Moves all the dispatcher logic into the new _WatchPendingSetups

* websocket as well

* tweaks

* simplify logic

* preserve logic

* preserve logic

* lint

* adjust
2024-02-17 21:47:55 -05:00

786 lines
26 KiB
Python

"""Test component/platform setup."""
import asyncio
import threading
from unittest.mock import AsyncMock, Mock, patch
import pytest
import voluptuous as vol
from homeassistant import config_entries, setup
from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
from homeassistant.helpers.config_validation import (
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from .common import (
MockConfigEntry,
MockModule,
MockPlatform,
assert_setup_component,
mock_integration,
mock_platform,
)
@pytest.fixture
def mock_handlers():
"""Mock config flows."""
class MockFlowHandler(config_entries.ConfigFlow):
"""Define a mock flow handler."""
VERSION = 1
with patch.dict(config_entries.HANDLERS, {"comp": MockFlowHandler}):
yield
async def test_validate_component_config(hass: HomeAssistant) -> None:
"""Test validating component configuration."""
config_schema = vol.Schema({"comp_conf": {"hello": str}}, required=True)
mock_integration(hass, MockModule("comp_conf", config_schema=config_schema))
with assert_setup_component(0):
assert not await setup.async_setup_component(hass, "comp_conf", {})
hass.data.pop(setup.DATA_SETUP)
with assert_setup_component(0):
assert not await setup.async_setup_component(
hass, "comp_conf", {"comp_conf": None}
)
hass.data.pop(setup.DATA_SETUP)
with assert_setup_component(0):
assert not await setup.async_setup_component(
hass, "comp_conf", {"comp_conf": {}}
)
hass.data.pop(setup.DATA_SETUP)
with assert_setup_component(0):
assert not await setup.async_setup_component(
hass,
"comp_conf",
{"comp_conf": {"hello": "world", "invalid": "extra"}},
)
hass.data.pop(setup.DATA_SETUP)
with assert_setup_component(1):
assert await setup.async_setup_component(
hass, "comp_conf", {"comp_conf": {"hello": "world"}}
)
async def test_validate_platform_config(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test validating platform configuration."""
platform_schema = PLATFORM_SCHEMA.extend({"hello": str})
platform_schema_base = PLATFORM_SCHEMA_BASE.extend({})
mock_integration(
hass,
MockModule("platform_conf", platform_schema_base=platform_schema_base),
)
mock_platform(
hass,
"whatever.platform_conf",
MockPlatform(platform_schema=platform_schema),
)
with assert_setup_component(0):
assert await setup.async_setup_component(
hass,
"platform_conf",
{"platform_conf": {"platform": "not_existing", "hello": "world"}},
)
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("platform_conf")
with assert_setup_component(1):
assert await setup.async_setup_component(
hass,
"platform_conf",
{"platform_conf": {"platform": "whatever", "hello": "world"}},
)
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("platform_conf")
with assert_setup_component(1):
assert await setup.async_setup_component(
hass,
"platform_conf",
{"platform_conf": [{"platform": "whatever", "hello": "world"}]},
)
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("platform_conf")
# Any falsey platform config will be ignored (None, {}, etc)
with assert_setup_component(0) as config:
assert await setup.async_setup_component(
hass, "platform_conf", {"platform_conf": None}
)
assert "platform_conf" in hass.config.components
assert not config["platform_conf"] # empty
assert await setup.async_setup_component(
hass, "platform_conf", {"platform_conf": {}}
)
assert "platform_conf" in hass.config.components
assert not config["platform_conf"] # empty
async def test_validate_platform_config_2(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test component PLATFORM_SCHEMA_BASE prio over PLATFORM_SCHEMA."""
platform_schema = PLATFORM_SCHEMA.extend({"hello": str})
platform_schema_base = PLATFORM_SCHEMA_BASE.extend({"hello": "world"})
mock_integration(
hass,
MockModule(
"platform_conf",
platform_schema=platform_schema,
platform_schema_base=platform_schema_base,
),
)
mock_platform(
hass,
"whatever.platform_conf",
MockPlatform("whatever", platform_schema=platform_schema),
)
with assert_setup_component(1):
assert await setup.async_setup_component(
hass,
"platform_conf",
{
# pass
"platform_conf": {"platform": "whatever", "hello": "world"},
# fail: key hello violates component platform_schema_base
"platform_conf 2": {"platform": "whatever", "hello": "there"},
},
)
async def test_validate_platform_config_3(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test fallback to component PLATFORM_SCHEMA."""
component_schema = PLATFORM_SCHEMA_BASE.extend({"hello": str})
platform_schema = PLATFORM_SCHEMA.extend({"cheers": str, "hello": "world"})
mock_integration(
hass, MockModule("platform_conf", platform_schema=component_schema)
)
mock_platform(
hass,
"whatever.platform_conf",
MockPlatform("whatever", platform_schema=platform_schema),
)
with assert_setup_component(1):
assert await setup.async_setup_component(
hass,
"platform_conf",
{
# pass
"platform_conf": {"platform": "whatever", "hello": "world"},
# fail: key hello violates component platform_schema
"platform_conf 2": {"platform": "whatever", "hello": "there"},
},
)
async def test_validate_platform_config_4(hass: HomeAssistant) -> None:
"""Test entity_namespace in PLATFORM_SCHEMA."""
component_schema = PLATFORM_SCHEMA_BASE
platform_schema = PLATFORM_SCHEMA
mock_integration(
hass,
MockModule("platform_conf", platform_schema_base=component_schema),
)
mock_platform(
hass,
"whatever.platform_conf",
MockPlatform(platform_schema=platform_schema),
)
with assert_setup_component(1):
assert await setup.async_setup_component(
hass,
"platform_conf",
{
"platform_conf": {
# pass: entity_namespace accepted by PLATFORM_SCHEMA
"platform": "whatever",
"entity_namespace": "yummy",
}
},
)
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("platform_conf")
async def test_component_not_found(hass: HomeAssistant) -> None:
"""setup_component should not crash if component doesn't exist."""
assert await setup.async_setup_component(hass, "non_existing", {}) is False
async def test_component_not_double_initialized(hass: HomeAssistant) -> None:
"""Test we do not set up a component twice."""
mock_setup = Mock(return_value=True)
mock_integration(hass, MockModule("comp", setup=mock_setup))
assert await setup.async_setup_component(hass, "comp", {})
assert mock_setup.called
mock_setup.reset_mock()
assert await setup.async_setup_component(hass, "comp", {})
assert not mock_setup.called
async def test_component_not_installed_if_requirement_fails(
hass: HomeAssistant,
) -> None:
"""Component setup should fail if requirement can't install."""
hass.config.skip_pip = False
mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"]))
with patch("homeassistant.util.package.install_package", return_value=False):
assert not await setup.async_setup_component(hass, "comp", {})
assert "comp" not in hass.config.components
async def test_component_not_setup_twice_if_loaded_during_other_setup(
hass: HomeAssistant,
) -> None:
"""Test component setup while waiting for lock is not set up twice."""
result = []
async def async_setup(hass, config):
"""Tracking Setup."""
result.append(1)
mock_integration(hass, MockModule("comp", async_setup=async_setup))
def setup_component():
"""Set up the component."""
setup.setup_component(hass, "comp", {})
thread = threading.Thread(target=setup_component)
thread.start()
await setup.async_setup_component(hass, "comp", {})
await hass.async_add_executor_job(thread.join)
assert len(result) == 1
async def test_component_not_setup_missing_dependencies(hass: HomeAssistant) -> None:
"""Test we do not set up a component if not all dependencies loaded."""
deps = ["maybe_existing"]
mock_integration(hass, MockModule("comp", dependencies=deps))
assert not await setup.async_setup_component(hass, "comp", {})
assert "comp" not in hass.config.components
hass.data.pop(setup.DATA_SETUP)
mock_integration(hass, MockModule("comp2", dependencies=deps))
mock_integration(hass, MockModule("maybe_existing"))
assert await setup.async_setup_component(hass, "comp2", {})
async def test_component_failing_setup(hass: HomeAssistant) -> None:
"""Test component that fails setup."""
mock_integration(hass, MockModule("comp", setup=lambda hass, config: False))
assert not await setup.async_setup_component(hass, "comp", {})
assert "comp" not in hass.config.components
async def test_component_exception_setup(hass: HomeAssistant) -> None:
"""Test component that raises exception during setup."""
def exception_setup(hass, config):
"""Raise exception."""
raise Exception("fail!")
mock_integration(hass, MockModule("comp", setup=exception_setup))
assert not await setup.async_setup_component(hass, "comp", {})
assert "comp" not in hass.config.components
async def test_component_setup_with_validation_and_dependency(
hass: HomeAssistant,
) -> None:
"""Test all config is passed to dependencies."""
def config_check_setup(hass, config):
"""Test that config is passed in."""
if config.get("comp_a", {}).get("valid", False):
return True
raise Exception(f"Config not passed in: {config}")
platform = MockPlatform()
mock_integration(hass, MockModule("comp_a", setup=config_check_setup))
mock_integration(
hass,
MockModule("platform_a", setup=config_check_setup, dependencies=["comp_a"]),
)
mock_platform(hass, "platform_a.switch", platform)
await setup.async_setup_component(
hass,
"switch",
{"comp_a": {"valid": True}, "switch": {"platform": "platform_a"}},
)
await hass.async_block_till_done()
assert "comp_a" in hass.config.components
async def test_platform_specific_config_validation(hass: HomeAssistant) -> None:
"""Test platform that specifies config."""
platform_schema = PLATFORM_SCHEMA.extend({"valid": True}, extra=vol.PREVENT_EXTRA)
mock_setup = Mock(spec_set=True)
mock_platform(
hass,
"platform_a.switch",
MockPlatform(platform_schema=platform_schema, setup_platform=mock_setup),
)
with assert_setup_component(0, "switch"), patch(
"homeassistant.setup.async_notify_setup_error"
) as mock_notify:
assert await setup.async_setup_component(
hass,
"switch",
{"switch": {"platform": "platform_a", "invalid": True}},
)
await hass.async_block_till_done()
assert mock_setup.call_count == 0
assert len(mock_notify.mock_calls) == 1
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("switch")
with assert_setup_component(0), patch(
"homeassistant.setup.async_notify_setup_error"
) as mock_notify:
assert await setup.async_setup_component(
hass,
"switch",
{
"switch": {
"platform": "platform_a",
"valid": True,
"invalid_extra": True,
}
},
)
await hass.async_block_till_done()
assert mock_setup.call_count == 0
assert len(mock_notify.mock_calls) == 1
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("switch")
with assert_setup_component(1, "switch"), patch(
"homeassistant.setup.async_notify_setup_error"
) as mock_notify:
assert await setup.async_setup_component(
hass,
"switch",
{"switch": {"platform": "platform_a", "valid": True}},
)
await hass.async_block_till_done()
assert mock_setup.call_count == 1
assert len(mock_notify.mock_calls) == 0
async def test_disable_component_if_invalid_return(hass: HomeAssistant) -> None:
"""Test disabling component if invalid return."""
mock_integration(
hass, MockModule("disabled_component", setup=lambda hass, config: None)
)
assert not await setup.async_setup_component(hass, "disabled_component", {})
assert "disabled_component" not in hass.config.components
hass.data.pop(setup.DATA_SETUP)
mock_integration(
hass,
MockModule("disabled_component", setup=lambda hass, config: False),
)
assert not await setup.async_setup_component(hass, "disabled_component", {})
assert "disabled_component" not in hass.config.components
hass.data.pop(setup.DATA_SETUP)
mock_integration(
hass, MockModule("disabled_component", setup=lambda hass, config: True)
)
assert await setup.async_setup_component(hass, "disabled_component", {})
assert "disabled_component" in hass.config.components
async def test_all_work_done_before_start(hass: HomeAssistant) -> None:
"""Test all init work done till start."""
call_order = []
async def component1_setup(hass, config):
"""Set up mock component."""
await discovery.async_discover(
hass, "test_component2", {}, "test_component2", {}
)
await discovery.async_discover(
hass, "test_component3", {}, "test_component3", {}
)
return True
def component_track_setup(hass, config):
"""Set up mock component."""
call_order.append(1)
return True
mock_integration(hass, MockModule("test_component1", async_setup=component1_setup))
mock_integration(hass, MockModule("test_component2", setup=component_track_setup))
mock_integration(hass, MockModule("test_component3", setup=component_track_setup))
@callback
def track_start(event):
"""Track start event."""
call_order.append(2)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, track_start)
hass.add_job(setup.async_setup_component(hass, "test_component1", {}))
await hass.async_block_till_done()
await hass.async_start()
assert call_order == [1, 1, 2]
async def test_component_warn_slow_setup(hass: HomeAssistant) -> None:
"""Warn we log when a component setup takes a long time."""
mock_integration(hass, MockModule("test_component1"))
with patch.object(hass.loop, "call_later") as mock_call:
result = await setup.async_setup_component(hass, "test_component1", {})
assert result
assert mock_call.called
assert len(mock_call.mock_calls) == 3
timeout, logger_method = mock_call.mock_calls[0][1][:2]
assert timeout == setup.SLOW_SETUP_WARNING
assert logger_method == setup._LOGGER.warning
assert mock_call().cancel.called
async def test_platform_no_warn_slow(hass: HomeAssistant) -> None:
"""Do not warn for long entity setup time."""
mock_integration(
hass, MockModule("test_component1", platform_schema=PLATFORM_SCHEMA)
)
with patch.object(hass.loop, "call_later") as mock_call:
result = await setup.async_setup_component(hass, "test_component1", {})
assert result
assert len(mock_call.mock_calls) == 0
async def test_platform_error_slow_setup(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Don't block startup more than SLOW_SETUP_MAX_WAIT."""
with patch.object(setup, "SLOW_SETUP_MAX_WAIT", 0.1):
called = []
async def async_setup(*args):
"""Tracking Setup."""
called.append(1)
await asyncio.sleep(2)
mock_integration(hass, MockModule("test_component1", async_setup=async_setup))
result = await setup.async_setup_component(hass, "test_component1", {})
assert len(called) == 1
assert not result
assert "'test_component1' is taking longer than 0.1 seconds" in caplog.text
async def test_when_setup_already_loaded(hass: HomeAssistant) -> None:
"""Test when setup."""
calls = []
async def mock_callback(hass, component):
"""Mock callback."""
calls.append(component)
setup.async_when_setup(hass, "test", mock_callback)
await hass.async_block_till_done()
assert calls == []
hass.config.components.add("test")
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "test"})
await hass.async_block_till_done()
assert calls == ["test"]
# Event listener should be gone
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "test"})
await hass.async_block_till_done()
assert calls == ["test"]
# Should be called right away
setup.async_when_setup(hass, "test", mock_callback)
await hass.async_block_till_done()
assert calls == ["test", "test"]
async def test_async_when_setup_or_start_already_loaded(hass: HomeAssistant) -> None:
"""Test when setup or start."""
calls = []
async def mock_callback(hass, component):
"""Mock callback."""
calls.append(component)
setup.async_when_setup_or_start(hass, "test", mock_callback)
await hass.async_block_till_done()
assert calls == []
hass.config.components.add("test")
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "test"})
await hass.async_block_till_done()
assert calls == ["test"]
# Event listener should be gone
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "test"})
await hass.async_block_till_done()
assert calls == ["test"]
# Should be called right away
setup.async_when_setup_or_start(hass, "test", mock_callback)
await hass.async_block_till_done()
assert calls == ["test", "test"]
setup.async_when_setup_or_start(hass, "not_loaded", mock_callback)
await hass.async_block_till_done()
assert calls == ["test", "test"]
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert calls == ["test", "test", "not_loaded"]
async def test_setup_import_blows_up(hass: HomeAssistant) -> None:
"""Test that we handle it correctly when importing integration blows up."""
with patch(
"homeassistant.loader.Integration.get_component", side_effect=ImportError
):
assert not await setup.async_setup_component(hass, "sun", {})
async def test_parallel_entry_setup(hass: HomeAssistant, mock_handlers) -> None:
"""Test config entries are set up in parallel."""
MockConfigEntry(domain="comp", data={"value": 1}).add_to_hass(hass)
MockConfigEntry(domain="comp", data={"value": 2}).add_to_hass(hass)
calls = []
async def mock_async_setup_entry(hass, entry):
"""Mock setting up an entry."""
calls.append(entry.data["value"])
await asyncio.sleep(0)
calls.append(entry.data["value"])
return True
mock_integration(
hass,
MockModule(
"comp",
async_setup_entry=mock_async_setup_entry,
),
)
mock_platform(hass, "comp.config_flow", None)
await setup.async_setup_component(hass, "comp", {})
assert calls == [1, 2, 1, 2]
async def test_integration_disabled(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test we can disable an integration."""
disabled_reason = "Dependency contains code that breaks Home Assistant"
mock_integration(
hass,
MockModule("test_component1", partial_manifest={"disabled": disabled_reason}),
)
result = await setup.async_setup_component(hass, "test_component1", {})
assert not result
assert disabled_reason in caplog.text
async def test_integration_logs_is_custom(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test we highlight it's a custom component when errors happen."""
mock_integration(
hass,
MockModule("test_component1"),
built_in=False,
)
with patch(
"homeassistant.setup.async_process_deps_reqs",
side_effect=HomeAssistantError("Boom"),
):
result = await setup.async_setup_component(hass, "test_component1", {})
assert not result
assert "Setup failed for custom integration 'test_component1': Boom" in caplog.text
async def test_async_get_loaded_integrations(hass: HomeAssistant) -> None:
"""Test we can enumerate loaded integrations."""
hass.config.components.add("notbase")
hass.config.components.add("switch")
hass.config.components.add("notbase.switch")
hass.config.components.add("myintegration")
hass.config.components.add("device_tracker")
hass.config.components.add("other.device_tracker")
hass.config.components.add("myintegration.light")
assert setup.async_get_loaded_integrations(hass) == {
"other",
"switch",
"notbase",
"myintegration",
"device_tracker",
}
async def test_integration_no_setup(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test we fail integration setup without setup functions."""
mock_integration(
hass,
MockModule("test_integration_without_setup", setup=False),
)
result = await setup.async_setup_component(
hass, "test_integration_without_setup", {}
)
assert not result
assert "No setup or config entry setup function defined" in caplog.text
async def test_integration_only_setup_entry(hass: HomeAssistant) -> None:
"""Test we have an integration with only a setup entry method."""
mock_integration(
hass,
MockModule(
"test_integration_only_entry",
setup=False,
async_setup_entry=AsyncMock(return_value=True),
),
)
assert await setup.async_setup_component(hass, "test_integration_only_entry", {})
async def test_async_start_setup(hass: HomeAssistant) -> None:
"""Test setup started context manager keeps track of setup times."""
with setup.async_start_setup(hass, ["august"]):
assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august"], float)
with setup.async_start_setup(hass, ["august"]):
assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august_2"], float)
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], float)
assert "august_2" not in hass.data[setup.DATA_SETUP_TIME]
async def test_async_start_setup_platforms(hass: HomeAssistant) -> None:
"""Test setup started context manager keeps track of setup times for platforms."""
with setup.async_start_setup(hass, ["august.sensor"]):
assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august.sensor"], float)
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], float)
assert "sensor" not in hass.data[setup.DATA_SETUP_TIME]
async def test_setup_config_entry_from_yaml(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test attempting to setup an integration which only supports config_entries."""
expected_warning = (
"The 'test_integration_only_entry' integration does not support YAML setup, "
"please remove it from your configuration"
)
mock_integration(
hass,
MockModule(
"test_integration_only_entry",
setup=False,
async_setup_entry=AsyncMock(return_value=True),
),
)
assert await setup.async_setup_component(hass, "test_integration_only_entry", {})
assert expected_warning not in caplog.text
caplog.clear()
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("test_integration_only_entry")
# There should be a warning, but setup should not fail
assert await setup.async_setup_component(
hass, "test_integration_only_entry", {"test_integration_only_entry": None}
)
assert expected_warning in caplog.text
caplog.clear()
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("test_integration_only_entry")
# There should be a warning, but setup should not fail
assert await setup.async_setup_component(
hass, "test_integration_only_entry", {"test_integration_only_entry": {}}
)
assert expected_warning in caplog.text
caplog.clear()
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("test_integration_only_entry")
# There should be a warning, but setup should not fail
assert await setup.async_setup_component(
hass,
"test_integration_only_entry",
{"test_integration_only_entry": {"hello": "world"}},
)
assert expected_warning in caplog.text
caplog.clear()
hass.data.pop(setup.DATA_SETUP)
hass.config.components.remove("test_integration_only_entry")