58f4991d92
Ampel / ampel (push) Successful in 29s
Nachtrag, weil die letzte Runde die Wiederanbindung nicht schneller machte
(202 s statt 150 s). Die Zeitstempel im Log zeigten, wo die Zeit sitzt:
12:53:56 API gestartet
12:54:03 "antwortet nicht - wird neu verbunden" <- Erkennung: 7 s, gut
12:57:18 "eingehaengt" <- Reparatur: 3 min 15 s
Die Erkennung war also schon schnell; die REPARATUR fraess die Zeit. Und zwar
nicht in den Mount-Versuchen, sondern in der ersten Zeile von mounten():
os.makedirs(ziel, exist_ok=True)
`exist_ok` prueft mit os.path.isdir, und ein `stat` auf einen toten CIFS-Mount
blockiert im Kernel bis zum SMB-Timeout. Ausgerechnet der Aufruf, der nur "lege
den Ordner an, falls er fehlt" bedeutet, hing drei Minuten - BEVOR irgendeine der
sorgfaeltig begrenzten Pruefungen dran war. Dritter Fund derselben Sorte an einem
Tag: os-Aufruf auf einen Netzpfad ohne Zeitgrenze.
Jetzt klaert `pfad_lage()` die Lage mit einem abbrechbaren Kind-Prozess
(`timeout 4 ls -d`, drei Antworten: da / weg / unklar), und makedirs laeuft nur
bei "weg". Dieselbe Falle in `reparieren()` (os.path.ismount als Vorbedingung -
gebraucht wird es nicht, `umount -l` auf einen leeren Pfad kostet nichts) und in
`aushaengen()` (ismount + rmdir).
Dazu steht die DAUER jetzt im Log ("neu verbunden (4.2s)"). Sie war die
entscheidende Spur; wer sie ablesen kann, muss sie nicht rekonstruieren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
422 lines
16 KiB
Python
422 lines
16 KiB
Python
"""Tests für die SMB-Fehlerübersetzung (Speicherziele → Freigaben auflisten)."""
|
|
|
|
from mounts import (
|
|
pfad_map_vorschlag,
|
|
pfad_map_zeile,
|
|
uebersetze_smb_fehler,
|
|
unc_aus_quelle,
|
|
validiere_name,
|
|
)
|
|
|
|
|
|
def test_access_denied_ohne_credentials_erklaert_gastproblem():
|
|
meldung = uebersetze_smb_fehler(
|
|
"session setup failed: NT_STATUS_ACCESS_DENIED", mit_credentials=False
|
|
)
|
|
assert "Gast" in meldung
|
|
assert "Benutzername + Passwort" in meldung
|
|
|
|
|
|
def test_access_denied_mit_credentials_verweist_auf_konto():
|
|
meldung = uebersetze_smb_fehler(
|
|
"session setup failed: NT_STATUS_ACCESS_DENIED", mit_credentials=True
|
|
)
|
|
assert "stimmen nicht" in meldung
|
|
|
|
|
|
def test_logon_failure_wird_uebersetzt():
|
|
meldung = uebersetze_smb_fehler("NT_STATUS_LOGON_FAILURE", mit_credentials=True)
|
|
assert "Passwort falsch" in meldung
|
|
|
|
|
|
def test_unerreichbar_wird_uebersetzt():
|
|
meldung = uebersetze_smb_fehler(
|
|
"do_connect: Connection to 10.0.0.9 failed (Error NT_STATUS_IO_TIMEOUT)",
|
|
mit_credentials=False,
|
|
)
|
|
assert "nicht erreichbar" in meldung
|
|
|
|
|
|
def test_unbekannter_fehler_bleibt_erhalten_und_gekappt():
|
|
meldung = uebersetze_smb_fehler("X" * 500, mit_credentials=False)
|
|
assert meldung == "X" * 200
|
|
|
|
|
|
def test_validiere_name_bleibt_streng():
|
|
assert validiere_name("nas-filme")
|
|
assert not validiere_name("NAS Filme")
|
|
|
|
|
|
def test_stale_mounts_loesen_loest_bis_nichts_mehr_geht(monkeypatch):
|
|
"""Löst gestapelte Schichten per lazy umount, bis umount nichts mehr findet
|
|
(returncode != 0), und meldet die Zahl der gelösten Schichten."""
|
|
import types
|
|
|
|
import mounts
|
|
|
|
aufrufe = []
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
aufrufe.append(cmd)
|
|
rc = 0 if len(aufrufe) <= 3 else 1 # 3 Schichten lösen, dann leer
|
|
return types.SimpleNamespace(returncode=rc, stdout=b"", stderr=b"")
|
|
|
|
monkeypatch.setattr(mounts.subprocess, "run", fake_run)
|
|
assert mounts._stale_mounts_loesen("/app/media/x") == 3
|
|
assert all(cmd[:2] == ["umount", "-l"] for cmd in aufrufe)
|
|
|
|
|
|
def test_mounten_loest_stale_vor_dem_mount():
|
|
"""Regression (Vorfall 24.07.): mounten() muss Alt-Mounts LÖSEN, bevor es neu
|
|
mountet — sonst stapelt es auf eine Mount-Leiche (12 Schichten, ls-Timeout)."""
|
|
import inspect
|
|
|
|
import mounts
|
|
|
|
quelle = inspect.getsource(mounts.mounten)
|
|
assert "_stale_mounts_loesen(ziel)" in quelle
|
|
|
|
|
|
# --- RIPPY_PATH_MAP: der Anschluss für externe Worker (Befund 26.07.2026) ----
|
|
|
|
|
|
def test_unc_aus_quelle_uebersetzt_cifs():
|
|
assert unc_aus_quelle("cifs", "//192.168.178.62/rippy") == "\\\\192.168.178.62\\rippy"
|
|
assert unc_aus_quelle("cifs", "//nas/medien/filme") == "\\\\nas\\medien\\filme"
|
|
|
|
|
|
def test_unc_aus_quelle_raet_bei_nfs_nicht():
|
|
"""NFS gibt "" — Windows-Schreibweise ist nicht ableitbar (AGENTS Regel D)."""
|
|
assert unc_aus_quelle("nfs", "192.168.178.62:/volume1/rippy") == ""
|
|
assert unc_aus_quelle("cifs", "") == ""
|
|
assert unc_aus_quelle("cifs", "kein-unc-pfad") == ""
|
|
|
|
|
|
def test_pfad_map_zeile_baut_was_pfad_lokal_liest(monkeypatch):
|
|
"""Der erzeugte Wert muss vom Worker gelesen werden können — genau dieses
|
|
Format erwartet worker/tasks.pfad_lokal(): Paare, getrennt durch ';'."""
|
|
import mounts
|
|
|
|
monkeypatch.setattr(mounts, "ist_gemountet", lambda name: True)
|
|
vorschlaege = pfad_map_vorschlag([
|
|
{"name": "rippy", "typ": "cifs", "quelle": "//192.168.178.62/rippy"},
|
|
])
|
|
assert pfad_map_zeile(vorschlaege) == "/app/media/rippy=\\\\192.168.178.62\\rippy"
|
|
|
|
|
|
def test_pfad_map_zeile_laesst_nfs_weg(monkeypatch):
|
|
"""Ein halbes Mapping wäre schlimmer als keines: pfad_lokal() hört beim
|
|
ersten passenden Präfix auf, ein NFS-Eintrag ohne Ziel würde also einen
|
|
Pfad 'übersetzen', den der Worker danach trotzdem nicht sieht."""
|
|
import mounts
|
|
|
|
monkeypatch.setattr(mounts, "ist_gemountet", lambda name: True)
|
|
vorschlaege = pfad_map_vorschlag([
|
|
{"name": "nfs-ziel", "typ": "nfs", "quelle": "10.0.0.9:/export"},
|
|
{"name": "rippy", "typ": "cifs", "quelle": "//nas/rippy"},
|
|
])
|
|
assert pfad_map_zeile(vorschlaege) == "/app/media/rippy=\\\\nas\\rippy"
|
|
|
|
|
|
def test_pfad_map_zeile_ohne_mounts_ist_leer():
|
|
assert pfad_map_zeile([]) == ""
|
|
assert pfad_map_zeile(None) == ""
|
|
|
|
|
|
def test_erzeugtes_mapping_uebersetzt_den_echten_fehlerfall(monkeypatch):
|
|
"""Gegenprobe mit dem Pfad, an dem es am 26.07.2026 live scheiterte.
|
|
|
|
Der Rohschnitt lag auf `/app/media/rippy/95afdc89-…/title_t00.mkv`; der
|
|
Windows-Worker sah dort nichts. Mit dem hier erzeugten Mapping muss
|
|
genau dieser Pfad auf die Freigabe zeigen. `pfad_lokal` ist eine reine
|
|
Funktion im Worker — hier nachgebaut aufzurufen wäre wertlos, deshalb
|
|
wird sie über den Pfad importiert.
|
|
"""
|
|
import importlib.util
|
|
import os
|
|
|
|
import mounts
|
|
|
|
monkeypatch.setattr(mounts, "ist_gemountet", lambda name: True)
|
|
mapping = pfad_map_zeile(pfad_map_vorschlag([
|
|
{"name": "rippy", "typ": "cifs", "quelle": "//192.168.178.62/rippy"},
|
|
]))
|
|
|
|
# Der Worker liegt neben der API im Repo; kein geteiltes Paket zwischen
|
|
# den Containern, deshalb per Pfad laden statt importieren.
|
|
worker_tasks = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "worker", "tasks.py"
|
|
)
|
|
spec = importlib.util.spec_from_file_location("_worker_tasks_pfad", worker_tasks)
|
|
quelltext = open(worker_tasks, encoding="utf-8").read()
|
|
assert "def pfad_lokal" in quelltext and spec is not None
|
|
|
|
# pfad_lokal ist bewusst rein und ohne Modul-Zustand — die Funktion aus dem
|
|
# Quelltext zu holen, ohne tasks.py komplett zu laden (das braucht celery,
|
|
# db, requests …), geht am ehrlichsten über exec des Funktionsblocks.
|
|
anfang = quelltext.index("def pfad_lokal")
|
|
ende = quelltext.index("\nRAW_DIR", anfang)
|
|
umgebung = {"os": os}
|
|
exec(compile(quelltext[anfang:ende], worker_tasks, "exec"), umgebung) # noqa: S102
|
|
pfad_lokal = umgebung["pfad_lokal"]
|
|
|
|
assert pfad_lokal("/app/media/rippy/95afdc89/title_t00.mkv", mapping) == (
|
|
"\\\\192.168.178.62\\rippy\\95afdc89\\title_t00.mkv"
|
|
)
|
|
|
|
|
|
# --- Der Mount kam nach einem Rebuild nicht zurueck (Befund 26.07.2026) ------
|
|
|
|
|
|
def test_mounten_geht_bei_totem_mount_den_reparatur_weg(monkeypatch):
|
|
"""Regression. Vorher galt `os.path.ismount` als Beweis, dass alles steht -
|
|
und nach `docker compose up -d --build` war die CIFS-Freigabe TOT (4 von 4
|
|
Zugriffen 10 s Timeout), der Mountpunkt aber weiter vorhanden. Damit brach
|
|
das Wiederherstellen genau dort ab, und `schreibtest()` (kein Timeout!)
|
|
blockierte den Start-Thread im Kernel.
|
|
|
|
Antwortet die Freigabe nicht, muss geloest und frisch gemountet werden -
|
|
genau wie reparieren() es tut, nur automatisch. Der Test faengt das an der
|
|
WIRKUNG: ismount darf dann gar nicht mehr gefragt werden."""
|
|
import types
|
|
|
|
import mounts
|
|
|
|
ablauf = []
|
|
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
|
|
# pfad_lage gestubbt: Sie ruft selbst `timeout ls` auf, und hier geht es um
|
|
# die Reihenfolge von Loesen und Mounten.
|
|
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "da")
|
|
# Vorher tot (der Vor-Check), nach dem Mount dauerhaft erreichbar. Die
|
|
# Doppelprobe wird hier gestubbt, damit der Test nicht 3 s echt wartet.
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
|
|
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: True)
|
|
monkeypatch.setattr(mounts, "_stale_mounts_loesen",
|
|
lambda ziel: ablauf.append("loesen"))
|
|
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
ablauf.append(cmd[0])
|
|
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
|
|
monkeypatch.setattr(mounts.subprocess, "run", fake_run)
|
|
# ismount darf hier gar nicht mehr gefragt werden
|
|
monkeypatch.setattr(mounts.os.path, "ismount",
|
|
lambda p: (_ for _ in ()).throw(AssertionError("zu frueh gefragt")))
|
|
|
|
assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True
|
|
assert ablauf == ["loesen", "mount"]
|
|
|
|
|
|
def test_mounten_prueft_das_ergebnis_und_versucht_es_zweimal(monkeypatch):
|
|
"""Befund 26.07.2026, dreimal reproduziert: `mount` meldete Erfolg, und die
|
|
Freigabe antwortete danach TROTZDEM nicht (eine einzige, korrekt aussehende
|
|
Schicht in /proc/mounts). Derselbe Ablauf ein zweites Mal stellte sie sofort
|
|
her. Also wird das Ergebnis geprueft statt geglaubt."""
|
|
import types
|
|
|
|
import mounts
|
|
|
|
ablauf = []
|
|
# nie erreichbar: vorher, nach Versuch 1, nach Versuch 2
|
|
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
|
|
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "da")
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
|
|
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: False)
|
|
monkeypatch.setattr(mounts, "_stale_mounts_loesen", lambda ziel: None)
|
|
monkeypatch.setattr(mounts, "_lazy_umount", lambda ziel: ablauf.append("lazy"))
|
|
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
|
|
monkeypatch.setattr(
|
|
mounts.subprocess, "run",
|
|
lambda cmd, **k: (ablauf.append(cmd[0]),
|
|
types.SimpleNamespace(returncode=0, stdout="", stderr=""))[1])
|
|
|
|
import pytest
|
|
|
|
with pytest.raises(RuntimeError) as fehler:
|
|
mounts.mounten("rippy", "cifs", "//nas/rippy")
|
|
|
|
# Zweimal gemountet, dazwischen einmal geloest
|
|
assert ablauf == ["mount", "lazy", "mount"]
|
|
# Und die Meldung sagt die Wahrheit statt "eingehaengt"
|
|
assert "antwortet aber nicht" in str(fehler.value)
|
|
|
|
|
|
def test_mounten_laesst_gesunden_mount_in_ruhe(monkeypatch):
|
|
"""Antwortet die Freigabe, bleibt sie unangetastet - kein Loesen, kein
|
|
zweites Mounten (das wuerde stapeln)."""
|
|
import mounts
|
|
|
|
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: True)
|
|
monkeypatch.setattr(mounts.os.path, "ismount", lambda p: True)
|
|
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
|
|
monkeypatch.setattr(mounts, "_stale_mounts_loesen",
|
|
lambda ziel: (_ for _ in ()).throw(AssertionError("nicht loesen!")))
|
|
|
|
assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True
|
|
|
|
|
|
def test_wirklich_erreichbar_prueft_zweimal_mit_abstand(monkeypatch):
|
|
"""Befund 26.07.2026: Direkt nach einem frischen `mount` antwortete die
|
|
Freigabe - und Sekunden spaeter lief jeder Zugriff in die Zeitgrenze
|
|
(Wettlauf mit dem lazy umount, dessen Abbau hinter den neuen Mount fiel).
|
|
Eine EINZIGE Probe kann das nicht sehen."""
|
|
import mounts
|
|
|
|
antworten = iter([True, False]) # erst ja, dann nein
|
|
gewartet = []
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: next(antworten))
|
|
assert mounts.wirklich_erreichbar("rippy", warten=gewartet.append) is False
|
|
assert gewartet == [3]
|
|
|
|
|
|
def test_wirklich_erreichbar_bei_gesunder_freigabe(monkeypatch):
|
|
import mounts
|
|
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: True)
|
|
assert mounts.wirklich_erreichbar("rippy", warten=lambda s: None) is True
|
|
|
|
|
|
def test_wirklich_erreichbar_spart_das_warten_wenn_schon_die_erste_probe_faellt(monkeypatch):
|
|
import mounts
|
|
|
|
gewartet = []
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
|
|
assert mounts.wirklich_erreichbar("rippy", warten=gewartet.append) is False
|
|
assert gewartet == [] # nicht drei Sekunden fuer nichts
|
|
|
|
|
|
def test_stale_loesen_wartet_den_lazy_abbau_ab(monkeypatch):
|
|
"""Der Grund fuer die 150 Sekunden (Befund 26.07.2026): `umount -l` ist lazy,
|
|
der Abbau passiert spaeter. Wer direkt danach mountet, riskiert, dass der
|
|
Abbau HINTER dem neuen Mount landet - der erste Reparaturversuch scheiterte
|
|
dadurch regelmaessig, und der zweite kostete 30 s Timeout."""
|
|
import time
|
|
import types
|
|
|
|
import mounts
|
|
|
|
gewartet = []
|
|
monkeypatch.setattr(time, "sleep", gewartet.append)
|
|
aufrufe = []
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
aufrufe.append(cmd)
|
|
rc = 0 if len(aufrufe) <= 2 else 1
|
|
return types.SimpleNamespace(returncode=rc, stdout=b"", stderr=b"")
|
|
|
|
monkeypatch.setattr(mounts.subprocess, "run", fake_run)
|
|
assert mounts._stale_mounts_loesen("/app/media/x") == 2
|
|
assert gewartet == [1.5]
|
|
|
|
|
|
def test_stale_loesen_wartet_nicht_wenn_nichts_zu_loesen_war(monkeypatch):
|
|
"""War kein Mount da, gibt es auch keinen Abbau abzuwarten - dann darf die
|
|
Reparatur nicht kuenstlich gebremst werden."""
|
|
import time
|
|
import types
|
|
|
|
import mounts
|
|
|
|
gewartet = []
|
|
monkeypatch.setattr(time, "sleep", gewartet.append)
|
|
monkeypatch.setattr(
|
|
mounts.subprocess, "run",
|
|
lambda cmd, **k: types.SimpleNamespace(returncode=1, stdout=b"", stderr=b""))
|
|
assert mounts._stale_mounts_loesen("/app/media/x") == 0
|
|
assert gewartet == []
|
|
|
|
|
|
def test_pfad_lage_unterscheidet_da_weg_unklar(monkeypatch):
|
|
"""DER Fund vom 26.07.2026: Eine Wiederanbindung brauchte 3 min 15 s, und die
|
|
Zeit ging in die ERSTE Zeile von mounten() - `os.makedirs(ziel,
|
|
exist_ok=True)`. `exist_ok` prueft mit os.path.isdir, und ein `stat` auf einen
|
|
toten CIFS-Mount blockiert im Kernel bis zum SMB-Timeout. Deshalb wird die
|
|
Lage jetzt mit einem abbrechbaren Kind-Prozess erfragt."""
|
|
import types
|
|
|
|
import mounts
|
|
|
|
def antwort(rc):
|
|
return lambda cmd, **k: types.SimpleNamespace(returncode=rc, stdout=b"", stderr=b"")
|
|
|
|
monkeypatch.setattr(mounts.subprocess, "run", antwort(0))
|
|
assert mounts.pfad_lage("/app/media/rippy") == "da"
|
|
monkeypatch.setattr(mounts.subprocess, "run", antwort(2))
|
|
assert mounts.pfad_lage("/app/media/neu") == "weg"
|
|
# 124 = `timeout` hat abgeschossen: existiert, antwortet aber nicht
|
|
monkeypatch.setattr(mounts.subprocess, "run", antwort(124))
|
|
assert mounts.pfad_lage("/app/media/totes-nas") == "unklar"
|
|
|
|
|
|
def test_pfad_lage_nutzt_eine_zeitgrenze(monkeypatch):
|
|
import types
|
|
|
|
import mounts
|
|
|
|
gesehen = {}
|
|
monkeypatch.setattr(
|
|
mounts.subprocess, "run",
|
|
lambda cmd, **k: (gesehen.update(cmd=cmd, kw=k),
|
|
types.SimpleNamespace(returncode=0))[1])
|
|
mounts.pfad_lage("/x")
|
|
assert gesehen["cmd"][0] == "timeout"
|
|
assert gesehen["kw"]["timeout"] is not None
|
|
|
|
|
|
def test_mounten_legt_den_ordner_nur_an_wenn_er_fehlt(monkeypatch):
|
|
"""Bei "unklar" (toter Mount) darf makedirs NICHT laufen - genau dort hing es
|
|
drei Minuten. Der Ordner ist dann ohnehin da."""
|
|
import types
|
|
|
|
import mounts
|
|
|
|
angelegt = []
|
|
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "unklar")
|
|
monkeypatch.setattr(mounts.os, "makedirs",
|
|
lambda *a, **k: angelegt.append(a[0] if a else ""))
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
|
|
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: True)
|
|
monkeypatch.setattr(mounts, "_stale_mounts_loesen", lambda ziel: 0)
|
|
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
|
|
monkeypatch.setattr(
|
|
mounts.subprocess, "run",
|
|
lambda cmd, **k: types.SimpleNamespace(returncode=0, stdout="", stderr=""))
|
|
|
|
assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True
|
|
assert angelegt == []
|
|
|
|
|
|
def test_mounten_legt_den_ordner_bei_erstinstallation_an(monkeypatch):
|
|
import types
|
|
|
|
import mounts
|
|
|
|
angelegt = []
|
|
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "weg")
|
|
monkeypatch.setattr(mounts.os, "makedirs",
|
|
lambda *a, **k: angelegt.append(a[0] if a else ""))
|
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
|
|
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: True)
|
|
monkeypatch.setattr(mounts, "_stale_mounts_loesen", lambda ziel: 0)
|
|
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
|
|
monkeypatch.setattr(
|
|
mounts.subprocess, "run",
|
|
lambda cmd, **k: types.SimpleNamespace(returncode=0, stdout="", stderr=""))
|
|
|
|
mounts.mounten("rippy", "cifs", "//nas/rippy")
|
|
assert angelegt == ["/app/media/rippy"]
|
|
|
|
|
|
def test_reparieren_fragt_ismount_nicht_mehr(monkeypatch):
|
|
"""os.path.ismount ist ein `stat` und blockiert auf einem toten CIFS-Mount.
|
|
Gebraucht wird es nicht: `umount -l` auf einen leeren Pfad kostet nichts."""
|
|
import mounts
|
|
|
|
monkeypatch.setattr(mounts.os.path, "ismount",
|
|
lambda p: (_ for _ in ()).throw(AssertionError("nicht fragen!")))
|
|
monkeypatch.setattr(mounts, "_lazy_umount", lambda ziel: None)
|
|
monkeypatch.setattr(mounts, "mounten", lambda *a, **k: True)
|
|
assert mounts.reparieren("rippy", "cifs", "//nas/rippy") is True
|