Files
shelfmark/tests/core/test_queue.py
T
splitsec2 f0f0d46381 fix(queue): first terminal status wins, so a late COMPLETE cannot overwrite a cancel (#1429)
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.
2026-10-20 01:00:21 -04:00

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