Skip to content

Commit 6294979

Browse files
Den A EvDen A Ev
authored andcommitted
fix(ci): expert infrastructure stabilization (loop collision, state leak, pytest-env)
1 parent 5df2e46 commit 6294979

File tree

2 files changed

+53
-67
lines changed

2 files changed

+53
-67
lines changed

.github/workflows/ci.yml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,23 @@ jobs:
8484
run: |
8585
python -m pip install --upgrade pip
8686
pip install -r requirements.txt
87-
pip install pytest pytest-asyncio pytest-cov
87+
pip install pytest pytest-asyncio pytest-cov pytest-env
8888
89-
- name: Run Unit Tests
89+
- name: Run Backend Unit Tests
90+
working-directory: backend
91+
run: pytest tests/ -v --tb=short --ignore=tests/e2e/ --cov=app --cov-report=xml --cov-report=term-missing
92+
env:
93+
PYTHONPATH: .
94+
DATABASE_URL: sqlite+aiosqlite:///./test_unit.db
95+
REDIS_URL: redis://localhost:6379/0
96+
SECRET_KEY: test-secret-key-for-ci
97+
98+
- name: Run Backend E2E Tests
9099
working-directory: backend
91-
run: pytest tests/ -v --tb=short --cov=app --cov-report=xml --cov-report=term-missing
100+
run: pytest tests/e2e/ -v --tb=short
92101
env:
93102
PYTHONPATH: .
94-
DATABASE_URL: postgresql://test:test@localhost:5432/test
95-
TEST_DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/test
103+
DATABASE_URL: sqlite+aiosqlite:///./test_e2e.db
96104
REDIS_URL: redis://localhost:6379/0
97105
SECRET_KEY: test-secret-key-for-ci
98106

backend/tests/e2e/conftest.py

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -80,56 +80,8 @@ async def _mock_get_current_user() -> UserResponse:
8080
# ---------------------------------------------------------------------------
8181

8282

83-
# ---------------------------------------------------------------------------
84-
# Shared event loop for test session
85-
# ---------------------------------------------------------------------------
86-
87-
_test_loop = None
88-
89-
90-
def _get_test_loop() -> asyncio.AbstractEventLoop:
91-
"""Return a shared event loop for synchronous async execution."""
92-
global _test_loop
93-
if _test_loop is None or _test_loop.is_closed():
94-
_test_loop = asyncio.new_event_loop()
95-
asyncio.set_event_loop(_test_loop)
96-
return _test_loop
97-
98-
99-
# ---------------------------------------------------------------------------
100-
# Application assembly
101-
# ---------------------------------------------------------------------------
102-
103-
10483
def _create_test_app() -> FastAPI:
10584
"""Create a minimal FastAPI app with all routers for E2E testing."""
106-
from app.database import engine, Base
107-
108-
# Models... (keeping the imports I added)
109-
from app.models.notification import NotificationDB # noqa: F401
110-
from app.models.user import User # noqa: F401
111-
from app.models.bounty_table import BountyTable # noqa: F401
112-
from app.models.agent import Agent # noqa: F401
113-
from app.models.dispute import DisputeDB, DisputeHistoryDB # noqa: F401
114-
from app.models.contributor import ContributorTable # noqa: F401
115-
from app.models.submission import SubmissionDB # noqa: F401
116-
from app.models.tables import ( # noqa: F401
117-
PayoutTable,
118-
BuybackTable,
119-
ReputationHistoryTable,
120-
BountySubmissionTable,
121-
)
122-
from app.models.review import AIReviewScoreDB # noqa: F401
123-
from app.models.lifecycle import BountyLifecycleLogDB # noqa: F401
124-
from app.models.escrow import EscrowTable, EscrowLedgerTable # noqa: F401
125-
from app.models.boost import BountyBoostTable # noqa: F401
126-
127-
async def _async_init():
128-
async with engine.begin() as conn:
129-
await conn.run_sync(Base.metadata.create_all)
130-
131-
_get_test_loop().run_until_complete(_async_init())
132-
13385
# get_current_user is already imported at module level
13486
from app.api.auth import router as auth_router
13587
from app.api.bounties import router as bounties_router
@@ -181,33 +133,59 @@ async def global_exception_handler(request: Request, exc: Exception):
181133
app = _create_test_app()
182134

183135

184-
@pytest.fixture(autouse=True)
185-
def clear_stores():
186-
"""Reset all in-memory stores, database tables, and factory counters between tests."""
187-
from sqlalchemy import delete, text
136+
@pytest_asyncio.fixture(scope="session", autouse=True)
137+
async def init_db():
138+
"""Initialise the test database once per session on the shared event loop."""
139+
from app.database import engine, Base
140+
import app.models.notification # noqa: F401
141+
import app.models.user # noqa: F401
142+
import app.models.bounty_table # noqa: F401
143+
import app.models.agent # noqa: F401
144+
import app.models.dispute # noqa: F401
145+
import app.models.contributor # noqa: F401
146+
import app.models.submission # noqa: F401
147+
import app.models.tables # noqa: F401
148+
import app.models.review # noqa: F401
149+
import app.models.lifecycle # noqa: F401
150+
import app.models.escrow # noqa: F401
151+
import app.models.boost # noqa: F401
152+
153+
if os.path.exists(TEST_DB_PATH):
154+
os.remove(TEST_DB_PATH)
155+
156+
async with engine.begin() as conn:
157+
await conn.run_sync(Base.metadata.create_all)
158+
yield
159+
if os.path.exists(TEST_DB_PATH):
160+
try:
161+
os.remove(TEST_DB_PATH)
162+
except PermissionError:
163+
pass
164+
165+
166+
@pytest_asyncio.fixture(autouse=True)
167+
async def truncate_db_tables():
168+
"""Truncate all tables between tests to ensure strict isolation."""
169+
from sqlalchemy import text
188170
from app.database import engine, Base
189171
from app.services import bounty_service, contributor_service
190172
from app.services.payout_service import reset_stores as reset_payout_stores
191173
from tests.e2e.factories import reset_counters
192174

193-
async def _db_cleanup():
194-
async with engine.begin() as conn:
195-
await conn.execute(text("PRAGMA foreign_keys = OFF"))
196-
for table in reversed(Base.metadata.sorted_tables):
197-
await conn.execute(delete(table))
198-
await conn.execute(text("PRAGMA foreign_keys = ON"))
199-
200-
_get_test_loop().run_until_complete(_db_cleanup())
175+
async with engine.begin() as conn:
176+
await conn.execute(text("PRAGMA foreign_keys = OFF"))
177+
for table in reversed(Base.metadata.sorted_tables):
178+
await conn.execute(text(f"DELETE FROM {table.name}"))
179+
await conn.execute(text("PRAGMA foreign_keys = ON"))
201180

202-
# Reset internal service caches if any
181+
# Reset internal service caches
203182
if hasattr(bounty_service, "_bounty_store"):
204183
bounty_service._bounty_store.clear()
205184
if hasattr(contributor_service, "_store"):
206185
contributor_service._store.clear()
207186

208187
reset_payout_stores()
209188
reset_counters()
210-
yield
211189

212190

213191
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)