test_bed_jog.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. """Unit tests for the bed-jog and home-axes endpoints (#791).
  2. Tests:
  3. POST /api/v1/printers/{printer_id}/bed-jog?distance=<mm>&force=<bool>
  4. POST /api/v1/printers/{printer_id}/home-axes?axes=<z|xy|all>
  5. """
  6. from unittest.mock import MagicMock, patch
  7. import pytest
  8. from httpx import AsyncClient
  9. class TestBedJogAPI:
  10. @pytest.mark.asyncio
  11. async def test_bed_jog_not_found(self, async_client: AsyncClient):
  12. response = await async_client.post("/api/v1/printers/99999/bed-jog?distance=10")
  13. assert response.status_code == 404
  14. @pytest.mark.asyncio
  15. async def test_bed_jog_zero_distance_rejected(self, async_client: AsyncClient, printer_factory):
  16. printer = await printer_factory(name="P1")
  17. response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=0")
  18. assert response.status_code == 400
  19. assert "distance" in response.json()["detail"].lower()
  20. @pytest.mark.asyncio
  21. async def test_bed_jog_too_large_rejected(self, async_client: AsyncClient, printer_factory):
  22. printer = await printer_factory(name="P1")
  23. response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=500")
  24. assert response.status_code == 400
  25. @pytest.mark.asyncio
  26. async def test_bed_jog_not_connected(self, async_client: AsyncClient, printer_factory):
  27. printer = await printer_factory(name="Disconnected")
  28. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  29. mock_pm.get_client.return_value = None
  30. response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10")
  31. assert response.status_code == 400
  32. assert "not connected" in response.json()["detail"].lower()
  33. @pytest.mark.asyncio
  34. async def test_bed_jog_send_failure(self, async_client: AsyncClient, printer_factory):
  35. printer = await printer_factory(name="P1")
  36. mock_client = MagicMock()
  37. mock_client.send_gcode.return_value = False
  38. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  39. mock_pm.get_client.return_value = mock_client
  40. response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10")
  41. assert response.status_code == 500
  42. @pytest.mark.asyncio
  43. async def test_bed_jog_success_without_force(self, async_client: AsyncClient, printer_factory):
  44. """When force=false the M211 guard lines must not be emitted."""
  45. printer = await printer_factory(name="P1")
  46. mock_client = MagicMock()
  47. mock_client.send_gcode.return_value = True
  48. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  49. mock_pm.get_client.return_value = mock_client
  50. response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10&force=false")
  51. assert response.status_code == 200
  52. sent_gcode = mock_client.send_gcode.call_args[0][0]
  53. assert "G91" in sent_gcode
  54. assert "G1 Z10.00" in sent_gcode
  55. assert "G90" in sent_gcode
  56. assert "M211" not in sent_gcode
  57. @pytest.mark.asyncio
  58. async def test_bed_jog_success_with_force(self, async_client: AsyncClient, printer_factory):
  59. """force=true must wrap the move in M211 S0 / M211 S1."""
  60. printer = await printer_factory(name="P1")
  61. mock_client = MagicMock()
  62. mock_client.send_gcode.return_value = True
  63. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  64. mock_pm.get_client.return_value = mock_client
  65. response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=-5&force=true")
  66. assert response.status_code == 200
  67. sent_gcode = mock_client.send_gcode.call_args[0][0]
  68. lines = sent_gcode.splitlines()
  69. assert lines[0] == "M211 S0"
  70. assert lines[-1] == "M211 S1"
  71. assert "G1 Z-5.00" in sent_gcode
  72. class TestHomeAxesAPI:
  73. @pytest.mark.asyncio
  74. async def test_home_axes_not_found(self, async_client: AsyncClient):
  75. response = await async_client.post("/api/v1/printers/99999/home-axes?axes=z")
  76. assert response.status_code == 404
  77. @pytest.mark.asyncio
  78. async def test_home_axes_invalid(self, async_client: AsyncClient, printer_factory):
  79. printer = await printer_factory(name="P1")
  80. response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=bogus")
  81. assert response.status_code == 400
  82. @pytest.mark.asyncio
  83. @pytest.mark.parametrize("axes", ["z", "xy", "all"])
  84. async def test_home_axes_always_runs_full_home(self, async_client: AsyncClient, printer_factory, axes):
  85. # Regression for #1052: regardless of the axes argument, the endpoint must send a bare
  86. # `G28` so the printer's safe auto-home sequence (toolhead park → XY home → Z home) runs.
  87. # Sending `G28 Z` alone on H2C/H2D/H2S/X1 can crash the bed into the toolhead.
  88. printer = await printer_factory(name="P1")
  89. mock_client = MagicMock()
  90. mock_client.send_gcode.return_value = True
  91. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  92. mock_pm.get_client.return_value = mock_client
  93. response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes={axes}")
  94. assert response.status_code == 200
  95. mock_client.send_gcode.assert_called_once_with("G28")
  96. @pytest.mark.asyncio
  97. async def test_home_axes_not_connected(self, async_client: AsyncClient, printer_factory):
  98. printer = await printer_factory(name="D")
  99. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  100. mock_pm.get_client.return_value = None
  101. response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=z")
  102. assert response.status_code == 400