Ampel / ampel (push) Failing after 48s
WAS: Der Ablauf ist aus tasks.py heraus (ablauf.py, ohne Celery), die
LocalQueue wird bedient, ein Laeufer arbeitet Auftraege im selben Prozess
ab, und Rippy meldet sich mit gemessenen Faehigkeiten selbst als Arbeiter.
WARUM: "Rippy fuer Windows soll standalone funktionieren" (Commander). Bis
hierher konnte die Windows-App alles ANZEIGEN und nichts TUN — ein Rip waere
eingereiht worden und fuer immer liegengeblieben, weil niemand ihn holt.
DIE TRENNUNG: rip_disc hing an GENAU DREI Celery-Stellen in 234 Zeilen —
self.update_state, _transcode_queue, transcode_files.apply_async. Alle drei
sind Fragen der ZUSTELLUNG, nicht des Ablaufs. Sie sind jetzt Rueckrufe:
tasks.py reicht die Celery-Fassung herein, standalone.py die lokale. OHNE
Rueckruf komprimiert derselbe Prozess weiter — genau das, was ein
Ein-Prozess-Rippy braucht. Der Ablauf selbst ist Zeile fuer Zeile derselbe;
der Docker-Betrieb merkt vom Umbau nichts (Task-Namen, Argumente, Queues
unveraendert).
EINE ZUSTELL-STELLE statt drei: celery_client.abschicken() bedient alle
Auftragsarten. Vorher rief jede Stelle send_task selbst auf — der
Standalone-Betrieb haette an drei Stellen umgebogen werden muessen, beim
naechsten Auftragstyp an einer vierten.
WEITERER BLOCKER GEFUNDEN: ablauf.py holte detect_disc_type fest aus dem
LINUX-Treiber, in einem try/except. Unter Windows waere es damit IMMER None
gewesen und Rippen "hart verriegelt" — Rippy haette alles angezeigt und
nichts gerippt, ohne dass irgendwo ein Fehler stuende. Jetzt fragt es den
Treiber-Port.
WERKZEUGE: ripping.py und caps.py suchten nur im PATH. Auf dem Commander-PC
gemessen, vorher/nachher:
vorher check_makemkv_installed() -> False (obwohl installiert)
erkenne_encoder() -> nur CPU
nachher MakeMKV 1.18.4 C:\Program Files (x86)\MakeMKV\makemkvcon64.exe
HandBrake 1.11.2 ueber die API geholt, in 2,7 s
Encoder cpu-x264, cpu-x265, cpu-av1, VCE, VCE-AV1
107 Presets, Ryzen 7 9700X, 16 Kerne, avx512f
VCE ist die Hardwarebeschleunigung der Radeon — die hat Rippy auf diesem
Rechner vorher nie gesehen, weil es HandBrake gar nicht fand.
NEUE ROUTEN: GET /system/werkzeuge (was liegt wo, in welcher Fassung, gibt
es Neueres) und POST /system/werkzeuge/{name}/holen. HandBrake kommt
vollautomatisch von GitHub. MakeMKV wird NICHT mitgeliefert — Rippy laedt
die offizielle Datei und startet sie (Black-Box-Trennung, KONZEPT.md § 6).
makemkv.com antwortete beim Bauen mit HTTP 525; das wird im Klartext
gemeldet, und eine selbst geholte Datei bleibt moeglich.
HERZSCHLAG: /capabilities las die workers-Tabelle, die bisher nur der
Celery-Herzschlag fuellte. Im Standalone-Betrieb stand dort "0 Worker" und
die Encoder-Auswahl im UI blieb LEER — auf einem Rechner, der alles kann.
Jetzt meldet sich der Prozess selbst, mit dem, was caps.py MISST.
GEMESSEN, aus der fertigen EXE (29,0 MB):
bereit nach 1 s, keine Fehler im Log
Werkzeuge: beide gefunden, mit Version und Pfad
Worker: 1 (TobisNicerPC), 5 Encoder, 107 Presets
Die ganze Kette ist als Test festgehalten (test_kette.py): zustellen ->
einreihen -> Laeufer -> ablauf -> Job endet in einem EHRLICHEN Zustand.
Ohne Laufwerk geprueft, und das ist der wichtigere Fall: Ein Rip auf ein
totes Geraet muss zuegig scheitern, nicht auf "pending" haengenbleiben.
GEMESSEN: ruff sauber, 512 Tests gruen + 15 uebersprungen (vorher 489).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
5.6 KiB
Python
146 lines
5.6 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)",
|
|
)
|
|
|
|
|
|
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",
|
|
)
|
|
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
|