Skip to content

Commit b3645ca

Browse files
feat(tests): use isolated test_e2e schema for E2E tests
- E2E tests now run in separate 'test_e2e' PostgreSQL schema - No Docker container needed - uses existing DATABASE_URL - Production data in 'public' schema remains untouched - Schema is created before tests and dropped after Changes: - Rename ci-unittest.yml → ci-tests.yml - Remove Docker PostgreSQL service from CI - Move API tests to tests/integration/api/ (SQLite) - Add E2E tests in tests/e2e/ (PostgreSQL schema) - Add expire_all() to fix relationship loading 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b0bc886 commit b3645ca

9 files changed

Lines changed: 656 additions & 306 deletions

File tree

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: "CI: Unit Tests"
1+
name: "CI: Tests"
22
run-name: "Tests: ${{ github.ref_name }}"
33

44
on:
@@ -16,11 +16,16 @@ on:
1616

1717
jobs:
1818
test:
19-
name: Run Unit Tests
19+
name: Run Tests
2020
runs-on: ubuntu-latest
2121
permissions:
2222
contents: read
2323

24+
env:
25+
# E2E tests use isolated test_e2e schema (production data unaffected)
26+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
27+
ENVIRONMENT: test
28+
2429
steps:
2530
- uses: actions/checkout@v6
2631
with:
@@ -66,11 +71,18 @@ jobs:
6671
if: steps.check.outputs.should_test == 'true'
6772
run: uv sync --extra test
6873

69-
- name: Run unit and integration tests with coverage
74+
- name: Run all tests with coverage
7075
if: steps.check.outputs.should_test == 'true'
71-
run: uv run pytest tests/unit tests/integration -v --tb=short --cov=core --cov=api --cov-report=term-missing --cov-report=xml
72-
# Note: E2E tests require database dependency override (not implemented yet)
73-
# Integration tests now work with SQLite using custom database types
76+
run: |
77+
uv run pytest tests/unit tests/integration tests/e2e \
78+
-v --tb=short \
79+
--cov=core --cov=api \
80+
--cov-report=term-missing \
81+
--cov-report=xml
82+
# Tests include:
83+
# - Unit tests: Fast, mocked dependencies
84+
# - Integration tests: SQLite in-memory for repository layer
85+
# - E2E tests: Real PostgreSQL with isolated test_e2e schema
7486

7587
- name: Upload coverage to GitHub Artifacts
7688
if: steps.check.outputs.should_test == 'true' && always()

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,7 @@ async def test_db_with_data(test_session):
154154
test_session.add_all([scatter_matplotlib, scatter_seaborn, bar_matplotlib])
155155
await test_session.commit()
156156

157+
# Expire all cached objects to ensure fresh loading with relationships
158+
test_session.expire_all()
159+
157160
yield test_session

tests/e2e/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""End-to-End tests for pyplots API."""
1+
"""E2E tests with real PostgreSQL database (isolated test_e2e schema)."""

tests/e2e/conftest.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""
2+
E2E test fixtures with real PostgreSQL database.
3+
4+
Uses a separate 'test_e2e' schema to isolate test data from production.
5+
Tests are skipped if DATABASE_URL is not set.
6+
"""
7+
8+
import os
9+
10+
import pytest
11+
from sqlalchemy import text
12+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
13+
14+
from core.database.models import Base
15+
16+
17+
TEST_SCHEMA = "test_e2e"
18+
19+
# Test data constants
20+
TEST_IMAGE_URL = "https://storage.googleapis.com/pyplots-images/test/plot.png"
21+
TEST_THUMB_URL = "https://storage.googleapis.com/pyplots-images/test/thumb.png"
22+
23+
24+
@pytest.fixture(scope="session")
25+
def event_loop():
26+
"""Create event loop for session-scoped async fixtures."""
27+
import asyncio
28+
29+
policy = asyncio.get_event_loop_policy()
30+
loop = policy.new_event_loop()
31+
yield loop
32+
loop.close()
33+
34+
35+
@pytest.fixture(scope="session")
36+
async def pg_engine():
37+
"""
38+
Create PostgreSQL engine and setup test schema.
39+
40+
Creates a separate 'test_e2e' schema to isolate tests from production data.
41+
The schema is dropped and recreated at the start of the test session.
42+
"""
43+
database_url = os.environ.get("DATABASE_URL")
44+
if not database_url:
45+
pytest.skip("DATABASE_URL not set - skipping PostgreSQL E2E tests")
46+
47+
engine = create_async_engine(database_url, echo=False)
48+
49+
# Create test schema and tables
50+
async with engine.begin() as conn:
51+
await conn.execute(text(f"DROP SCHEMA IF EXISTS {TEST_SCHEMA} CASCADE"))
52+
await conn.execute(text(f"CREATE SCHEMA {TEST_SCHEMA}"))
53+
await conn.execute(text(f"SET search_path TO {TEST_SCHEMA}"))
54+
await conn.run_sync(Base.metadata.create_all)
55+
56+
yield engine
57+
58+
# Cleanup: Drop entire test schema
59+
async with engine.begin() as conn:
60+
await conn.execute(text(f"DROP SCHEMA IF EXISTS {TEST_SCHEMA} CASCADE"))
61+
await engine.dispose()
62+
63+
64+
@pytest.fixture
65+
async def pg_session(pg_engine):
66+
"""Create session with test schema."""
67+
async_session = async_sessionmaker(pg_engine, class_=AsyncSession, expire_on_commit=False)
68+
async with async_session() as session:
69+
# Ensure we're in test schema
70+
await session.execute(text(f"SET search_path TO {TEST_SCHEMA}"))
71+
yield session
72+
await session.rollback()
73+
74+
75+
@pytest.fixture
76+
async def pg_db_with_data(pg_session):
77+
"""
78+
Seed test schema with sample data.
79+
80+
Creates the same test data as tests/conftest.py:test_db_with_data
81+
but in the PostgreSQL test schema.
82+
"""
83+
from core.database.models import Impl, Library, Spec
84+
85+
# Create libraries
86+
matplotlib_lib = Library(
87+
id="matplotlib",
88+
name="Matplotlib",
89+
version="3.10.0",
90+
documentation_url="https://matplotlib.org",
91+
description="Comprehensive library for visualizations",
92+
)
93+
seaborn_lib = Library(
94+
id="seaborn",
95+
name="Seaborn",
96+
version="0.13.0",
97+
documentation_url="https://seaborn.pydata.org",
98+
description="Statistical data visualization",
99+
)
100+
pg_session.add_all([matplotlib_lib, seaborn_lib])
101+
102+
# Create specs
103+
scatter_spec = Spec(
104+
id="scatter-basic",
105+
title="Basic Scatter Plot",
106+
description="A basic scatter plot",
107+
applications=["data visualization", "correlation analysis"],
108+
data=["numeric"],
109+
notes=["Use for 2D data"],
110+
tags={"plot_type": ["scatter"], "domain": ["statistics"], "data_type": ["numeric"], "features": ["basic"]},
111+
issue=42,
112+
suggested="contributor",
113+
)
114+
bar_spec = Spec(
115+
id="bar-grouped",
116+
title="Grouped Bar Chart",
117+
description="A grouped bar chart",
118+
applications=["comparisons"],
119+
data=["categorical", "numeric"],
120+
notes=["Good for comparing categories"],
121+
tags={"plot_type": ["bar"], "domain": ["statistics"], "data_type": ["categorical"], "features": ["grouped"]},
122+
issue=43,
123+
suggested="contributor2",
124+
)
125+
pg_session.add_all([scatter_spec, bar_spec])
126+
await pg_session.commit()
127+
128+
# Create implementations
129+
scatter_matplotlib = Impl(
130+
spec_id="scatter-basic",
131+
library_id="matplotlib",
132+
code="import matplotlib.pyplot as plt\n# scatter plot code",
133+
preview_url=TEST_IMAGE_URL.replace("plot", "scatter-matplotlib"),
134+
preview_thumb=TEST_THUMB_URL.replace("thumb", "scatter-matplotlib-thumb"),
135+
quality_score=92.5,
136+
generated_by="claude",
137+
python_version="3.13",
138+
library_version="3.10.0",
139+
)
140+
scatter_seaborn = Impl(
141+
spec_id="scatter-basic",
142+
library_id="seaborn",
143+
code="import seaborn as sns\n# scatter plot code",
144+
preview_url=TEST_IMAGE_URL.replace("plot", "scatter-seaborn"),
145+
preview_thumb=TEST_THUMB_URL.replace("thumb", "scatter-seaborn-thumb"),
146+
quality_score=95.0,
147+
generated_by="claude",
148+
python_version="3.13",
149+
library_version="0.13.0",
150+
)
151+
bar_matplotlib = Impl(
152+
spec_id="bar-grouped",
153+
library_id="matplotlib",
154+
code="import matplotlib.pyplot as plt\n# bar chart code",
155+
preview_url=TEST_IMAGE_URL.replace("plot", "bar-matplotlib"),
156+
quality_score=88.0,
157+
generated_by="claude",
158+
python_version="3.13",
159+
library_version="3.10.0",
160+
)
161+
pg_session.add_all([scatter_matplotlib, scatter_seaborn, bar_matplotlib])
162+
await pg_session.commit()
163+
164+
# Expire all cached objects to ensure fresh loading with relationships
165+
pg_session.expire_all()
166+
167+
yield pg_session

0 commit comments

Comments
 (0)