1
0

test_api_patterns.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. """
  2. Unit tests for pattern API endpoints.
  3. Tests the following endpoints:
  4. - GET /list_theta_rho_files
  5. - GET /list_theta_rho_files_with_metadata
  6. - POST /get_theta_rho_coordinates
  7. - POST /run_theta_rho (when disconnected)
  8. """
  9. import pytest
  10. from unittest.mock import patch, MagicMock, AsyncMock
  11. import os
  12. class TestListThetaRhoFiles:
  13. """Tests for /list_theta_rho_files endpoint."""
  14. @pytest.mark.asyncio
  15. async def test_list_theta_rho_files(self, async_client):
  16. """Test list_theta_rho_files returns list of pattern files."""
  17. mock_files = ["circle.thr", "spiral.thr", "custom/pattern.thr"]
  18. with patch("main.pattern_manager.list_theta_rho_files", return_value=mock_files):
  19. response = await async_client.get("/list_theta_rho_files")
  20. assert response.status_code == 200
  21. data = response.json()
  22. assert isinstance(data, list)
  23. assert len(data) == 3
  24. assert "circle.thr" in data
  25. assert "spiral.thr" in data
  26. assert "custom/pattern.thr" in data
  27. @pytest.mark.asyncio
  28. async def test_list_theta_rho_files_empty(self, async_client):
  29. """Test list_theta_rho_files returns empty list when no patterns."""
  30. with patch("main.pattern_manager.list_theta_rho_files", return_value=[]):
  31. response = await async_client.get("/list_theta_rho_files")
  32. assert response.status_code == 200
  33. data = response.json()
  34. assert data == []
  35. class TestListThetaRhoFilesWithMetadata:
  36. """Tests for /list_theta_rho_files_with_metadata endpoint."""
  37. @pytest.mark.asyncio
  38. async def test_list_theta_rho_files_with_metadata(self, async_client, tmp_path):
  39. """Test list_theta_rho_files_with_metadata returns files with metadata."""
  40. mock_files = ["circle.thr"]
  41. # The endpoint has two paths:
  42. # 1. If metadata_cache.json exists, use it
  43. # 2. Otherwise, use ThreadPoolExecutor with process_file
  44. # We'll test the fallback path by having the cache file not exist
  45. patterns_dir = tmp_path / "patterns"
  46. patterns_dir.mkdir()
  47. (patterns_dir / "circle.thr").write_text("0 0.5\n1 0.6")
  48. with patch("main.pattern_manager.list_theta_rho_files", return_value=mock_files):
  49. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  50. # Simulate cache file not existing
  51. with patch("builtins.open", side_effect=FileNotFoundError):
  52. response = await async_client.get("/list_theta_rho_files_with_metadata")
  53. assert response.status_code == 200
  54. data = response.json()
  55. assert isinstance(data, list)
  56. assert len(data) == 1
  57. # The response structure has 'path', 'name', 'category', 'date_modified', 'coordinates_count'
  58. item = data[0]
  59. assert item["path"] == "circle.thr"
  60. assert item["name"] == "circle"
  61. assert "category" in item
  62. assert "date_modified" in item
  63. assert "coordinates_count" in item
  64. class TestGetThetaRhoCoordinates:
  65. """Tests for /get_theta_rho_coordinates endpoint."""
  66. @pytest.mark.asyncio
  67. async def test_get_theta_rho_coordinates_valid_file(self, async_client, tmp_path):
  68. """Test getting coordinates from a valid file."""
  69. # Create test pattern file
  70. patterns_dir = tmp_path / "patterns"
  71. patterns_dir.mkdir()
  72. test_file = patterns_dir / "test.thr"
  73. test_file.write_text("0.0 0.5\n1.57 0.8\n3.14 0.3\n")
  74. mock_coordinates = [(0.0, 0.5), (1.57, 0.8), (3.14, 0.3)]
  75. with patch("main.THETA_RHO_DIR", str(patterns_dir)):
  76. with patch("main.parse_theta_rho_file", return_value=mock_coordinates):
  77. response = await async_client.post(
  78. "/get_theta_rho_coordinates",
  79. json={"file_name": "test.thr"}
  80. )
  81. assert response.status_code == 200
  82. data = response.json()
  83. assert "coordinates" in data
  84. assert len(data["coordinates"]) == 3
  85. assert data["coordinates"][0] == [0.0, 0.5]
  86. assert data["coordinates"][1] == [1.57, 0.8]
  87. assert data["coordinates"][2] == [3.14, 0.3]
  88. @pytest.mark.asyncio
  89. async def test_get_theta_rho_coordinates_file_not_found(self, async_client, tmp_path):
  90. """Test getting coordinates from non-existent file returns error.
  91. Note: The endpoint returns 500 because it catches the HTTPException and re-raises it.
  92. """
  93. patterns_dir = tmp_path / "patterns"
  94. patterns_dir.mkdir()
  95. with patch("main.THETA_RHO_DIR", str(patterns_dir)):
  96. response = await async_client.post(
  97. "/get_theta_rho_coordinates",
  98. json={"file_name": "nonexistent.thr"}
  99. )
  100. # The endpoint wraps the 404 in a 500 due to exception handling
  101. assert response.status_code in [404, 500]
  102. data = response.json()
  103. assert "not found" in data["detail"].lower()
  104. class TestRunThetaRho:
  105. """Tests for /run_theta_rho endpoint."""
  106. @pytest.mark.asyncio
  107. async def test_run_theta_rho_when_disconnected(self, async_client, mock_state, tmp_path):
  108. """Test run_theta_rho fails gracefully when disconnected."""
  109. # The endpoint checks file existence first, then connection
  110. patterns_dir = tmp_path / "patterns"
  111. patterns_dir.mkdir()
  112. test_file = patterns_dir / "circle.thr"
  113. test_file.write_text("0 0.5")
  114. mock_state.conn = None
  115. mock_state.is_homing = False
  116. with patch("main.state", mock_state):
  117. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  118. response = await async_client.post(
  119. "/run_theta_rho",
  120. json={
  121. "file_name": "circle.thr",
  122. "pre_execution": "none"
  123. }
  124. )
  125. assert response.status_code == 400
  126. data = response.json()
  127. assert "not established" in data["detail"].lower() or "not connected" in data["detail"].lower()
  128. @pytest.mark.asyncio
  129. async def test_run_theta_rho_during_homing(self, async_client, mock_state, tmp_path):
  130. """Test run_theta_rho fails when homing is in progress."""
  131. patterns_dir = tmp_path / "patterns"
  132. patterns_dir.mkdir()
  133. test_file = patterns_dir / "circle.thr"
  134. test_file.write_text("0 0.5")
  135. mock_state.is_homing = True
  136. mock_state.conn = MagicMock()
  137. mock_state.conn.is_connected.return_value = True
  138. with patch("main.state", mock_state):
  139. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  140. response = await async_client.post(
  141. "/run_theta_rho",
  142. json={
  143. "file_name": "circle.thr",
  144. "pre_execution": "none"
  145. }
  146. )
  147. assert response.status_code == 409
  148. data = response.json()
  149. assert "homing" in data["detail"].lower()
  150. @pytest.mark.asyncio
  151. async def test_run_theta_rho_file_not_found(self, async_client, mock_state, tmp_path):
  152. """Test run_theta_rho returns 404 for non-existent file."""
  153. patterns_dir = tmp_path / "patterns"
  154. patterns_dir.mkdir()
  155. mock_state.conn = MagicMock()
  156. mock_state.conn.is_connected.return_value = True
  157. mock_state.is_homing = False
  158. with patch("main.state", mock_state):
  159. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  160. response = await async_client.post(
  161. "/run_theta_rho",
  162. json={
  163. "file_name": "nonexistent.thr",
  164. "pre_execution": "none"
  165. }
  166. )
  167. assert response.status_code == 404
  168. data = response.json()
  169. assert "not found" in data["detail"].lower()
  170. class TestStopExecution:
  171. """Tests for /stop_execution endpoint."""
  172. @pytest.mark.asyncio
  173. async def test_stop_execution(self, async_client, mock_state):
  174. """Test stop_execution endpoint."""
  175. mock_state.is_homing = False
  176. mock_state.conn = MagicMock()
  177. mock_state.conn.is_connected.return_value = True
  178. with patch("main.state", mock_state):
  179. with patch("main.pattern_manager.stop_actions", new_callable=AsyncMock, return_value=True):
  180. response = await async_client.post("/stop_execution")
  181. assert response.status_code == 200
  182. data = response.json()
  183. assert data["success"] is True
  184. @pytest.mark.asyncio
  185. async def test_stop_execution_when_disconnected(self, async_client, mock_state):
  186. """Test stop_execution fails when not connected."""
  187. mock_state.conn = None
  188. with patch("main.state", mock_state):
  189. response = await async_client.post("/stop_execution")
  190. assert response.status_code == 400
  191. data = response.json()
  192. assert "not established" in data["detail"].lower()
  193. class TestPauseResumeExecution:
  194. """Tests for /pause_execution and /resume_execution endpoints."""
  195. @pytest.mark.asyncio
  196. async def test_pause_execution(self, async_client):
  197. """Test pause_execution endpoint."""
  198. with patch("main.pattern_manager.pause_execution", return_value=True):
  199. response = await async_client.post("/pause_execution")
  200. assert response.status_code == 200
  201. data = response.json()
  202. assert data["success"] is True
  203. @pytest.mark.asyncio
  204. async def test_resume_execution(self, async_client):
  205. """Test resume_execution endpoint."""
  206. with patch("main.pattern_manager.resume_execution", return_value=True):
  207. response = await async_client.post("/resume_execution")
  208. assert response.status_code == 200
  209. data = response.json()
  210. assert data["success"] is True
  211. class TestDeleteThetaRhoFile:
  212. """Tests for /delete_theta_rho_file endpoint."""
  213. @pytest.mark.asyncio
  214. async def test_delete_theta_rho_file_success(self, async_client, tmp_path):
  215. """Test deleting an existing pattern file."""
  216. patterns_dir = tmp_path / "patterns"
  217. patterns_dir.mkdir()
  218. test_file = patterns_dir / "test.thr"
  219. test_file.write_text("0 0.5")
  220. # Must patch pattern_manager.THETA_RHO_DIR which is what the endpoint uses
  221. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  222. with patch("modules.core.cache_manager.delete_pattern_cache", return_value=True):
  223. response = await async_client.post(
  224. "/delete_theta_rho_file",
  225. json={"file_name": "test.thr"}
  226. )
  227. assert response.status_code == 200
  228. data = response.json()
  229. assert data["success"] is True
  230. # Verify file was actually deleted
  231. assert not test_file.exists()
  232. @pytest.mark.asyncio
  233. async def test_delete_theta_rho_file_not_found(self, async_client, tmp_path):
  234. """Test deleting a non-existent file returns error."""
  235. patterns_dir = tmp_path / "patterns"
  236. patterns_dir.mkdir()
  237. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  238. response = await async_client.post(
  239. "/delete_theta_rho_file",
  240. json={"file_name": "nonexistent.thr"}
  241. )
  242. assert response.status_code == 404