mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-14 17:08:24 +00:00
Kept small on purpose so it's easy to review. I have a few more small fixes like this from going back over recently merged PRs. Would you rather get them as separate PRs like this one, or combined into one? #1361 made `cancel_download` write under the queue lock, which stops a cancel from overwriting a download that finished first. The other direction is still open. The worker checks `cancel_flag.is_set()` and then calls `update_status(COMPLETE)` a few lines later in `_process_single_download`. If a cancel lands between those two lines the task goes CANCELLED, then the worker writes COMPLETE over it. `_apply_status_locked` allows terminal to terminal moves, so the terminal hook also fires a second time and records two events for one task. This makes the first terminal status win: `_apply_status_locked` ignores a different terminal status once the task already has one. Requeueing from a terminal status still works because QUEUED isn't terminal. I replaced the existing race test. It patched `queue.update_status`, which `cancel_download` no longer calls, so it never exercised the path it names, and its last assertion only looked at the final hook event. The new tests call the queue directly: complete then cancel is refused, cancel then complete stays cancelled with one hook event, error then complete stays error, and a terminal task can still be requeued. The two late-write tests fail on current `main`. Ruff and basedpyright are clean. `tests/core` and `tests/download` pass apart from 4 `seleniumbase` import failures in my environment that also fail on `main`. The one red check is the existing clock-dependent test `test_a_queued_torrent_asks_for_a_grace_once`, which fails on any runner that booted recently. It is fixed in #1426 and is not caused by this change.
109 lines
3.6 KiB
Python
109 lines
3.6 KiB
Python
"""Tests for queue hook failure handling and cancellation."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
from shelfmark.core.models import DownloadTask, QueueStatus
|
|
from shelfmark.core.queue import BookQueue
|
|
|
|
|
|
def _make_task(task_id: str = "task-1") -> DownloadTask:
|
|
return DownloadTask(
|
|
task_id=task_id,
|
|
source="direct_download",
|
|
title="Example Title",
|
|
user_id=1,
|
|
username="alice",
|
|
)
|
|
|
|
|
|
def test_add_logs_queue_hook_failures():
|
|
queue = BookQueue()
|
|
|
|
def broken_hook(task_id: str, task: DownloadTask) -> None:
|
|
raise RuntimeError("boom")
|
|
|
|
queue.set_queue_hook(broken_hook)
|
|
|
|
with patch("shelfmark.core.queue.logger.warning") as mock_warning:
|
|
assert queue.add(_make_task()) is True
|
|
|
|
mock_warning.assert_called_once()
|
|
args = mock_warning.call_args.args
|
|
assert args[0] == "Queue hook failed while adding task %s: %s"
|
|
assert args[1] == "task-1"
|
|
assert str(args[2]) == "boom"
|
|
|
|
|
|
def test_enqueue_existing_logs_queue_hook_failures():
|
|
queue = BookQueue()
|
|
assert queue.add(_make_task("task-2")) is True
|
|
|
|
def broken_hook(task_id: str, task: DownloadTask) -> None:
|
|
raise RuntimeError("boom")
|
|
|
|
queue.set_queue_hook(broken_hook)
|
|
|
|
with patch("shelfmark.core.queue.logger.warning") as mock_warning:
|
|
assert queue.enqueue_existing("task-2") is True
|
|
|
|
mock_warning.assert_called_once()
|
|
args = mock_warning.call_args.args
|
|
assert args[0] == "Queue hook failed while requeueing task %s: %s"
|
|
assert args[1] == "task-2"
|
|
assert str(args[2]) == "boom"
|
|
|
|
|
|
def _downloading_task(queue: BookQueue, task_id: str) -> list[QueueStatus]:
|
|
"""Put a task in DOWNLOADING and return the list its terminal hook appends to."""
|
|
assert queue.add(_make_task(task_id)) is True
|
|
assert queue.get_next() is not None
|
|
queue.update_status(task_id, QueueStatus.DOWNLOADING)
|
|
|
|
terminal_events: list[QueueStatus] = []
|
|
queue.set_terminal_status_hook(lambda _task_id, status, _task: terminal_events.append(status))
|
|
return terminal_events
|
|
|
|
|
|
def test_cancel_does_not_overwrite_a_download_that_finished_first():
|
|
"""A cancel that arrives after the download completed is refused and changes nothing."""
|
|
queue = BookQueue()
|
|
terminal_events = _downloading_task(queue, "race-task")
|
|
|
|
queue.update_status("race-task", QueueStatus.COMPLETE)
|
|
|
|
assert queue.cancel_download("race-task") is False
|
|
assert queue.get_task_status("race-task") == QueueStatus.COMPLETE
|
|
assert terminal_events == [QueueStatus.COMPLETE]
|
|
|
|
|
|
def test_a_late_complete_does_not_overwrite_a_cancel():
|
|
"""The worker checks its cancel flag and then writes COMPLETE; a cancel in between wins."""
|
|
queue = BookQueue()
|
|
terminal_events = _downloading_task(queue, "race-task")
|
|
|
|
assert queue.cancel_download("race-task") is True
|
|
queue.update_status("race-task", QueueStatus.COMPLETE)
|
|
|
|
assert queue.get_task_status("race-task") == QueueStatus.CANCELLED
|
|
assert terminal_events == [QueueStatus.CANCELLED]
|
|
|
|
|
|
def test_a_task_that_ended_in_error_is_not_completed_afterwards():
|
|
queue = BookQueue()
|
|
terminal_events = _downloading_task(queue, "race-task")
|
|
|
|
queue.update_status("race-task", QueueStatus.ERROR)
|
|
queue.update_status("race-task", QueueStatus.COMPLETE)
|
|
|
|
assert queue.get_task_status("race-task") == QueueStatus.ERROR
|
|
assert terminal_events == [QueueStatus.ERROR]
|
|
|
|
|
|
def test_a_terminal_task_can_still_be_requeued():
|
|
queue = BookQueue()
|
|
_downloading_task(queue, "race-task")
|
|
queue.update_status("race-task", QueueStatus.ERROR)
|
|
|
|
assert queue.enqueue_existing("race-task") is True
|
|
assert queue.get_task_status("race-task") == QueueStatus.QUEUED
|