diff --git a/src/rippy/einrichtung.py b/src/rippy/einrichtung.py new file mode 100644 index 0000000..fa086df --- /dev/null +++ b/src/rippy/einrichtung.py @@ -0,0 +1,260 @@ +"""Der Einrichtungs-Assistent: prüfen, fragen, installieren. + +## Der Befund des Commanders (28.08.2026) + +> „Dann hätte ich beim ‚Setup' auch erwartet das nen echtes setup passiert — +> wo will ich das hinspeichern, nen pre requirement check usw. Da kommt +> garnichts Rippy geht einfach auf." + +Er hat recht. Ein Doppelklick auf `RippySetup.exe` installierte stillschweigend +nach `%LOCALAPPDATA%\\Rippy` und öffnete das Fenster. Der Nutzer wurde nichts +gefragt und erfuhr nichts — auch nicht, ob sein Rechner überhaupt kann, was +Rippy braucht. Wenn hinterher etwas fehlte, stand er davor und wusste nicht, +seit wann. + +## Was hier passiert und was NICHT + +Dieses Modul enthält die **Prüfungen und die Entscheidungen** — keine +Oberfläche. Das Fenster steckt in `setup_fenster.py`, damit hier alles ohne +WebView2, ohne Fenster und ohne Installation prüfbar bleibt. + +## Die drei Sorten Befund + + ok geht + warnung geht, aber der Nutzer sollte es wissen + fehler geht NICHT — die Installation würde nichts nützen + +Nur `fehler` blockiert. Eine Warnung, die den Knopf sperrt, ist eine +Bevormundung; ein Fehler, der nur warnt, ist eine Falle. Ein Beispiel für +jede Sorte: + +* **Kein optisches Laufwerk** ist eine WARNUNG. Rippy ohne Laufwerk ist eine + reine Komprimier-Maschine — ein vorgesehener Betriebsfall (`install.sh` sagt + das seit Langem: „läuft trotzdem durch"). +* **Kein MakeMKV** ist eine WARNUNG, solange Rippy es holen kann. +* **Kein Schreibrecht am Zielordner** ist ein FEHLER. Da hilft kein Zureden. + +## Warum jede Prüfung sagt, was zu tun ist + +Ein Befund ohne Abhilfe lässt den Nutzer ratlos zurück. Deshalb hat jeder +Eintrag ein `abhilfe`-Feld — leer nur dann, wenn wirklich nichts zu tun ist. +""" + +import os +import shutil +import socket +import sys + +OK = "ok" +WARNUNG = "warnung" +FEHLER = "fehler" + +# Was Rippy mindestens braucht. Gemessen, nicht geschätzt: Das Paket allein ist +# 56 MB, dazu die entpackte HandBrake-Fassung (~74 MB), die Datenbank und der +# WebView2-Zwischenspeicher. 500 MB sind reichlich und lassen Luft. +MINDESTPLATZ_MB = 500 + +STANDARD_PORT = 7788 + + +def _eintrag(name, zustand, text, abhilfe=""): + return {"name": name, "zustand": zustand, "text": text, "abhilfe": abhilfe} + + +# ── Die einzelnen Prüfungen ───────────────────────────────────────────── +def pruefe_windows(version=None) -> dict: + """Windows 10 oder neuer. Darunter gibt es kein WebView2.""" + if os.name != "nt": + return _eintrag("Betriebssystem", FEHLER, "Kein Windows", + "Für Linux gibt es die Docker- und die Headless-Fassung.") + bau = version if version is not None else sys.getwindowsversion().build + if bau >= 22000: + return _eintrag("Betriebssystem", OK, "Windows 11 (Build %d)" % bau) + if bau >= 10240: + return _eintrag("Betriebssystem", OK, "Windows 10 (Build %d)" % bau) + return _eintrag("Betriebssystem", FEHLER, "Windows-Build %d ist zu alt" % bau, + "Rippy braucht mindestens Windows 10.") + + +def pruefe_webview(version=None) -> dict: + """Ohne WebView2 gibt es kein Fenster — aber Rippy läuft trotzdem. + + Deshalb WARNUNG und nicht Fehler: Der Rückfall auf den Browser + funktioniert, er ist nur weniger schön. + """ + if version is None: + from rippy import fenster + + version = fenster.webview2_version() + if version: + return _eintrag("Fenster (WebView2)", OK, version) + return _eintrag( + "Fenster (WebView2)", WARNUNG, "nicht gefunden", + 'Rippy öffnet sich dann im Browser. Die Laufzeit gibt es kostenlos ' + 'bei Microsoft (Suchwort „WebView2 Evergreen Runtime").') + + +def pruefe_platz(ziel: str, frei_mb=None) -> dict: + """Genug Platz am Zielort? Gemessen am nächsten vorhandenen Ordner.""" + from rippy import betrieb + + if frei_mb is None: + pfad = betrieb.naechster_vorhandener(ziel) + if not pfad: + return _eintrag("Speicherplatz", FEHLER, + "Der Ort ist nicht erreichbar: %s" % ziel, + "Einen anderen Zielordner wählen.") + try: + frei_mb = shutil.disk_usage(pfad).free // (1024 * 1024) + except OSError as e: + return _eintrag("Speicherplatz", FEHLER, "Nicht lesbar (%s)" % e, + "Einen anderen Zielordner wählen.") + if frei_mb >= MINDESTPLATZ_MB: + return _eintrag("Speicherplatz", OK, "%.1f GB frei" % (frei_mb / 1024)) + return _eintrag("Speicherplatz", FEHLER, + "nur %d MB frei, gebraucht werden %d MB" + % (frei_mb, MINDESTPLATZ_MB), + "Platz schaffen oder einen anderen Zielordner wählen.") + + +def pruefe_schreibrecht(ziel: str, probe=None) -> dict: + """Darf Rippy dorthin schreiben? Wirklich probiert, nicht geraten. + + Ein `os.access(..., W_OK)` lügt unter Windows: Es beantwortet die Frage + nach dem Schreibrecht anhand des Nur-Lesen-Flags, nicht anhand der + Zugriffsrechte. Also wird eine Datei angelegt und wieder gelöscht. + """ + if probe is not None: + geklappt, grund = probe + else: + geklappt, grund = _schreibprobe(ziel) + if geklappt: + return _eintrag("Schreibrecht", OK, ziel) + return _eintrag("Schreibrecht", FEHLER, "%s (%s)" % (ziel, grund), + 'Einen Ordner unterhalb von „Benutzer" wählen — dort ' + 'braucht Rippy keine Administratorrechte.') + + +def _schreibprobe(ziel: str): + import tempfile + + try: + os.makedirs(ziel, exist_ok=True) + with tempfile.NamedTemporaryFile(dir=ziel, prefix=".rippy-probe", + delete=True): + pass + return True, "" + except OSError as e: + return False, e.strerror or str(e) + + +def pruefe_port(port: int = STANDARD_PORT, belegt=None) -> dict: + """Ist der Port frei? Wenn nicht, kann Rippy nicht bedienen.""" + if belegt is None: + belegt = _port_belegt(port) + if not belegt: + return _eintrag("Netzwerk-Port", OK, "%d ist frei" % port) + # Kein Fehler: Sehr oft ist es ein bereits laufendes Rippy, und dann ist + # eine Neuinstallation genau richtig. + return _eintrag("Netzwerk-Port", WARNUNG, "%d ist belegt" % port, + "Läuft Rippy schon? Sonst im Setup einen anderen Port " + "wählen.") + + +def _port_belegt(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(0.5) + try: + s.bind(("127.0.0.1", port)) + return False + except OSError: + return True + + +def pruefe_laufwerk(laufwerke=None) -> dict: + """Ein optisches Laufwerk. WARNUNG, kein Fehler. + + Rippy ohne Laufwerk ist eine reine Komprimier-Maschine — ein vorgesehener + Betriebsfall, den `install.sh` seit Langem ausdrücklich zulässt. + """ + if laufwerke is None: + try: + from rippy import drives + + laufwerke = drives.treiber().list_optical_devices() + except Exception: # noqa: BLE001 + laufwerke = [] + if laufwerke: + return _eintrag("Optisches Laufwerk", OK, + ", ".join(str(x) for x in laufwerke)) + return _eintrag("Optisches Laufwerk", WARNUNG, "keins gefunden", + "Ohne Laufwerk kann Rippy nur komprimieren, nicht rippen. " + "Ein USB-Brenner reicht.") + + +def pruefe_werkzeuge(bestand=None) -> list: + """MakeMKV und HandBrake — je ein eigener Eintrag. + + MakeMKV fehlt => WARNUNG, weil das Setup es holen kann. Ein Fehler wäre + es erst, wenn es danach immer noch fehlt (siehe `tools/einrichten.py`). + """ + from rippy.tools import katalog + + bestand = katalog.bestand() if bestand is None else bestand + ergebnis = [] + for name, eintrag in sorted(bestand.items()): + titel = eintrag.get("titel", name) + if eintrag.get("da"): + ergebnis.append(_eintrag(titel, OK, + eintrag.get("version") or "gefunden")) + elif name == "handbrake": + ergebnis.append(_eintrag(titel, OK, "wird mitgeliefert")) + else: + ergebnis.append(_eintrag( + titel, WARNUNG, "nicht gefunden", + "Das Setup lädt es von makemkv.com und startet den Installer " + "des Herstellers. Ohne MakeMKV kann Rippy keine Disc lesen.")) + return ergebnis + + +# ── Alles zusammen ────────────────────────────────────────────────────── +def alle_pruefungen(ziel: str, port: int = STANDARD_PORT) -> list: + """Der vollständige Voraussetzungs-Bericht, in Anzeigereihenfolge.""" + return ([pruefe_windows(), pruefe_webview(), pruefe_laufwerk()] + + pruefe_werkzeuge() + + [pruefe_platz(ziel), pruefe_schreibrecht(ziel), pruefe_port(port)]) + + +def darf_installieren(pruefungen: list) -> bool: + """Blockiert NUR bei einem echten Fehler. (reine Funktion) + + Eine Warnung, die den Knopf sperrt, ist eine Bevormundung; ein Fehler, + der nur warnt, ist eine Falle. + """ + return not any(p["zustand"] == FEHLER for p in pruefungen) + + +def zusammenfassung(pruefungen: list) -> str: + """Ein Satz über den Gesamtzustand. (reine Funktion)""" + fehler = [p for p in pruefungen if p["zustand"] == FEHLER] + warnungen = [p for p in pruefungen if p["zustand"] == WARNUNG] + if fehler: + return "Installation nicht möglich: " + ", ".join(p["name"] for p in fehler) + if warnungen: + return "Bereit — mit Hinweisen zu: " + ", ".join(p["name"] for p in warnungen) + return "Alles bereit." + + +def vorgaben() -> dict: + """Womit die Maske vorbelegt wird.""" + from rippy import betrieb + from rippy.platform import win_registry as reg + + return { + "ziel": reg.standard_installationsordner(), + "ablage": betrieb.ablage_vorgabe({}, container=False, system="windows"), + "port": STANDARD_PORT, + "autostart": True, + "verknuepfungen": True, + "werkzeuge_holen": True, + } diff --git a/src/rippy/setup_fenster.py b/src/rippy/setup_fenster.py new file mode 100644 index 0000000..0e55b87 --- /dev/null +++ b/src/rippy/setup_fenster.py @@ -0,0 +1,389 @@ +"""Das Fenster des Einrichtungs-Assistenten. + +## Warum es das gibt + +Commander am 28.08.2026: + +> „Dann hätte ich beim ‚Setup' auch erwartet das nen echtes setup passiert — +> wo will ich das hinspeichern, nen pre requirement check usw. Da kommt +> garnichts Rippy geht einfach auf." + +## Warum WebView2 und nicht ein Windows-Dialog + +Weil es schon da ist. Rippy bringt WebView2 ohnehin für sein Hauptfenster mit +(Entscheid 4 in `KONZEPT-V2.md` § 10) — der Assistent kostet damit **null +zusätzliche Bytes**. Ein Dialog aus Win32-Bausteinen wäre hunderte Zeilen +ctypes für etwas, das hier fünfzig Zeilen HTML sind; `tkinter` ist im Bau +ausdrücklich ausgeschlossen. + +## Warum die Prüfungen NICHT hier stehen + +Sie stehen in `einrichtung.py`, und zwar vollständig ohne Fenster prüfbar. +Was hier steht, lässt sich nicht testen — also steht hier so wenig wie +möglich: Aufbau, Anzeige, und die Brücke zwischen beidem. + +## Was passiert, wenn das Fenster nicht aufgeht + +Dann wird ohne Assistent installiert, mit den Vorgaben, und gesagt warum. Ein +Setup, das gar nichts tut, weil sein Fenster nicht aufging, wäre schlimmer +als eines, das nicht fragt. +""" + +import threading + +from rippy import einrichtung + +TITEL = "Rippy einrichten" +BREITE = 860 +HOEHE = 760 + + +class Bruecke: + """Was die Seite aufrufen darf. Bewusst klein und ohne Zustand im HTML. + + Jede Methode gibt Daten zurück, keine Erfolgsmeldungen — dieselbe Regel + wie überall sonst in diesem Projekt. + """ + + def __init__(self): + self.ergebnis = None + self._fenster = None + self._fortschritt = [] + + # ── Auskunft ──────────────────────────────────────────────────────── + def vorgaben(self) -> dict: + return einrichtung.vorgaben() + + def pruefen(self, ziel: str = "", port: int = 0) -> dict: + """Der Voraussetzungs-Bericht für die angegebenen Werte.""" + vorgabe = einrichtung.vorgaben() + befunde = einrichtung.alle_pruefungen(ziel or vorgabe["ziel"], + int(port or vorgabe["port"])) + return { + "befunde": befunde, + "darf": einrichtung.darf_installieren(befunde), + "text": einrichtung.zusammenfassung(befunde), + } + + def ordner_waehlen(self, vorschlag: str = "") -> str: + """Ein echter Ordner-Dialog. Leer, wenn abgebrochen wurde.""" + import webview + + try: + gewaehlt = self._fenster.create_file_dialog( + webview.FOLDER_DIALOG, directory=vorschlag or "") + except Exception: # noqa: BLE001 + return "" + if not gewaehlt: + return "" + return gewaehlt[0] if isinstance(gewaehlt, (list, tuple)) else str(gewaehlt) + + def fortschritt(self) -> list: + """Was seit dem letzten Abruf passiert ist. Leert sich dabei.""" + zeilen, self._fortschritt = self._fortschritt, [] + return zeilen + + # ── Handeln ───────────────────────────────────────────────────────── + def installieren(self, optionen: dict) -> dict: + """Installiert im Hintergrund-Thread und gibt sofort zurück. + + Sofort, weil das Holen von MakeMKV Minuten dauern kann: Ein blockierter + Aufruf ließe das Fenster einfrieren, und ein eingefrorenes Fenster + sieht aus wie ein Absturz. + """ + threading.Thread(target=self._installieren_wirklich, + args=(optionen or {},), daemon=True).start() + return {"gestartet": True} + + def _installieren_wirklich(self, optionen: dict) -> None: + from rippy import windows_app + + def melden(text, anteil=None): + self._fortschritt.append(str(text)) + + try: + melden("Dateien werden abgelegt …") + ziel = windows_app.installieren( + ziel=optionen.get("ziel") or None, + autostart=bool(optionen.get("autostart", True)), + starten_danach=False, + verknuepfen=bool(optionen.get("verknuepfungen", True)), + ) + if optionen.get("ablage"): + melden("Ablage wird gespeichert …") + _ablage_speichern(optionen["ablage"]) + if optionen.get("werkzeuge_holen", True): + melden("Werkzeuge werden geprüft …") + from rippy.tools import einrichten as werkzeug_einrichtung + + bericht = werkzeug_einrichtung.sicherstellen(fortschritt=melden) + melden(werkzeug_einrichtung.bericht(bericht)) + bereit = bericht["bereit"] + else: + bereit = True + self.ergebnis = {"ok": True, "ziel": ziel, "bereit": bereit} + melden("Fertig.") + except Exception as ex: # noqa: BLE001 + # Nicht verschlucken: Ohne Konsole waere ein Absturz hier voellig + # unsichtbar, und der Nutzer saehe ein Fenster, in dem nichts mehr + # passiert. + self.ergebnis = {"ok": False, + "fehler": "%s: %s" % (type(ex).__name__, ex)} + melden("Fehlgeschlagen: %s" % ex) + + def stand(self) -> dict: + """None, solange es läuft. Danach das Ergebnis.""" + return self.ergebnis or {} + + def schliessen(self) -> None: + try: + self._fenster.destroy() + except Exception: # noqa: BLE001 + pass + + +def _ablage_speichern(pfad: str) -> None: + """Die gewählte Ablage in die Einstellungen schreiben. + + Über die laufende Datenbank, nicht über eine Konfigurationsdatei: Das UI + liest `outputDir` aus den Einstellungen, und genau dort muss der Wert + landen, damit die Wahl im Assistenten überhaupt etwas bewirkt. + """ + from rippy import config, daemon, store + + werte = config.laden() + store.verbinden(config.datenbank_url(werte)) + import sys + + sys.path.insert(0, daemon._api_pfad()) + import db as api_db # noqa: E402 + + api_db.init_db() + aktuell = api_db.get_settings() or {} + aktuell["outputDir"] = pfad + api_db.save_settings(aktuell) + + +def starten() -> dict: + """Öffnet den Assistenten. Gibt zurück, was daraus geworden ist. + + `{}` heißt: abgebrochen oder nicht möglich. Der Aufrufer entscheidet dann, + ob er ohne Assistent installiert. + """ + import webview + + bruecke = Bruecke() + fenster = webview.create_window(TITEL, html=SEITE, js_api=bruecke, + width=BREITE, height=HOEHE, + min_size=(720, 560), text_select=True) + bruecke._fenster = fenster + webview.start(gui="edgechromium", private_mode=False) + return bruecke.ergebnis or {} + + +def moeglich() -> tuple: + """Kann der Assistent überhaupt aufgehen? (ja/nein, Begründung)""" + from rippy import fenster as fenster_modul + + return fenster_modul.verfuegbar() + + +# ── Die Seite ─────────────────────────────────────────────────────────── +# Bewusst EINE Datei ohne Abhängigkeiten: kein Vite-Bau, kein Nachladen, keine +# Schriftart aus dem Netz. Der Assistent muss auch auf einem Rechner ohne +# Internet aufgehen — dort wird er am dringendsten gebraucht. +SEITE = r""" + + +
+

Rippy einrichten

+
Wird geprüft, was dein Rechner mitbringt — danach entscheidest du, wohin.
+
+
+
+

Voraussetzungen

+
wird geprüft …
+
+ +
+

Wohin

+ + + +
+ +
+

Sonst noch

+ + + +
+ +
+

Läuft

+
+
+
+ + + +""" + +# Damit die Seite auch ohne laufendes Fenster prüfbar ist (siehe Tests). +SEITE_LAENGE = len(SEITE) + + +def seite_enthaelt(text: str) -> bool: + """Kleine Hilfe für Tests: Steht etwas in der Seite?""" + return text in SEITE diff --git a/src/rippy/test_einrichtung.py b/src/rippy/test_einrichtung.py new file mode 100644 index 0000000..8b5eebd --- /dev/null +++ b/src/rippy/test_einrichtung.py @@ -0,0 +1,188 @@ +"""Der Einrichtungs-Assistent — die Pruefungen, ohne etwas zu installieren. + +## Warum es diese Tests gibt + +Commander am 28.08.2026: *„Dann haette ich beim ‚Setup' auch erwartet das nen +echtes setup passiert — wo will ich das hinspeichern, nen pre requirement +check usw. Da kommt garnichts Rippy geht einfach auf."* + +Er hatte recht: Ein Doppelklick installierte stillschweigend und oeffnete das +Fenster. Der Nutzer wurde nichts gefragt und erfuhr nichts. + +Alle Pruefungen sind eingespritzt — kein Fenster, keine Installation, kein +WebView2. Sonst waere „kein Laufwerk" nur auf einem Rechner ohne Laufwerk +pruefbar, und genau dort sieht es niemand. +""" + +import os + +from rippy import einrichtung as e + + +def _p(zustand, name="X"): + return {"name": name, "zustand": zustand, "text": "", "abhilfe": ""} + + +# ── Was blockiert und was nicht ───────────────────────────────────────── +def test_ohne_fehler_darf_installiert_werden(): + assert e.darf_installieren([_p(e.OK), _p(e.WARNUNG)]) is True + + +def test_ein_fehler_blockiert(): + assert e.darf_installieren([_p(e.OK), _p(e.FEHLER)]) is False + + +def test_eine_warnung_blockiert_NICHT(): + """Eine Warnung, die den Knopf sperrt, ist eine Bevormundung. Rippy ohne + optisches Laufwerk ist ein vorgesehener Betriebsfall — eine reine + Komprimier-Maschine.""" + assert e.darf_installieren([_p(e.WARNUNG), _p(e.WARNUNG)]) is True + + +# ── Die einzelnen Pruefungen ──────────────────────────────────────────── +def test_windows_10_und_11_gehen(): + assert e.pruefe_windows(22631)["zustand"] == e.OK + assert e.pruefe_windows(19045)["zustand"] == e.OK + + +def test_zu_altes_windows_ist_ein_fehler(): + befund = e.pruefe_windows(7601) # Windows 7 + assert befund["zustand"] == e.FEHLER + assert befund["abhilfe"], "ein Befund ohne Abhilfe laesst den Nutzer ratlos" + + +def test_fehlendes_webview_ist_nur_eine_warnung(): + """Ohne WebView2 faellt Rippy auf den Browser zurueck. Das ist weniger + schoen, aber es LAEUFT — ein Fehler waere hier eine Luege.""" + befund = e.pruefe_webview(version="") + assert befund["zustand"] == e.WARNUNG + assert "Browser" in befund["abhilfe"] + + +def test_vorhandenes_webview_ist_ok(): + assert e.pruefe_webview(version="151.0.4129.107")["zustand"] == e.OK + + +def test_zu_wenig_platz_ist_ein_fehler(): + befund = e.pruefe_platz("C:\\egal", frei_mb=100) + assert befund["zustand"] == e.FEHLER + assert "500" in befund["text"] + + +def test_genug_platz_ist_ok(): + assert e.pruefe_platz("C:\\egal", frei_mb=5000)["zustand"] == e.OK + + +def test_fehlendes_schreibrecht_ist_ein_fehler(): + """`os.access(..., W_OK)` luegt unter Windows — es beantwortet die Frage + anhand des Nur-Lesen-Flags, nicht anhand der Zugriffsrechte. Deshalb wird + wirklich geschrieben.""" + befund = e.pruefe_schreibrecht("C:\\Programme\\Rippy", + probe=(False, "Zugriff verweigert")) + assert befund["zustand"] == e.FEHLER + assert "Benutzer" in befund["abhilfe"] + + +def test_schreibprobe_am_echten_ordner(tmp_path): + befund = e.pruefe_schreibrecht(str(tmp_path)) + assert befund["zustand"] == e.OK + # Und die Probe darf nichts hinterlassen. + assert list(tmp_path.iterdir()) == [] + + +def test_belegter_port_ist_nur_eine_warnung(): + """Sehr oft ist es ein bereits laufendes Rippy — und dann ist eine + Neuinstallation genau richtig.""" + befund = e.pruefe_port(7788, belegt=True) + assert befund["zustand"] == e.WARNUNG + assert "Rippy" in befund["abhilfe"] + + +def test_freier_port_ist_ok(): + assert e.pruefe_port(7788, belegt=False)["zustand"] == e.OK + + +def test_kein_laufwerk_ist_nur_eine_warnung(): + """install.sh sagt seit Langem: ohne Laufwerk „laeuft trotzdem durch, dann + ist das eine reine KOMPRIMIER-Maschine".""" + befund = e.pruefe_laufwerk(laufwerke=[]) + assert befund["zustand"] == e.WARNUNG + assert "komprimieren" in befund["abhilfe"] + + +def test_gefundenes_laufwerk_wird_genannt(): + befund = e.pruefe_laufwerk(laufwerke=["\\\\.\\G:"]) + assert befund["zustand"] == e.OK + assert "G:" in befund["text"] + + +# ── Die Werkzeuge ─────────────────────────────────────────────────────── +def _bestand(makemkv, handbrake): + return { + "makemkv": {"titel": "MakeMKV", "da": makemkv, "version": "1.18.4", + "pflicht": True}, + "handbrake": {"titel": "HandBrakeCLI", "da": handbrake, + "version": "1.11.2", "pflicht": False}, + } + + +def test_fehlendes_makemkv_ist_eine_warnung_mit_ausweg(): + befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(False, True))} + assert befunde["MakeMKV"]["zustand"] == e.WARNUNG + assert "makemkv.com" in befunde["MakeMKV"]["abhilfe"] + + +def test_fehlendes_handbrake_ist_kein_problem(): + """Es liegt im Paket — das Setup legt es beim Installieren daneben.""" + befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(True, False))} + assert befunde["HandBrakeCLI"]["zustand"] == e.OK + assert "mitgeliefert" in befunde["HandBrakeCLI"]["text"] + + +def test_vorhandene_werkzeuge_zeigen_ihre_version(): + befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(True, True))} + assert befunde["MakeMKV"]["text"] == "1.18.4" + + +# ── Die Zusammenfassung ───────────────────────────────────────────────── +def test_zusammenfassung_nennt_die_fehler_beim_namen(): + text = e.zusammenfassung([_p(e.FEHLER, "Speicherplatz"), _p(e.OK, "Port")]) + assert "nicht möglich" in text + assert "Speicherplatz" in text + + +def test_zusammenfassung_nennt_die_hinweise(): + text = e.zusammenfassung([_p(e.WARNUNG, "MakeMKV"), _p(e.OK, "Port")]) + assert "MakeMKV" in text + assert "nicht möglich" not in text + + +def test_alles_gruen_sagt_es_auch_so(): + assert e.zusammenfassung([_p(e.OK), _p(e.OK)]) == "Alles bereit." + + +def test_jede_pruefung_hat_dieselbe_form(): + """Ein fehlendes Feld waere in der Oberflaeche ein `undefined`.""" + for befund in ([e.pruefe_windows(22631), e.pruefe_webview(version=""), + e.pruefe_laufwerk(laufwerke=[]), + e.pruefe_platz("C:\\x", frei_mb=9999), + e.pruefe_port(7788, belegt=False)] + + e.pruefe_werkzeuge(_bestand(False, False))): + assert set(befund) == {"name", "zustand", "text", "abhilfe"}, befund + assert befund["zustand"] in (e.OK, e.WARNUNG, e.FEHLER) + if befund["zustand"] != e.OK: + assert befund["abhilfe"], ( + "%s sagt nicht, was zu tun ist" % befund["name"]) + + +# ── Die Vorgaben ──────────────────────────────────────────────────────── +def test_vorgaben_sind_vollstaendig(): + if os.name != "nt": + import pytest + pytest.skip("Registry-Vorgaben gibt es nur unter Windows") + v = e.vorgaben() + assert set(v) == {"ziel", "ablage", "port", "autostart", + "verknuepfungen", "werkzeuge_holen"} + assert v["port"] == 7788 + assert v["ziel"] and v["ablage"] + assert "/app/" not in v["ablage"], "Container-Pfad in einer Windows-Vorgabe" diff --git a/src/rippy/test_setup_fenster.py b/src/rippy/test_setup_fenster.py new file mode 100644 index 0000000..7896cc3 --- /dev/null +++ b/src/rippy/test_setup_fenster.py @@ -0,0 +1,122 @@ +"""Der Einrichtungs-Assistent: Bruecke und Seite, ohne ein Fenster zu oeffnen. + +Ein Fenster laesst sich im Test nicht bedienen. Geprueft wird deshalb das, +was DAHINTER haengt: was die Seite abfragen darf, was beim Installieren +passiert, und dass ein Fehlschlag sichtbar wird statt zu verschwinden. + +Hinweis zu den Backslashes: Windows-Pfade stehen hier in ROHEN Zeichenketten +(`r"..."`). In einer normalen waere `C:\\Rippy` die ungueltige Escape-Sequenz +Backslash-R. +""" + +from rippy import setup_fenster + + +class LeeresFenster: + def __init__(self, antwort=None): + self.antwort = antwort + self.zerstoert = False + + def create_file_dialog(self, *a, **k): + if self.antwort is None: + raise RuntimeError("kein Dialog") + return self.antwort + + def destroy(self): + self.zerstoert = True + + +# ── Die Seite ─────────────────────────────────────────────────────────── +def test_die_seite_fragt_nach_dem_ablageort(): + """Die ausdrueckliche Frage des Commanders: „wo will ich das + hinspeichern".""" + assert setup_fenster.seite_enthaelt('id="ablage"') + assert setup_fenster.seite_enthaelt("Fertige Filme") + + +def test_die_seite_zeigt_die_voraussetzungen(): + """„nen pre requirement check usw." — vorher kam da gar nichts.""" + assert setup_fenster.seite_enthaelt("Voraussetzungen") + assert setup_fenster.seite_enthaelt("pywebview.api.pruefen") + + +def test_die_seite_laedt_nichts_aus_dem_netz(): + """Der Assistent muss auf einem Rechner OHNE Internet aufgehen — dort + wird er am dringendsten gebraucht.""" + for muster in ("http://", "https://", "