test_slot_unlink_grace.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """slot_unlink_grace holds an automatic slot unlink until the slot has stayed
  2. empty for the grace period (#3186)."""
  3. import asyncio
  4. from unittest.mock import patch
  5. import pytest
  6. from backend.app.services import slot_unlink_grace as grace
  7. KEY = ("inventory", 1, 2, 33)
  8. @pytest.fixture
  9. def clock():
  10. now = [1000.0]
  11. with patch.object(grace, "_now", lambda: now[0]):
  12. yield now
  13. def test_a_first_sighting_is_held(clock):
  14. assert grace.removal_confirmed(7, KEY) is False
  15. def test_confirmed_once_the_grace_period_has_passed(clock):
  16. grace.removal_confirmed(7, KEY)
  17. clock[0] += grace.GRACE_SECONDS - 1
  18. assert grace.removal_confirmed(7, KEY) is False
  19. clock[0] += 1
  20. assert grace.removal_confirmed(7, KEY) is True
  21. def test_settle_forgets_a_slot_the_pass_did_not_hold_again(clock):
  22. grace.removal_confirmed(7, KEY)
  23. grace.settle(7, "inventory", set())
  24. clock[0] += grace.GRACE_SECONDS
  25. assert grace.removal_confirmed(7, KEY) is False, "the recovered slot must start a fresh clock"
  26. def test_settle_keeps_other_printers_and_scopes(clock):
  27. grace.removal_confirmed(7, KEY)
  28. grace.removal_confirmed(8, KEY)
  29. grace.removal_confirmed(7, ("spoolman", 1, 2, 41))
  30. grace.settle(8, "inventory", set())
  31. grace.settle(7, "spoolman", set())
  32. clock[0] += grace.GRACE_SECONDS
  33. assert grace.removal_confirmed(7, KEY) is True
  34. def test_a_hold_nobody_renewed_starts_over(clock):
  35. """A pass that did not run (Spoolman unreachable, printer offline) cannot
  36. have watched the slot stay empty, so its old first sighting does not count."""
  37. grace.removal_confirmed(7, KEY)
  38. clock[0] += 3 * grace.GRACE_SECONDS
  39. assert grace.removal_confirmed(7, KEY) is False
  40. def test_is_held_follows_the_hold(clock):
  41. assert grace.is_held(7, KEY) is False
  42. grace.removal_confirmed(7, KEY)
  43. assert grace.is_held(7, KEY) is True
  44. grace.settle(7, "inventory", set())
  45. assert grace.is_held(7, KEY) is False
  46. def test_is_held_ignores_a_stale_hold(clock):
  47. grace.removal_confirmed(7, KEY)
  48. clock[0] += 3 * grace.GRACE_SECONDS
  49. assert grace.is_held(7, KEY) is False
  50. def test_forget_slot_drops_every_hold_on_that_slot_only(clock):
  51. grace.removal_confirmed(7, ("spoolman", 1, 2, 41))
  52. grace.removal_confirmed(7, ("inventory", 1, 2, 99))
  53. grace.removal_confirmed(7, ("spoolman", 1, 3, 42))
  54. grace.removal_confirmed(8, ("spoolman", 1, 2, 41))
  55. grace.forget_slot(7, 1, 2)
  56. assert grace.is_held(7, ("spoolman", 1, 2, 41)) is False
  57. assert grace.is_held(7, ("inventory", 1, 2, 99)) is False
  58. assert grace.is_held(7, ("spoolman", 1, 3, 42)) is True
  59. assert grace.is_held(8, ("spoolman", 1, 2, 41)) is True
  60. def test_no_recheck_without_a_registered_callback(clock):
  61. with patch.object(grace, "_recheck", None):
  62. grace.removal_confirmed(7, KEY)
  63. assert grace._recheck_tasks == {}
  64. @pytest.mark.asyncio
  65. async def test_holds_on_one_printer_share_a_single_recheck(clock):
  66. async def recheck(printer_id: int) -> None:
  67. pass
  68. with patch.object(grace, "_recheck", recheck):
  69. grace.removal_confirmed(7, KEY)
  70. grace.removal_confirmed(7, ("inventory", 1, 3, 34))
  71. grace.removal_confirmed(8, KEY)
  72. assert sorted(grace._recheck_tasks) == [7, 8]
  73. for task in grace._recheck_tasks.values():
  74. task.cancel()
  75. await asyncio.gather(*grace._recheck_tasks.values(), return_exceptions=True)
  76. @pytest.mark.asyncio
  77. async def test_the_recheck_runs_the_callback_and_frees_its_slot():
  78. calls: list[int] = []
  79. async def recheck(printer_id: int) -> None:
  80. # Freed before the callback, so a hold the re-check renews can
  81. # schedule the next one.
  82. assert printer_id not in grace._recheck_tasks
  83. calls.append(printer_id)
  84. with patch.object(grace, "_recheck", recheck):
  85. grace._recheck_tasks[7] = asyncio.current_task()
  86. await grace._run_recheck(7, -1)
  87. assert calls == [7]
  88. @pytest.mark.asyncio
  89. async def test_a_failing_recheck_is_logged_not_raised():
  90. async def recheck(printer_id: int) -> None:
  91. raise RuntimeError("boom")
  92. with patch.object(grace, "_recheck", recheck), patch.object(grace.logger, "exception") as log:
  93. await grace._run_recheck(7, -1)
  94. log.assert_called_once()