35cfcbcb07
Ampel / ampel (push) Successful in 27s
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>
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 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
|