Skip to content

Commit 5403a93

Browse files
committed
test: add e2e tests
1 parent b213331 commit 5403a93

File tree

10 files changed

+375
-17
lines changed

10 files changed

+375
-17
lines changed

Dockerfile.e2e

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM python:3.10-slim
2+
3+
WORKDIR /app
4+
5+
# Install system dependencies
6+
RUN apt-get update && \
7+
apt-get install -y curl && \
8+
apt-get clean && \
9+
rm -rf /var/lib/apt/lists/*
10+
11+
# Install dependencies first (for better caching)
12+
COPY requirements.txt .
13+
RUN pip install --no-cache-dir -r requirements.txt
14+
15+
# Install testing dependencies
16+
RUN pip install --no-cache-dir pytest pytest-cov requests selenium webdriver-manager pytest-selenium
17+
18+
# Copy application code
19+
COPY . .
20+
21+
# Make Python scripts executable
22+
RUN chmod +x tests/e2e/*.py
23+
24+
# Set environment variables
25+
ENV PYTHONUNBUFFERED=1
26+
27+
# Default command runs pytest
28+
CMD ["pytest", "-xvs", "tests/e2e/"]

README.md

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -145,27 +145,35 @@ cursor.registerCommandEnhancer(async (command, context) => {
145145

146146
## 🧪 Testing
147147

148-
This project includes comprehensive tests to ensure reliability:
149-
150-
### Running Tests
148+
### Running Unit Tests
151149

150+
Run the included unit and integration tests:
152151
```bash
153-
# Run all tests with coverage report
154-
pytest
152+
./run_tests.sh
153+
```
155154

156-
# Run just unit tests
157-
pytest tests/unit
155+
### End-to-End Testing with Docker
158156

159-
# Run integration tests
160-
pytest tests/integration
157+
The project includes end-to-end tests that validate the full application stack using Docker:
161158

162-
# Run with specific marker
163-
pytest -m unit
164-
```
159+
1. Ensure Docker and docker-compose are installed on your system.
165160

166-
### Test Data
161+
2. Run the E2E test suite:
162+
```bash
163+
./run_e2e_tests.sh
164+
```
165+
166+
This will:
167+
- Build all necessary Docker containers
168+
- Start the application stack with test data
169+
- Run E2E tests against the live services
170+
- Clean up containers when done
167171

168-
Sample test data is located in the `tests/data` directory.
172+
For CI/CD environments, add the following to your workflow:
173+
```yaml
174+
- name: Run E2E Tests
175+
run: ./run_e2e_tests.sh
176+
```
169177
170178
## 📊 Performance Monitoring
171179

api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
# Check if running in Docker with external Chroma
5252
CHROMA_HOST = os.getenv("CHROMA_HOST")
5353
CHROMA_PORT = os.getenv("CHROMA_PORT")
54+
USE_PERSISTENT_CHROMA = os.getenv("USE_PERSISTENT_CHROMA", "false").lower() == "true"
5455

5556
# Initialize FastAPI app
5657
app = FastAPI(
@@ -110,7 +111,7 @@ async def global_exception_handler(request: Request, exc: Exception):
110111

111112
# Initialize Chroma client - either local or remote
112113
try:
113-
if CHROMA_HOST and CHROMA_PORT:
114+
if CHROMA_HOST and CHROMA_PORT and not USE_PERSISTENT_CHROMA:
114115
logger.info(f"Connecting to Chroma server at {CHROMA_HOST}:{CHROMA_PORT}")
115116
chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=int(CHROMA_PORT))
116117
else:
@@ -128,7 +129,7 @@ async def global_exception_handler(request: Request, exc: Exception):
128129
logger.info("Creating a new collection. Please run indexer.py to populate it.")
129130
try:
130131
# Try to create the collection if it doesn't exist
131-
if CHROMA_HOST and CHROMA_PORT:
132+
if CHROMA_HOST and CHROMA_PORT and not USE_PERSISTENT_CHROMA:
132133
chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=int(CHROMA_PORT))
133134
else:
134135
# Updated client initialization for newer ChromaDB versions

docker-compose.e2e.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
services:
2+
# Chroma Vector Database
3+
chroma:
4+
image: chromadb/chroma:latest
5+
volumes:
6+
- chroma_data:/chroma/chroma
7+
environment:
8+
- ALLOW_RESET=true
9+
- ANONYMIZED_TELEMETRY=false
10+
healthcheck:
11+
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat"]
12+
interval: 10s
13+
timeout: 5s
14+
retries: 5
15+
ports:
16+
- "8001:8000" # Map to different port to avoid conflict with API
17+
18+
# API Service
19+
api:
20+
build: .
21+
volumes:
22+
- ./:/app # Mount code for development
23+
- chroma_index:/app/chroma_index # Vector DB persistence
24+
environment:
25+
- OPENAI_API_KEY=${OPENAI_API_KEY:-mock_key}
26+
- CHROMA_HOST=chroma
27+
- CHROMA_PORT=8000
28+
- MOCK_EMBEDDINGS=true
29+
- USE_PERSISTENT_CHROMA=true # Use persistent local storage instead of HTTP
30+
ports:
31+
- "8000:8000"
32+
depends_on:
33+
chroma:
34+
condition: service_healthy
35+
command: uvicorn api:app --host 0.0.0.0 --port 8000
36+
37+
# Indexer - set up initial index
38+
indexer:
39+
build: .
40+
volumes:
41+
- ./:/app # Mount code for development
42+
- chroma_index:/app/chroma_index # Vector DB persistence
43+
- ./tests/data:/ignition_project # Use test data for indexing
44+
environment:
45+
- OPENAI_API_KEY=${OPENAI_API_KEY:-mock_key}
46+
- CHROMA_HOST=chroma
47+
- CHROMA_PORT=8000
48+
- MOCK_EMBEDDINGS=true
49+
- USE_PERSISTENT_CHROMA=true # Use persistent local storage instead of HTTP
50+
depends_on:
51+
chroma:
52+
condition: service_healthy
53+
command: python indexer.py /ignition_project
54+
55+
# E2E Test Runner
56+
e2e:
57+
build:
58+
context: .
59+
dockerfile: Dockerfile.e2e
60+
volumes:
61+
- ./:/app # Mount code for development
62+
environment:
63+
- API_HOST=api
64+
- API_PORT=8000
65+
- MOCK_EMBEDDINGS=true
66+
depends_on:
67+
- api
68+
- indexer
69+
command: pytest -xvs tests/e2e/
70+
71+
volumes:
72+
chroma_data: # Persistent volume for Chroma DB
73+
chroma_index: # Persistent volume for our index

indexer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@
5151
# Check if running in Docker with external Chroma
5252
CHROMA_HOST = os.getenv("CHROMA_HOST")
5353
CHROMA_PORT = os.getenv("CHROMA_PORT")
54+
USE_PERSISTENT_CHROMA = os.getenv("USE_PERSISTENT_CHROMA", "false").lower() == "true"
5455

5556
app = typer.Typer()
5657

5758

5859
def setup_chroma_client():
5960
"""Set up and return a Chroma client with persistence."""
60-
if CHROMA_HOST and CHROMA_PORT:
61+
if CHROMA_HOST and CHROMA_PORT and not USE_PERSISTENT_CHROMA:
6162
print(f"Connecting to Chroma server at {CHROMA_HOST}:{CHROMA_PORT}")
6263
return chromadb.HttpClient(host=CHROMA_HOST, port=int(CHROMA_PORT))
6364
else:

last_index_time.pkl

0 Bytes
Binary file not shown.

run_e2e_tests.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Color output
5+
GREEN='\033[0;32m'
6+
RED='\033[0;31m'
7+
BLUE='\033[0;34m'
8+
YELLOW='\033[1;33m'
9+
NC='\033[0m' # No Color
10+
11+
echo -e "${BLUE}=== Ignition RAG Agent E2E Test Suite ===${NC}"
12+
13+
# Check if Docker is available
14+
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
15+
echo -e "${RED}Error: Docker and docker-compose are required for E2E tests${NC}"
16+
exit 1
17+
fi
18+
19+
# Clean up function
20+
function cleanup {
21+
echo -e "${BLUE}Cleaning up containers...${NC}"
22+
docker-compose -f docker-compose.e2e.yml down -v
23+
}
24+
25+
# Trap to ensure cleanup on exit
26+
trap cleanup EXIT
27+
28+
# Build and start containers
29+
echo -e "${BLUE}Building Docker containers for E2E tests...${NC}"
30+
docker-compose -f docker-compose.e2e.yml build
31+
32+
# First start the chroma and API services
33+
echo -e "${BLUE}Starting Chroma and API services...${NC}"
34+
docker-compose -f docker-compose.e2e.yml up -d chroma api
35+
36+
# Wait a moment for the service to initialize
37+
echo -e "${BLUE}Waiting for services to initialize...${NC}"
38+
sleep 5
39+
40+
# Run indexer
41+
echo -e "${BLUE}Running indexer...${NC}"
42+
docker-compose -f docker-compose.e2e.yml up --exit-code-from indexer indexer
43+
44+
# Run the E2E tests
45+
echo -e "${BLUE}Running E2E tests...${NC}"
46+
docker-compose -f docker-compose.e2e.yml up --exit-code-from e2e e2e
47+
48+
# Store the exit code
49+
EXIT_CODE=$?
50+
51+
if [ "$EXIT_CODE" == "0" ]; then
52+
echo -e "${GREEN}E2E tests passed successfully!${NC}"
53+
exit 0
54+
else
55+
echo -e "${RED}E2E tests failed with exit code $EXIT_CODE${NC}"
56+
exit 1
57+
fi

tests/e2e/conftest.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
import pytest
3+
import requests
4+
import time
5+
import logging
6+
7+
# Configure logging
8+
logging.basicConfig(
9+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
10+
)
11+
logger = logging.getLogger("e2e-tests")
12+
13+
# Constants
14+
API_HOST = os.environ.get("API_HOST", "localhost")
15+
API_PORT = os.environ.get("API_PORT", "8000")
16+
API_URL = f"http://{API_HOST}:{API_PORT}"
17+
MAX_RETRIES = 30
18+
RETRY_INTERVAL = 5
19+
20+
21+
@pytest.fixture(scope="session", autouse=True)
22+
def wait_for_api():
23+
"""Wait for the API to be available before running tests."""
24+
logger.info(f"Waiting for API at {API_URL} to be ready")
25+
26+
for attempt in range(MAX_RETRIES):
27+
try:
28+
response = requests.get(f"{API_URL}/health")
29+
if response.status_code == 200:
30+
logger.info(f"API is ready after {attempt + 1} attempts")
31+
# Give the API a moment to fully initialize
32+
time.sleep(2)
33+
return
34+
except requests.RequestException as e:
35+
logger.info(f"API connection error: {e}")
36+
37+
logger.info(
38+
f"API not ready yet, retrying in {RETRY_INTERVAL} seconds... (attempt {attempt + 1}/{MAX_RETRIES})"
39+
)
40+
time.sleep(RETRY_INTERVAL)
41+
42+
pytest.fail("API service did not become available in time")
43+
44+
45+
@pytest.fixture(scope="session")
46+
def api_url():
47+
"""Provide API URL to test functions."""
48+
return API_URL

tests/e2e/test_e2e.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import requests
4+
import pytest
5+
import sys
6+
import json
7+
8+
# Add parent directory to path
9+
sys.path.append(
10+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11+
)
12+
13+
14+
class TestE2E:
15+
"""End-to-end tests for the Ignition RAG system."""
16+
17+
def test_api_health(self, api_url):
18+
"""Test API health endpoint."""
19+
response = requests.get(f"{api_url}/health")
20+
assert response.status_code == 200
21+
data = response.json()
22+
assert data["status"] == "healthy"
23+
24+
def test_query_endpoint(self, api_url):
25+
"""Test the query endpoint with a basic question."""
26+
query_data = {
27+
"query": "Tell me about the tank system",
28+
"top_k": 3,
29+
"filter_type": None,
30+
"filter_path": None,
31+
}
32+
33+
response = requests.post(f"{api_url}/query", json=query_data)
34+
35+
assert response.status_code == 200
36+
data = response.json()
37+
assert "results" in data
38+
assert "total" in data
39+
assert "mock_used" in data
40+
assert len(data["results"]) > 0
41+
42+
# Check first result has the expected structure
43+
first_result = data["results"][0]
44+
assert "content" in first_result
45+
assert "metadata" in first_result
46+
assert "similarity" in first_result
47+
48+
def test_multi_turn_conversation(self, api_url):
49+
"""Test a multi-turn conversation."""
50+
# First query
51+
query1_data = {
52+
"query": "What is in the tank view?",
53+
"top_k": 3,
54+
"filter_type": "perspective",
55+
"filter_path": None,
56+
}
57+
58+
response1 = requests.post(f"{api_url}/query", json=query1_data)
59+
60+
assert response1.status_code == 200
61+
data1 = response1.json()
62+
assert "results" in data1
63+
assert "total" in data1
64+
65+
# Follow-up query
66+
query2_data = {
67+
"query": "Tell me more about its components",
68+
"top_k": 3,
69+
"filter_type": None,
70+
"filter_path": None,
71+
}
72+
73+
response2 = requests.post(f"{api_url}/query", json=query2_data)
74+
75+
assert response2.status_code == 200
76+
data2 = response2.json()
77+
assert "results" in data2
78+
assert "total" in data2

0 commit comments

Comments
 (0)