Files
rippy/docker/worker/test_original_aufheben.py
Hitonabi 35cfcbcb07
Ampel / ampel (push) Successful in 27s
style: echte Umlaute im ganzen Projekt + Installer im Rippy-Look
Commander-Rueckmeldung: Umlaute fehlen. Ausloeser war sichtbar der Installer -
"Diese Maschine uebernimmt die Video-Kompression fuer Rippy" stand woertlich im
Screenshot.

## Die .ps1-Falle war loesbar, nicht unumgehbar

Bisher galt: ausgelieferte .ps1 MUESSEN ASCII sein, weil PowerShell 5.1 sie
ohne BOM als ANSI liest. Das ist nur die halbe Wahrheit - gemessen mit echtem
powershell.exe 5.1:

    ohne BOM:  $s = "Größe: äöü"   + Unerwartetes Token -> Skript kaputt
    mit  BOM:  Groesse: aeoeue         + laeuft, Length 16 korrekt

Alle drei Skripte sind jetzt UTF-8 MIT BOM und tragen echte Umlaute; unter 5.1
gegengeprueft (BOM vorhanden, Parser fehlerfrei, Text korrekt gelesen). Der
Kopfkommentar sagt das jetzt richtig statt "ASCII-only".

## Umstellung: Text ja, Bezeichner nein

Umlaute gehoeren in Kommentare und Anzeigetexte, nicht in Funktionsnamen oder
Datenschluessel. Deshalb je Sprache das passende Werkzeug:

- Python: ueber den TOKENIZER - angefasst wurden ausschliesslich COMMENT- und
  STRING-Tokens. 156 Stellen. Code ist damit garantiert unberuehrt.
- TypeScript: nur // und /* */ Kommentare sowie JSX-Text (kann per Definition
  kein Bezeichner sein). 24 Stellen. `const waehlen`, `let laeuft`, `plaetze`,
  `GeraetInfo` sind nachweislich unversehrt.
- install.sh: Anzeigetext, aber die Shell-Funktionen (gruen/rot/gelb/titel) und
  der Schalter --nur-pruefen bleiben ASCII - das sind Schnittstellen.
- Markdown: 0 Aenderungen, die Doku hatte schon Umlaute.

ZWEI FEHLER MEINES KONVERTERS, beide von Werkzeugen gefangen:

1. In f-Strings steht in {...} CODE, kein Text. Aus f"{groesse}" wurde
   f"{größe}", waehrend die Variable groesse hiess - Ruff meldete F821
   "Undefined name". Der Konverter lagert Einsetzungen jetzt aus.
2. Ein Dict-Schluessel wurde umbenannt: die Wortliste enthaelt das PRAEFIX
   "uebersprung", der Tabu-Schutz prueft aber ganze Woerter. bericht[...] ist
   wieder ASCII - Umlaute in Datenschluesseln brechen JSON-Runden und DB-Felder.

## Installer im Rippy-Look

Statt hellgrau jetzt dieselben Toene wie das Web-UI (aus lib/design.ts
uebernommen): slate-900 Flaeche, dunkle Eingabefelder, Amber-Hauptknopf wie
"Los geht's" im Wizard. Oben ein Kopfbereich mit dem Farbverlauf
amber -> indigo -> purple und dem Disc-Symbol - in WinForms per Paint-Ereignis
gezeichnet, weil es dort keine Verlaeufe von der Stange gibt.

Geprueft, nicht gehofft: Das Fenster wurde headless in ein PNG gerendert
(DrawToBitmap) und angesehen - Verlauf, Symbol, Umlaute und Farben sitzen.
RippyWorkerSetup.exe neu gebaut (57344 -> 60416 Bytes); Umlaute und das
requireAdministrator-Manifest sind in der .exe verifiziert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:37:23 +02:00

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