style: Zeilenenden zurueck auf LF (kein Inhalt geaendert)

In 35370fd habe ich fuenf Dateien versehentlich von LF auf CRLF umgestellt.
Der Inhalt stimmte, aber der Diff war unlesbar: 1363 geaenderte Zeilen fuer
30 echte in RipTargetModal.tsx, 1184 fuer 29 in setup_fenster.py. Nachgewiesen
mit `git diff --ignore-cr-at-eol` — damit blieben 56/9 uebrig.

Ursache: Ein Rueckschreiben im Textmodus stellt die ganze Datei um. Die Regel
stand als Warnung schon in meinem Gedaechtnis, dort aber nur in der anderen
Richtung (CRLF -> LF). Sie gilt in beide.

Dieser Commit aendert AUSSCHLIESSLICH Zeilenenden. Nachgewiesen:
`git diff --ignore-cr-at-eol` listet keine dieser fuenf Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-29 11:52:08 +02:00
co-authored by Claude Opus 5
parent 6881a63fb7
commit d0281f7a1b
5 changed files with 1863 additions and 1863 deletions
File diff suppressed because it is too large Load Diff
+141 -141
View File
@@ -1,141 +1,141 @@
/*
* Worauf läuft Rippy — und was kann dieser Betrieb überhaupt?
*
* ## Der Befund des Commanders (28.08.2026)
*
* Im Windows-Fenster stand auf der Server-Status-Kachel:
*
* Worker erreichbar: 0 von 1
* Kein Worker antwortet — ohne ihn läuft kein Rip. Prüfen: docker compose ps
* Container-Platte: unbekannt
* Freigaben: keine eingehängt
*
* Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
* Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt dort
* selbst. Sein Urteil: „Du hast ja quasi nur rippy genommen und die docker
* installation für Windows gebaut."
*
* ## Warum ein Provider und kein Abruf je Seite
*
* Dieselbe Begründung wie beim Ereignis-Strom nebenan: Vier Seiten, die
* dasselbe abfragen, sind vier Abrufe — und vier Gelegenheiten, dass eine
* davon einen anderen Stand hat als die anderen. Der Betrieb ändert sich zur
* Laufzeit nicht; er wird EINMAL geholt.
*
* ## Warum die Vorgabe „Docker" ist
*
* Solange die Antwort noch unterwegs ist, muss irgendetwas gelten. Die
* Docker-Annahme ist hier die richtige Vorgabe — sie zeigt MEHR, und ein kurz
* zu viel angezeigter Bereich ist harmloser als ein Bedienelement, das für
* einen Augenblick verschwindet und wieder auftaucht.
*
* Ein FEHLGESCHLAGENER Abruf ist etwas anderes als „noch unterwegs":
* `geladen` bleibt dann false, und wer das wissen will, kann es abfragen. Ein
* Verbindungsabriss ist keine Aussage über die Welt (siehe useEventStream).
*/
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { api } from './api'
export interface BetriebsFaehigkeiten {
/** Gibt es andere Maschinen, die Jobs übernehmen? */
externe_worker: boolean
/** Kann Rippy Netzwerk-Freigaben selbst einhängen? */
freigaben_einhaengen: boolean
/** Sind Pfade wie /app/media überhaupt gemeint? */
container_pfade: boolean
/** Kann Rippy MakeMKV/HandBrake selbst beschaffen? */
werkzeuge_verwalten: boolean
/** Darf die Oberflaeche ausserhalb der Medien-Wurzel blaettern? */
frei_blaettern: boolean
}
export interface Betrieb {
modus: 'standalone' | 'verteilt'
plattform: 'windows' | 'linux' | 'macos'
im_container: boolean
kann: BetriebsFaehigkeiten
/** Womit ein Ablage-Feld vorbelegt wird — je Betrieb ein anderer Ort. */
ablage_vorgabe: string
/** Wohin die Rohdaten wandern, wenn niemand etwas anderes waehlt. */
arbeits_vorgabe: string
/** Der Befehl zum Nachsehen. LEER heißt: es gibt keinen, den der Nutzer
* ausführen könnte — dann darf auch keiner dastehen. */
hilfe_befehl: string
/** Ist die Auskunft schon da? False heißt „noch unterwegs ODER nicht
* erreichbar" — nicht „es gibt keinen Betrieb". */
geladen: boolean
}
const VORGABE: Betrieb = {
modus: 'verteilt',
plattform: 'linux',
im_container: true,
kann: {
externe_worker: true,
freigaben_einhaengen: true,
container_pfade: true,
werkzeuge_verwalten: false,
frei_blaettern: false,
},
ablage_vorgabe: '/app/media',
arbeits_vorgabe: '/app/temp',
hilfe_befehl: 'docker compose -p rippy ps',
geladen: false,
}
const BetriebContext = createContext<Betrieb>(VORGABE)
export function BetriebProvider({ children }: { children: ReactNode }) {
const [betrieb, setBetrieb] = useState<Betrieb>(VORGABE)
useEffect(() => {
let abgemeldet = false
api.get('/betrieb')
.then(antwort => {
const d = antwort.data
// Nur übernehmen, was WIRKLICH ankommt. Ein halb gefülltes Objekt
// hieße in JavaScript `undefined` — und `undefined` ist falsch, also
// verschwände ein Bereich stillschweigend.
if (abgemeldet || !d || !d.kann) return
setBetrieb({
modus: d.modus === 'verteilt' ? 'verteilt' : 'standalone',
plattform: d.plattform || 'linux',
im_container: !!d.im_container,
kann: {
externe_worker: !!d.kann.externe_worker,
freigaben_einhaengen: !!d.kann.freigaben_einhaengen,
container_pfade: !!d.kann.container_pfade,
werkzeuge_verwalten: !!d.kann.werkzeuge_verwalten,
frei_blaettern: !!d.kann.frei_blaettern,
},
ablage_vorgabe: d.ablage_vorgabe || '',
arbeits_vorgabe: d.arbeits_vorgabe || '',
hilfe_befehl: d.hilfe_befehl || '',
geladen: true,
})
})
.catch(() => {
// Nichts tun. Ein misslungener Abruf ist keine Aussage über den
// Betrieb — die Vorgabe bleibt stehen, `geladen` bleibt false.
})
return () => { abgemeldet = true }
}, [])
return <BetriebContext.Provider value={betrieb}>{children}</BetriebContext.Provider>
}
export function useBetrieb(): Betrieb {
return useContext(BetriebContext)
}
/**
* Kurzform für den häufigsten Fall: Läuft Rippy als eigenständige App
* (Windows-Client oder Docker-All-in-One)?
*
* Bewusst NICHT „ist Windows": Ein Docker-All-in-One hat auch keinen zweiten
* Worker. Wer nach der Plattform fragt, obwohl er die Fähigkeit meint, baut
* die nächste falsche Annahme ein.
*/
export function useAlleinbetrieb(): boolean {
return !useBetrieb().kann.externe_worker
}
/*
* Worauf läuft Rippy — und was kann dieser Betrieb überhaupt?
*
* ## Der Befund des Commanders (28.08.2026)
*
* Im Windows-Fenster stand auf der Server-Status-Kachel:
*
* Worker erreichbar: 0 von 1
* Kein Worker antwortet — ohne ihn läuft kein Rip. Prüfen: docker compose ps
* Container-Platte: unbekannt
* Freigaben: keine eingehängt
*
* Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
* Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt dort
* selbst. Sein Urteil: „Du hast ja quasi nur rippy genommen und die docker
* installation für Windows gebaut."
*
* ## Warum ein Provider und kein Abruf je Seite
*
* Dieselbe Begründung wie beim Ereignis-Strom nebenan: Vier Seiten, die
* dasselbe abfragen, sind vier Abrufe — und vier Gelegenheiten, dass eine
* davon einen anderen Stand hat als die anderen. Der Betrieb ändert sich zur
* Laufzeit nicht; er wird EINMAL geholt.
*
* ## Warum die Vorgabe „Docker" ist
*
* Solange die Antwort noch unterwegs ist, muss irgendetwas gelten. Die
* Docker-Annahme ist hier die richtige Vorgabe — sie zeigt MEHR, und ein kurz
* zu viel angezeigter Bereich ist harmloser als ein Bedienelement, das für
* einen Augenblick verschwindet und wieder auftaucht.
*
* Ein FEHLGESCHLAGENER Abruf ist etwas anderes als „noch unterwegs":
* `geladen` bleibt dann false, und wer das wissen will, kann es abfragen. Ein
* Verbindungsabriss ist keine Aussage über die Welt (siehe useEventStream).
*/
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { api } from './api'
export interface BetriebsFaehigkeiten {
/** Gibt es andere Maschinen, die Jobs übernehmen? */
externe_worker: boolean
/** Kann Rippy Netzwerk-Freigaben selbst einhängen? */
freigaben_einhaengen: boolean
/** Sind Pfade wie /app/media überhaupt gemeint? */
container_pfade: boolean
/** Kann Rippy MakeMKV/HandBrake selbst beschaffen? */
werkzeuge_verwalten: boolean
/** Darf die Oberflaeche ausserhalb der Medien-Wurzel blaettern? */
frei_blaettern: boolean
}
export interface Betrieb {
modus: 'standalone' | 'verteilt'
plattform: 'windows' | 'linux' | 'macos'
im_container: boolean
kann: BetriebsFaehigkeiten
/** Womit ein Ablage-Feld vorbelegt wird — je Betrieb ein anderer Ort. */
ablage_vorgabe: string
/** Wohin die Rohdaten wandern, wenn niemand etwas anderes waehlt. */
arbeits_vorgabe: string
/** Der Befehl zum Nachsehen. LEER heißt: es gibt keinen, den der Nutzer
* ausführen könnte — dann darf auch keiner dastehen. */
hilfe_befehl: string
/** Ist die Auskunft schon da? False heißt „noch unterwegs ODER nicht
* erreichbar" — nicht „es gibt keinen Betrieb". */
geladen: boolean
}
const VORGABE: Betrieb = {
modus: 'verteilt',
plattform: 'linux',
im_container: true,
kann: {
externe_worker: true,
freigaben_einhaengen: true,
container_pfade: true,
werkzeuge_verwalten: false,
frei_blaettern: false,
},
ablage_vorgabe: '/app/media',
arbeits_vorgabe: '/app/temp',
hilfe_befehl: 'docker compose -p rippy ps',
geladen: false,
}
const BetriebContext = createContext<Betrieb>(VORGABE)
export function BetriebProvider({ children }: { children: ReactNode }) {
const [betrieb, setBetrieb] = useState<Betrieb>(VORGABE)
useEffect(() => {
let abgemeldet = false
api.get('/betrieb')
.then(antwort => {
const d = antwort.data
// Nur übernehmen, was WIRKLICH ankommt. Ein halb gefülltes Objekt
// hieße in JavaScript `undefined` — und `undefined` ist falsch, also
// verschwände ein Bereich stillschweigend.
if (abgemeldet || !d || !d.kann) return
setBetrieb({
modus: d.modus === 'verteilt' ? 'verteilt' : 'standalone',
plattform: d.plattform || 'linux',
im_container: !!d.im_container,
kann: {
externe_worker: !!d.kann.externe_worker,
freigaben_einhaengen: !!d.kann.freigaben_einhaengen,
container_pfade: !!d.kann.container_pfade,
werkzeuge_verwalten: !!d.kann.werkzeuge_verwalten,
frei_blaettern: !!d.kann.frei_blaettern,
},
ablage_vorgabe: d.ablage_vorgabe || '',
arbeits_vorgabe: d.arbeits_vorgabe || '',
hilfe_befehl: d.hilfe_befehl || '',
geladen: true,
})
})
.catch(() => {
// Nichts tun. Ein misslungener Abruf ist keine Aussage über den
// Betrieb — die Vorgabe bleibt stehen, `geladen` bleibt false.
})
return () => { abgemeldet = true }
}, [])
return <BetriebContext.Provider value={betrieb}>{children}</BetriebContext.Provider>
}
export function useBetrieb(): Betrieb {
return useContext(BetriebContext)
}
/**
* Kurzform für den häufigsten Fall: Läuft Rippy als eigenständige App
* (Windows-Client oder Docker-All-in-One)?
*
* Bewusst NICHT „ist Windows": Ein Docker-All-in-One hat auch keinen zweiten
* Worker. Wer nach der Plattform fragt, obwohl er die Fähigkeit meint, baut
* die nächste falsche Annahme ein.
*/
export function useAlleinbetrieb(): boolean {
return !useBetrieb().kann.externe_worker
}
+224 -224
View File
@@ -1,224 +1,224 @@
"""Kind-Prozesse ohne Fenster — und die Ausgabe ohne Konsole.
Ein Konsolenfenster laesst sich in einem Test nicht ansehen. Geprueft wird
deshalb das, woran es beim ersten Anlauf gescheitert ist: die ENTSCHEIDUNG,
wohin die Ausgabe geht, wenn es keine Konsole gibt.
## Warum das nicht Nebensache ist
Die EXE wird seit dem 28.08.2026 als Fenster-Programm gebaut, damit beim
Doppelklick keine schwarze Box aufgeht. Damit ist `sys.stdout` aber `None`.
Ohne Umleitung wuerde jede Bibliothek, die auf `stderr` schreibt, mit
`AttributeError: 'NoneType' object has no attribute 'write'` sterben — mitten
im Start, und nirgends stuende etwas. Genau der stille Fehlschlag, vor dem
AGENTS.md warnt.
"""
import os
import subprocess
import sys
import pytest
from rippy.platform import winlauf
nur_windows = pytest.mark.skipif(os.name != "nt", reason="nur unter Windows")
# ── Kind-Prozesse ───────────────────────────────────────────────────────
def test_flag_ist_auf_windows_gesetzt_und_sonst_null():
"""Auf Linux ist CREATE_NO_WINDOW nicht vorhanden; `creationflags=0` wird
dort akzeptiert und ignoriert. So laeuft derselbe Code auf beiden Seiten."""
if os.name == "nt":
assert winlauf.OHNE_FENSTER == subprocess.CREATE_NO_WINDOW
else:
assert winlauf.OHNE_FENSTER == 0
# ── Gibt es ueberhaupt eine Ausgabe? ────────────────────────────────────
def test_fehlende_standardausgabe_wird_erkannt(monkeypatch):
"""Der Fall in der fertigen EXE: PyInstaller setzt `sys.stdout` auf None."""
monkeypatch.setattr(sys, "stdout", None)
assert winlauf.ohne_konsole() is True
def test_ein_strom_ohne_dateinummer_zaehlt_auch_als_ohne_konsole(monkeypatch):
"""pytest ersetzt stdout durch einen Auffang-Puffer. Der hat kein
`fileno` — und ein Meldungsfenster waere in einem Testlauf das Letzte,
was jemand gebrauchen kann."""
class Puffer:
write = staticmethod(lambda _: None)
monkeypatch.setattr(sys, "stdout", Puffer())
assert winlauf.ohne_konsole() is True
def test_mit_echter_ausgabe_ist_alles_in_ordnung(monkeypatch, tmp_path):
datei = tmp_path / "echt.txt"
with open(datei, "w", encoding="utf-8") as f:
monkeypatch.setattr(sys, "stdout", f)
assert winlauf.ohne_konsole() is False
# ── Die Protokolldatei ──────────────────────────────────────────────────
def test_protokoll_liegt_bei_rippy():
"""Nicht im TEMP: Was Rippy ueber sich aufschreibt, gehoert zu Rippy und
verschwindet beim Deinstallieren mit."""
pfad = winlauf.protokolldatei(os.path.join("C:" + os.sep, "Basis"))
assert pfad.endswith(os.path.join("Rippy", "rippy.log"))
assert pfad.startswith(os.path.join("C:" + os.sep, "Basis"))
def test_umleiten_schreibt_wirklich(tmp_path, monkeypatch):
ziel = str(tmp_path / "unterordner" / "rippy.log")
vorher_out, vorher_err = sys.stdout, sys.stderr
try:
assert winlauf.ausgabe_umleiten(ziel) == ziel
print("eine Zeile mit Umlaut: ä")
sys.stdout.flush()
finally:
sys.stdout, sys.stderr = vorher_out, vorher_err
assert "eine Zeile mit Umlaut" in open(ziel, encoding="utf-8").read()
def test_umleiten_haengt_an_statt_zu_ueberschreiben(tmp_path):
"""Sonst waere nach jedem Neustart das Protokoll des letzten Fehlers weg
— also genau das, was man dann sucht."""
ziel = str(tmp_path / "rippy.log")
vorher_out, vorher_err = sys.stdout, sys.stderr
try:
winlauf.ausgabe_umleiten(ziel)
print("erster Start")
sys.stdout.flush()
winlauf.ausgabe_umleiten(ziel)
print("zweiter Start")
sys.stdout.flush()
finally:
sys.stdout, sys.stderr = vorher_out, vorher_err
inhalt = open(ziel, encoding="utf-8").read()
assert "erster Start" in inhalt and "zweiter Start" in inhalt
def test_unbeschreibbarer_ort_laesst_den_start_nicht_platzen(monkeypatch):
"""Lieber ins Nichts schreiben als beim Start sterben. Ein Programm, das
wegen seiner Protokolldatei nicht hochkommt, ist schlimmer als eines
ohne Protokoll."""
def geht_nicht(*a, **k):
raise OSError("kein Platz")
monkeypatch.setattr(winlauf.os, "makedirs", geht_nicht)
vorher_out, vorher_err = sys.stdout, sys.stderr
try:
winlauf.ausgabe_umleiten("/gibt/es/nicht/rippy.log")
print("das darf nicht werfen")
finally:
sys.stdout, sys.stderr = vorher_out, vorher_err
# ── Das Meldungsfenster ─────────────────────────────────────────────────
def test_ohne_windows_kein_meldungsfenster():
if os.name == "nt":
pytest.skip("prueft das Verhalten auf Nicht-Windows")
assert winlauf.meldung_zeigen("egal") is False
assert winlauf.an_elternkonsole_haengen() is False
# ── Der Auspack-Ordner (Befund 28.08.2026) ──────────────────────────────
#
# Commander, mit Bildschirmfoto:
#
# Failed to remove temporary directory:
# C:\Users\TobisPC\AppData\Local\Temp\_MEI0000b0882
#
# „Und manchmal kommt dieser fehler."
#
# GEMESSEN in seinem Temp-Ordner: 20 zurueckgelassene _MEI-Ordner, zusammen
# 1,1 GB. Der aus der Meldung liess sich hinterher anstandslos loeschen — die
# Sperre war also voruebergehend, es ist ein Wettlauf.
#
# Ursache: `starte_hintergrund` und `starte_fensterprozess` starten Rippy.exe
# erneut. `Popen` ohne `env=` reicht PyInstallers Auspack-Zeiger weiter, also
# laufen beide Kinder im Ordner des Elternprozesses. Der beendet sich zuerst,
# will loeschen — und die Kinder halten die DLLs offen.
def test_der_auspack_zeiger_wird_dem_kind_NICHT_mitgegeben():
"""Der Test, der 1,1 GB Reste verhindert haette."""
eltern = {"PATH": "/usr/bin", "_MEIPASS2": "/tmp/_MEI123",
"_PYI_APPLICATION_HOME_DIR": "/tmp/_MEI123",
"_PYI_ARCHIVE_FILE": "/x/Rippy.exe",
"_PYI_PARENT_PROCESS_LEVEL": "1"}
kind = winlauf.umgebung_ohne_bundle(eltern)
for name in winlauf.PYI_ZEIGER:
assert name not in kind, "%s wuerde das Kind in den Ordner der Eltern schicken" % name
assert kind["PATH"] == "/usr/bin", "der Rest der Umgebung muss bleiben"
def test_die_elternumgebung_wird_nicht_veraendert():
"""Ein `del os.environ[...]` haette den eigenen Prozess beschaedigt."""
eltern = {"_MEIPASS2": "/tmp/_MEI123"}
winlauf.umgebung_ohne_bundle(eltern)
assert eltern == {"_MEIPASS2": "/tmp/_MEI123"}
def test_beide_startwege_reichen_den_zeiger_nicht_weiter():
"""Der Waechter: Wer kuenftig einen dritten Startweg baut, muss `env=`
setzen. Sonst kommen die Reste zurueck, und zwar still."""
import re
quelle = os.path.join(os.path.dirname(__file__), "..", "windows_app.py")
with open(os.path.abspath(quelle), encoding="utf-8") as f:
text = f.read()
stellen = [m.start() for m in re.finditer(r"subprocess\.Popen\(\[programm", text)]
assert len(stellen) == 2, "Startwege haben sich geaendert: %d gefunden" % len(stellen)
for stelle in stellen:
aufruf = text[stelle:stelle + 300]
assert "env=umgebung_ohne_bundle()" in aufruf, (
"Dieser Start reicht den Auspack-Zeiger weiter:\n" + aufruf[:120])
def test_aufraeumen_laesst_einen_BENUTZTEN_ordner_unangetastet(tmp_path):
"""Die wichtigste Eigenschaft: Ein laufender Rippy darf nichts verlieren.
Ein blindes `rmtree(ignore_errors=True)` haette ihm die halbe Bibliothek
weggeraeumt, bevor es an der gesperrten DLL scheitert.
"""
lebt = tmp_path / "_MEI111111"
lebt.mkdir()
(lebt / "python312.dll").write_bytes(b"MZ")
(lebt / "wichtig.pyd").write_bytes(b"x")
def gesperrt(pfad):
raise OSError(32, "Datei wird von einem anderen Prozess verwendet")
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="",
jetzt_loeschen=gesperrt) == 0
assert (lebt / "wichtig.pyd").exists(), "an einem benutzten Ordner wird NICHTS angefasst"
def test_aufraeumen_entfernt_die_reste(tmp_path):
for name in ("_MEI000035002", "_MEI0000b0882"):
ordner = tmp_path / name
ordner.mkdir()
(ordner / "python312.dll").write_bytes(b"MZ")
(ordner / "base_library.zip").write_bytes(b"PK")
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 2
assert not list(tmp_path.glob("_MEI*"))
def test_aufraeumen_raeumt_den_EIGENEN_ordner_nicht_weg(tmp_path):
"""Sonst saegte Rippy waehrend des Startens an seinem eigenen Ast."""
eigen = tmp_path / "_MEI999999"
eigen.mkdir()
(eigen / "python312.dll").write_bytes(b"MZ")
assert winlauf.reste_aufraeumen(str(tmp_path), eigener=str(eigen)) == 0
assert eigen.exists()
def test_aufraeumen_ignoriert_fremde_ordner(tmp_path):
"""Was nicht `_MEI` heisst, geht uns nichts an."""
(tmp_path / "wichtige-daten").mkdir()
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 0
assert (tmp_path / "wichtige-daten").exists()
"""Kind-Prozesse ohne Fenster — und die Ausgabe ohne Konsole.
Ein Konsolenfenster laesst sich in einem Test nicht ansehen. Geprueft wird
deshalb das, woran es beim ersten Anlauf gescheitert ist: die ENTSCHEIDUNG,
wohin die Ausgabe geht, wenn es keine Konsole gibt.
## Warum das nicht Nebensache ist
Die EXE wird seit dem 28.08.2026 als Fenster-Programm gebaut, damit beim
Doppelklick keine schwarze Box aufgeht. Damit ist `sys.stdout` aber `None`.
Ohne Umleitung wuerde jede Bibliothek, die auf `stderr` schreibt, mit
`AttributeError: 'NoneType' object has no attribute 'write'` sterben — mitten
im Start, und nirgends stuende etwas. Genau der stille Fehlschlag, vor dem
AGENTS.md warnt.
"""
import os
import subprocess
import sys
import pytest
from rippy.platform import winlauf
nur_windows = pytest.mark.skipif(os.name != "nt", reason="nur unter Windows")
# ── Kind-Prozesse ───────────────────────────────────────────────────────
def test_flag_ist_auf_windows_gesetzt_und_sonst_null():
"""Auf Linux ist CREATE_NO_WINDOW nicht vorhanden; `creationflags=0` wird
dort akzeptiert und ignoriert. So laeuft derselbe Code auf beiden Seiten."""
if os.name == "nt":
assert winlauf.OHNE_FENSTER == subprocess.CREATE_NO_WINDOW
else:
assert winlauf.OHNE_FENSTER == 0
# ── Gibt es ueberhaupt eine Ausgabe? ────────────────────────────────────
def test_fehlende_standardausgabe_wird_erkannt(monkeypatch):
"""Der Fall in der fertigen EXE: PyInstaller setzt `sys.stdout` auf None."""
monkeypatch.setattr(sys, "stdout", None)
assert winlauf.ohne_konsole() is True
def test_ein_strom_ohne_dateinummer_zaehlt_auch_als_ohne_konsole(monkeypatch):
"""pytest ersetzt stdout durch einen Auffang-Puffer. Der hat kein
`fileno` — und ein Meldungsfenster waere in einem Testlauf das Letzte,
was jemand gebrauchen kann."""
class Puffer:
write = staticmethod(lambda _: None)
monkeypatch.setattr(sys, "stdout", Puffer())
assert winlauf.ohne_konsole() is True
def test_mit_echter_ausgabe_ist_alles_in_ordnung(monkeypatch, tmp_path):
datei = tmp_path / "echt.txt"
with open(datei, "w", encoding="utf-8") as f:
monkeypatch.setattr(sys, "stdout", f)
assert winlauf.ohne_konsole() is False
# ── Die Protokolldatei ──────────────────────────────────────────────────
def test_protokoll_liegt_bei_rippy():
"""Nicht im TEMP: Was Rippy ueber sich aufschreibt, gehoert zu Rippy und
verschwindet beim Deinstallieren mit."""
pfad = winlauf.protokolldatei(os.path.join("C:" + os.sep, "Basis"))
assert pfad.endswith(os.path.join("Rippy", "rippy.log"))
assert pfad.startswith(os.path.join("C:" + os.sep, "Basis"))
def test_umleiten_schreibt_wirklich(tmp_path, monkeypatch):
ziel = str(tmp_path / "unterordner" / "rippy.log")
vorher_out, vorher_err = sys.stdout, sys.stderr
try:
assert winlauf.ausgabe_umleiten(ziel) == ziel
print("eine Zeile mit Umlaut: ä")
sys.stdout.flush()
finally:
sys.stdout, sys.stderr = vorher_out, vorher_err
assert "eine Zeile mit Umlaut" in open(ziel, encoding="utf-8").read()
def test_umleiten_haengt_an_statt_zu_ueberschreiben(tmp_path):
"""Sonst waere nach jedem Neustart das Protokoll des letzten Fehlers weg
— also genau das, was man dann sucht."""
ziel = str(tmp_path / "rippy.log")
vorher_out, vorher_err = sys.stdout, sys.stderr
try:
winlauf.ausgabe_umleiten(ziel)
print("erster Start")
sys.stdout.flush()
winlauf.ausgabe_umleiten(ziel)
print("zweiter Start")
sys.stdout.flush()
finally:
sys.stdout, sys.stderr = vorher_out, vorher_err
inhalt = open(ziel, encoding="utf-8").read()
assert "erster Start" in inhalt and "zweiter Start" in inhalt
def test_unbeschreibbarer_ort_laesst_den_start_nicht_platzen(monkeypatch):
"""Lieber ins Nichts schreiben als beim Start sterben. Ein Programm, das
wegen seiner Protokolldatei nicht hochkommt, ist schlimmer als eines
ohne Protokoll."""
def geht_nicht(*a, **k):
raise OSError("kein Platz")
monkeypatch.setattr(winlauf.os, "makedirs", geht_nicht)
vorher_out, vorher_err = sys.stdout, sys.stderr
try:
winlauf.ausgabe_umleiten("/gibt/es/nicht/rippy.log")
print("das darf nicht werfen")
finally:
sys.stdout, sys.stderr = vorher_out, vorher_err
# ── Das Meldungsfenster ─────────────────────────────────────────────────
def test_ohne_windows_kein_meldungsfenster():
if os.name == "nt":
pytest.skip("prueft das Verhalten auf Nicht-Windows")
assert winlauf.meldung_zeigen("egal") is False
assert winlauf.an_elternkonsole_haengen() is False
# ── Der Auspack-Ordner (Befund 28.08.2026) ──────────────────────────────
#
# Commander, mit Bildschirmfoto:
#
# Failed to remove temporary directory:
# C:\Users\TobisPC\AppData\Local\Temp\_MEI0000b0882
#
# „Und manchmal kommt dieser fehler."
#
# GEMESSEN in seinem Temp-Ordner: 20 zurueckgelassene _MEI-Ordner, zusammen
# 1,1 GB. Der aus der Meldung liess sich hinterher anstandslos loeschen — die
# Sperre war also voruebergehend, es ist ein Wettlauf.
#
# Ursache: `starte_hintergrund` und `starte_fensterprozess` starten Rippy.exe
# erneut. `Popen` ohne `env=` reicht PyInstallers Auspack-Zeiger weiter, also
# laufen beide Kinder im Ordner des Elternprozesses. Der beendet sich zuerst,
# will loeschen — und die Kinder halten die DLLs offen.
def test_der_auspack_zeiger_wird_dem_kind_NICHT_mitgegeben():
"""Der Test, der 1,1 GB Reste verhindert haette."""
eltern = {"PATH": "/usr/bin", "_MEIPASS2": "/tmp/_MEI123",
"_PYI_APPLICATION_HOME_DIR": "/tmp/_MEI123",
"_PYI_ARCHIVE_FILE": "/x/Rippy.exe",
"_PYI_PARENT_PROCESS_LEVEL": "1"}
kind = winlauf.umgebung_ohne_bundle(eltern)
for name in winlauf.PYI_ZEIGER:
assert name not in kind, "%s wuerde das Kind in den Ordner der Eltern schicken" % name
assert kind["PATH"] == "/usr/bin", "der Rest der Umgebung muss bleiben"
def test_die_elternumgebung_wird_nicht_veraendert():
"""Ein `del os.environ[...]` haette den eigenen Prozess beschaedigt."""
eltern = {"_MEIPASS2": "/tmp/_MEI123"}
winlauf.umgebung_ohne_bundle(eltern)
assert eltern == {"_MEIPASS2": "/tmp/_MEI123"}
def test_beide_startwege_reichen_den_zeiger_nicht_weiter():
"""Der Waechter: Wer kuenftig einen dritten Startweg baut, muss `env=`
setzen. Sonst kommen die Reste zurueck, und zwar still."""
import re
quelle = os.path.join(os.path.dirname(__file__), "..", "windows_app.py")
with open(os.path.abspath(quelle), encoding="utf-8") as f:
text = f.read()
stellen = [m.start() for m in re.finditer(r"subprocess\.Popen\(\[programm", text)]
assert len(stellen) == 2, "Startwege haben sich geaendert: %d gefunden" % len(stellen)
for stelle in stellen:
aufruf = text[stelle:stelle + 300]
assert "env=umgebung_ohne_bundle()" in aufruf, (
"Dieser Start reicht den Auspack-Zeiger weiter:\n" + aufruf[:120])
def test_aufraeumen_laesst_einen_BENUTZTEN_ordner_unangetastet(tmp_path):
"""Die wichtigste Eigenschaft: Ein laufender Rippy darf nichts verlieren.
Ein blindes `rmtree(ignore_errors=True)` haette ihm die halbe Bibliothek
weggeraeumt, bevor es an der gesperrten DLL scheitert.
"""
lebt = tmp_path / "_MEI111111"
lebt.mkdir()
(lebt / "python312.dll").write_bytes(b"MZ")
(lebt / "wichtig.pyd").write_bytes(b"x")
def gesperrt(pfad):
raise OSError(32, "Datei wird von einem anderen Prozess verwendet")
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="",
jetzt_loeschen=gesperrt) == 0
assert (lebt / "wichtig.pyd").exists(), "an einem benutzten Ordner wird NICHTS angefasst"
def test_aufraeumen_entfernt_die_reste(tmp_path):
for name in ("_MEI000035002", "_MEI0000b0882"):
ordner = tmp_path / name
ordner.mkdir()
(ordner / "python312.dll").write_bytes(b"MZ")
(ordner / "base_library.zip").write_bytes(b"PK")
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 2
assert not list(tmp_path.glob("_MEI*"))
def test_aufraeumen_raeumt_den_EIGENEN_ordner_nicht_weg(tmp_path):
"""Sonst saegte Rippy waehrend des Startens an seinem eigenen Ast."""
eigen = tmp_path / "_MEI999999"
eigen.mkdir()
(eigen / "python312.dll").write_bytes(b"MZ")
assert winlauf.reste_aufraeumen(str(tmp_path), eigener=str(eigen)) == 0
assert eigen.exists()
def test_aufraeumen_ignoriert_fremde_ordner(tmp_path):
"""Was nicht `_MEI` heisst, geht uns nichts an."""
(tmp_path / "wichtige-daten").mkdir()
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 0
assert (tmp_path / "wichtige-daten").exists()
+600 -600
View File
File diff suppressed because it is too large Load Diff
+193 -193
View File
@@ -1,193 +1,193 @@
"""Der Betriebsmodus und seine Faehigkeiten.
## Warum es diese Tests gibt
Commander am 28.08.2026, zum Windows-Fenster:
Worker erreichbar: 0 von 1
Kein Worker antwortet — Pruefen: docker compose ps
Container-Platte: unbekannt
Freigaben: keine eingehaengt
Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Die Oberflaeche hatte
nie erfahren, worauf sie laeuft — also hat sie angenommen.
Alle Pruefungen hier sind eingespritzt: Jeder Betriebsfall ist pruefbar, ohne
ihn herzustellen. Das ist der Punkt — sonst waere „laeuft im Container" nur
auf einem Container-Rechner testbar, und genau dort faellt der Windows-Fehler
nie auf.
"""
from rippy import betrieb
DOCKER = {"profil": "api", "queue": {"treiber": "celery", "broker": "redis://x"}}
WINDOWS = {"profil": "standalone", "queue": {"treiber": "lokal"}}
# ── Plattform ───────────────────────────────────────────────────────────
def test_plattform_wird_zu_einem_klaren_namen():
assert betrieb.plattform("win32") == "windows"
assert betrieb.plattform("linux") == "linux"
assert betrieb.plattform("darwin") == "macos"
def test_unbekannte_plattform_gilt_als_linux():
"""FreeBSD und Verwandte verhalten sich wie Linux — und ein Rueckfall auf
etwas Bekanntes ist besser als ein leeres Feld."""
assert betrieb.plattform("freebsd14") == "linux"
# ── Container ───────────────────────────────────────────────────────────
def test_container_wird_gemessen_nicht_geraten():
"""`/.dockerenv` legt Docker selbst an — verlaesslicher als eine
Umgebungsvariable, die jeder setzen und vergessen kann."""
assert betrieb.im_container(lambda p: p == "/.dockerenv") is True
assert betrieb.im_container(lambda p: False) is False
# ── Modus ───────────────────────────────────────────────────────────────
def test_celery_heisst_verteilt():
assert betrieb.modus(DOCKER) == "verteilt"
def test_lokale_queue_heisst_standalone():
assert betrieb.modus(WINDOWS) == "standalone"
def test_ohne_angaben_gilt_standalone():
"""Der harmlosere Rueckfall: Ein standalone-Rippy zeigt nichts an, was es
nicht gibt. Andersherum entstuenden die Phantom-Worker wieder."""
assert betrieb.modus({}) == "standalone"
# ── Die Faehigkeiten — hier sass der gemeldete Fehler ───────────────────
def test_windows_hat_KEINE_externen_worker():
"""DER Befund: „Worker erreichbar: 0 von 1 — Kein Worker antwortet."
Unter Windows rippt Rippy selbst; es gibt keinen zweiten, auf den man
warten koennte. Die Meldung war nicht nur unpassend, sie war falsch."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["externe_worker"] is False
def test_windows_kennt_KEINE_container_pfade():
"""`/app/media` ist ein Pfad IM Container. Auf Windows heisst der Ordner
anders und liegt woanders — ein damit vorbelegtes Feld ist schlicht
falsch."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["container_pfade"] is False
assert "/app/" not in a["ablage_vorgabe"]
def test_windows_haengt_keine_freigaben_ein():
"""Unter Windows gibt man einen UNC-Pfad an, fertig. Eine Maske zum
Einhaengen waere eine Bedienung, die ins Leere fuehrt."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["freigaben_einhaengen"] is False
def test_windows_darf_seine_werkzeuge_selbst_pflegen():
"""Im Image steckt alles fest, ein Update ist ein Rebuild. Nur die native
Installation kann MakeMKV/HandBrake selbst beschaffen."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["werkzeuge_verwalten"] is True
def test_docker_behaelt_alles_was_es_hatte():
"""Die Windows-Reparatur darf dem Docker-Betrieb nichts wegnehmen."""
a = betrieb.auskunft(DOCKER, container=True, plattform_name="linux")
assert a["kann"]["externe_worker"] is True
assert a["kann"]["container_pfade"] is True
assert a["kann"]["freigaben_einhaengen"] is True
assert a["ablage_vorgabe"] == "/app/media"
assert a["hilfe_befehl"] == "docker compose -p rippy ps"
def test_der_docker_hinweis_taucht_ausserhalb_von_docker_NICHT_auf():
"""„Pruefen: docker compose ps" auf einem Windows-PC ist ein Rat, den
niemand befolgen kann."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["hilfe_befehl"] == ""
def test_docker_allinone_hat_container_aber_keine_externen_worker():
"""Der Fall, an dem ein blosser Modus-Name zerbrechen wuerde: EIN
Container, der alles selbst macht. Er hat Container-Pfade, aber niemanden,
der von aussen Jobs uebernimmt."""
a = betrieb.auskunft({"profil": "standalone", "queue": {"treiber": "lokal"}},
container=True, plattform_name="linux")
assert a["kann"]["container_pfade"] is True
assert a["kann"]["externe_worker"] is False
# ── Die Ablage-Vorgabe ──────────────────────────────────────────────────
def test_eingestellte_ablage_schlaegt_jede_vorgabe():
a = betrieb.auskunft({"storage": {"medien": "D:\\Filme"}},
container=False, plattform_name="win32")
assert a["ablage_vorgabe"] == "D:\\Filme"
def test_windows_vorgabe_liegt_beim_nutzer(monkeypatch):
monkeypatch.setenv("USERPROFILE", "C:\\Users\\Test")
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["ablage_vorgabe"].startswith("C:\\Users\\Test")
assert a["ablage_vorgabe"].endswith("Rippy")
# ── Der Vertrag als Ganzes ──────────────────────────────────────────────
def test_die_auskunft_ist_vollstaendig():
"""Ein fehlendes Feld waere im UI ein `undefined` — und `undefined` ist
in JavaScript falsch, also verschwaende die Oberflaeche stillschweigend
einen Bereich. Lieber hier rot."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert set(a) == {"modus", "plattform", "im_container", "kann",
"ablage_vorgabe", "arbeits_vorgabe", "hilfe_befehl"}
assert set(a["kann"]) == {"externe_worker", "freigaben_einhaengen",
"container_pfade", "werkzeuge_verwalten",
"frei_blaettern"}
assert all(isinstance(v, bool) for v in a["kann"].values())
# ── Wo Platz gemessen wird ──────────────────────────────────────────────
def test_windows_misst_NICHT_in_app_media():
"""DER Befund: Im Windows-Fenster stand „Platz fuer Rippy: unbekannt".
`main.py` mass fest an `/app/media` und `/app/temp`. Beide gibt es unter
Windows nicht, `shutil.disk_usage` warf, die Liste blieb leer — und aus
einer Nichtauskunft wurde eine Anzeige, die wie eine Auskunft aussieht.
"""
orte = betrieb.platz_orte(WINDOWS, container=False, plattform_name="win32")
assert orte, "es muss mindestens ein Ort gemessen werden"
assert all("/app/" not in o["pfad"] for o in orte), orte
def test_docker_misst_weiter_die_container_pfade():
orte = betrieb.platz_orte(DOCKER, container=True, plattform_name="linux")
assert [o["pfad"] for o in orte] == ["/app/media", "/app/temp"]
def test_gleiches_laufwerk_wird_nicht_doppelt_gezaehlt():
"""Zweimal dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskuenfte
und ist eine."""
orte = betrieb.platz_orte({"storage": {"medien": r"D:\Filme"}},
container=False, plattform_name="win32")
assert len(orte) == 1, orte
def test_zweites_laufwerk_bekommt_eine_eigene_zeile():
orte = betrieb.platz_orte(
{"storage": {"medien": r"D:\Filme", "temp": r"E:\Arbeit"}},
container=False, plattform_name="win32")
assert len(orte) == 2, orte
def test_noch_nicht_angelegter_ordner_faellt_auf_das_laufwerk_zurueck():
"""Frisch installiert gibt es den Ablage-Ordner noch nicht. Der Nutzer
will trotzdem wissen, ob auf dem LAUFWERK Platz ist."""
da = r"C:\Users\Test"
assert betrieb.naechster_vorhandener(
r"C:\Users\Test\Videos\Rippy", existiert=lambda p: p == da) == da
def test_wenn_gar_nichts_existiert_wird_nichts_behauptet():
assert betrieb.naechster_vorhandener(r"Z:\gibt\es\nicht",
existiert=lambda p: False) == ""
"""Der Betriebsmodus und seine Faehigkeiten.
## Warum es diese Tests gibt
Commander am 28.08.2026, zum Windows-Fenster:
Worker erreichbar: 0 von 1
Kein Worker antwortet — Pruefen: docker compose ps
Container-Platte: unbekannt
Freigaben: keine eingehaengt
Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Die Oberflaeche hatte
nie erfahren, worauf sie laeuft — also hat sie angenommen.
Alle Pruefungen hier sind eingespritzt: Jeder Betriebsfall ist pruefbar, ohne
ihn herzustellen. Das ist der Punkt — sonst waere „laeuft im Container" nur
auf einem Container-Rechner testbar, und genau dort faellt der Windows-Fehler
nie auf.
"""
from rippy import betrieb
DOCKER = {"profil": "api", "queue": {"treiber": "celery", "broker": "redis://x"}}
WINDOWS = {"profil": "standalone", "queue": {"treiber": "lokal"}}
# ── Plattform ───────────────────────────────────────────────────────────
def test_plattform_wird_zu_einem_klaren_namen():
assert betrieb.plattform("win32") == "windows"
assert betrieb.plattform("linux") == "linux"
assert betrieb.plattform("darwin") == "macos"
def test_unbekannte_plattform_gilt_als_linux():
"""FreeBSD und Verwandte verhalten sich wie Linux — und ein Rueckfall auf
etwas Bekanntes ist besser als ein leeres Feld."""
assert betrieb.plattform("freebsd14") == "linux"
# ── Container ───────────────────────────────────────────────────────────
def test_container_wird_gemessen_nicht_geraten():
"""`/.dockerenv` legt Docker selbst an — verlaesslicher als eine
Umgebungsvariable, die jeder setzen und vergessen kann."""
assert betrieb.im_container(lambda p: p == "/.dockerenv") is True
assert betrieb.im_container(lambda p: False) is False
# ── Modus ───────────────────────────────────────────────────────────────
def test_celery_heisst_verteilt():
assert betrieb.modus(DOCKER) == "verteilt"
def test_lokale_queue_heisst_standalone():
assert betrieb.modus(WINDOWS) == "standalone"
def test_ohne_angaben_gilt_standalone():
"""Der harmlosere Rueckfall: Ein standalone-Rippy zeigt nichts an, was es
nicht gibt. Andersherum entstuenden die Phantom-Worker wieder."""
assert betrieb.modus({}) == "standalone"
# ── Die Faehigkeiten — hier sass der gemeldete Fehler ───────────────────
def test_windows_hat_KEINE_externen_worker():
"""DER Befund: „Worker erreichbar: 0 von 1 — Kein Worker antwortet."
Unter Windows rippt Rippy selbst; es gibt keinen zweiten, auf den man
warten koennte. Die Meldung war nicht nur unpassend, sie war falsch."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["externe_worker"] is False
def test_windows_kennt_KEINE_container_pfade():
"""`/app/media` ist ein Pfad IM Container. Auf Windows heisst der Ordner
anders und liegt woanders — ein damit vorbelegtes Feld ist schlicht
falsch."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["container_pfade"] is False
assert "/app/" not in a["ablage_vorgabe"]
def test_windows_haengt_keine_freigaben_ein():
"""Unter Windows gibt man einen UNC-Pfad an, fertig. Eine Maske zum
Einhaengen waere eine Bedienung, die ins Leere fuehrt."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["freigaben_einhaengen"] is False
def test_windows_darf_seine_werkzeuge_selbst_pflegen():
"""Im Image steckt alles fest, ein Update ist ein Rebuild. Nur die native
Installation kann MakeMKV/HandBrake selbst beschaffen."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["kann"]["werkzeuge_verwalten"] is True
def test_docker_behaelt_alles_was_es_hatte():
"""Die Windows-Reparatur darf dem Docker-Betrieb nichts wegnehmen."""
a = betrieb.auskunft(DOCKER, container=True, plattform_name="linux")
assert a["kann"]["externe_worker"] is True
assert a["kann"]["container_pfade"] is True
assert a["kann"]["freigaben_einhaengen"] is True
assert a["ablage_vorgabe"] == "/app/media"
assert a["hilfe_befehl"] == "docker compose -p rippy ps"
def test_der_docker_hinweis_taucht_ausserhalb_von_docker_NICHT_auf():
"""„Pruefen: docker compose ps" auf einem Windows-PC ist ein Rat, den
niemand befolgen kann."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["hilfe_befehl"] == ""
def test_docker_allinone_hat_container_aber_keine_externen_worker():
"""Der Fall, an dem ein blosser Modus-Name zerbrechen wuerde: EIN
Container, der alles selbst macht. Er hat Container-Pfade, aber niemanden,
der von aussen Jobs uebernimmt."""
a = betrieb.auskunft({"profil": "standalone", "queue": {"treiber": "lokal"}},
container=True, plattform_name="linux")
assert a["kann"]["container_pfade"] is True
assert a["kann"]["externe_worker"] is False
# ── Die Ablage-Vorgabe ──────────────────────────────────────────────────
def test_eingestellte_ablage_schlaegt_jede_vorgabe():
a = betrieb.auskunft({"storage": {"medien": "D:\\Filme"}},
container=False, plattform_name="win32")
assert a["ablage_vorgabe"] == "D:\\Filme"
def test_windows_vorgabe_liegt_beim_nutzer(monkeypatch):
monkeypatch.setenv("USERPROFILE", "C:\\Users\\Test")
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert a["ablage_vorgabe"].startswith("C:\\Users\\Test")
assert a["ablage_vorgabe"].endswith("Rippy")
# ── Der Vertrag als Ganzes ──────────────────────────────────────────────
def test_die_auskunft_ist_vollstaendig():
"""Ein fehlendes Feld waere im UI ein `undefined` — und `undefined` ist
in JavaScript falsch, also verschwaende die Oberflaeche stillschweigend
einen Bereich. Lieber hier rot."""
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
assert set(a) == {"modus", "plattform", "im_container", "kann",
"ablage_vorgabe", "arbeits_vorgabe", "hilfe_befehl"}
assert set(a["kann"]) == {"externe_worker", "freigaben_einhaengen",
"container_pfade", "werkzeuge_verwalten",
"frei_blaettern"}
assert all(isinstance(v, bool) for v in a["kann"].values())
# ── Wo Platz gemessen wird ──────────────────────────────────────────────
def test_windows_misst_NICHT_in_app_media():
"""DER Befund: Im Windows-Fenster stand „Platz fuer Rippy: unbekannt".
`main.py` mass fest an `/app/media` und `/app/temp`. Beide gibt es unter
Windows nicht, `shutil.disk_usage` warf, die Liste blieb leer — und aus
einer Nichtauskunft wurde eine Anzeige, die wie eine Auskunft aussieht.
"""
orte = betrieb.platz_orte(WINDOWS, container=False, plattform_name="win32")
assert orte, "es muss mindestens ein Ort gemessen werden"
assert all("/app/" not in o["pfad"] for o in orte), orte
def test_docker_misst_weiter_die_container_pfade():
orte = betrieb.platz_orte(DOCKER, container=True, plattform_name="linux")
assert [o["pfad"] for o in orte] == ["/app/media", "/app/temp"]
def test_gleiches_laufwerk_wird_nicht_doppelt_gezaehlt():
"""Zweimal dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskuenfte
und ist eine."""
orte = betrieb.platz_orte({"storage": {"medien": r"D:\Filme"}},
container=False, plattform_name="win32")
assert len(orte) == 1, orte
def test_zweites_laufwerk_bekommt_eine_eigene_zeile():
orte = betrieb.platz_orte(
{"storage": {"medien": r"D:\Filme", "temp": r"E:\Arbeit"}},
container=False, plattform_name="win32")
assert len(orte) == 2, orte
def test_noch_nicht_angelegter_ordner_faellt_auf_das_laufwerk_zurueck():
"""Frisch installiert gibt es den Ablage-Ordner noch nicht. Der Nutzer
will trotzdem wissen, ob auf dem LAUFWERK Platz ist."""
da = r"C:\Users\Test"
assert betrieb.naechster_vorhandener(
r"C:\Users\Test\Videos\Rippy", existiert=lambda p: p == da) == da
def test_wenn_gar_nichts_existiert_wird_nichts_behauptet():
assert betrieb.naechster_vorhandener(r"Z:\gibt\es\nicht",
existiert=lambda p: False) == ""