test_failed_print_bogus_remain_3511.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """A print stopped before its first layer charges nothing, whatever the AMS reads (#3511).
  2. The reporter's X2D failed 290 s in. The 3MF named slot 2 on AMS0-T3 and worked
  3. out 0 g at layer 0, but did not claim the tray, so the remain% fallback charged
  4. it from the AMS reading at the stop: 100% at the start, a transient 0% at the
  5. end, the whole 1000 g spool.
  6. """
  7. from datetime import datetime, timezone
  8. from types import SimpleNamespace
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. import pytest
  11. from backend.app.services.usage_tracker import PrintSession, _active_sessions, _track_from_3mf, on_print_complete
  12. from backend.tests.unit.test_usage_tracker import (
  13. _make_archive,
  14. _make_assignment,
  15. _make_spool,
  16. _mock_db_sequential,
  17. )
  18. pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
  19. # The reporter's plate: one PETG filament in slot 2, mapped to AMS0-T3.
  20. USAGE = [{"slot_id": 2, "used_g": 28.05, "type": "PETG", "color": ""}]
  21. MAPPING = [65535, 3]
  22. def _stopped_state(
  23. remain_at_stop: int = 0, mqtt_mapping: list | None = MAPPING, last_loaded_tray: int = 3
  24. ) -> SimpleNamespace:
  25. raw = {"ams": [{"id": 0, "tray": [{"id": 3, "remain": remain_at_stop, "tray_type": "PETG"}]}]}
  26. if mqtt_mapping is not None:
  27. raw["mapping"] = mqtt_mapping
  28. return SimpleNamespace(
  29. progress=0,
  30. layer_num=0,
  31. total_layers=120,
  32. tray_now=255,
  33. last_loaded_tray=last_loaded_tray,
  34. tray_change_log=[],
  35. spool_change_log=[],
  36. raw_data=raw,
  37. )
  38. def _threemf(usage):
  39. path = MagicMock()
  40. path.exists.return_value = True
  41. settings = patch("backend.app.core.config.settings")
  42. return (
  43. settings,
  44. patch("backend.app.utils.threemf_tools.extract_filament_usage_from_3mf", return_value=usage),
  45. patch("backend.app.utils.threemf_tools.extract_layer_filament_usage_from_3mf", return_value=None),
  46. path,
  47. )
  48. async def _track(usage, ams_mapping, mqtt_mapping=MAPPING, last_loaded_tray=3):
  49. printer_manager = MagicMock()
  50. printer_manager.get_status.return_value = _stopped_state(
  51. mqtt_mapping=mqtt_mapping, last_loaded_tray=last_loaded_tray
  52. )
  53. handled: set = set()
  54. db = _mock_db_sequential([_make_archive(archive_id=32), None, _make_assignment(spool_id=38, ams_id=0, tray_id=3)])
  55. settings, extract, layers, path = _threemf(usage)
  56. with settings as mock_settings, extract, layers:
  57. mock_settings.base_dir.__truediv__ = MagicMock(return_value=path)
  58. results = await _track_from_3mf(
  59. printer_id=1,
  60. archive_id=32,
  61. status="failed",
  62. print_name="Desiccant core",
  63. handled_trays=handled,
  64. printer_manager=printer_manager,
  65. db=db,
  66. ams_mapping=ams_mapping,
  67. )
  68. return results, handled
  69. class TestTheThreeMfClaimsTheTray:
  70. async def test_a_mapped_slot_stopped_before_using_any_is_handled(self):
  71. results, handled = await _track(USAGE, MAPPING)
  72. assert results == []
  73. assert (0, 3) in handled
  74. async def test_a_tray_guessed_by_position_is_left_to_the_fallback(self):
  75. # No mapping from the dispatch nor from the printer, and no tray loaded.
  76. _, handled = await _track(USAGE, None, mqtt_mapping=None, last_loaded_tray=-1)
  77. assert handled == set()
  78. class TestTheCompletion:
  79. @pytest.fixture(autouse=True)
  80. def _clean(self):
  81. _active_sessions.clear()
  82. with patch("backend.app.api.routes.settings.get_setting", new_callable=AsyncMock, return_value=None):
  83. yield
  84. _active_sessions.clear()
  85. async def test_the_reporters_print_charges_nothing(self):
  86. _active_sessions[1] = PrintSession(
  87. printer_id=1,
  88. print_name="Desiccant core",
  89. started_at=datetime.now(timezone.utc),
  90. tray_remain_start={(0, 3): 100},
  91. tray_now_at_start=3,
  92. ams_mapping=MAPPING,
  93. )
  94. printer_manager = MagicMock()
  95. printer_manager.get_status.return_value = _stopped_state(remain_at_stop=0)
  96. spool = _make_spool(spool_id=38)
  97. assignment = _make_assignment(spool_id=38, ams_id=0, tray_id=3)
  98. # The 3MF path's archive, queue item and assignment; then what the
  99. # fallback would read if it ran: the assignment and the spool.
  100. db = _mock_db_sequential([_make_archive(archive_id=32), None, assignment, assignment, spool])
  101. settings, extract, layers, path = _threemf(USAGE)
  102. with settings as mock_settings, extract, layers:
  103. mock_settings.base_dir.__truediv__ = MagicMock(return_value=path)
  104. results = await on_print_complete(
  105. printer_id=1,
  106. data={"status": "failed"},
  107. printer_manager=printer_manager,
  108. db=db,
  109. archive_id=32,
  110. ams_mapping=MAPPING,
  111. )
  112. assert results == []
  113. db.add.assert_not_called()