- session.py: новый лог-памп не поднимается, пока предыдущий не завершился (stopping/is_alive), параллельное чтение кольца исключено - __init__: счётчик pending закрывает окно гонки setup/unload последней записи; warning при не-словарном fglair: в YAML - trial: properties_missing — отдельная ошибка формы/abort (en/ru/strings) - ConfigEntryNotReady: «порт 0» больше не показывается - тесты: контраст одинакового явного порта, неблокирующий unload (тикер loop) - план H5: публикация pyfglair именно на PyPI (manylinux x86_64/aarch64)
120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
"""Setup/unload ConfigEntry: клиент стартует, останавливается, ошибки порта."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import socket
|
|
|
|
from helpers import free_port
|
|
from homeassistant.core import HomeAssistant
|
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
from custom_components.fglair.const import CONF_LISTEN_PORT, DOMAIN
|
|
from pyfglair import Prop, State
|
|
|
|
|
|
async def test_setup_unload(hass: HomeAssistant, config_entry, socket_enabled):
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
client = config_entry.runtime_data
|
|
assert client is not None
|
|
assert isinstance(client.state, State)
|
|
|
|
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
assert client.cached(Prop.OPERATION_MODE) is None
|
|
|
|
|
|
async def test_two_entries_default_port(hass: HomeAssistant, entry_data, socket_enabled):
|
|
"""Две записи без явного listen_port не конфликтуют (порт 0 → эфемерный)."""
|
|
first = MockConfigEntry(domain=DOMAIN, data=entry_data, unique_id="DSN-1")
|
|
second = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={**entry_data, "dsn": "DSN-2"},
|
|
unique_id="DSN-2",
|
|
)
|
|
first.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(first.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
second.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(second.entry_id)
|
|
await hass.async_block_till_done()
|
|
assert first.state.name == "LOADED"
|
|
assert second.state.name == "LOADED"
|
|
|
|
first_client = first.runtime_data
|
|
assert await hass.config_entries.async_unload(first.entry_id)
|
|
assert first_client.cached(Prop.OPERATION_MODE) is None
|
|
assert second.state.name == "LOADED"
|
|
assert await hass.config_entries.async_unload(second.entry_id)
|
|
|
|
|
|
async def test_two_entries_same_explicit_port(
|
|
hass: HomeAssistant, entry_data, socket_enabled
|
|
):
|
|
"""Явно заданный один порт на две записи — вторая SETUP_RETRY."""
|
|
port = free_port()
|
|
first = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={**entry_data, CONF_LISTEN_PORT: port},
|
|
unique_id="DSN-1",
|
|
)
|
|
first.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(first.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
second = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={**entry_data, "dsn": "DSN-2", CONF_LISTEN_PORT: port},
|
|
unique_id="DSN-2",
|
|
)
|
|
second.add_to_hass(hass)
|
|
assert not await hass.config_entries.async_setup(second.entry_id)
|
|
await hass.async_block_till_done()
|
|
assert second.state.name == "SETUP_RETRY"
|
|
assert await hass.config_entries.async_unload(first.entry_id)
|
|
|
|
|
|
async def test_unload_does_not_block_loop(
|
|
hass: HomeAssistant, config_entry, socket_enabled
|
|
):
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
ticks = 0
|
|
|
|
async def ticker() -> None:
|
|
nonlocal ticks
|
|
while True:
|
|
await asyncio.sleep(0.01)
|
|
ticks += 1
|
|
|
|
task = asyncio.create_task(ticker())
|
|
await asyncio.sleep(0.05)
|
|
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
task.cancel()
|
|
assert ticks >= 3
|
|
|
|
|
|
async def test_setup_port_busy(hass: HomeAssistant, entry_data, socket_enabled):
|
|
port = free_port()
|
|
sock = socket.socket()
|
|
sock.bind(("127.0.0.1", port))
|
|
sock.listen(1)
|
|
try:
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={**entry_data, CONF_LISTEN_PORT: port},
|
|
unique_id=entry_data["dsn"],
|
|
)
|
|
entry.add_to_hass(hass)
|
|
assert not await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
assert entry.state.name == "SETUP_RETRY"
|
|
finally:
|
|
sock.close()
|