Spaces:
Sleeping
Sleeping
Favor unit tests
Browse files- CLAUDE.md +9 -6
- README.md +3 -2
- tests/integration/test_e2e_ada_analysis.py +0 -74
- tests/unit/test_ui.py +145 -0
- tests/unit/tools/test_project_handler.py +23 -0
CLAUDE.md
CHANGED
|
@@ -49,8 +49,9 @@ tests/
|
|
| 49 |
│ │ ├── test_ada_converter_agent.py # Unit tests for AdaConverterAgent
|
| 50 |
│ │ ├── test_unit_test_generator_agent.py # Unit tests for UnitTestGeneratorAgent
|
| 51 |
│ │ └── tictactoe.ads # Sample Ada file for testing
|
| 52 |
-
│
|
| 53 |
-
│
|
|
|
|
| 54 |
├── integration/
|
| 55 |
│ └── test_e2e_ada_analysis.py # End-to-end tests with Playwright
|
| 56 |
├── test_data/
|
|
@@ -85,17 +86,19 @@ uv.lock # UV lockfile for reproducible dependencies
|
|
| 85 |
* Handles error conditions and provides fallback responses for generation failures
|
| 86 |
|
| 87 |
## Testing Strategy
|
| 88 |
-
* **Unit Tests**: Test individual agents and
|
| 89 |
-
* Located in `tests/unit/agents/`
|
| 90 |
* Use mock LLM models to avoid external dependencies
|
| 91 |
-
*
|
|
|
|
| 92 |
* Run with: `uv run pytest tests/unit/ -v`
|
| 93 |
|
| 94 |
* **Integration Tests**: Test complete user workflows end-to-end
|
| 95 |
* Located in `tests/integration/`
|
| 96 |
* Use Playwright for browser automation with Chromium
|
| 97 |
-
*
|
| 98 |
* Requires Ollama running for local development
|
|
|
|
| 99 |
* Run with: `uv run pytest tests/integration/ --browser chromium`
|
| 100 |
|
| 101 |
* **Test Configuration**:
|
|
|
|
| 49 |
│ │ ├── test_ada_converter_agent.py # Unit tests for AdaConverterAgent
|
| 50 |
│ │ ├── test_unit_test_generator_agent.py # Unit tests for UnitTestGeneratorAgent
|
| 51 |
│ │ └── tictactoe.ads # Sample Ada file for testing
|
| 52 |
+
│ ├── tools/
|
| 53 |
+
│ │ └── test_project_handler.py # Unit tests for project handler
|
| 54 |
+
│ └── test_ui.py # Unit tests for UI file selection logic
|
| 55 |
├── integration/
|
| 56 |
│ └── test_e2e_ada_analysis.py # End-to-end tests with Playwright
|
| 57 |
├── test_data/
|
|
|
|
| 86 |
* Handles error conditions and provides fallback responses for generation failures
|
| 87 |
|
| 88 |
## Testing Strategy
|
| 89 |
+
* **Unit Tests**: Test individual agents, utilities, and UI components in isolation
|
| 90 |
+
* Located in `tests/unit/agents/`, `tests/unit/tools/`, and `tests/unit/test_ui.py`
|
| 91 |
* Use mock LLM models to avoid external dependencies
|
| 92 |
+
* Comprehensive error handling and edge case coverage
|
| 93 |
+
* Fast execution (~0.94s for 19 tests)
|
| 94 |
* Run with: `uv run pytest tests/unit/ -v`
|
| 95 |
|
| 96 |
* **Integration Tests**: Test complete user workflows end-to-end
|
| 97 |
* Located in `tests/integration/`
|
| 98 |
* Use Playwright for browser automation with Chromium
|
| 99 |
+
* Focus on core workflow: zip upload, file extraction, file selection, and analysis display
|
| 100 |
* Requires Ollama running for local development
|
| 101 |
+
* Streamlined to 1 comprehensive test (~27s execution)
|
| 102 |
* Run with: `uv run pytest tests/integration/ --browser chromium`
|
| 103 |
|
| 104 |
* **Test Configuration**:
|
README.md
CHANGED
|
@@ -123,8 +123,9 @@ tests/
|
|
| 123 |
│ │ ├── test_ada_converter_agent.py
|
| 124 |
│ │ ├── test_unit_test_generator_agent.py
|
| 125 |
│ │ └── tictactoe.ads # Test Ada file
|
| 126 |
-
│
|
| 127 |
-
│
|
|
|
|
| 128 |
├── integration/ # E2E tests (browser automation)
|
| 129 |
│ └── test_e2e_ada_analysis.py
|
| 130 |
├── test_data/ # Sample test files
|
|
|
|
| 123 |
│ │ ├── test_ada_converter_agent.py
|
| 124 |
│ │ ├── test_unit_test_generator_agent.py
|
| 125 |
│ │ └── tictactoe.ads # Test Ada file
|
| 126 |
+
│ ├── tools/
|
| 127 |
+
│ │ └── test_project_handler.py
|
| 128 |
+
│ └── test_ui.py # UI file selection logic tests
|
| 129 |
├── integration/ # E2E tests (browser automation)
|
| 130 |
│ └── test_e2e_ada_analysis.py
|
| 131 |
├── test_data/ # Sample test files
|
tests/integration/test_e2e_ada_analysis.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
| 1 |
"""End-to-end integration tests for Ada Assistant using Playwright."""
|
| 2 |
|
| 3 |
-
import os
|
| 4 |
import time
|
| 5 |
import pytest
|
| 6 |
from playwright.sync_api import Page, expect
|
| 7 |
import subprocess
|
| 8 |
-
import tempfile
|
| 9 |
-
import shutil
|
| 10 |
|
| 11 |
|
| 12 |
pytestmark = [pytest.mark.integration, pytest.mark.e2e]
|
|
@@ -121,75 +118,4 @@ def test_ada_project_upload_and_analysis(page: Page, gradio_app, test_zip_path):
|
|
| 121 |
print("✓ Integration test passed: File upload, extraction, and selection workflow completed")
|
| 122 |
|
| 123 |
|
| 124 |
-
def test_non_ada_file_selection(page: Page, gradio_app):
|
| 125 |
-
"""Test that non-Ada files don't trigger analysis."""
|
| 126 |
-
_ = gradio_app # Suppress unused parameter warning
|
| 127 |
-
|
| 128 |
-
# Create a temporary zip with non-Ada files
|
| 129 |
-
with tempfile.TemporaryDirectory() as temp_dir:
|
| 130 |
-
# Create a Python file
|
| 131 |
-
python_file = os.path.join(temp_dir, "test.py")
|
| 132 |
-
with open(python_file, "w") as f:
|
| 133 |
-
f.write("print('Hello World')")
|
| 134 |
-
|
| 135 |
-
# Create zip file
|
| 136 |
-
zip_path = os.path.join(temp_dir, "non_ada_project.zip")
|
| 137 |
-
shutil.make_archive(zip_path[:-4], 'zip', temp_dir, "test.py")
|
| 138 |
-
|
| 139 |
-
# Navigate to the app
|
| 140 |
-
page.goto("http://localhost:7860")
|
| 141 |
-
page.wait_for_selector("text=Ada Assistant - Project Analyzer", timeout=30000)
|
| 142 |
-
|
| 143 |
-
# Upload the non-Ada zip file
|
| 144 |
-
file_input = page.locator('[data-testid="file-upload"]')
|
| 145 |
-
file_input.set_input_files(zip_path)
|
| 146 |
-
|
| 147 |
-
# Wait for processing
|
| 148 |
-
try:
|
| 149 |
-
page.wait_for_selector("text=Project extracted to:", timeout=10000)
|
| 150 |
-
except:
|
| 151 |
-
time.sleep(5) # Fallback wait
|
| 152 |
-
|
| 153 |
-
# Click on the Python file
|
| 154 |
-
python_file_element = page.locator("text=test.py").first
|
| 155 |
-
if python_file_element.is_visible():
|
| 156 |
-
python_file_element.click()
|
| 157 |
-
time.sleep(2)
|
| 158 |
-
|
| 159 |
-
# Verify that analysis sections remain unchanged (should still show default message)
|
| 160 |
-
# Check if the default message is still visible (meaning no analysis was triggered)
|
| 161 |
-
select_file_text = page.locator("text=Select file to view analysis")
|
| 162 |
-
expect(select_file_text).to_be_visible()
|
| 163 |
-
|
| 164 |
|
| 165 |
-
def test_upload_invalid_file(page: Page, gradio_app):
|
| 166 |
-
"""Test handling of invalid file upload."""
|
| 167 |
-
_ = gradio_app # Suppress unused parameter warning
|
| 168 |
-
|
| 169 |
-
# Create a temporary text file (not a zip)
|
| 170 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as temp_file:
|
| 171 |
-
temp_file.write("This is not a zip file")
|
| 172 |
-
temp_path = temp_file.name
|
| 173 |
-
|
| 174 |
-
try:
|
| 175 |
-
# Navigate to the app
|
| 176 |
-
page.goto("http://localhost:7860")
|
| 177 |
-
page.wait_for_selector("text=Ada Assistant - Project Analyzer", timeout=30000)
|
| 178 |
-
|
| 179 |
-
# Try to upload the text file
|
| 180 |
-
file_input = page.locator('[data-testid="file-upload"]')
|
| 181 |
-
file_input.set_input_files(temp_path)
|
| 182 |
-
|
| 183 |
-
# Wait a moment to see if error handling occurs
|
| 184 |
-
time.sleep(3)
|
| 185 |
-
|
| 186 |
-
# Check for error message in project status
|
| 187 |
-
project_status = page.locator('label:has-text("Project Status")')
|
| 188 |
-
if project_status.is_visible():
|
| 189 |
-
# For invalid files, the project status should either show an error or remain unchanged
|
| 190 |
-
# We just verify the status area is present
|
| 191 |
-
expect(project_status).to_be_visible()
|
| 192 |
-
|
| 193 |
-
finally:
|
| 194 |
-
# Cleanup
|
| 195 |
-
os.unlink(temp_path)
|
|
|
|
| 1 |
"""End-to-end integration tests for Ada Assistant using Playwright."""
|
| 2 |
|
|
|
|
| 3 |
import time
|
| 4 |
import pytest
|
| 5 |
from playwright.sync_api import Page, expect
|
| 6 |
import subprocess
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
pytestmark = [pytest.mark.integration, pytest.mark.e2e]
|
|
|
|
| 118 |
print("✓ Integration test passed: File upload, extraction, and selection workflow completed")
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/unit/test_ui.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for UI components and logic."""
|
| 2 |
+
|
| 3 |
+
import unittest.mock as mock
|
| 4 |
+
import tempfile
|
| 5 |
+
import os
|
| 6 |
+
from src.ui import create_file_selection_handler, handle_zip_upload
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class TestFileSelectionHandler:
|
| 10 |
+
"""Test cases for file selection handler logic."""
|
| 11 |
+
|
| 12 |
+
def test_handle_non_ada_file_returns_empty_results(self):
|
| 13 |
+
"""Test that non-Ada files return empty results and hidden components."""
|
| 14 |
+
# Create mock agents
|
| 15 |
+
mock_analyzer = mock.Mock()
|
| 16 |
+
mock_converter = mock.Mock()
|
| 17 |
+
mock_test_generator = mock.Mock()
|
| 18 |
+
|
| 19 |
+
# Create the handler
|
| 20 |
+
handler = create_file_selection_handler(mock_analyzer, mock_converter, mock_test_generator)
|
| 21 |
+
|
| 22 |
+
# Test with Python file
|
| 23 |
+
result = handler("test.py")
|
| 24 |
+
|
| 25 |
+
# Should return empty strings and False visibility for all components
|
| 26 |
+
analysis_result, python_code, unit_tests, analysis_visible, python_visible, tests_visible = result
|
| 27 |
+
|
| 28 |
+
assert analysis_result == ""
|
| 29 |
+
assert python_code == ""
|
| 30 |
+
assert unit_tests == ""
|
| 31 |
+
assert analysis_visible.visible == False
|
| 32 |
+
assert python_visible.visible == False
|
| 33 |
+
assert tests_visible.visible == False
|
| 34 |
+
|
| 35 |
+
# Ensure no agents were called
|
| 36 |
+
mock_analyzer.extract_business_logic.assert_not_called()
|
| 37 |
+
mock_converter.convert_to_python.assert_not_called()
|
| 38 |
+
mock_test_generator.generate_unit_tests.assert_not_called()
|
| 39 |
+
|
| 40 |
+
def test_handle_empty_file_selection_returns_empty_results(self):
|
| 41 |
+
"""Test that empty file selection returns empty results."""
|
| 42 |
+
# Create mock agents
|
| 43 |
+
mock_analyzer = mock.Mock()
|
| 44 |
+
mock_converter = mock.Mock()
|
| 45 |
+
mock_test_generator = mock.Mock()
|
| 46 |
+
|
| 47 |
+
# Create the handler
|
| 48 |
+
handler = create_file_selection_handler(mock_analyzer, mock_converter, mock_test_generator)
|
| 49 |
+
|
| 50 |
+
# Test with None/empty selection
|
| 51 |
+
result = handler(None)
|
| 52 |
+
|
| 53 |
+
# Should return empty strings and False visibility for all components
|
| 54 |
+
analysis_result, python_code, unit_tests, analysis_visible, python_visible, tests_visible = result
|
| 55 |
+
|
| 56 |
+
assert analysis_result == ""
|
| 57 |
+
assert python_code == ""
|
| 58 |
+
assert unit_tests == ""
|
| 59 |
+
assert analysis_visible.visible == False
|
| 60 |
+
assert python_visible.visible == False
|
| 61 |
+
assert tests_visible.visible == False
|
| 62 |
+
|
| 63 |
+
# Ensure no agents were called
|
| 64 |
+
mock_analyzer.extract_business_logic.assert_not_called()
|
| 65 |
+
mock_converter.convert_to_python.assert_not_called()
|
| 66 |
+
mock_test_generator.generate_unit_tests.assert_not_called()
|
| 67 |
+
|
| 68 |
+
def test_handle_various_non_ada_file_extensions(self):
|
| 69 |
+
"""Test that various non-Ada file extensions are properly rejected."""
|
| 70 |
+
# Create mock agents
|
| 71 |
+
mock_analyzer = mock.Mock()
|
| 72 |
+
mock_converter = mock.Mock()
|
| 73 |
+
mock_test_generator = mock.Mock()
|
| 74 |
+
|
| 75 |
+
# Create the handler
|
| 76 |
+
handler = create_file_selection_handler(mock_analyzer, mock_converter, mock_test_generator)
|
| 77 |
+
|
| 78 |
+
# Test various non-Ada file types
|
| 79 |
+
non_ada_files = ["test.py", "main.c", "config.txt", "data.json", "readme.md", "script.sh"]
|
| 80 |
+
|
| 81 |
+
for file_path in non_ada_files:
|
| 82 |
+
result = handler(file_path)
|
| 83 |
+
analysis_result, python_code, unit_tests, analysis_visible, python_visible, tests_visible = result
|
| 84 |
+
|
| 85 |
+
# All should return empty results
|
| 86 |
+
assert analysis_result == "", f"Failed for {file_path}"
|
| 87 |
+
assert python_code == "", f"Failed for {file_path}"
|
| 88 |
+
assert unit_tests == "", f"Failed for {file_path}"
|
| 89 |
+
assert analysis_visible.visible == False, f"Failed for {file_path}"
|
| 90 |
+
assert python_visible.visible == False, f"Failed for {file_path}"
|
| 91 |
+
assert tests_visible.visible == False, f"Failed for {file_path}"
|
| 92 |
+
|
| 93 |
+
# Ensure no agents were called for any file
|
| 94 |
+
mock_analyzer.extract_business_logic.assert_not_called()
|
| 95 |
+
mock_converter.convert_to_python.assert_not_called()
|
| 96 |
+
mock_test_generator.generate_unit_tests.assert_not_called()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class TestZipUploadHandler:
|
| 100 |
+
"""Test cases for zip upload handler logic."""
|
| 101 |
+
|
| 102 |
+
def test_handle_zip_upload_with_none_file(self):
|
| 103 |
+
"""Test that None file upload returns appropriate message."""
|
| 104 |
+
result = handle_zip_upload(None)
|
| 105 |
+
|
| 106 |
+
# Should return None path and error message
|
| 107 |
+
extracted_path, message = result
|
| 108 |
+
assert extracted_path is None
|
| 109 |
+
assert message == "No zip file uploaded"
|
| 110 |
+
|
| 111 |
+
def test_handle_zip_upload_with_invalid_file(self):
|
| 112 |
+
"""Test that invalid zip file returns error message."""
|
| 113 |
+
# Create a mock file object with a non-zip file
|
| 114 |
+
mock_file = mock.Mock()
|
| 115 |
+
|
| 116 |
+
# Create a temporary text file
|
| 117 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as temp_file:
|
| 118 |
+
temp_file.write("This is not a zip file")
|
| 119 |
+
mock_file.name = temp_file.name
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
result = handle_zip_upload(mock_file)
|
| 123 |
+
|
| 124 |
+
# Current behavior: extract_project returns error string, handle_zip_upload treats it as path
|
| 125 |
+
extracted_path, message = result
|
| 126 |
+
assert "Error processing file:" in extracted_path
|
| 127 |
+
assert "Project extracted to:" in message
|
| 128 |
+
assert "Error processing file:" in message
|
| 129 |
+
finally:
|
| 130 |
+
# Cleanup
|
| 131 |
+
os.unlink(mock_file.name)
|
| 132 |
+
|
| 133 |
+
def test_handle_zip_upload_with_nonexistent_file(self):
|
| 134 |
+
"""Test that non-existent file returns error message."""
|
| 135 |
+
# Create a mock file object with non-existent path
|
| 136 |
+
mock_file = mock.Mock()
|
| 137 |
+
mock_file.name = "/path/that/does/not/exist.zip"
|
| 138 |
+
|
| 139 |
+
result = handle_zip_upload(mock_file)
|
| 140 |
+
|
| 141 |
+
# Current behavior: extract_project returns error string, handle_zip_upload treats it as path
|
| 142 |
+
extracted_path, message = result
|
| 143 |
+
assert "Error processing file:" in extracted_path
|
| 144 |
+
assert "Project extracted to:" in message
|
| 145 |
+
assert "Error processing file:" in message
|
tests/unit/tools/test_project_handler.py
CHANGED
|
@@ -60,6 +60,29 @@ class TestUnzipProject:
|
|
| 60 |
finally:
|
| 61 |
# Cleanup
|
| 62 |
os.unlink(temp_zip.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
class TestBuildDirectoryTree:
|
|
|
|
| 60 |
finally:
|
| 61 |
# Cleanup
|
| 62 |
os.unlink(temp_zip.name)
|
| 63 |
+
|
| 64 |
+
def test_unzip_project_with_invalid_zip_file(self):
|
| 65 |
+
"""Test unzip_project raises exception for invalid ZIP files."""
|
| 66 |
+
# Create a text file that's not a ZIP
|
| 67 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as temp_file:
|
| 68 |
+
temp_file.write("This is not a zip file")
|
| 69 |
+
temp_path = temp_file.name
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
# Should raise an exception when trying to unzip invalid file
|
| 73 |
+
with pytest.raises(zipfile.BadZipFile):
|
| 74 |
+
unzip_project(temp_path)
|
| 75 |
+
finally:
|
| 76 |
+
# Cleanup
|
| 77 |
+
os.unlink(temp_path)
|
| 78 |
+
|
| 79 |
+
def test_unzip_project_with_nonexistent_file(self):
|
| 80 |
+
"""Test unzip_project raises exception for non-existent files."""
|
| 81 |
+
nonexistent_path = "/path/that/does/not/exist.zip"
|
| 82 |
+
|
| 83 |
+
# Should raise FileNotFoundError for non-existent file
|
| 84 |
+
with pytest.raises(FileNotFoundError):
|
| 85 |
+
unzip_project(nonexistent_path)
|
| 86 |
|
| 87 |
|
| 88 |
class TestBuildDirectoryTree:
|