Files
rippy/docker/worker/test_erreichbarkeit.py
HitonabiandClaude Opus 5 1b84ec2a45
Ampel / ampel (push) Successful in 1m20s
fix(windows): Vollstaendiger Rundgang durch die Docker-Reste
Commander: „Bro, du musst alles was rippy jetzt im code hat für Windows
Bauen! Jeden pfad, alles wo die tools drauf zugreifen. Diese Rippy version
MUSS 100% Windows Kompatibel sein. Prüfe bitte den kompletten Quellcode nach
Docker Resten."

Systematisch gesucht statt Fundstelle fuer Fundstelle: feste POSIX-Pfade,
Linux-Programme, POSIX-eigene Aufrufe, `shutil.which`, `posixpath` auf echten
Pfaden, Container-Texte. Sechs echte Fehler dabei.

## 1. `/dev/{name}` in drei Endpunkten — der schwerste

Das UI ruft `/devices/{id}/eject`, `/scan-tracks` und `/tracks` mit der
Kennung aus der Geraeteliste auf, unter Windows also `G`. Gebaut wurde daraus
`/dev/G` — steht in keiner Laufwerksliste. **Auswerfen und „Disc scannen"
antworteten unter Windows IMMER mit 404**, ohne dass irgendwo stand, warum.

Hin- und Rueckweg gehoeren zusammen: Beide Treiber haben jetzt `kennung()`
und `pfad_zu_kennung()`. Wer die Kennung vergibt, loest sie auch auf.

## 2. `os.path.isdir("/app")` — zum zweiten Mal

Nach `caps.py` (heute frueh) auch in `ablauf.py`: Der eigenstaendige
Windows-Rippy hielt sich fuer einen FREMDEN Worker und haette sich selbst
vorgeworfen, Container-Pfade nicht zu erreichen — auf einer Maschine ohne
Container. Die Entscheidung ist jetzt einspritzbar; vorher hing der Test
daran, ob es einen Ordner `/app` gibt.

## 3. `shutil.which` in `schluessel.py`

Ausgerechnet im Modul, das es NUR unter Windows gibt: Es suchte makemkvcon im
PATH, wo unter Windows nie ein Programm aus „Programme" steht. Die
Schluessel-Automatik fuer 4K-UHD lief damit nie an.

## 4. `posixpath.join` auf echten Pfaden

`rohdaten.py` baute `C:\Roh/datei.mkv` — gemischte Trenner, die im UI falsch
aussehen und jeden Vergleich brechen.

## 5. Container-Pfad in einer Nutzermeldung

„Roh-Datei bleibt in /app/temp erhalten" nennt jetzt den echten Ordner. Wer
die Datei retten will, sucht sonst am falschen Ort.

## 6. Container-Pfade als UI-Vorbelegung

Rip-Dialog und `useBetrieb` starteten mit `/app/media`, bis die Antwort da
war. Leer ist ehrlicher: Es behauptet nichts.

## Und HandBrakes „Code 0"

Code 0 heisst ERFOLG. Rippy meldete trotzdem „fehlgeschlagen", weil die Datei
nicht am erwarteten Ort lag: **HandBrake bestimmt den Container aus dem
PRESET, nicht aus der Endung** — ein MP4-Preset schreibt `.mp4` neben das
verlangte `.mkv`. Jetzt erzwingt `--format` den Container passend zur Endung
(an HandBrake 1.11.2 gegengeprueft), und falls doch etwas daneben liegt, wird
es gefunden statt weggeworfen.

## Der Waechter

`test_keine_container_reste.py` prueft mechanisch, dass im Windows-Weg kein
Container-Pfad ohne Begruendung steht. Die Ausnahmen stehen namentlich mit
Grund da (Linux-Zweige, benannte Rueckfaelle) — und ein zweiter Test wirft
jede Ausnahme raus, die niemand mehr braucht.

Ueber den Tokenizer, nicht ueber „faengt mit Anfuehrungszeichen an": Der
erste Anlauf blieb prompt an seinem eigenen `r\"\"\"`-Docstring haengen.

887 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 16:14:10 +02:00

155 lines
6.2 KiB
Python

"""Tests für die Erreichbarkeits-Prüfung vor der Kompression.
Diese Prüfung hatte KEINEN einzigen Test — und genau deshalb ist sie am
26.07.2026 im ersten vollständigen Durchlauf umgefallen: Der Rip lief sauber
durch (40 GB Akira), die Kompression brach 0,2 s später ab mit „Dieser Worker
erreicht das Ziel nicht", obwohl die Freigabe erreichbar war. Es fehlten nur
zwei noch nie angelegte Ordner.
"""
import ablauf as tasks
# --- Der eigentliche Fehler: nur EINE fehlende Ebene war erlaubt -------------
def _ohne_schluss_trenner(p):
"""`\\\\NAS\\rippy\\` und `\\\\NAS\\rippy` sind derselbe Ordner.
Wichtig, weil `ntpath.dirname` für die UNC-Wurzel MIT abschließendem
Backslash zurückkommt. Das echte `os.path.isdir` stört das nicht — eine
Attrappe, die stur vergleicht, hätte hier einen Fehler erfunden, den es
nicht gibt.
"""
return p.rstrip("\\/") or p
def _fake_isdir(vorhandene):
"""isdir-Ersatz, der nur die genannten Pfade kennt."""
bekannt = {_ohne_schluss_trenner(p) for p in vorhandene}
return lambda p: _ohne_schluss_trenner(p) in bekannt
def test_findet_den_naechsten_vorhandenen_ordner():
da = {"\\\\NAS\\rippy"}
assert tasks.erster_vorhandener_ordner(
"\\\\NAS\\rippy\\movies\\Akira (1988)",
isdir=_fake_isdir(da), dirname=lambda p: p.rsplit("\\", 1)[0] if "\\" in p.strip("\\") else "",
) == "\\\\NAS\\rippy"
def test_zwei_fehlende_ebenen_sind_in_ordnung():
"""DER Vorfall: `movies` UND der Filmordner fehlten beide.
`os.makedirs` legt die ganze Kette an — es genügt also, dass irgendein
Vorfahre existiert. Die alte Prüfung sah nur nach `dirname` und erklärte
damit jeden ERSTEN Film in einer neuen Ablage für unerreichbar.
"""
da = {"/share"}
assert tasks.erster_vorhandener_ordner(
"/share/movies/Akira (1988)", isdir=_fake_isdir(da)) == "/share"
def test_gar_nichts_erreichbar_gibt_leer():
"""Ist die Freigabe wirklich weg, muss die Prüfung anschlagen."""
assert tasks.erster_vorhandener_ordner(
"/share/movies/Film", isdir=_fake_isdir(set())) == ""
def test_sucht_nicht_endlos_auf_einer_toten_freigabe():
"""Jeder Schritt kostet auf einer toten Freigabe einen Timeout."""
gefragt = []
def zaehlend(p):
gefragt.append(p)
return False
tiefer_pfad = "/" + "/".join(f"e{i}" for i in range(50))
assert tasks.erster_vorhandener_ordner(tiefer_pfad, isdir=zaehlend) == ""
assert len(gefragt) <= tasks.MAX_ELTERN_STUFEN
def test_leerer_pfad_stuerzt_nicht_ab():
assert tasks.erster_vorhandener_ordner("", isdir=_fake_isdir(set())) == ""
# --- Die Meldung: eine falsche Ursache ist teurer als gar keine -------------
def _pruefen(monkeypatch, vorhandene, mapping, im_container=False):
import ntpath
kennt = _fake_isdir(vorhandene)
monkeypatch.setattr(tasks.os.path, "isdir",
lambda p: kennt(p) or (im_container and p == "/app"))
monkeypatch.setattr(tasks.os.path, "dirname", ntpath.dirname)
monkeypatch.setenv("RIPPY_PATH_MAP", mapping)
return tasks._erreichbarkeit_pruefen(
"/app/media/rippy/job1", "\\\\NAS\\rippy\\job1",
"/app/media/rippy/movies/Akira (1988)", "\\\\NAS\\rippy\\movies\\Akira (1988)",
# „Fremd" heisst: Rippy laeuft woanders als dieser Worker. Bis zum
# 29.08.2026 wurde das an der Existenz von `/app` festgemacht — damit
# hing dieser Test am laufenden Rechner, und unter Windows hielt sich
# der eigenstaendige Rippy fuer einen fremden Worker.
fremd=not im_container,
)
def test_alles_da_meldet_nichts(monkeypatch):
assert _pruefen(monkeypatch, {"\\\\NAS\\rippy\\job1", "\\\\NAS\\rippy\\movies\\Akira (1988)"},
"/app/media/rippy=\\\\NAS\\rippy") == ""
def test_ziel_fehlt_ganz_ist_kein_fehler(monkeypatch):
"""Der Vorfall vom 26.07.2026 — jetzt geht er durch."""
assert _pruefen(monkeypatch, {"\\\\NAS\\rippy\\job1", "\\\\NAS\\rippy"},
"/app/media/rippy=\\\\NAS\\rippy") == ""
def test_fehlende_quelle_bleibt_ein_fehler(monkeypatch):
meldung = _pruefen(monkeypatch, {"\\\\NAS\\rippy"}, "/app/media/rippy=\\\\NAS\\rippy")
assert "Quelle" in meldung
assert "Rohdaten sind NICHT verloren" in meldung
def test_uebersetzter_aber_unerreichbarer_pfad_beschuldigt_nicht_die_karte(monkeypatch):
"""⚠️ Genau die Falschaussage aus dem Vorfall.
Die Meldung lautete „RIPPY_PATH_MAP … deckt diesen Pfad aber nicht ab",
während sie ihn im selben Satz korrekt übersetzt hatte. Wer dem folgte,
suchte in der Karte statt in der Freigabe.
"""
meldung = _pruefen(monkeypatch, set(), "/app/media/rippy=\\\\NAS\\rippy")
assert "deckt diesen Pfad aber nicht ab" not in meldung
assert "Übersetzt wurde er korrekt" in meldung
assert "Freigabe verbunden" in meldung
def test_nicht_uebersetzter_pfad_beschuldigt_sehr_wohl_die_karte(monkeypatch):
"""Hier ist die Karte wirklich schuld — der Pfad blieb unverändert."""
monkeypatch.setattr(tasks.os.path, "isdir", lambda p: False)
monkeypatch.setenv("RIPPY_PATH_MAP", "/app/temp=Y:\\temp")
meldung = tasks._erreichbarkeit_pruefen(
"/app/media/rippy/job1", "/app/media/rippy/job1",
"/app/media/movies/X", "/app/media/movies/X",
# Ausdruecklich ein FREMDER Worker — nur den betrifft die Pfad-Karte.
# Vorher ergab sich das aus „es gibt kein /app", und genau daran hing
# der Test am laufenden Rechner (Befund 29.08.2026).
fremd=True,
)
assert "deckt diesen Pfad aber nicht ab" in meldung
assert "von keinem Eintrag übersetzt" in meldung
def test_ohne_mapping_nennt_die_wahre_ursache(monkeypatch):
meldung = _pruefen(monkeypatch, set(), "")
assert "nicht gesetzt" in meldung
assert "RIPPY_PATH_MAP" in meldung
def test_artikel_stimmt_in_beiden_meldungen(monkeypatch):
"""„erreicht die Ziel" stand wörtlich im Log des Commanders."""
quelle = _pruefen(monkeypatch, set(), "")
assert "erreicht die Quelle" in quelle
# Quelle da, Ziel unerreichbar (kein einziger Vorfahre) → Ziel-Meldung
ziel = _pruefen(monkeypatch, {"\\\\NAS\\rippy\\job1"},
"/app/media/rippy=\\\\NAS\\rippy")
assert "erreicht das Ziel" in ziel