Aufgebaut wie ha-ninlight: die Abbilder liegen unter ninstrip_firmware im Konfigurationsverzeichnis, Home Assistant liefert sie selbst unter einer einmaligen Adresse aus, und zwar genau die Bytes, die vorher geprueft wurden. Dazu firmware.py (Anhaenger, Versionsreihenfolge, WLAN-Pruefung, Katalog), http.py, urls.py und shared.py. Drei Unterschiede zu ninlight, alle aus der Firmware heraus: * Verglichen wird device_type - die Platine -, nicht product. Letzteres ist bei allen Platinen 'ninstrip'; die Abbilder unterscheiden sich aber hart, die K1Y invertiert das Datensignal und die KL-RGBC3 nicht. Firmware ohne dieses Feld bekommt gar kein Angebot. * Die WLAN-Pruefung sucht die SSID unverpackt: ein ESP32-Abbild ist nicht gzip-verpackt wie das des W600. An einem mit sdkconfig.local gebauten Abbild gegengeprueft - die SSID steht genau einmal NUL-gerahmt drin. * Der Fehlerschluessel heisst device_url_too_long statt lamp_url_too_long. Dazu das Testgeruest, das es hier bisher gar nicht gab: 204 Tests in tests_unit/ (ohne HA-Instanz) und tests/ (mit), Abdeckung 97 %, dazu scripts/check.sh als Ersatz fuer die fehlende CI. Die Nutzdaten in tests_unit/samples.py sind an der Stehlampe abgefragt. Beim Gegenpruefen per Mutation fiel auf, dass die Platinenpruefung unmittelbar vor dem Senden ungeprueft war - der Test dazu ist jetzt da und faellt, wenn man die Pruefung entfernt.
132 lines
4.0 KiB
Python
132 lines
4.0 KiB
Python
"""Fixtures fuer die Tests, die gegen eine Home-Assistant-Instanz laufen."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Generator
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
|
from homeassistant.core import HomeAssistant
|
|
import pytest
|
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
from custom_components.ninstrip.api import NinstripClient
|
|
from custom_components.ninstrip.const import DOMAIN
|
|
from tests_unit.samples import (
|
|
HOST_K1Y,
|
|
INFO_K1Y,
|
|
MAC_K1Y,
|
|
PRESETS_K1Y,
|
|
SETTINGS_COMPILED,
|
|
STATE_K1Y,
|
|
TOKEN,
|
|
)
|
|
|
|
CLIENT_PATCH_TARGETS = [
|
|
"custom_components.ninstrip.NinstripClient",
|
|
"custom_components.ninstrip.config_flow.NinstripClient",
|
|
]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def auto_enable_custom_integrations(enable_custom_integrations: None) -> Generator[None]:
|
|
"""custom_components/ in jedem Home-Assistant-Test laden."""
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def strip_state() -> dict[str, Any]:
|
|
return deepcopy(STATE_K1Y)
|
|
|
|
|
|
@pytest.fixture
|
|
def strip_info() -> dict[str, Any]:
|
|
return deepcopy(INFO_K1Y)
|
|
|
|
|
|
@pytest.fixture
|
|
def strip_settings() -> dict[str, Any]:
|
|
return deepcopy(SETTINGS_COMPILED)
|
|
|
|
|
|
@pytest.fixture
|
|
def strip_presets() -> dict[str, Any]:
|
|
return deepcopy(PRESETS_K1Y)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_client(
|
|
strip_state: dict[str, Any],
|
|
strip_info: dict[str, Any],
|
|
strip_settings: dict[str, Any],
|
|
strip_presets: dict[str, Any],
|
|
) -> Generator[MagicMock]:
|
|
"""Ein Geraet, dessen Antworten den veraenderbaren strip_*-Fixtures folgen."""
|
|
client = MagicMock(spec=NinstripClient)
|
|
client.host = HOST_K1Y
|
|
client.get_state = AsyncMock(side_effect=lambda: deepcopy(strip_state))
|
|
client.get_info = AsyncMock(side_effect=lambda: deepcopy(strip_info))
|
|
client.get_settings = AsyncMock(side_effect=lambda: deepcopy(strip_settings))
|
|
client.get_presets = AsyncMock(side_effect=lambda: deepcopy(strip_presets))
|
|
|
|
async def set_state(**fields: Any) -> dict[str, Any]:
|
|
strip_state.update(fields)
|
|
return deepcopy(strip_state)
|
|
|
|
client.set_state = AsyncMock(side_effect=set_state)
|
|
|
|
async def set_sacn(**fields: Any) -> None:
|
|
# Wie das Geraet: es merkt sich, was es annimmt, und der Aufrufer
|
|
# liest den Block danach zurueck.
|
|
strip_settings.setdefault("sacn", {}).update(fields)
|
|
|
|
client.set_sacn = AsyncMock(side_effect=set_sacn)
|
|
client.apply_preset = AsyncMock(return_value=None)
|
|
client.save_preset = AsyncMock(return_value=None)
|
|
client.reboot = AsyncMock(return_value=None)
|
|
client.fw_update = AsyncMock(return_value=None)
|
|
patches = [patch(target, return_value=client) for target in CLIENT_PATCH_TARGETS]
|
|
for p in patches:
|
|
p.start()
|
|
yield client
|
|
for p in patches:
|
|
p.stop()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config_entry() -> MockConfigEntry:
|
|
return MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="ninstrip ad4c",
|
|
unique_id=MAC_K1Y,
|
|
data={CONF_HOST: HOST_K1Y, CONF_TOKEN: TOKEN},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def init_integration(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_client: MagicMock
|
|
) -> MockConfigEntry:
|
|
mock_config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
return mock_config_entry
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
|
"""Tests des Einrichtungsablaufs duerfen den Eintrag nicht wirklich aufsetzen."""
|
|
with patch("custom_components.ninstrip.async_setup_entry", return_value=True) as mock:
|
|
yield mock
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def firmware_dir(tmp_path: Path) -> Generator[Path]:
|
|
"""Jeder Test bekommt seinen eigenen, anfangs leeren Firmware-Ordner."""
|
|
verzeichnis = tmp_path / "ninstrip_firmware"
|
|
with patch("custom_components.ninstrip.shared.firmware_directory", return_value=verzeichnis):
|
|
yield verzeichnis
|