1
0

test_connection_manager.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. """
  2. Unit tests for the FluidNC HTTP connection layer.
  3. The backend drives the board over HTTP now (no serial/GRBL transport), so these
  4. tests cover the FluidNCClient command/route formatting and the connection_manager
  5. helpers that sit on top of it. HTTP is mocked — no board required.
  6. """
  7. import pytest
  8. from unittest.mock import patch, MagicMock
  9. from modules.connection.fluidnc_client import FluidNCClient
  10. def _resp(text="ok", json_data=None, status=200):
  11. r = MagicMock()
  12. r.text = text
  13. r.status_code = status
  14. r.raise_for_status = MagicMock()
  15. if json_data is not None:
  16. r.json = MagicMock(return_value=json_data)
  17. return r
  18. class TestFluidNCClient:
  19. """Command / route formatting for the board client."""
  20. def test_run_pattern_uses_sd_run(self):
  21. client = FluidNCClient("http://board")
  22. with patch("modules.connection.fluidnc_client.requests") as req:
  23. req.get.return_value = _resp()
  24. client.run_pattern("/patterns/star.thr")
  25. url, kwargs = req.get.call_args[0][0], req.get.call_args[1]
  26. assert url == "http://board/command"
  27. assert kwargs["params"] == {"plain": "$SD/Run=/patterns/star.thr"}
  28. def test_run_pattern_with_clear_uses_sand_run(self):
  29. client = FluidNCClient("http://board")
  30. with patch("modules.connection.fluidnc_client.requests") as req:
  31. req.get.return_value = _resp()
  32. client.run_pattern("/patterns/star.thr", clear="adaptive")
  33. assert req.get.call_args[1]["params"] == {
  34. "plain": "$Sand/Run=/patterns/star.thr clear=adaptive"
  35. }
  36. def test_set_feed_mm(self):
  37. client = FluidNCClient("http://board")
  38. with patch("modules.connection.fluidnc_client.requests") as req:
  39. req.get.return_value = _resp()
  40. client.set_feed(mm=850)
  41. assert req.get.call_args[0][0] == "http://board/sand_feed"
  42. assert req.get.call_args[1]["params"] == {"mm": 850}
  43. def test_goto_includes_only_given_axes(self):
  44. client = FluidNCClient("http://board")
  45. with patch("modules.connection.fluidnc_client.requests") as req:
  46. req.get.return_value = _resp()
  47. client.goto(rho=0)
  48. assert req.get.call_args[1]["params"] == {"rho": 0}
  49. def test_stop_hits_sand_stop(self):
  50. client = FluidNCClient("http://board")
  51. with patch("modules.connection.fluidnc_client.requests") as req:
  52. req.get.return_value = _resp()
  53. client.stop()
  54. assert req.get.call_args[0][0] == "http://board/sand_stop"
  55. def test_get_status_returns_json(self):
  56. client = FluidNCClient("http://board")
  57. with patch("modules.connection.fluidnc_client.requests") as req:
  58. req.get.return_value = _resp(json_data={"state": "Idle", "theta": 1.0})
  59. st = client.get_status()
  60. assert st["state"] == "Idle"
  61. def test_reachable_true_and_false(self):
  62. client = FluidNCClient("http://board")
  63. with patch("modules.connection.fluidnc_client.requests") as req:
  64. req.get.return_value = _resp(json_data={"state": "Idle"})
  65. assert client.reachable() is True
  66. assert client.is_connected() is True
  67. req.get.side_effect = Exception("timeout")
  68. assert client.reachable() is False
  69. assert client.is_connected() is False
  70. def test_upload_file_field_naming(self):
  71. """The multipart file part is named by the full SD path, plus a '<path>S' size field."""
  72. client = FluidNCClient("http://board")
  73. with patch("modules.connection.fluidnc_client.requests") as req:
  74. req.post.return_value = _resp(text="{}", json_data={})
  75. client.upload_file("/playlists/a.txt", b"hello", "/playlists")
  76. kwargs = req.post.call_args[1]
  77. assert kwargs["params"] == {"path": "/playlists"}
  78. assert kwargs["data"] == {"/playlists/a.txtS": "5"}
  79. assert "/playlists/a.txt" in kwargs["files"]
  80. def test_delete_file_action(self):
  81. client = FluidNCClient("http://board")
  82. with patch("modules.connection.fluidnc_client.requests") as req:
  83. req.get.return_value = _resp(text="{}", json_data={"status": "ok"})
  84. client.delete_file("patterns", "old.thr")
  85. assert req.get.call_args[1]["params"]["action"] == "delete"
  86. assert req.get.call_args[1]["params"]["filename"] == "old.thr"
  87. class TestConnectionManagerHelpers:
  88. """Helpers in connection_manager that sit on top of the client."""
  89. def test_normalize_board_url(self):
  90. from modules.connection import connection_manager as cm
  91. assert cm._normalize_board_url("192.168.1.5") == "http://192.168.1.5"
  92. assert cm._normalize_board_url("http://x/") == "http://x"
  93. assert cm._normalize_board_url("") == ""
  94. def test_list_serial_ports_returns_board_url(self, mock_state):
  95. from modules.connection import connection_manager as cm
  96. mock_state.board_url = "http://192.168.1.9"
  97. with patch("modules.connection.connection_manager.state", mock_state):
  98. ports = cm.list_serial_ports()
  99. assert ports == ["http://192.168.1.9"]
  100. def test_apply_status_maps_fields(self, mock_state):
  101. from modules.connection import connection_manager as cm
  102. with patch("modules.connection.connection_manager.state", mock_state):
  103. cm.apply_status({"theta": 1.23, "rho": 0.5, "feed": 900})
  104. assert mock_state.current_theta == 1.23
  105. assert mock_state.current_rho == 0.5
  106. assert mock_state.speed == 900
  107. def test_is_machine_idle_true(self, mock_state):
  108. from modules.connection import connection_manager as cm
  109. mock_state.conn.get_status.return_value = {"state": "Idle"}
  110. with patch("modules.connection.connection_manager.state", mock_state):
  111. assert cm.is_machine_idle() is True
  112. def test_is_machine_idle_running(self, mock_state):
  113. from modules.connection import connection_manager as cm
  114. mock_state.conn.get_status.return_value = {"state": "Run"}
  115. with patch("modules.connection.connection_manager.state", mock_state):
  116. assert cm.is_machine_idle() is False
  117. def test_is_machine_idle_no_connection(self, mock_state):
  118. from modules.connection import connection_manager as cm
  119. mock_state.conn = None
  120. with patch("modules.connection.connection_manager.state", mock_state):
  121. assert cm.is_machine_idle() is False