test_api_patterns.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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.pattern_manager.stop_actions", 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. # Mock check_table_is_idle to return False (something is playing)
  198. with patch("main.pattern_manager.check_table_is_idle", return_value=False):
  199. with patch("main.pattern_manager.pause_execution", return_value=True):
  200. response = await async_client.post("/pause_execution")
  201. assert response.status_code == 200
  202. data = response.json()
  203. assert data["success"] is True
  204. @pytest.mark.asyncio
  205. async def test_pause_execution_when_idle(self, async_client):
  206. """Test pause_execution returns 400 when nothing is playing."""
  207. # Mock check_table_is_idle to return True (table is idle)
  208. with patch("main.pattern_manager.check_table_is_idle", return_value=True):
  209. response = await async_client.post("/pause_execution")
  210. assert response.status_code == 400
  211. data = response.json()
  212. assert "nothing is currently playing" in data["detail"].lower()
  213. @pytest.mark.asyncio
  214. async def test_resume_execution(self, async_client):
  215. """Test resume_execution endpoint."""
  216. # Mock state.pause_requested to True (execution is paused)
  217. from main import state
  218. original_value = state.pause_requested
  219. try:
  220. state.pause_requested = True
  221. with patch("main.pattern_manager.resume_execution", return_value=True):
  222. response = await async_client.post("/resume_execution")
  223. finally:
  224. state.pause_requested = original_value
  225. assert response.status_code == 200
  226. data = response.json()
  227. assert data["success"] is True
  228. @pytest.mark.asyncio
  229. async def test_resume_execution_when_not_paused(self, async_client):
  230. """Test resume_execution returns 400 when not paused."""
  231. # Mock state.pause_requested to False (not paused)
  232. from main import state
  233. original_value = state.pause_requested
  234. try:
  235. state.pause_requested = False
  236. response = await async_client.post("/resume_execution")
  237. finally:
  238. state.pause_requested = original_value
  239. assert response.status_code == 400
  240. data = response.json()
  241. assert "not paused" in data["detail"].lower()
  242. class TestDeleteThetaRhoFile:
  243. """Tests for /delete_theta_rho_file endpoint."""
  244. @pytest.mark.asyncio
  245. async def test_delete_theta_rho_file_success(self, async_client, tmp_path):
  246. """Test deleting an existing pattern file."""
  247. patterns_dir = tmp_path / "patterns"
  248. patterns_dir.mkdir()
  249. test_file = patterns_dir / "test.thr"
  250. test_file.write_text("0 0.5")
  251. # Must patch pattern_manager.THETA_RHO_DIR which is what the endpoint uses
  252. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  253. with patch("modules.core.cache_manager.delete_pattern_cache", return_value=True):
  254. response = await async_client.post(
  255. "/delete_theta_rho_file",
  256. json={"file_name": "test.thr"}
  257. )
  258. assert response.status_code == 200
  259. data = response.json()
  260. assert data["success"] is True
  261. # Verify file was actually deleted
  262. assert not test_file.exists()
  263. @pytest.mark.asyncio
  264. async def test_delete_theta_rho_file_not_found(self, async_client, tmp_path):
  265. """Test deleting a non-existent file returns error."""
  266. patterns_dir = tmp_path / "patterns"
  267. patterns_dir.mkdir()
  268. with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
  269. response = await async_client.post(
  270. "/delete_theta_rho_file",
  271. json={"file_name": "nonexistent.thr"}
  272. )
  273. assert response.status_code == 404