test_batch_status_after_cancel.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. """An order or grouping reports itself finished however its last job ended."""
  2. from __future__ import annotations
  3. from types import SimpleNamespace
  4. from unittest.mock import MagicMock, patch
  5. import pytest
  6. from httpx import AsyncClient
  7. from sqlalchemy import update
  8. from backend.app.models.print_queue import PrintQueueItem
  9. from backend.tests.integration.test_print_batch_orders import _create_order, _queue_item
  10. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  11. async def _batch_status(client: AsyncClient, batch_id: int) -> str:
  12. response = await client.get(f"/api/v1/queue/batches/{batch_id}")
  13. assert response.status_code == 200, response.text
  14. return response.json()["status"]
  15. async def test_a_grouping_whose_last_job_is_cancelled_is_cancelled(
  16. async_client: AsyncClient, printer_factory, archive_factory
  17. ):
  18. printer = await printer_factory()
  19. archive = await archive_factory(printer.id)
  20. ids = [
  21. (await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": archive.id})).json()[
  22. "id"
  23. ]
  24. for _ in range(2)
  25. ]
  26. grouping = (await async_client.post("/api/v1/queue/batches", json={"name": "G", "item_ids": ids})).json()
  27. for item_id in ids:
  28. assert (await async_client.post(f"/api/v1/queue/{item_id}/cancel")).status_code == 200
  29. assert await _batch_status(async_client, grouping["id"]) == "cancelled"
  30. async def test_an_order_whose_last_run_is_closed_by_the_sweep_is_completed(
  31. async_client: AsyncClient, db_session, printer_factory, archive_factory
  32. ):
  33. import backend.app.core.database as database
  34. from backend.app.services.print_scheduler import PrintScheduler
  35. printer = await printer_factory()
  36. archive = await archive_factory(printer.id)
  37. order = await _create_order(async_client, archive.id, [{"plate_id": 1, "quantity_target": 1}])
  38. run = await _queue_item(async_client, printer.id, archive.id, order["id"], plate_id=1)
  39. await db_session.execute(update(PrintQueueItem).where(PrintQueueItem.id == run["id"]).values(status="printing"))
  40. await db_session.commit()
  41. scheduler = PrintScheduler()
  42. scheduler._terminal_since[printer.id] = -10_000.0
  43. with (
  44. patch("backend.app.services.print_scheduler.async_session", database.async_session),
  45. patch(
  46. "backend.app.services.print_scheduler.printer_manager.get_status",
  47. MagicMock(return_value=SimpleNamespace(state="FINISH", connected=True)),
  48. ),
  49. ):
  50. await scheduler._close_stranded_printing_items()
  51. assert await _batch_status(async_client, order["id"]) == "completed"
  52. async def test_a_grouping_whose_file_is_deleted_is_cancelled(async_client: AsyncClient, db_session, printer_factory):
  53. from backend.app.models.library import LibraryFile
  54. from backend.app.services.library_trash import release_queue_references
  55. printer = await printer_factory()
  56. file = LibraryFile(filename="part.3mf", file_path="part.3mf", file_type="3mf", file_size=1)
  57. db_session.add(file)
  58. await db_session.commit()
  59. item = PrintQueueItem(printer_id=printer.id, library_file_id=file.id, status="pending", position=1)
  60. db_session.add(item)
  61. await db_session.commit()
  62. grouping = (await async_client.post("/api/v1/queue/batches", json={"name": "G", "item_ids": [item.id]})).json()
  63. await release_queue_references(db_session, [file.id])
  64. await db_session.commit()
  65. assert await _batch_status(async_client, grouping["id"]) == "cancelled"
  66. async def _grouping(client: AsyncClient, printer_id: int, archive_id: int) -> tuple[dict, list[int]]:
  67. ids = [
  68. (await client.post("/api/v1/queue/", json={"printer_id": printer_id, "archive_id": archive_id})).json()["id"]
  69. for _ in range(2)
  70. ]
  71. grouping = (await client.post("/api/v1/queue/batches", json={"name": "G", "item_ids": ids})).json()
  72. return grouping, ids
  73. async def test_a_grouping_whose_last_job_fails_at_dispatch_is_finished(
  74. async_client: AsyncClient, db_session, printer_factory, archive_factory
  75. ):
  76. from backend.app.services.print_scheduler import PrintScheduler
  77. printer = await printer_factory()
  78. archive = await archive_factory(printer.id)
  79. grouping, (done, last) = await _grouping(async_client, printer.id, archive.id)
  80. await db_session.execute(update(PrintQueueItem).where(PrintQueueItem.id == done).values(status="completed"))
  81. await db_session.commit()
  82. await PrintScheduler()._fail_dispatch(db_session, await db_session.get(PrintQueueItem, last), "Printer not found")
  83. assert await _batch_status(async_client, grouping["id"]) == "completed"
  84. async def test_a_grouping_whose_last_open_job_is_deleted_is_finished(
  85. async_client: AsyncClient, printer_factory, archive_factory
  86. ):
  87. printer = await printer_factory()
  88. archive = await archive_factory(printer.id)
  89. grouping, (first, last) = await _grouping(async_client, printer.id, archive.id)
  90. assert (await async_client.post(f"/api/v1/queue/{first}/cancel")).status_code == 200
  91. assert (await async_client.delete(f"/api/v1/queue/{last}")).status_code == 200
  92. assert await _batch_status(async_client, grouping["id"]) == "cancelled"
  93. async def _settle(db_session, statuses: dict[int, str]) -> None:
  94. from backend.app.services.print_batch import refresh_batches_for_items
  95. for item_id, status in statuses.items():
  96. await db_session.execute(update(PrintQueueItem).where(PrintQueueItem.id == item_id).values(status=status))
  97. await refresh_batches_for_items(db_session, list(statuses))
  98. await db_session.commit()
  99. @pytest.mark.parametrize(
  100. ("ends", "expected"),
  101. [
  102. (("failed", "failed"), "failed"),
  103. (("failed", "cancelled"), "failed"),
  104. (("failed", "skipped"), "active"),
  105. (("failed", "completed"), "completed"),
  106. ],
  107. )
  108. async def test_a_grouping_that_produced_nothing_but_failures_is_failed(
  109. async_client: AsyncClient, db_session, printer_factory, archive_factory, ends, expected
  110. ):
  111. printer = await printer_factory()
  112. archive = await archive_factory(printer.id)
  113. grouping, ids = await _grouping(async_client, printer.id, archive.id)
  114. await _settle(db_session, dict(zip(ids, ends, strict=True)))
  115. assert await _batch_status(async_client, grouping["id"]) == expected
  116. async def test_a_failed_grouping_takes_a_new_job_and_is_open_again(
  117. async_client: AsyncClient, db_session, printer_factory, archive_factory
  118. ):
  119. printer = await printer_factory()
  120. archive = await archive_factory(printer.id)
  121. grouping, ids = await _grouping(async_client, printer.id, archive.id)
  122. await _settle(db_session, dict.fromkeys(ids, "failed"))
  123. added = await async_client.post(
  124. "/api/v1/queue/", json={"printer_id": printer.id, "archive_id": archive.id, "batch_id": grouping["id"]}
  125. )
  126. assert added.status_code == 200, added.text
  127. assert await _batch_status(async_client, grouping["id"]) == "active"
  128. async def test_an_order_whose_runs_all_failed_still_owes_them(
  129. async_client: AsyncClient, db_session, printer_factory, archive_factory
  130. ):
  131. printer = await printer_factory()
  132. archive = await archive_factory(printer.id)
  133. order = await _create_order(async_client, archive.id, [{"plate_id": 1, "quantity_target": 1}])
  134. run = await _queue_item(async_client, printer.id, archive.id, order["id"], plate_id=1)
  135. await _settle(db_session, {run["id"]: "failed"})
  136. assert await _batch_status(async_client, order["id"]) == "active"