| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- """
- Unit tests for pattern API endpoints.
- Tests the following endpoints:
- - GET /list_theta_rho_files
- - GET /list_theta_rho_files_with_metadata
- - POST /get_theta_rho_coordinates
- - POST /run_theta_rho (when disconnected)
- """
- import pytest
- from unittest.mock import patch, MagicMock, AsyncMock
- import os
- class TestListThetaRhoFiles:
- """Tests for /list_theta_rho_files endpoint."""
- @pytest.mark.asyncio
- async def test_list_theta_rho_files(self, async_client):
- """Test list_theta_rho_files returns list of pattern files."""
- mock_files = ["circle.thr", "spiral.thr", "custom/pattern.thr"]
- with patch("main.pattern_manager.list_theta_rho_files", return_value=mock_files):
- response = await async_client.get("/list_theta_rho_files")
- assert response.status_code == 200
- data = response.json()
- assert isinstance(data, list)
- assert len(data) == 3
- assert "circle.thr" in data
- assert "spiral.thr" in data
- assert "custom/pattern.thr" in data
- @pytest.mark.asyncio
- async def test_list_theta_rho_files_empty(self, async_client):
- """Test list_theta_rho_files returns empty list when no patterns."""
- with patch("main.pattern_manager.list_theta_rho_files", return_value=[]):
- response = await async_client.get("/list_theta_rho_files")
- assert response.status_code == 200
- data = response.json()
- assert data == []
- class TestListThetaRhoFilesWithMetadata:
- """Tests for /list_theta_rho_files_with_metadata endpoint."""
- @pytest.mark.asyncio
- async def test_list_theta_rho_files_with_metadata(self, async_client, tmp_path):
- """Test list_theta_rho_files_with_metadata returns files with metadata."""
- mock_files = ["circle.thr"]
- # The endpoint has two paths:
- # 1. If metadata_cache.json exists, use it
- # 2. Otherwise, use ThreadPoolExecutor with process_file
- # We'll test the fallback path by having the cache file not exist
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- (patterns_dir / "circle.thr").write_text("0 0.5\n1 0.6")
- with patch("main.pattern_manager.list_theta_rho_files", return_value=mock_files):
- with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
- # Simulate cache file not existing
- with patch("builtins.open", side_effect=FileNotFoundError):
- response = await async_client.get("/list_theta_rho_files_with_metadata")
- assert response.status_code == 200
- data = response.json()
- assert isinstance(data, list)
- assert len(data) == 1
- # The response structure has 'path', 'name', 'category', 'date_modified', 'coordinates_count'
- item = data[0]
- assert item["path"] == "circle.thr"
- assert item["name"] == "circle"
- assert "category" in item
- assert "date_modified" in item
- assert "coordinates_count" in item
- class TestGetThetaRhoCoordinates:
- """Tests for /get_theta_rho_coordinates endpoint."""
- @pytest.mark.asyncio
- async def test_get_theta_rho_coordinates_valid_file(self, async_client, tmp_path):
- """Test getting coordinates from a valid file."""
- # Create test pattern file
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- test_file = patterns_dir / "test.thr"
- test_file.write_text("0.0 0.5\n1.57 0.8\n3.14 0.3\n")
- mock_coordinates = [(0.0, 0.5), (1.57, 0.8), (3.14, 0.3)]
- with patch("main.THETA_RHO_DIR", str(patterns_dir)):
- with patch("main.parse_theta_rho_file", return_value=mock_coordinates):
- response = await async_client.post(
- "/get_theta_rho_coordinates",
- json={"file_name": "test.thr"}
- )
- assert response.status_code == 200
- data = response.json()
- assert "coordinates" in data
- assert len(data["coordinates"]) == 3
- assert data["coordinates"][0] == [0.0, 0.5]
- assert data["coordinates"][1] == [1.57, 0.8]
- assert data["coordinates"][2] == [3.14, 0.3]
- @pytest.mark.asyncio
- async def test_get_theta_rho_coordinates_file_not_found(self, async_client, tmp_path):
- """Test getting coordinates from non-existent file returns error.
- Note: The endpoint returns 500 because it catches the HTTPException and re-raises it.
- """
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- with patch("main.THETA_RHO_DIR", str(patterns_dir)):
- response = await async_client.post(
- "/get_theta_rho_coordinates",
- json={"file_name": "nonexistent.thr"}
- )
- # The endpoint wraps the 404 in a 500 due to exception handling
- assert response.status_code in [404, 500]
- data = response.json()
- assert "not found" in data["detail"].lower()
- class TestRunThetaRho:
- """Tests for /run_theta_rho endpoint."""
- @pytest.mark.asyncio
- async def test_run_theta_rho_when_disconnected(self, async_client, mock_state, tmp_path):
- """Test run_theta_rho fails gracefully when disconnected."""
- # The endpoint checks file existence first, then connection
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- test_file = patterns_dir / "circle.thr"
- test_file.write_text("0 0.5")
- mock_state.conn = None
- mock_state.is_homing = False
- with patch("main.state", mock_state):
- with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
- response = await async_client.post(
- "/run_theta_rho",
- json={
- "file_name": "circle.thr",
- "pre_execution": "none"
- }
- )
- assert response.status_code == 400
- data = response.json()
- assert "not established" in data["detail"].lower() or "not connected" in data["detail"].lower()
- @pytest.mark.asyncio
- async def test_run_theta_rho_during_homing(self, async_client, mock_state, tmp_path):
- """Test run_theta_rho fails when homing is in progress."""
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- test_file = patterns_dir / "circle.thr"
- test_file.write_text("0 0.5")
- mock_state.is_homing = True
- mock_state.conn = MagicMock()
- mock_state.conn.is_connected.return_value = True
- with patch("main.state", mock_state):
- with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
- response = await async_client.post(
- "/run_theta_rho",
- json={
- "file_name": "circle.thr",
- "pre_execution": "none"
- }
- )
- assert response.status_code == 409
- data = response.json()
- assert "homing" in data["detail"].lower()
- @pytest.mark.asyncio
- async def test_run_theta_rho_file_not_found(self, async_client, mock_state, tmp_path):
- """Test run_theta_rho returns 404 for non-existent file."""
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- mock_state.conn = MagicMock()
- mock_state.conn.is_connected.return_value = True
- mock_state.is_homing = False
- with patch("main.state", mock_state):
- with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
- response = await async_client.post(
- "/run_theta_rho",
- json={
- "file_name": "nonexistent.thr",
- "pre_execution": "none"
- }
- )
- assert response.status_code == 404
- data = response.json()
- assert "not found" in data["detail"].lower()
- class TestStopExecution:
- """Tests for /stop_execution endpoint."""
- @pytest.mark.asyncio
- async def test_stop_execution(self, async_client, mock_state):
- """Test stop_execution endpoint."""
- mock_state.is_homing = False
- mock_state.conn = MagicMock()
- mock_state.conn.is_connected.return_value = True
- with patch("main.state", mock_state):
- with patch("main.pattern_manager.stop_actions", new_callable=AsyncMock, return_value=True):
- response = await async_client.post("/stop_execution")
- assert response.status_code == 200
- data = response.json()
- assert data["success"] is True
- @pytest.mark.asyncio
- async def test_stop_execution_when_disconnected(self, async_client, mock_state):
- """Test stop_execution fails when not connected."""
- mock_state.conn = None
- with patch("main.state", mock_state):
- response = await async_client.post("/stop_execution")
- assert response.status_code == 400
- data = response.json()
- assert "not established" in data["detail"].lower()
- class TestPauseResumeExecution:
- """Tests for /pause_execution and /resume_execution endpoints."""
- @pytest.mark.asyncio
- async def test_pause_execution(self, async_client):
- """Test pause_execution endpoint."""
- with patch("main.pattern_manager.pause_execution", return_value=True):
- response = await async_client.post("/pause_execution")
- assert response.status_code == 200
- data = response.json()
- assert data["success"] is True
- @pytest.mark.asyncio
- async def test_resume_execution(self, async_client):
- """Test resume_execution endpoint."""
- with patch("main.pattern_manager.resume_execution", return_value=True):
- response = await async_client.post("/resume_execution")
- assert response.status_code == 200
- data = response.json()
- assert data["success"] is True
- class TestDeleteThetaRhoFile:
- """Tests for /delete_theta_rho_file endpoint."""
- @pytest.mark.asyncio
- async def test_delete_theta_rho_file_success(self, async_client, tmp_path):
- """Test deleting an existing pattern file."""
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- test_file = patterns_dir / "test.thr"
- test_file.write_text("0 0.5")
- # Must patch pattern_manager.THETA_RHO_DIR which is what the endpoint uses
- with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
- with patch("modules.core.cache_manager.delete_pattern_cache", return_value=True):
- response = await async_client.post(
- "/delete_theta_rho_file",
- json={"file_name": "test.thr"}
- )
- assert response.status_code == 200
- data = response.json()
- assert data["success"] is True
- # Verify file was actually deleted
- assert not test_file.exists()
- @pytest.mark.asyncio
- async def test_delete_theta_rho_file_not_found(self, async_client, tmp_path):
- """Test deleting a non-existent file returns error."""
- patterns_dir = tmp_path / "patterns"
- patterns_dir.mkdir()
- with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)):
- response = await async_client.post(
- "/delete_theta_rho_file",
- json={"file_name": "nonexistent.thr"}
- )
- assert response.status_code == 404
|