test_api_patterns.py 13 KB

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