Skip to content

Commit 6ec659a

Browse files
Lukas Geigerclaude
andcommitted
fix: Thread-Safety Queue.pending+status_counts, i18n locale.Error-Fallback (Bugsweep)
Bug 1 (models.py): Queue.pending property iterierte self.tasks ohne Lock -- Race Condition wenn UI-Thread add() und Worker-Thread pending lesen gleichzeitig. Fix: pending unter self._lock; neue status_counts()-Methode fuer _refresh_status. Bug 2 (tray.py + models.py): _refresh_status() iterierte self.queue.tasks direkt ohne Lock (Zeile 273). Fix: status_counts() gibt (n_pending, n_failed) atomar unter Lock zurueck; tray.py nutzt status_counts() statt direktem tasks-Zugriff. Bug 3 (i18n.py): detect_language() fing nur ValueError/TypeError -- locale.Error (Subclass von Exception) konnte ungefangen eskalieren auf Systemen mit kaputtem Locale. Fix: except Exception mit Kommentar. Tests: 3 neue Tests (test_queue_status_counts_thread_safe, test_queue_pending_property_thread_safe, test_detect_language_locale_error_falls_back_to_german). 95/95 gruen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 48a3e75 commit 6ec659a

5 files changed

Lines changed: 68 additions & 5 deletions

File tree

src/cloudlockfixer/i18n.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ def detect_language() -> Language:
293293
lang, _ = locale.getlocale()
294294
if lang and lang.lower().startswith("en"):
295295
return "en"
296-
except (ValueError, TypeError):
296+
except Exception:
297+
# locale.Error (Subclass von Exception) und ValueError/TypeError
298+
# bei ungültiger oder nicht gesetzter System-Locale abfangen.
297299
pass
298300
return "de"
299301

src/cloudlockfixer/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,20 @@ def add(self, task: Task) -> Task:
182182

183183
@property
184184
def pending(self) -> list[Task]:
185-
return [t for t in self.tasks if t.status in ("pending", "running")]
185+
with self._lock:
186+
return [t for t in self.tasks if t.status in ("pending", "running")]
187+
188+
def status_counts(self) -> tuple[int, int]:
189+
"""(n_pending, n_failed_with_retries) — Thread-sicher unter Lock."""
190+
with self._lock:
191+
n_pending = sum(
192+
1 for t in self.tasks if t.status in ("pending", "running")
193+
)
194+
n_failed = sum(
195+
1 for t in self.tasks
196+
if t.status == "pending" and t.retry_count > 0
197+
)
198+
return n_pending, n_failed
186199

187200
def save(self) -> None:
188201
with self._lock:

src/cloudlockfixer/tray.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,7 @@ def _set_status(self, text: str) -> None:
269269
def _refresh_status(self) -> None:
270270
if not self._running:
271271
self.queue.load()
272-
n = len(self.queue.pending)
273-
failed = sum(1 for t_ in self.queue.tasks
274-
if t_.status == "pending" and t_.retry_count > 0)
272+
n, failed = self.queue.status_counts()
275273
txt = (t("status_no_tasks") if n == 0
276274
else t("status_open", n=n, failed=failed))
277275
self._set_status(txt)

tests/test_core.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,43 @@ def test_worker_runs_local_task(tmp_path):
296296
assert (tmp_path / "new").exists()
297297

298298

299+
def test_queue_status_counts_thread_safe(tmp_path):
300+
"""Bug-Fix (Bug-Sweep): status_counts() muss Thread-sicher unter Lock laufen.
301+
pending-Property und tasks-Direktzugriff in _refresh_status ersetzt durch
302+
status_counts() um Race Condition zwischen UI- und Worker-Thread zu vermeiden."""
303+
q = Queue(tmp_path)
304+
# Leere Queue
305+
n_pending, n_failed = q.status_counts()
306+
assert n_pending == 0 and n_failed == 0
307+
308+
# Einen Task hinzufügen
309+
src = _mkdir_with_file(tmp_path / "src1")
310+
t1 = Task(chain=[Step(op="rename", src=str(src), arg="dst1")])
311+
q.add(t1)
312+
n_pending, n_failed = q.status_counts()
313+
assert n_pending == 1 and n_failed == 0
314+
315+
# retry_count > 0 -> zählt als "failed_with_retries"
316+
q.tasks[0].retry_count = 1
317+
n_pending, n_failed = q.status_counts()
318+
assert n_pending == 1 and n_failed == 1
319+
320+
# Status "done" -> nicht mehr pending
321+
q.tasks[0].status = "done"
322+
n_pending, n_failed = q.status_counts()
323+
assert n_pending == 0 and n_failed == 0
324+
325+
326+
def test_queue_pending_property_thread_safe(tmp_path):
327+
"""Bug-Fix (Bug-Sweep): pending-Property muss Lock halten beim Lesen von tasks."""
328+
q = Queue(tmp_path)
329+
src = _mkdir_with_file(tmp_path / "src2")
330+
q.add(Task(chain=[Step(op="rename", src=str(src), arg="dst2")]))
331+
pending = q.pending
332+
assert len(pending) == 1
333+
assert pending[0].status == "pending"
334+
335+
299336
# ── CLI ─────────────────────────────────────────────────────────────
300337

301338
def test_cli_chain_invalid_op_returns_2(tmp_path, monkeypatch):

tests/test_i18n.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ def test_detect_language_fallback(monkeypatch):
7373
assert detect_language() == "de"
7474

7575

76+
def test_detect_language_locale_error_falls_back_to_german(monkeypatch):
77+
"""Bug-Fix: locale.getlocale() kann locale.Error werfen (Subclass von Exception,
78+
nicht von ValueError/TypeError). Muss sicher auf 'de' zurückfallen."""
79+
import locale as locale_mod
80+
81+
def raise_locale_error():
82+
raise locale_mod.Error("Ungültige Locale-Konfiguration")
83+
84+
monkeypatch.setattr("locale.getlocale", raise_locale_error)
85+
result = detect_language()
86+
assert result == "de", f"Erwartet 'de' als Fallback bei locale.Error, war: {result}"
87+
88+
7689
def test_available_keys_sorted():
7790
keys = available_keys()
7891
assert keys == sorted(keys)

0 commit comments

Comments
 (0)