Przeglądaj źródła

test(backend-testing-01): add pattern API endpoint tests

- /list_theta_rho_files tests (list, empty)
- /list_theta_rho_files_with_metadata tests (structure)
- /get_theta_rho_coordinates tests (valid file, not found)
- /run_theta_rho tests (disconnected, during homing, not found)
- /stop_execution tests (success, disconnected)
- /pause_execution and /resume_execution tests
- /delete_theta_rho_file tests (success, not found)

14 tests total, all passing
tuanchris 1 tydzień temu
rodzic
commit
1f77ebae05
1 zmienionych plików z 304 dodań i 0 usunięć
  1. 304 0
      tests/unit/test_api_patterns.py

+ 304 - 0
tests/unit/test_api_patterns.py

@@ -0,0 +1,304 @@
+"""
+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