| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- """slot_unlink_grace holds an automatic slot unlink until the slot has stayed
- empty for the grace period (#3186)."""
- import asyncio
- from unittest.mock import patch
- import pytest
- from backend.app.services import slot_unlink_grace as grace
- KEY = ("inventory", 1, 2, 33)
- @pytest.fixture
- def clock():
- now = [1000.0]
- with patch.object(grace, "_now", lambda: now[0]):
- yield now
- def test_a_first_sighting_is_held(clock):
- assert grace.removal_confirmed(7, KEY) is False
- def test_confirmed_once_the_grace_period_has_passed(clock):
- grace.removal_confirmed(7, KEY)
- clock[0] += grace.GRACE_SECONDS - 1
- assert grace.removal_confirmed(7, KEY) is False
- clock[0] += 1
- assert grace.removal_confirmed(7, KEY) is True
- def test_settle_forgets_a_slot_the_pass_did_not_hold_again(clock):
- grace.removal_confirmed(7, KEY)
- grace.settle(7, "inventory", set())
- clock[0] += grace.GRACE_SECONDS
- assert grace.removal_confirmed(7, KEY) is False, "the recovered slot must start a fresh clock"
- def test_settle_keeps_other_printers_and_scopes(clock):
- grace.removal_confirmed(7, KEY)
- grace.removal_confirmed(8, KEY)
- grace.removal_confirmed(7, ("spoolman", 1, 2, 41))
- grace.settle(8, "inventory", set())
- grace.settle(7, "spoolman", set())
- clock[0] += grace.GRACE_SECONDS
- assert grace.removal_confirmed(7, KEY) is True
- def test_a_hold_nobody_renewed_starts_over(clock):
- """A pass that did not run (Spoolman unreachable, printer offline) cannot
- have watched the slot stay empty, so its old first sighting does not count."""
- grace.removal_confirmed(7, KEY)
- clock[0] += 3 * grace.GRACE_SECONDS
- assert grace.removal_confirmed(7, KEY) is False
- def test_is_held_follows_the_hold(clock):
- assert grace.is_held(7, KEY) is False
- grace.removal_confirmed(7, KEY)
- assert grace.is_held(7, KEY) is True
- grace.settle(7, "inventory", set())
- assert grace.is_held(7, KEY) is False
- def test_is_held_ignores_a_stale_hold(clock):
- grace.removal_confirmed(7, KEY)
- clock[0] += 3 * grace.GRACE_SECONDS
- assert grace.is_held(7, KEY) is False
- def test_forget_slot_drops_every_hold_on_that_slot_only(clock):
- grace.removal_confirmed(7, ("spoolman", 1, 2, 41))
- grace.removal_confirmed(7, ("inventory", 1, 2, 99))
- grace.removal_confirmed(7, ("spoolman", 1, 3, 42))
- grace.removal_confirmed(8, ("spoolman", 1, 2, 41))
- grace.forget_slot(7, 1, 2)
- assert grace.is_held(7, ("spoolman", 1, 2, 41)) is False
- assert grace.is_held(7, ("inventory", 1, 2, 99)) is False
- assert grace.is_held(7, ("spoolman", 1, 3, 42)) is True
- assert grace.is_held(8, ("spoolman", 1, 2, 41)) is True
- def test_no_recheck_without_a_registered_callback(clock):
- with patch.object(grace, "_recheck", None):
- grace.removal_confirmed(7, KEY)
- assert grace._recheck_tasks == {}
- @pytest.mark.asyncio
- async def test_holds_on_one_printer_share_a_single_recheck(clock):
- async def recheck(printer_id: int) -> None:
- pass
- with patch.object(grace, "_recheck", recheck):
- grace.removal_confirmed(7, KEY)
- grace.removal_confirmed(7, ("inventory", 1, 3, 34))
- grace.removal_confirmed(8, KEY)
- assert sorted(grace._recheck_tasks) == [7, 8]
- for task in grace._recheck_tasks.values():
- task.cancel()
- await asyncio.gather(*grace._recheck_tasks.values(), return_exceptions=True)
- @pytest.mark.asyncio
- async def test_the_recheck_runs_the_callback_and_frees_its_slot():
- calls: list[int] = []
- async def recheck(printer_id: int) -> None:
- # Freed before the callback, so a hold the re-check renews can
- # schedule the next one.
- assert printer_id not in grace._recheck_tasks
- calls.append(printer_id)
- with patch.object(grace, "_recheck", recheck):
- grace._recheck_tasks[7] = asyncio.current_task()
- await grace._run_recheck(7, -1)
- assert calls == [7]
- @pytest.mark.asyncio
- async def test_a_failing_recheck_is_logged_not_raised():
- async def recheck(printer_id: int) -> None:
- raise RuntimeError("boom")
- with patch.object(grace, "_recheck", recheck), patch.object(grace.logger, "exception") as log:
- await grace._run_recheck(7, -1)
- log.assert_called_once()
|