diff --git a/AGENTS.md b/AGENTS.md index 66e0d3e77..a0a9715ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,16 @@ callers of anything whose signature or behavior you changed (`grep` for the symb CI runs the whole suite on the PR (`pytest.yml`, MariaDB + PostgreSQL). That is the place for full-suite coverage; local runs stay scoped. +**In a git worktree, run tests against a temporary database.** Every checkout shares +`romm_test`, and one on another branch can migrate it to a revision yours lacks, so +every test errors in setup with `Can't locate revision`. Set `ROMM_TEST_DB_TAG` to a +tag unique to the worktree: the run creates and migrates `romm_test_tmp_` (one per +xdist worker), then drops it at the end. Never `stamp` or downgrade the shared `romm_test`. + +```bash +ROMM_TEST_DB_TAG=my_worktree uv run pytest -n auto tests/handler/ +``` + --- ## Environment setup diff --git a/backend/conftest.py b/backend/conftest.py index 43b7e7694..0c1b472b3 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -1,4 +1,5 @@ import os +import re # Tests must never inherit DB_NAME from the ambient environment (e.g. a # sourced .env pointing at a real dev/prod database) -- the autouse @@ -12,5 +13,10 @@ import os # # The Redis cache needs no equivalent handling: under pytest it is an in-process # FakeRedis, so each worker process is already isolated. +# +# ROMM_TEST_DB_TAG runs use temporary databases that tests/conftest.py drops at +# session end; the `tmp_` prefix keeps them apart from the untagged worker names. +_tag = re.sub(r"[^A-Za-z0-9_]", "_", os.environ.get("ROMM_TEST_DB_TAG", "")) +_base = f"romm_test_tmp_{_tag}" if _tag else "romm_test" _xdist_worker = os.environ.get("PYTEST_XDIST_WORKER") -os.environ["DB_NAME"] = f"romm_test_{_xdist_worker}" if _xdist_worker else "romm_test" +os.environ["DB_NAME"] = f"{_base}_{_xdist_worker}" if _xdist_worker else _base diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index eff7e04f2..5d2eeefa4 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -133,6 +133,30 @@ def _ensure_database_exists() -> None: admin_engine.dispose() +def _drop_database() -> None: + """Drop the temporary database a ROMM_TEST_DB_TAG run created.""" + url = ConfigManager.get_db_engine() + db_name = url.database + if not db_name or not re.fullmatch(r"[A-Za-z0-9_]+", db_name): + return + + engine.dispose() + sync_engine.dispose() + if ROMM_DB_DRIVER in ("mariadb", "mysql"): + admin_engine = create_engine(url.set(database="information_schema")) + with admin_engine.begin() as conn: + conn.execute(text(f"DROP DATABASE IF EXISTS `{db_name}`")) + elif ROMM_DB_DRIVER == "postgresql": + admin_engine = create_engine( + url.set(database="postgres"), isolation_level="AUTOCOMMIT" + ) + with admin_engine.connect() as conn: + conn.execute(text(f'DROP DATABASE IF EXISTS "{db_name}" WITH (FORCE)')) + else: + return + admin_engine.dispose() + + @pytest.fixture(autouse=True) def raise_on_response_mismatch(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(response_validation, "RAISE_ON_MISMATCH", True) @@ -151,7 +175,12 @@ def lenient(monkeypatch: pytest.MonkeyPatch) -> MagicMock: @pytest.fixture(scope="session", autouse=True) def setup_database(): _ensure_database_exists() - alembic.config.main(argv=["upgrade", "head"]) + try: + alembic.config.main(argv=["upgrade", "head"]) + yield + finally: + if os.environ.get("ROMM_TEST_DB_TAG"): + _drop_database() @pytest.fixture(autouse=True)