Grotti 3.7.0: Treiber für Kobra 4 und Marlin, smartere Dateien-Auswahl und einstellbare Etiketten.

Senden und Abbruch laufen über den Treiber, Orca-Doppelzeilen und tote Ideen-API fallen weg, Drucker folgen der Datei, Etikettenformat liegt in den Einstellungen.
This commit is contained in:
Dominic Gundel committed 2026-11-02 14:03:57 +01:00
1 parent 5081212212
commit db6a85a703
54 files changed
+3379 -384

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
"""Marlin-Dienst starten, ohne ihn zu beenden. Kein echtes USB-Gerät."""
import sys
import tempfile
import threading
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from grotti import marlin_supervisor as sup
class SupervisorTests(unittest.TestCase):
def tearDown(self):
sup.disable()
def test_running_service_is_not_started_again(self):
starts = []
service = sup.MarlinSupervisor(
Path("."),
healthy=lambda: True,
start=lambda folder: starts.append(folder),
wait_s=0.1,
)
result = service.ensure()
self.assertTrue(result["running"])
self.assertFalse(result["started"])
self.assertEqual(starts, [])
def test_starts_once_when_down(self):
state = {"up": False}
starts = []
def healthy():
return state["up"]
def start(folder):
starts.append(folder)
state["up"] = True
service = sup.MarlinSupervisor(Path("."), healthy=healthy, start=start, wait_s=0.2)
first = service.ensure()
second = service.ensure()
self.assertTrue(first["started"])
self.assertEqual(len(starts), 1)
self.assertFalse(second["started"])
self.assertTrue(second["running"])
def test_failed_start_does_not_repeat_immediately(self):
starts = []
service = sup.MarlinSupervisor(
Path("."),
healthy=lambda: False,
start=lambda folder: starts.append(1),
wait_s=0.05,
)
service.ensure()
service.ensure()
self.assertEqual(len(starts), 1)
def test_parallel_ensure_starts_once(self):
gate = threading.Event()
starts = []
def start(folder):
starts.append(1)
gate.wait(1)
service = sup.MarlinSupervisor(
Path("."),
healthy=lambda: False,
start=start,
wait_s=0.05,
)
threads = [threading.Thread(target=service.ensure) for _ in range(4)]
for thread in threads:
thread.start()
gate.set()
for thread in threads:
thread.join(2)
self.assertEqual(len(starts), 1)
def test_spawn_is_detached_and_runs_the_module(self):
seen = {}
class Proc:
pid = 42
def popen(**kwargs):
seen.update(kwargs)
if kwargs.get("creationflags", 0) & sup._CREATE_BREAKAWAY:
raise OSError("breakaway")
return Proc()
with tempfile.TemporaryDirectory() as folder:
pid = sup.spawn_detached(folder, popen=popen)
self.assertEqual(pid, 42)
self.assertEqual(seen["args"][1:], ["-m", "grotti.marlin_daemon"])
self.assertIs(seen["stdin"], sup.subprocess.DEVNULL)
if sup.os.name == "nt":
flags = seen["creationflags"]
self.assertTrue(flags & sup._DETACHED)
self.assertFalse(flags & sup._CREATE_BREAKAWAY)
else:
self.assertTrue(seen["start_new_session"])
def test_enable_reuses_a_live_daemon(self):
sup.enable(Path("."))
sup._supervisor.healthy = lambda: True
sup._supervisor.start = lambda folder: (_ for _ in ()).throw(AssertionError("gestartet"))
result = sup.ensure_if_enabled()
self.assertTrue(result["running"])
self.assertFalse(result["started"])