test_progress_milestones_3211.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """Progress milestones count from the first layer, not from the start of preparation (#3211).
  2. An A1 mini (firmware 01.07.01.00) reports ``mc_percent`` 85 in the first frame
  3. of a print, while still preheating the bed at layer 0. Through its calibration
  4. the value then reads 3, 7, 40 and 44, and the first real layer starts at 45.
  5. Read as progress, the 85 sent the 75% notification together with Print
  6. Started -- with "Unknown" remaining time, since that frame reports 0 -- and
  7. with 75 then on record, 25 and 50 could never fire.
  8. """
  9. from types import SimpleNamespace
  10. from unittest.mock import MagicMock, patch
  11. import pytest
  12. from backend.app import main as main_module
  13. pytestmark = pytest.mark.unit
  14. PRINTER = 1
  15. @pytest.fixture(autouse=True)
  16. def _clean_milestones():
  17. main_module._last_progress_milestone.clear()
  18. yield
  19. main_module._last_progress_milestone.clear()
  20. def _frame(progress: float, layer_num: int, total_layers: int = 128, stg_cur: int = 0) -> SimpleNamespace:
  21. return SimpleNamespace(progress=progress, layer_num=layer_num, total_layers=total_layers, stg_cur=stg_cur)
  22. def _run(frames) -> list[int]:
  23. sent = []
  24. for progress, layer, *rest in frames:
  25. milestone = main_module._progress_milestone_to_notify(PRINTER, _frame(progress, layer, *rest))
  26. if milestone is not None:
  27. sent.append(milestone)
  28. return sent
  29. # The reporter's print, frame by frame from the support bundle: preparation at
  30. # layer 0, then the print proper from layer 1.
  31. A1_MINI_PRINT = [
  32. (85, 0),
  33. (3, 0),
  34. (7, 0),
  35. (40, 0),
  36. (44, 0),
  37. (45, 1),
  38. (50, 1),
  39. (52, 2),
  40. (75, 30),
  41. (97, 120),
  42. (100, 128),
  43. ]
  44. def test_the_reported_print_sends_each_milestone_once_and_none_at_the_start():
  45. assert _run(A1_MINI_PRINT) == [25, 50, 75]
  46. def test_nothing_is_sent_during_preparation():
  47. assert _run(A1_MINI_PRINT[:5]) == []
  48. assert main_module._last_progress_milestone.get(PRINTER, 0) == 0
  49. def test_the_first_layer_catches_up_to_the_milestone_already_passed():
  50. """The print proper starts at 45 here, so 25 goes out at 45 -- once."""
  51. assert _run([(85, 0), (45, 1), (46, 1)]) == [25]
  52. def test_a_printer_without_layer_data_keeps_the_old_behaviour():
  53. """No layer count means nothing to gate on; never notifying would be worse."""
  54. assert _run([(10, 0, 0), (30, 0, 0), (60, 0, 0), (80, 0, 0)]) == [25, 50, 75]
  55. def test_without_a_layer_count_the_preparation_stage_holds_it_back():
  56. """A first frame with no total_layer_num leaves nothing to gate on by layers.
  57. The A1 mini's preparation stages, from the bundle: 2 (bed preheating),
  58. 4, 14 and 1, then 0 from the first layer. Until the pushall brings the
  59. total, the stage is what tells preparation from printing.
  60. """
  61. frames = [(85, 0, 0, 2), (3, 0, 0, 4), (40, 0, 0, 14), (44, 0, 0, 1), (45, 1, 128, 0), (50, 2, 128, 0)]
  62. assert _run(frames) == [25, 50]
  63. @pytest.mark.parametrize("no_stage", [-1, 255])
  64. def test_no_stage_at_all_is_not_preparation(no_stage):
  65. assert _run([(30, 0, 0, no_stage)]) == [25]
  66. def test_a_normal_print_is_unchanged():
  67. frames = [(1, 1), (24, 10), (25, 11), (49, 20), (50, 21), (74, 30), (75, 31), (100, 40)]
  68. assert _run(frames) == [25, 50, 75]
  69. @pytest.mark.asyncio
  70. async def test_a_new_print_starts_its_milestones_from_zero():
  71. """A printer can go from FINISH at 100% straight into the next print.
  72. The status path only resets on progress below 5 while not printing, which
  73. that never shows, so the previous print's 75 would block every milestone
  74. of the next one. The print-start callback resets it.
  75. """
  76. assert _run(A1_MINI_PRINT) == [25, 50, 75]
  77. class _Stop(Exception):
  78. pass
  79. # The reset is one of the first things on_print_start does; the rest of it
  80. # (archiving, usage tracking, notifications) is not under test here, so the
  81. # call is stopped at the next statement.
  82. stop_here = SimpleNamespace(pop=MagicMock(side_effect=_Stop))
  83. with patch.object(main_module, "_kill_switch_notification_tasks", stop_here), pytest.raises(_Stop):
  84. await main_module.on_print_start(PRINTER, {})
  85. assert main_module._last_progress_milestone[PRINTER] == 0
  86. assert _run(A1_MINI_PRINT) == [25, 50, 75]