2023-07-19 09:10:21 +02:00
|
|
|
"""Tests for Adaptive Lighting HASS utils."""
|
|
|
|
|
|
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
|
|
Group 7: tests — 94 passing in 0.57s (53/53 + 9.4 deferred)
Test infrastructure:
- Add `pytest-homeassistant-custom-component` (PHACC) as a `test`
dependency group. PHACC ships `hass`, `enable_custom_integrations`,
`MockConfigEntry`, and friends without needing to clone HA core as a
sibling directory. Modernizes the test setup from upstream's
`setup-symlinks` pattern.
- conftest.py: add `auto_enable_custom_integrations` autouse fixture
so HA discovers the integration under `custom_components/` during
tests. Keep the upstream template-deprecation no-op.
- Import paths: all tests now import from `custom_components.adaptive_lighting`
(not `homeassistant.components.adaptive_lighting`) and from
`pytest_homeassistant_custom_component.common` (not `tests.common`).
New tests (groups 7.2-7.16):
- tests/test_color_and_brightness.py (16 tests): TestBrightnessCurve
asserts min-before-sunrise, midpoint-at-event, max-during-day, sunset
ramp symmetry. TestColorTempCurve verifies the same shape applies to
K. TestSunPosition checks the synthetic +1/-1/0 derivation.
TestTanhDayCurveDirect exercises the helper directly.
- tests/test_config_flow.py (12 tests): six sections in order, each
section contains only its specified fields, conditional visibility
of send_split_delay, default sun entities, strict-typed entity
selectors, NumberSelector slider/box configs, BooleanSelector for
every flag, full user→create-entry flow, YAML-managed entry aborts
with `yaml_managed` reason, options flow renders the sectioned schema.
- tests/test_init.py (6 tests): successful setup on current version,
stale version raises ConfigEntryError via async_migrate_entry,
unload is clean, tombstone removes orphan sleep entity + logs INFO,
tombstone is idempotent, tombstone respects config_entry ownership.
Source modernizations driven by the tests:
- __init__.py: add `async_migrate_entry` that surfaces the
"incompatible — delete and recreate" message and sets the entry to
MIGRATION_ERROR. Removed direct `ConfigEntryError` from
`async_setup_entry` (HA routes version mismatches through the
migration handler now).
- switch.py: removed upstream's YAML-managed-entry auto-remove hack.
YAML profiles now load normally and the options flow handles the
"you must edit configuration.yaml" message (spec R7).
Existing tests updated to PHACC paths: test_adaptation_utils.py
(38 tests), test_hass_utils.py (22 tests). Both pass without
modification beyond the import fix.
Deleted: tests/test_switch.py (2,999 LOC of upstream tests, most
covering sleep mode / take-over-control / manual-control state
machines that no longer exist; CDiT-specific switch tests deferred
to a follow-up change).
Result: `uv run --group test pytest tests/` → 94 passed in 0.57s.
Deferred from this change:
- 4.3: manual UI test that toggling a field and saving reloads cleanly
(requires a real HA instance, can't be done from CLI).
- 9.4: GitHub repo description / topics update (do via `gh repo edit`
outside the change scope).
openspec status: 4/4 artifacts complete; strict-validate green.
2026-05-16 15:11:49 +02:00
|
|
|
from custom_components.adaptive_lighting.adaptation_utils import ServiceData
|
|
|
|
|
from custom_components.adaptive_lighting.hass_utils import (
|
2024-04-06 10:14:54 +02:00
|
|
|
setup_service_call_interceptor,
|
|
|
|
|
)
|
2023-07-19 09:10:21 +02:00
|
|
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
|
|
|
|
from homeassistant.const import SERVICE_TURN_ON
|
|
|
|
|
from homeassistant.core import ServiceCall
|
|
|
|
|
from homeassistant.util.read_only_dict import ReadOnlyDict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_setup_service_call_interceptor(hass):
|
|
|
|
|
"""Test setup and removal of service call interceptor."""
|
|
|
|
|
service_func_mock = AsyncMock()
|
|
|
|
|
hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock)
|
|
|
|
|
|
|
|
|
|
async def service_call():
|
|
|
|
|
await hass.services.async_call(
|
|
|
|
|
LIGHT_DOMAIN,
|
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
|
{},
|
|
|
|
|
blocking=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Test if service is called
|
|
|
|
|
|
|
|
|
|
await service_call()
|
|
|
|
|
assert service_func_mock.call_count == 1
|
|
|
|
|
|
|
|
|
|
# Test if interceptor is called after setup
|
|
|
|
|
|
|
|
|
|
intercept_func_mock = AsyncMock()
|
|
|
|
|
remove_interceptor = setup_service_call_interceptor(
|
|
|
|
|
hass,
|
|
|
|
|
LIGHT_DOMAIN,
|
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
|
intercept_func_mock,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await service_call()
|
|
|
|
|
assert service_func_mock.call_count == 2
|
|
|
|
|
assert intercept_func_mock.call_count == 1
|
|
|
|
|
|
|
|
|
|
# Test if interceptor is no longer called after removal
|
|
|
|
|
|
|
|
|
|
remove_interceptor()
|
|
|
|
|
await service_call()
|
|
|
|
|
assert service_func_mock.call_count == 3
|
|
|
|
|
assert intercept_func_mock.call_count == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_service_call_interceptor_data_manipulation(hass):
|
|
|
|
|
"""Test service call data manipulation by service call interceptor."""
|
|
|
|
|
service_func_mock = AsyncMock()
|
|
|
|
|
hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock)
|
|
|
|
|
|
|
|
|
|
async def intercept_func(call: ServiceCall, data: ServiceData):
|
|
|
|
|
data["test1"] = "changed"
|
|
|
|
|
data["test2"] = "added"
|
|
|
|
|
|
|
|
|
|
setup_service_call_interceptor(
|
|
|
|
|
hass,
|
|
|
|
|
LIGHT_DOMAIN,
|
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
|
intercept_func,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await hass.services.async_call(
|
|
|
|
|
LIGHT_DOMAIN,
|
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
|
{"test1": "initial"},
|
|
|
|
|
blocking=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
(service_call,) = service_func_mock.call_args[0]
|
|
|
|
|
assert service_call.data == {"test1": "changed", "test2": "added"}
|
|
|
|
|
assert isinstance(service_call.data, ReadOnlyDict)
|