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,252 @@
|
||||
"""Anhaenger, Versionsreihenfolge, WLAN-Pruefung und der Abbild-Katalog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from custom_components.ninstrip import firmware
|
||||
from custom_components.ninstrip.firmware import (
|
||||
FirmwareCatalog,
|
||||
FirmwareVersion,
|
||||
InvalidImageError,
|
||||
contains_ssid,
|
||||
is_newer,
|
||||
parse_trailer,
|
||||
parse_version,
|
||||
)
|
||||
from tests_unit.images import CONTENT_WITH_SSID, CONTENT_WITHOUT_SSID, build_image
|
||||
|
||||
# Streng aufsteigend. Geprueft werden alle Nachbarpaare und alle Paare ueber
|
||||
# die ganze Liste.
|
||||
ORDER = [
|
||||
"0.1.0",
|
||||
"0.1.0-36-gca9bb",
|
||||
"0.1.0-47-g71a57",
|
||||
"0.1.1-rc1",
|
||||
"0.1.1-rc1-4-gab12",
|
||||
"0.1.1-rc2",
|
||||
"0.1.1",
|
||||
"0.2.0-rc1",
|
||||
"0.10.0",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("aelter", "neuer"), list(zip(ORDER, ORDER[1:], strict=False))) # noqa: RUF007
|
||||
def test_nachbarversionen(aelter: str, neuer: str) -> None:
|
||||
assert is_newer(neuer, aelter)
|
||||
assert not is_newer(aelter, neuer)
|
||||
|
||||
|
||||
def test_reihenfolge_gilt_ueber_die_ganze_liste() -> None:
|
||||
for i, aelter in enumerate(ORDER):
|
||||
for neuer in ORDER[i + 1 :]:
|
||||
assert is_newer(neuer, aelter), (neuer, aelter)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "erwartet"),
|
||||
[
|
||||
("0.1.0-36-gca9bb", FirmwareVersion((0, 1, 0), None, 36)),
|
||||
("0.1.0-38-gd3cc9", FirmwareVersion((0, 1, 0), None, 38)), # genau 15 Zeichen, mit Hash
|
||||
("0.1.0-36-g", FirmwareVersion((0, 1, 0), None, 36)), # Hash weggekuerzt
|
||||
("0.1.0-36", FirmwareVersion((0, 1, 0), None, 36)),
|
||||
("0.1.0-3-gab-dir", FirmwareVersion((0, 1, 0), None, 3)), # "-dirty" auf "-dir" gekuerzt
|
||||
("0.1.0-dirty", FirmwareVersion((0, 1, 0), None, 0)),
|
||||
("0.2.0-rc1-12-ga", FirmwareVersion((0, 2, 0), 1, 12)), # 15 Zeichen, Hash auf ein Zeichen
|
||||
("0.1.1-rc2", FirmwareVersion((0, 1, 1), 2, 0)),
|
||||
],
|
||||
)
|
||||
def test_version_lesen(text: str, erwartet: FirmwareVersion) -> None:
|
||||
assert parse_version(text) == erwartet
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"",
|
||||
# Genau das meldete die Firmware vor dem Tag v0.1.0: git describe
|
||||
# --always ohne Tags. Daran laesst sich keine Reihenfolge ablesen.
|
||||
"652b2e2-dirty",
|
||||
"v0.1.0", # das fuehrende "v" schneidet cmake/version.cmake ab
|
||||
"0.1",
|
||||
"0.1.0-",
|
||||
"0.10.10-rc10-12", # 15 Zeichen, endet auf Ziffern ohne "-g": Zahl kann gekuerzt sein
|
||||
"0.1.0-36-gXYZ",
|
||||
"0.1.0 ",
|
||||
],
|
||||
)
|
||||
def test_unlesbare_versionen(text: str) -> None:
|
||||
assert parse_version(text) is None
|
||||
|
||||
|
||||
def test_gleiche_commitzahl_mit_anderem_hash_ist_nicht_neuer() -> None:
|
||||
assert not is_newer("0.1.0-36-gffff0", "0.1.0-36-gca9bb")
|
||||
assert not is_newer("0.1.0-36-gca9bb", "0.1.0-36-gffff0")
|
||||
|
||||
|
||||
def test_eine_unlesbare_seite_ist_nie_neuer() -> None:
|
||||
assert not is_newer("652b2e2", "0.1.0")
|
||||
assert not is_newer("0.1.1", "652b2e2")
|
||||
|
||||
|
||||
def test_anhaenger_liest_nul_aufgefuellte_felder() -> None:
|
||||
assert parse_trailer(build_image("ninstrip-rgbc3", "0.1.0-38-gd3cc9")) == (
|
||||
"ninstrip-rgbc3",
|
||||
"0.1.0-38-gd3cc9",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("data", "grund"),
|
||||
[
|
||||
(b"\xff" * 512, "kurz"),
|
||||
(build_image(magic=b"NINX"), "NINL"),
|
||||
(build_image(fmt=2), "format"),
|
||||
(build_image(corrupt_hash=True), "sha256"),
|
||||
],
|
||||
)
|
||||
def test_ungueltige_abbilder(data: bytes, grund: str) -> None:
|
||||
with pytest.raises(InvalidImageError, match=grund):
|
||||
parse_trailer(data)
|
||||
|
||||
|
||||
def test_ssid_wird_nul_gerahmt_gefunden() -> None:
|
||||
assert contains_ssid(build_image(payload=CONTENT_WITH_SSID), "ninIOT")
|
||||
|
||||
|
||||
def test_ssid_fehlt_im_abbild_ohne_zugangsdaten() -> None:
|
||||
assert not contains_ssid(build_image(payload=CONTENT_WITHOUT_SSID), "ninIOT")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", [b"\x00xninIOT\x00", b"\x00ninIOTx\x00", b"ninIOT"])
|
||||
def test_ssid_muss_nul_gerahmt_sein(payload: bytes) -> None:
|
||||
assert not contains_ssid(build_image(payload=payload), "ninIOT")
|
||||
|
||||
|
||||
def test_eine_andere_ssid_passt_nicht() -> None:
|
||||
assert not contains_ssid(build_image(payload=CONTENT_WITH_SSID), "ninIOT-gast")
|
||||
|
||||
|
||||
def test_leere_ssid_passt_nie() -> None:
|
||||
assert not contains_ssid(build_image(payload=b"\x00\x00\x00"), "")
|
||||
|
||||
|
||||
def test_die_ssid_im_anhaenger_zaehlt_nicht() -> None:
|
||||
"""Sonst koennte der Anhaenger selbst die WLAN-Pruefung bestehen lassen."""
|
||||
abbild = build_image(payload=CONTENT_WITHOUT_SSID)
|
||||
manipuliert = bytearray(abbild)
|
||||
manipuliert[-200:-190] = b"\x00ninIOT\x00\x00\x00"
|
||||
assert not contains_ssid(bytes(manipuliert), "ninIOT")
|
||||
|
||||
|
||||
def _schreiben(verzeichnis: Path, name: str, data: bytes) -> Path:
|
||||
pfad = verzeichnis / name
|
||||
pfad.write_bytes(data)
|
||||
return pfad
|
||||
|
||||
|
||||
def test_durchlauf_legt_das_verzeichnis_an_und_nimmt_nur_gueltiges(tmp_path: Path) -> None:
|
||||
verzeichnis = tmp_path / "ninstrip_firmware"
|
||||
katalog = FirmwareCatalog(verzeichnis)
|
||||
assert katalog.scan() == ()
|
||||
assert verzeichnis.is_dir()
|
||||
_schreiben(verzeichnis, "a.img", build_image("ninstrip-k1y", "0.1.0-47-g71a57"))
|
||||
_schreiben(verzeichnis, "kaputt.img", build_image(corrupt_hash=True))
|
||||
_schreiben(verzeichnis, "notizen.txt", build_image())
|
||||
abbilder = katalog.scan()
|
||||
assert [(i.path.name, i.device_type, i.version) for i in abbilder] == [
|
||||
("a.img", "ninstrip-k1y", "0.1.0-47-g71a57")
|
||||
]
|
||||
assert katalog.images == abbilder
|
||||
|
||||
|
||||
def test_ungueltige_datei_wird_einmal_gemeldet(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
katalog = FirmwareCatalog(tmp_path)
|
||||
_schreiben(tmp_path, "kaputt.img", build_image(corrupt_hash=True))
|
||||
katalog.scan()
|
||||
katalog.scan()
|
||||
assert sum("kaputt.img" in r.getMessage() for r in caplog.records) == 1
|
||||
|
||||
|
||||
def test_erneuter_durchlauf_sieht_neue_und_geloeschte_dateien(tmp_path: Path) -> None:
|
||||
katalog = FirmwareCatalog(tmp_path)
|
||||
alt = _schreiben(tmp_path, "a.img", build_image(version="0.1.0-47-g71a57"))
|
||||
katalog.scan()
|
||||
_schreiben(tmp_path, "b.img", build_image(version="0.1.1-rc1"))
|
||||
alt.unlink()
|
||||
assert [i.version for i in katalog.scan()] == ["0.1.1-rc1"]
|
||||
|
||||
|
||||
def test_neuere_abbilder_nur_exakt_dieser_platine_neuestes_zuerst(tmp_path: Path) -> None:
|
||||
katalog = FirmwareCatalog(tmp_path)
|
||||
_schreiben(tmp_path, "1.img", build_image("ninstrip-k1y", "0.1.0-47-g71a57"))
|
||||
_schreiben(tmp_path, "2.img", build_image("ninstrip-k1y", "0.1.1-rc1"))
|
||||
# Neuer, aber fuer die andere Platine: die KL-RGBC3 invertiert das
|
||||
# Datensignal nicht, die K1Y schon.
|
||||
_schreiben(tmp_path, "3.img", build_image("ninstrip-rgbc3", "0.2.0"))
|
||||
_schreiben(tmp_path, "4.img", build_image("ninstrip", "0.5.0")) # Praefix des Typs
|
||||
_schreiben(tmp_path, "5.img", build_image("ninstrip-k1y", "0.1.0-36-gffff0")) # gleich alt
|
||||
_schreiben(tmp_path, "6.img", build_image("ninstrip-k1y", "0.1.0")) # aelter
|
||||
katalog.scan()
|
||||
neuer = katalog.newer_images("ninstrip-k1y", "0.1.0-36-gca9bb")
|
||||
assert [(i.device_type, i.version) for i in neuer] == [
|
||||
("ninstrip-k1y", "0.1.1-rc1"),
|
||||
("ninstrip-k1y", "0.1.0-47-g71a57"),
|
||||
]
|
||||
|
||||
|
||||
def test_verify_and_read_liefert_genau_die_geprueften_bytes(tmp_path: Path) -> None:
|
||||
katalog = FirmwareCatalog(tmp_path)
|
||||
daten = build_image(version="0.1.0-47-g71a57")
|
||||
pfad = _schreiben(tmp_path, "a.img", daten)
|
||||
(abbild,) = katalog.scan()
|
||||
assert katalog.verify_and_read(abbild) == daten
|
||||
pfad.write_bytes(build_image(version="0.1.1-rc1"))
|
||||
assert katalog.verify_and_read(abbild) is None
|
||||
pfad.unlink()
|
||||
assert katalog.verify_and_read(abbild) is None
|
||||
|
||||
|
||||
def test_verify_and_read_weist_gleich_grosse_aber_andere_datei_ab(tmp_path: Path) -> None:
|
||||
"""Gleiche Groesse und gleiche mtime, anderer Inhalt: der Anhaenger entscheidet."""
|
||||
katalog = FirmwareCatalog(tmp_path)
|
||||
pfad = _schreiben(tmp_path, "a.img", build_image("ninstrip-k1y", "0.1.0-47-g71a57"))
|
||||
(abbild,) = katalog.scan()
|
||||
stat = pfad.stat()
|
||||
pfad.write_bytes(build_image("ninstrip-k1y", "0.1.0-47-g71a58"))
|
||||
import os
|
||||
|
||||
os.utime(pfad, ns=(stat.st_atime_ns, stat.st_mtime_ns))
|
||||
assert katalog.verify_and_read(abbild) is None
|
||||
|
||||
|
||||
def test_ssid_pruefung_wird_je_abbild_und_ssid_gemerkt(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
katalog = FirmwareCatalog(tmp_path)
|
||||
_schreiben(tmp_path, "a.img", build_image(payload=CONTENT_WITH_SSID))
|
||||
(abbild,) = katalog.scan()
|
||||
aufrufe: list[str] = []
|
||||
echt = firmware.contains_ssid
|
||||
|
||||
def spion(data: bytes, ssid: str) -> bool:
|
||||
aufrufe.append(ssid)
|
||||
return echt(data, ssid)
|
||||
|
||||
monkeypatch.setattr(firmware, "contains_ssid", spion)
|
||||
assert katalog.contains_ssid(abbild, "ninIOT")
|
||||
assert katalog.contains_ssid(abbild, "ninIOT")
|
||||
assert not katalog.contains_ssid(abbild, "andere")
|
||||
assert aufrufe == ["ninIOT", "andere"]
|
||||
|
||||
|
||||
def test_ssid_pruefung_einer_geloeschten_datei_ist_falsch(tmp_path: Path) -> None:
|
||||
katalog = FirmwareCatalog(tmp_path)
|
||||
pfad = _schreiben(tmp_path, "a.img", build_image(payload=CONTENT_WITH_SSID))
|
||||
(abbild,) = katalog.scan()
|
||||
pfad.unlink()
|
||||
assert not katalog.contains_ssid(abbild, "ninIOT")
|
||||
Reference in New Issue
Block a user