test_scheduler_dispatch_failures.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. """How a dispatch that cannot go ahead ends.
  2. Before the upload: an exhausted cost-centre budget, a printer row that is gone
  3. and a disconnected printer each fail the item with their own message, upload
  4. nothing and power the printer off if it was powered on for the job. After the
  5. upload: a start command the printer refused while it stayed idle fails the
  6. item, deletes the uploaded file again and sends the failure notification. A
  7. refusal because the printer turned busy defers instead (#2598, see
  8. test_scheduler_busy_defer_2598).
  9. """
  10. from contextlib import ExitStack
  11. from types import SimpleNamespace
  12. from unittest.mock import AsyncMock, MagicMock, patch
  13. import pytest
  14. from fastapi import HTTPException
  15. from backend.app.models.print_queue import PrintQueueItem
  16. from backend.app.services.print_scheduler import PrintScheduler
  17. from backend.tests.unit.test_scheduler_busy_defer_2598 import _base_patches, _final_item, dispatch_case # noqa: F401
  18. IDLE = SimpleNamespace(state="IDLE", subtask_id=None, gcode_file=None)
  19. async def _dispatch(ctx, *, start_print=None, extra=(), before=None):
  20. scheduler = PrintScheduler()
  21. upload = AsyncMock(return_value=True)
  22. start = start_print or MagicMock(return_value=True)
  23. failed = AsyncMock()
  24. async with ctx.session_maker() as db:
  25. item = await db.get(PrintQueueItem, ctx.ids.item_id)
  26. if before is not None:
  27. await before(db, item)
  28. with ExitStack() as stack:
  29. for p in _base_patches(scheduler, ctx, upload, start, MagicMock(return_value=IDLE)):
  30. stack.enter_context(p)
  31. for p in extra:
  32. stack.enter_context(p)
  33. stack.enter_context(
  34. patch(
  35. "backend.app.services.notification_service.notification_service.on_queue_job_failed",
  36. failed,
  37. )
  38. )
  39. delete = stack.enter_context(
  40. patch("backend.app.services.print_scheduler.delete_file_async", AsyncMock(return_value=True))
  41. )
  42. power_off = scheduler._power_off_if_needed
  43. await scheduler._start_print(db, item)
  44. return SimpleNamespace(
  45. power_off=power_off, upload=upload, start=start, delete=delete, failed=failed, item=await _final_item(ctx)
  46. )
  47. class TestBeforeTheUpload:
  48. @pytest.mark.asyncio
  49. async def test_an_exhausted_budget(self, dispatch_case): # noqa: F811
  50. refused = AsyncMock(side_effect=HTTPException(400, "Cost center budget exceeded"))
  51. result = await _dispatch(
  52. dispatch_case, extra=[patch("backend.app.services.print_scheduler.validate_print_budget", refused)]
  53. )
  54. assert (result.item.status, result.item.error_message) == ("failed", "Cost center budget exceeded")
  55. result.upload.assert_not_awaited()
  56. result.start.assert_not_called()
  57. result.power_off.assert_awaited_once()
  58. @pytest.mark.asyncio
  59. async def test_a_printer_that_is_gone(self, dispatch_case): # noqa: F811
  60. async def gone(db, item):
  61. item.printer_id = 9999
  62. await db.commit()
  63. result = await _dispatch(dispatch_case, before=gone)
  64. assert (result.item.status, result.item.error_message) == ("failed", "Printer not found")
  65. result.upload.assert_not_awaited()
  66. result.power_off.assert_awaited_once()
  67. @pytest.mark.asyncio
  68. async def test_a_disconnected_printer(self, dispatch_case): # noqa: F811
  69. offline = patch(
  70. "backend.app.services.print_scheduler.printer_manager.is_connected", MagicMock(return_value=False)
  71. )
  72. result = await _dispatch(dispatch_case, extra=[offline])
  73. assert (result.item.status, result.item.error_message) == ("failed", "Printer not connected")
  74. result.upload.assert_not_awaited()
  75. result.start.assert_not_called()
  76. result.power_off.assert_awaited_once()
  77. class TestARefusedStartOnAnIdlePrinter:
  78. @pytest.mark.asyncio
  79. async def test_fails_the_item_and_removes_the_upload(self, dispatch_case): # noqa: F811
  80. result = await _dispatch(dispatch_case, start_print=MagicMock(return_value=False))
  81. assert (result.item.status, result.item.error_message) == ("failed", "Failed to send print command to printer")
  82. result.upload.assert_awaited_once()
  83. uploaded_to = result.upload.await_args.args[3]
  84. # One clears the way before the upload, one removes the upload again
  85. assert [c.args[2] for c in result.delete.await_args_list] == [uploaded_to, uploaded_to]
  86. result.failed.assert_awaited_once()
  87. assert result.failed.await_args.kwargs["printer_id"] == dispatch_case.ids.printer_id