140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
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]
|