"""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")