test_print_control.py 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. """One way to stop a print on a user's behalf.
  2. The printer card, the queue and the two webhook routes each used to send the
  3. stop and set the "stopped by the user" mark themselves, and one of them forgot
  4. the mark: the print then ended as failed, with a "print failed" notification
  5. and, on some printers, a mislabelled error such as a layer shift. They now all
  6. call print_control.stop_print_by_user, and only it (and the automatic kill
  7. switch, which is not a user stop) may send a stop command.
  8. """
  9. import ast
  10. from pathlib import Path
  11. from unittest.mock import patch
  12. import pytest
  13. from backend.app.services.print_control import stop_print_by_user
  14. BACKEND_APP = Path(__file__).resolve().parents[2] / "app"
  15. @pytest.fixture
  16. def marks():
  17. from backend.app.main import _user_stopped_printers
  18. _user_stopped_printers.clear()
  19. yield _user_stopped_printers
  20. _user_stopped_printers.clear()
  21. def test_a_sent_stop_marks_the_printer(marks):
  22. with patch("backend.app.services.print_control.printer_manager.stop_print", return_value=True) as send:
  23. assert stop_print_by_user(4) is True
  24. send.assert_called_once_with(4)
  25. assert 4 in marks
  26. def test_an_unsent_stop_leaves_no_mark(marks):
  27. with patch("backend.app.services.print_control.printer_manager.stop_print", return_value=False):
  28. assert stop_print_by_user(4) is False
  29. assert 4 not in marks
  30. def test_the_queue_can_mark_an_unsent_stop(marks):
  31. """An offline printer that later reports the job as failed must still read
  32. it as cancelled by the user (the queue closes the job at once)."""
  33. with patch("backend.app.services.print_control.printer_manager.stop_print", return_value=False):
  34. assert stop_print_by_user(4, mark_when_unsent=True) is False
  35. assert 4 in marks
  36. def test_a_failing_send_raises_and_marks_only_when_asked(marks):
  37. with patch("backend.app.services.print_control.printer_manager.stop_print", side_effect=OSError("mqtt down")):
  38. with pytest.raises(OSError):
  39. stop_print_by_user(4)
  40. assert 4 not in marks
  41. with pytest.raises(OSError):
  42. stop_print_by_user(4, mark_when_unsent=True)
  43. assert 4 in marks
  44. # Files allowed to send a stop command themselves: the shared function, the
  45. # printer manager and MQTT client that implement it, and main.py's kill switch,
  46. # which stops unauthorised prints automatically (not a user stop, no mark).
  47. ALLOWED = {
  48. "services/print_control.py",
  49. "services/printer_manager.py",
  50. "services/bambu_mqtt.py",
  51. "main.py",
  52. }
  53. def test_no_other_code_sends_a_stop():
  54. """Catches a new ``.stop_print(`` call. It cannot see a stop sent as a raw
  55. MQTT payload (the HMS dialog's "Stop Printing" in bambu_mqtt.py, whose
  56. route marks the stop itself); such a path must call mark_stopped_by_user."""
  57. offenders = []
  58. for path in sorted(BACKEND_APP.rglob("*.py")):
  59. rel = path.relative_to(BACKEND_APP).as_posix()
  60. if rel in ALLOWED:
  61. continue
  62. for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
  63. if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "stop_print":
  64. offenders.append(f"{rel}:{node.lineno}")
  65. assert not offenders, (
  66. "Stop a print through services.print_control.stop_print_by_user, which also sets the "
  67. f"stopped-by-user mark: {offenders}"
  68. )