- Add pytest markers (integration, e2e, slow) with skip logic - Add command line options (--run-integration, --run-e2e) - Create sample_project and sample_project_with_bug fixtures - Add test_integration.py with 10 LLM tests - Add test_e2e.py with 12 API server tests - Update COVERAGE.md to reflect ~65% complete Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
344 lines
8.7 KiB
Python
344 lines
8.7 KiB
Python
"""
|
|
Pytest configuration and fixtures.
|
|
|
|
Test categories:
|
|
- Unit tests: Run by default, no external dependencies
|
|
- Integration tests: Require Ollama, run with --run-integration
|
|
- E2E tests: Require running API server, run with --run-e2e
|
|
|
|
Usage:
|
|
pytest tests/ # Run unit tests only
|
|
pytest tests/ --run-integration # Include integration tests
|
|
pytest tests/ --run-e2e # Include E2E tests
|
|
pytest tests/ --run-integration --run-e2e # Run all tests
|
|
"""
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from src.main import app
|
|
|
|
|
|
# =============================================================================
|
|
# Command Line Options
|
|
# =============================================================================
|
|
|
|
def pytest_addoption(parser):
|
|
"""Add custom command line options."""
|
|
parser.addoption(
|
|
"--run-integration",
|
|
action="store_true",
|
|
default=False,
|
|
help="Run integration tests (require Ollama to be running)",
|
|
)
|
|
parser.addoption(
|
|
"--run-e2e",
|
|
action="store_true",
|
|
default=False,
|
|
help="Run E2E tests (require API server to be running)",
|
|
)
|
|
parser.addoption(
|
|
"--ollama-url",
|
|
action="store",
|
|
default="http://192.168.86.149:11434",
|
|
help="Ollama API URL for integration tests",
|
|
)
|
|
parser.addoption(
|
|
"--api-url",
|
|
action="store",
|
|
default="http://localhost:8095",
|
|
help="Webber API URL for E2E tests",
|
|
)
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
"""Skip integration/e2e tests unless explicitly requested."""
|
|
skip_integration = pytest.mark.skip(reason="need --run-integration option to run")
|
|
skip_e2e = pytest.mark.skip(reason="need --run-e2e option to run")
|
|
|
|
for item in items:
|
|
if "integration" in item.keywords and not config.getoption("--run-integration"):
|
|
item.add_marker(skip_integration)
|
|
if "e2e" in item.keywords and not config.getoption("--run-e2e"):
|
|
item.add_marker(skip_e2e)
|
|
|
|
|
|
# =============================================================================
|
|
# Basic Fixtures
|
|
# =============================================================================
|
|
|
|
@pytest.fixture
|
|
def anyio_backend():
|
|
"""Use asyncio for async tests."""
|
|
return "asyncio"
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
"""Async HTTP client for testing (no auth)."""
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=app),
|
|
base_url="http://test"
|
|
) as ac:
|
|
yield ac
|
|
|
|
|
|
@pytest.fixture
|
|
async def auth_client():
|
|
"""Async HTTP client with API key for authenticated requests."""
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=app),
|
|
base_url="http://test",
|
|
headers={"X-API-Key": "test-api-key"}
|
|
) as ac:
|
|
yield ac
|
|
|
|
|
|
# =============================================================================
|
|
# Integration Test Fixtures
|
|
# =============================================================================
|
|
|
|
@pytest.fixture
|
|
def ollama_url(request):
|
|
"""Get Ollama URL from command line or environment."""
|
|
return request.config.getoption("--ollama-url") or os.environ.get(
|
|
"OLLAMA_URL", "http://192.168.86.149:11434"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def api_url(request):
|
|
"""Get API URL from command line or environment."""
|
|
return request.config.getoption("--api-url") or os.environ.get(
|
|
"WEBBER_API_URL", "http://localhost:8095"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_project():
|
|
"""Create a sample Python project for testing."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
project = Path(tmpdir)
|
|
|
|
# Create a realistic project structure
|
|
(project / "src").mkdir()
|
|
(project / "tests").mkdir()
|
|
|
|
# Main application file
|
|
(project / "src" / "__init__.py").write_text("")
|
|
(project / "src" / "main.py").write_text('''"""Main application module."""
|
|
|
|
def greet(name: str) -> str:
|
|
"""Greet someone by name.
|
|
|
|
Args:
|
|
name: The name to greet
|
|
|
|
Returns:
|
|
A greeting string
|
|
"""
|
|
return f"Hello, {name}!"
|
|
|
|
|
|
def add(a: int, b: int) -> int:
|
|
"""Add two numbers.
|
|
|
|
Args:
|
|
a: First number
|
|
b: Second number
|
|
|
|
Returns:
|
|
Sum of a and b
|
|
"""
|
|
return a + b
|
|
|
|
|
|
def divide(a: float, b: float) -> float:
|
|
"""Divide two numbers.
|
|
|
|
Args:
|
|
a: Dividend
|
|
b: Divisor
|
|
|
|
Returns:
|
|
Result of a / b
|
|
|
|
Raises:
|
|
ValueError: If b is zero
|
|
"""
|
|
if b == 0:
|
|
raise ValueError("Cannot divide by zero")
|
|
return a / b
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(greet("World"))
|
|
''')
|
|
|
|
# Utility module
|
|
(project / "src" / "utils.py").write_text('''"""Utility functions."""
|
|
|
|
def is_even(n: int) -> bool:
|
|
"""Check if a number is even."""
|
|
return n % 2 == 0
|
|
|
|
|
|
def is_prime(n: int) -> bool:
|
|
"""Check if a number is prime."""
|
|
if n < 2:
|
|
return False
|
|
for i in range(2, int(n ** 0.5) + 1):
|
|
if n % i == 0:
|
|
return False
|
|
return True
|
|
|
|
|
|
def factorial(n: int) -> int:
|
|
"""Calculate factorial recursively."""
|
|
if n <= 1:
|
|
return 1
|
|
return n * factorial(n - 1)
|
|
|
|
|
|
def fibonacci(n: int) -> list[int]:
|
|
"""Generate Fibonacci sequence up to n terms."""
|
|
if n <= 0:
|
|
return []
|
|
if n == 1:
|
|
return [0]
|
|
|
|
fib = [0, 1]
|
|
for _ in range(2, n):
|
|
fib.append(fib[-1] + fib[-2])
|
|
return fib
|
|
''')
|
|
|
|
# Test file
|
|
(project / "tests" / "__init__.py").write_text("")
|
|
(project / "tests" / "test_main.py").write_text('''"""Tests for main module."""
|
|
import pytest
|
|
from src.main import greet, add, divide
|
|
|
|
|
|
def test_greet():
|
|
assert greet("World") == "Hello, World!"
|
|
|
|
|
|
def test_add():
|
|
assert add(2, 3) == 5
|
|
|
|
|
|
def test_divide():
|
|
assert divide(10, 2) == 5.0
|
|
|
|
|
|
def test_divide_by_zero():
|
|
with pytest.raises(ValueError):
|
|
divide(1, 0)
|
|
''')
|
|
|
|
# README
|
|
(project / "README.md").write_text('''# Sample Project
|
|
|
|
A simple Python project for testing Webber's code exploration.
|
|
|
|
## Features
|
|
|
|
- Greeting functionality
|
|
- Math utilities
|
|
- Comprehensive test suite
|
|
|
|
## Usage
|
|
|
|
```python
|
|
from src.main import greet, add
|
|
print(greet("World"))
|
|
print(add(2, 3))
|
|
```
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
pytest tests/
|
|
```
|
|
''')
|
|
|
|
# Configuration files
|
|
(project / "pyproject.toml").write_text('''[project]
|
|
name = "sample-project"
|
|
version = "0.1.0"
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
''')
|
|
|
|
yield project
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_project_with_bug():
|
|
"""Create a sample project with intentional bugs for testing."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
project = Path(tmpdir)
|
|
|
|
(project / "buggy.py").write_text('''"""Module with intentional bugs."""
|
|
|
|
def divide_numbers(a, b):
|
|
"""Divide two numbers - BUG: no zero check."""
|
|
return a / b # BUG: ZeroDivisionError if b is 0
|
|
|
|
|
|
def get_item(lst, index):
|
|
"""Get item from list - BUG: no bounds check."""
|
|
return lst[index] # BUG: IndexError if out of bounds
|
|
|
|
|
|
def parse_int(s):
|
|
"""Parse string to int - BUG: no error handling."""
|
|
return int(s) # BUG: ValueError if not a valid int
|
|
|
|
|
|
# TODO: Fix the division bug
|
|
# FIXME: Add bounds checking to get_item
|
|
''')
|
|
|
|
yield project
|
|
|
|
|
|
# =============================================================================
|
|
# E2E Test Fixtures
|
|
# =============================================================================
|
|
|
|
@pytest.fixture
|
|
async def live_client(api_url):
|
|
"""HTTP client for E2E tests against running server."""
|
|
async with AsyncClient(base_url=api_url, timeout=30.0) as client:
|
|
yield client
|
|
|
|
|
|
# =============================================================================
|
|
# Helper Functions
|
|
# =============================================================================
|
|
|
|
def assert_contains_any(text: str, substrings: list[str], case_sensitive: bool = False) -> bool:
|
|
"""Assert that text contains at least one of the substrings."""
|
|
check_text = text if case_sensitive else text.lower()
|
|
check_subs = substrings if case_sensitive else [s.lower() for s in substrings]
|
|
|
|
found = [s for s in check_subs if s in check_text]
|
|
assert found, f"Expected text to contain one of {substrings}, but none found in: {text[:200]}..."
|
|
return True
|
|
|
|
|
|
def assert_contains_all(text: str, substrings: list[str], case_sensitive: bool = False) -> bool:
|
|
"""Assert that text contains all of the substrings."""
|
|
check_text = text if case_sensitive else text.lower()
|
|
check_subs = substrings if case_sensitive else [s.lower() for s in substrings]
|
|
|
|
missing = [s for s in check_subs if s not in check_text]
|
|
assert not missing, f"Expected text to contain all of {substrings}, missing: {missing}"
|
|
return True
|