test_kill_switch_stop_archived_cancelled.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. """A print the kill switch stopped is archived as cancelled, with its reason.
  2. The printer reports a stop as "failed" or "aborted", and only stops made from
  3. Bambuddy's queue were turned into "cancelled". A kill-switch stop therefore
  4. went into the failure statistics as a failed print with no reason. It is now
  5. "cancelled" with the reason ``killSwitch``.
  6. """
  7. import asyncio
  8. from types import SimpleNamespace
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. import pytest
  11. from backend.app import main as main_module
  12. from backend.app.services.archive import ArchiveService
  13. from backend.tests.integration.test_external_print_confirmation_1898 import ( # noqa: F401
  14. _clear_print_state,
  15. _drive_print_start,
  16. )
  17. pytestmark = pytest.mark.integration
  18. @pytest.fixture(autouse=True)
  19. def _clean():
  20. for tracked in (main_module._active_prints, main_module._kill_switch_stopped, main_module._user_stopped_printers):
  21. tracked.clear()
  22. yield
  23. for tracked in (main_module._active_prints, main_module._kill_switch_stopped, main_module._user_stopped_printers):
  24. tracked.clear()
  25. async def _complete(printer_id, status, update):
  26. session = AsyncMock()
  27. session.__aenter__ = AsyncMock(return_value=session)
  28. session.__aexit__ = AsyncMock(return_value=False)
  29. # Plain mocks for what the reads return: an AsyncMock's results are
  30. # AsyncMocks too, so a plain call on one (completed_at.astimezone(),
  31. # .first()) leaves an unawaited coroutine, reported in a later test.
  32. for read in ("execute", "get", "scalar", "scalars"):
  33. setattr(session, read, AsyncMock(return_value=MagicMock()))
  34. tasks_before = set(asyncio.all_tasks())
  35. with (
  36. patch("backend.app.main.async_session", return_value=session),
  37. patch("backend.app.core.database.async_session", return_value=session),
  38. patch("backend.app.main.ws_manager") as ws,
  39. patch("backend.app.main.mqtt_relay") as relay,
  40. patch("backend.app.main.notification_service") as notif,
  41. patch("backend.app.main.smart_plug_manager") as plug,
  42. patch("backend.app.main.printer_manager") as pm,
  43. patch.object(ArchiveService, "update_archive_status", update),
  44. ):
  45. ws.send_print_complete = AsyncMock()
  46. ws.broadcast = AsyncMock()
  47. relay.on_print_complete = AsyncMock()
  48. notif.on_print_complete = AsyncMock()
  49. plug.on_print_complete = AsyncMock()
  50. pm.get_printer.return_value = None
  51. await main_module.on_print_complete(
  52. printer_id,
  53. {"status": status, "filename": "x.gcode", "subtask_name": "Kill_Me", "timelapse_was_active": False},
  54. )
  55. for task in asyncio.all_tasks() - tasks_before:
  56. task.cancel()
  57. try:
  58. await task
  59. except (asyncio.CancelledError, Exception):
  60. pass
  61. @pytest.mark.asyncio
  62. @pytest.mark.parametrize("reported", ["failed", "aborted"])
  63. async def test_a_kill_switch_stop_is_a_cancel_with_its_reason(reported):
  64. main_module._active_prints[(7, "Kill_Me.3mf")] = 55
  65. main_module._kill_switch_stopped.add(7)
  66. update = AsyncMock()
  67. await _complete(7, reported, update)
  68. kwargs = update.await_args.kwargs
  69. assert (kwargs["status"], kwargs["failure_reason"]) == ("cancelled", "killSwitch")
  70. assert 7 not in main_module._kill_switch_stopped
  71. @pytest.mark.asyncio
  72. async def test_a_print_that_failed_on_its_own_stays_failed():
  73. main_module._active_prints[(7, "Kill_Me.3mf")] = 55
  74. update = AsyncMock()
  75. await _complete(7, "failed", update)
  76. assert update.await_args.kwargs["status"] == "failed"
  77. @pytest.mark.asyncio
  78. async def test_the_next_print_starts_without_the_flag(test_engine, printer_factory, monkeypatch):
  79. """A stop whose completion never arrived must not mark the next print."""
  80. monkeypatch.setattr(main_module.printer_manager, "get_current_print_user", lambda printer_id: None)
  81. printer_id = (await printer_factory()).id
  82. main_module._kill_switch_stopped.add(printer_id)
  83. await _drive_print_start(test_engine, SimpleNamespace(id=printer_id), download_ok=False)
  84. assert printer_id not in main_module._kill_switch_stopped
  85. def test_the_reason_is_in_the_vocabulary():
  86. from backend.app.api.routes.print_log import _FAILURE_REASON_KEYS
  87. assert "killSwitch" in _FAILURE_REASON_KEYS
  88. def test_the_archive_editor_offers_the_same_vocabulary():
  89. """A reason the editor does not list shows untranslated, as its own cause."""
  90. import re
  91. from pathlib import Path
  92. from backend.app.api.routes.print_log import _FAILURE_REASON_KEYS
  93. source = (Path(__file__).parents[3] / "frontend/src/components/EditArchiveModal.tsx").read_text()
  94. listed = re.search(r"FAILURE_REASON_KEYS[^=]*=\s*\[(.*?)\]", source, re.S).group(1)
  95. assert set(re.findall(r"'([^']*)'", listed)) == _FAILURE_REASON_KEYS - {""}