from pathlib import Path import pytest from PyQt6.QtWidgets import QApplication from syncvito.config_store import ConfigStore from syncvito.coordinator import SyncCoordinator from syncvito.models import AppConfig, SyncTask, TaskRuntimeState from syncvito.sync_engine import BlockedFile, SyncResult @pytest.fixture def coordinator(tmp_path: Path): application = QApplication.instance() or QApplication([]) task = SyncTask(name="AppData", source=str(tmp_path / "source"), target=str(tmp_path / "target")) coordinator = SyncCoordinator(AppConfig(tasks=[task]), ConfigStore(tmp_path / "config.json")) yield coordinator coordinator._schedule_timer.stop() coordinator._retry_timer.stop() coordinator.stop() assert application is not None def test_many_locks_have_compact_banner_and_full_paths_in_log(coordinator: SyncCoordinator) -> None: banners = [] logs = [] coordinator.lock_changed.connect(banners.append) coordinator.log_created.connect(lambda *entry: logs.append(entry)) blocked = [BlockedFile(Path(f"AppData/Programm{index}/Cookies")) for index in range(300)] task = coordinator.config.tasks[0] coordinator._on_finished(task.id, SyncResult(copied=123, blocked=blocked)) assert len(banners[-1]) < 400 assert "123 kopiert" in banners[-1] assert "300 Dateien gesperrt" in banners[-1] assert "unvollständig" in banners[-1] assert len(logs) == 301 assert str(blocked[-1].path) in logs[-1][2] assert coordinator.task_state(task.id) is TaskRuntimeState.WAITING_FOR_RELEASE coordinator._on_finished(task.id, SyncResult(copied=300)) assert banners[-1] == "" assert task.id not in coordinator._retry_tasks assert coordinator.task_state(task.id) is TaskRuntimeState.ACTIVE def test_errors_are_visible_even_when_other_files_are_locked(coordinator: SyncCoordinator) -> None: logs = [] coordinator.log_created.connect(lambda *entry: logs.append(entry)) task = coordinator.config.tasks[0] coordinator._on_finished(task.id, SyncResult(blocked=[BlockedFile(Path("Cookies"))], errors=["Zugriff verweigert"])) assert coordinator.task_state(task.id) is TaskRuntimeState.ERROR assert any(level == "Fehler" and detail == "Zugriff verweigert" for _, level, detail in logs) @pytest.mark.parametrize("all_locked", [False, True]) def test_retry_can_run_after_partial_release( coordinator: SyncCoordinator, monkeypatch: pytest.MonkeyPatch, all_locked: bool ) -> None: task = coordinator.config.tasks[0] coordinator._retry_tasks[task.id] = [Path("dauerhaft"), Path("freigegeben")] queued = [] monkeypatch.setattr(coordinator, "enqueue", queued.append) monkeypatch.setattr("syncvito.coordinator.is_file_locked", lambda path: all_locked or path.name == "dauerhaft") coordinator._check_retries() assert queued == ([] if all_locked else [task.id]) def test_retry_queues_all_ready_tasks(coordinator: SyncCoordinator, monkeypatch: pytest.MonkeyPatch) -> None: coordinator._retry_tasks = {"first": [], "second": []} queued = [] monkeypatch.setattr(coordinator, "enqueue", queued.append) coordinator._check_retries() assert queued == ["first", "second"] def test_start_resets_stale_progress_before_worker_starts( coordinator: SyncCoordinator, monkeypatch: pytest.MonkeyPatch ) -> None: from types import SimpleNamespace messages = [] workers = [] coordinator.progress_changed.connect(lambda *entry: messages.append(entry)) def start(worker) -> None: assert messages and messages[-1][0] == 0 assert "AppData" in messages[-1][1] workers.append(worker) monkeypatch.setattr(coordinator, "pool", SimpleNamespace(start=start)) coordinator.enqueue(coordinator.config.tasks[0].id) assert len(workers) == 1 assert coordinator.current_overall_status() == ("active", "Synchronisierung läuft") def test_pause_and_resume_preserves_queue_and_state(coordinator: SyncCoordinator) -> None: task1 = coordinator.config.tasks[0] task2 = SyncTask(name="Documents", source="c:/src2", target="c:/dst2") coordinator.config.tasks.append(task2) # Global pausieren coordinator.set_paused(True) assert coordinator.config.settings.paused assert coordinator.current_overall_status() == ("paused", "Pausiert") # Aufgaben in Warteschlange einreihen coordinator.enqueue(task1.id) coordinator.enqueue(task2.id) assert coordinator.queued_task_ids == [task1.id, task2.id] assert coordinator.config.settings.pending_queue == [task1.id, task2.id] # Fortsetzen workers = [] from types import SimpleNamespace coordinator.pool = SimpleNamespace(start=workers.append) coordinator.set_paused(False) assert not coordinator.config.settings.paused assert len(workers) == 1 assert coordinator.running_task_id == task1.id assert coordinator.queued_task_ids == [task2.id] def test_stop_persists_running_task_and_queue_for_restart(coordinator: SyncCoordinator) -> None: task1 = coordinator.config.tasks[0] task2 = SyncTask(name="Documents", source="c:/src2", target="c:/dst2") coordinator.config.tasks.append(task2) coordinator._running_task_id = task1.id coordinator._queue.append(task2.id) coordinator.stop() assert coordinator.config.settings.paused is True assert coordinator.config.settings.pending_queue == [task1.id, task2.id]