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>
125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
"""Tests für _original_aufheben — der Pfad, der am 25.07.2026 die Platte füllte.
|
|
|
|
Der Kern: Es wird NICHT vorhergesagt, ob umgehaengt werden kann, sondern
|
|
os.rename versucht. Die alte Fassung verglich st_dev und lag damit falsch —
|
|
auf der Rippy-VM sind st_dev von /app/temp und /app/media identisch (2050),
|
|
os.rename scheitert zwischen ihnen trotzdem mit EXDEV, weil der Kernel den
|
|
Mount vergleicht und nicht das Gerät. Die Platzpruefung wurde deshalb
|
|
übersprungen und shutil.move kopierte 75 GB bei 37 GB frei.
|
|
"""
|
|
|
|
import errno
|
|
import os
|
|
|
|
import pytest
|
|
|
|
import ablauf as tasks
|
|
|
|
|
|
class FakeDb:
|
|
"""Faengt nur die Log-Zeilen ab — mehr braucht _original_aufheben nicht."""
|
|
|
|
def __init__(self):
|
|
self.logs = []
|
|
|
|
def add_log(self, level, source, message):
|
|
self.logs.append((level, message))
|
|
|
|
def meldungen(self):
|
|
return " ".join(m for _, m in self.logs)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_db(monkeypatch):
|
|
ersatz = FakeDb()
|
|
monkeypatch.setattr(tasks, "db", ersatz)
|
|
return ersatz
|
|
|
|
|
|
def _lege_rohdaten_an(tmp_path, groesse=2048):
|
|
raw = tmp_path / "raw" / "job-1"
|
|
raw.mkdir(parents=True)
|
|
(raw / "title_t00.mkv").write_bytes(b"x" * groesse)
|
|
final = tmp_path / "media" / "Film (2020)"
|
|
final.mkdir(parents=True)
|
|
return str(raw), str(final)
|
|
|
|
|
|
def test_umhaengen_wenn_derselbe_mount(tmp_path, fake_db):
|
|
"""Der gute Fall: rename klappt, nichts wird kopiert, kein Platz nötig."""
|
|
raw, final = _lege_rohdaten_an(tmp_path)
|
|
|
|
tasks._original_aufheben("job-1", raw, final)
|
|
|
|
assert os.path.isdir(os.path.join(final, "original"))
|
|
assert os.path.isfile(os.path.join(final, "original", "title_t00.mkv"))
|
|
assert not os.path.exists(raw)
|
|
assert "umgehängt" in fake_db.meldungen()
|
|
|
|
|
|
def test_bei_exdev_und_zu_wenig_platz_wird_nur_gewarnt(tmp_path, fake_db, monkeypatch):
|
|
"""Der Fall, der die Platte füllte: rename geht nicht, Platz reicht nicht.
|
|
|
|
Vorher lief hier eine Vollkopie an, weil die st_dev-Prüfung „gleiches
|
|
Dateisystem" meldete und die Platzpruefung deshalb ausblieb.
|
|
"""
|
|
raw, final = _lege_rohdaten_an(tmp_path)
|
|
|
|
def kein_rename(*_a, **_k):
|
|
raise OSError(errno.EXDEV, "Invalid cross-device link")
|
|
|
|
monkeypatch.setattr(tasks.os, "rename", kein_rename)
|
|
monkeypatch.setattr(tasks, "_frei_bytes", lambda _p: 1024) # weniger als die Rohdaten
|
|
|
|
tasks._original_aufheben("job-1", raw, final)
|
|
|
|
# Rohdaten bleiben unangetastet liegen, es wurde NICHTS kopiert
|
|
assert os.path.isfile(os.path.join(raw, "title_t00.mkv"))
|
|
assert not os.path.exists(os.path.join(final, "original"))
|
|
meldungen = fake_db.meldungen()
|
|
assert "NICHT aufgehoben" in meldungen
|
|
assert "anderen Mount" in meldungen
|
|
assert "Arbeitsverzeichnis" in meldungen # nennt die Abhilfe
|
|
|
|
|
|
def test_bei_exdev_und_genug_platz_wird_kopiert(tmp_path, fake_db, monkeypatch):
|
|
raw, final = _lege_rohdaten_an(tmp_path)
|
|
|
|
echtes_rename = os.rename
|
|
aufrufe = {"n": 0}
|
|
|
|
def rename_erst_exdev(*args, **kwargs):
|
|
# Nur der Versuch von _original_aufheben scheitert; shutil.move darf
|
|
# intern weiter umbenennen (es kopiert selbst und benennt Teile um).
|
|
aufrufe["n"] += 1
|
|
if aufrufe["n"] == 1:
|
|
raise OSError(errno.EXDEV, "Invalid cross-device link")
|
|
return echtes_rename(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(tasks.os, "rename", rename_erst_exdev)
|
|
monkeypatch.setattr(tasks, "_frei_bytes", lambda _p: 10 * 1024**3)
|
|
|
|
tasks._original_aufheben("job-1", raw, final)
|
|
|
|
assert os.path.isfile(os.path.join(final, "original", "title_t00.mkv"))
|
|
assert "Original behalten" in fake_db.meldungen()
|
|
|
|
|
|
def test_anderer_fehler_wird_ehrlich_gemeldet_und_reisst_job_nicht_mit(
|
|
tmp_path, fake_db, monkeypatch
|
|
):
|
|
"""Ein Fehler beim Aufheben darf den Job NIE scheitern lassen — die
|
|
komprimierte Datei ist zu diesem Zeitpunkt fertig und in Ordnung."""
|
|
raw, final = _lege_rohdaten_an(tmp_path)
|
|
|
|
def zugriff_verweigert(*_a, **_k):
|
|
raise OSError(errno.EACCES, "Permission denied")
|
|
|
|
monkeypatch.setattr(tasks.os, "rename", zugriff_verweigert)
|
|
|
|
tasks._original_aufheben("job-1", raw, final) # darf nicht werfen
|
|
|
|
meldungen = fake_db.meldungen()
|
|
assert "konnte nicht aufgehoben werden" in meldungen
|
|
assert raw in meldungen # sagt, WO die Rohdatei liegt
|