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