# #region TestSessionConfig [C:2] [TYPE Module] [SEMANTICS test, conftest, db, postgres, testcontainers] # @BRIEF Shared pytest fixtures and session configuration for backend tests. # @RELATION BINDS_TO -> [init_db] # @PRE All test modules use sys.path patching to import from src/. # @POST Database tables exist before test session executes. # @RATIONALE # Architecture: # - Global DATABASE_URL → temp-file SQLite (for modules importing src.core.database) # - Per-module engines → sqlite:///:memory: (for modules with local create_engine) # - FK enforcement on ALL engines via PRAGMA foreign_keys=ON # # Why: # - In-process SQLite is 5-10x faster than Docker PostgreSQL # - FK enforcement catches constraint violations (unlike plain SQLite) # - Per-module engines prevent 10GB+ shared-cache memory leak # - Temp file for global engine also prevents memory leak # # PostgreSQL mode (TEST_DB=postgres) → testcontainers or explicit DATABASE_URL. # # @REJECTED # sqlite:///file::memory:...?cache=shared — 10GB+ memory leak (all modules share one DB). # Plain SQLite without FK — silently ignores FK violations, masks bugs. # Always-on PostgresContainer — 5-10x slower, Docker dependency. import os from pathlib import Path import sys import tempfile from sqlalchemy import create_engine, event sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing") # ── Global engine: temp-file SQLite (not in-memory, not shared cache) ── # This prevents the 10GB memory leak from shared in-memory databases. _TEST_DB_FILE = tempfile.NamedTemporaryFile(suffix="_ss_tools.db", delete=False) _TEST_DB_PATH = _TEST_DB_FILE.name _TEST_DB_FILE.close() os.environ.setdefault("DATABASE_URL", f"sqlite:///{_TEST_DB_PATH}") os.environ.setdefault("AUTH_DATABASE_URL", f"sqlite:///{_TEST_DB_PATH}") # ── FK enforcement on the global engine ── from src.core.database import engine event.listen(engine, "connect", lambda c, _: c.execute("PRAGMA foreign_keys=ON")) # ── Helper: FK-enabled SQLite engine for per-module isolation ── def make_fk_engine(): """Create a per-module SQLite in-memory engine with FK enforcement. Each test module calls this once at module level to get its own isolated database. No shared state → no memory leak. """ eng = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False}) event.listen(eng, "connect", lambda c, _: c.execute("PRAGMA foreign_keys=ON")) return eng # ── CLI options ── def pytest_addoption(parser): parser.addoption( "--run-integration", action="store_true", default=False, help="Run integration tests (require Docker, Testcontainers, Superset container)", ) # ── Monkey-patch pathlib.Path.mkdir for /app paths that don't exist in test env ── # Tests run without /app directory (production path). When a test triggers # GitService init, _ensure_base_path_exists calls mkdir('/app/storage/...') # which fails. Intercept silently for /app paths only. import pathlib as _pathlib _ORIG_MKDIR = _pathlib.Path.mkdir def _safe_mkdir(self, mode=0o777, parents=False, exist_ok=False): p = str(self) if p.startswith("/app"): return # /app doesn't exist in test env — silently accept return _ORIG_MKDIR(self, mode, parents=parents, exist_ok=exist_ok) _pathlib.Path.mkdir = _safe_mkdir # ── Test session lifecycle ── def pytest_configure(config): print( f"\n[conftest] SQLite (global: {_TEST_DB_PATH}) + FK enforcement", file=sys.stderr, ) print( " Integration tests skipped by default. Use --run-integration to enable.\n", file=sys.stderr, ) def pytest_unconfigure(config): try: from src.core.database import engine as _global_engine from src.core.database import tasks_engine as _tasks_engine from src.core.database import auth_engine as _auth_engine _global_engine.dispose() _tasks_engine.dispose() _auth_engine.dispose() except Exception: pass try: os.unlink(_TEST_DB_PATH) except Exception: pass @pytest.fixture(scope="session", autouse=True) def ensure_db_tables(): """Create all tables on the global database.""" try: from src.core.database import init_db init_db() except Exception as exc: print(f"\n[conftest] init_db failed: {exc}", file=sys.stderr) raise @pytest.fixture(scope="session", autouse=True) def ensure_git_storage_dirs(ensure_db_tables): """Patch StorageConfig default to a writable temp dir for tests. The global pathlib.Path.mkdir monkey-patch (above) handles /app paths that don't exist in the test environment. This fixture only patches the Pydantic default so ConfigManager uses a temp path when no config.json exists. """ import os import tempfile from src.models.storage import StorageConfig test_root = tempfile.mkdtemp(prefix="ss_tools_test_storage_") test_repos = os.path.join(test_root, "repositories") os.makedirs(test_repos, exist_ok=True) StorageConfig.model_fields["root_path"].default = test_root StorageConfig.model_fields["repo_path"].default = "repositories" print(f"\n[conftest] Storage root_path default={test_root}", file=sys.stderr) # #endregion TestSessionConfig