feat: update-Entitaet fuer die Firmware und das Testgeruest
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.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Ein echter lokaler aiohttp-Server spielt das Geraet; kein Home Assistant im Spiel."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestServer
|
||||
import pytest
|
||||
|
||||
|
||||
@dataclass
|
||||
class Call:
|
||||
"""Eine Anfrage, die das gespielte Geraet bekommen hat."""
|
||||
|
||||
method: str
|
||||
path: str
|
||||
headers: dict[str, str]
|
||||
body: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeStrip:
|
||||
"""Vorgegebene Antworten je (METHODE, Pfad); merkt sich jeden Aufruf."""
|
||||
|
||||
host: str
|
||||
port: int
|
||||
routes: dict[tuple[str, str], tuple[int, str, float]] = field(default_factory=dict)
|
||||
calls: list[Call] = field(default_factory=list)
|
||||
|
||||
def reply(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
status: int = 200,
|
||||
payload: Any = None,
|
||||
*,
|
||||
raw: str | None = None,
|
||||
delay: float = 0.0,
|
||||
) -> None:
|
||||
body = raw if raw is not None else json.dumps(payload)
|
||||
self.routes[(method.upper(), path)] = (status, body, delay)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _enable_sockets(socket_enabled: None) -> None:
|
||||
"""pytest-homeassistant-custom-component sperrt Sockets; hier wird localhost gebraucht."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def strip(aiohttp_server: Callable[..., Awaitable[TestServer]]) -> FakeStrip:
|
||||
holder: list[FakeStrip] = []
|
||||
|
||||
async def handler(request: web.Request) -> web.Response:
|
||||
fake = holder[0]
|
||||
text = await request.text()
|
||||
try:
|
||||
body: Any = json.loads(text) if text else None
|
||||
except ValueError:
|
||||
body = text
|
||||
fake.calls.append(Call(request.method, request.path, dict(request.headers), body))
|
||||
route = fake.routes.get((request.method, request.path))
|
||||
if route is None:
|
||||
return web.Response(status=404, text="no route")
|
||||
status, text_out, delay = route
|
||||
if delay:
|
||||
await asyncio.sleep(delay)
|
||||
return web.Response(status=status, text=text_out)
|
||||
|
||||
app = web.Application()
|
||||
app.router.add_route("*", "/{tail:.*}", handler)
|
||||
server = await aiohttp_server(app)
|
||||
fake = FakeStrip(server.host, server.port)
|
||||
holder.append(fake)
|
||||
return fake
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def session() -> AsyncGenerator[aiohttp.ClientSession]:
|
||||
async with aiohttp.ClientSession() as client_session:
|
||||
yield client_session
|
||||
Reference in New Issue
Block a user