Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ dev = [
"hypothesis",
"deepdiff",
"pytest-xdist",
"testcontainers",
"pymysql",
"cryptography",

]
docs = [
"mkdocs-material",
Expand Down
50 changes: 18 additions & 32 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import contextlib
import json
from collections.abc import Iterator
from pathlib import Path
Expand All @@ -8,36 +7,32 @@
import httpx
import pytest
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item
from fastapi.testclient import TestClient
from sqlalchemy import Connection, Engine, text
from dotenv import load_dotenv
from sqlalchemy import Connection, text

from database.setup import expdb_database, user_database
from main import create_api
from routers.dependencies import expdb_connection, userdb_connection
load_dotenv()

PHP_API_URL = "http://openml-php-rest-api:80/api/v1/json"

PHP_API_URL = "http://openml-php-rest-api:80/api/v1/json"

@contextlib.contextmanager
def automatic_rollback(engine: Engine) -> Iterator[Connection]:
with engine.connect() as connection:
transaction = connection.begin()
yield connection
if transaction.is_active:
transaction.rollback()

def pytest_addoption(parser: Parser) -> None:
parser.addoption(
"--use-testcontainer", # CLI flag
action="store_true", # True if provided, False if omitted
help="Use testcontainers for database tests",
)

@pytest.fixture
def expdb_test() -> Connection:
with automatic_rollback(expdb_database()) as connection:
yield connection

def pytest_configure(config: Config) -> None:
use_test_container = config.getoption("--use-testcontainer")

@pytest.fixture
def user_test() -> Connection:
with automatic_rollback(user_database()) as connection:
yield connection
if use_test_container:
config.pluginmanager.import_plugin("tests.fixtures_testcontainer")
else:
config.pluginmanager.import_plugin("tests.fixtures_local")
Comment on lines 29 to 35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Plugin name mismatch — tests.fixtures_container should be tests.fixtures_testcontainer.

Line 33 imports "tests.fixtures_container" but the actual file is tests/fixtures_testcontainer.py. This will raise a module-not-found error whenever --use-test-container is passed.

Proposed fix
     if use_test_container:
-        config.pluginmanager.import_plugin("tests.fixtures_container")
+        config.pluginmanager.import_plugin("tests.fixtures_testcontainer")
     else:
         config.pluginmanager.import_plugin("tests.fixtures_local")
#!/bin/bash
# Verify the actual filename of the testcontainer fixtures module
fd "fixtures" tests/ --type f
🤖 Prompt for AI Agents
In `@tests/conftest.py` around lines 29 - 35, The pytest_configure function
imports the wrong plugin name when use_test_container is true; update the
import_plugin call in pytest_configure to import the actual module name
"tests.fixtures_testcontainer" instead of "tests.fixtures_container" so the
plugin resolves correctly (locate the import_plugin invocation inside
pytest_configure).



@pytest.fixture
Expand All @@ -46,15 +41,6 @@ def php_api() -> httpx.Client:
yield client


@pytest.fixture
def py_api(expdb_test: Connection, user_test: Connection) -> TestClient:
app = create_api()
# We use the lambda definitions because fixtures may not be called directly.
app.dependency_overrides[expdb_connection] = lambda: expdb_test
app.dependency_overrides[userdb_connection] = lambda: user_test
return TestClient(app)


@pytest.fixture
def dataset_130() -> Iterator[dict[str, Any]]:
json_path = Path(__file__).parent / "resources" / "datasets" / "dataset_130.json"
Expand Down Expand Up @@ -109,7 +95,7 @@ def persisted_flow(flow: Flow, expdb_test: Connection) -> Iterator[Flow]:
expdb_test.commit()


def pytest_collection_modifyitems(config: Config, items: list[Item]) -> None: # noqa: ARG001
def pytest_collection_modifyitems(items: list[Item]) -> None:
for test_item in items:
for fixture in test_item.fixturenames: # type: ignore[attr-defined]
test_item.own_markers.append(_pytest.mark.Mark(fixture, (), {}))
43 changes: 43 additions & 0 deletions tests/fixtures_local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import contextlib
from collections.abc import Iterator

import pytest
from dotenv import load_dotenv
from fastapi.testclient import TestClient
from sqlalchemy import Connection, Engine

from database.setup import expdb_database, user_database
from main import create_api
from routers.dependencies import expdb_connection, userdb_connection

load_dotenv()


@contextlib.contextmanager
def automatic_rollback(engine: Engine) -> Iterator[Connection]:
with engine.connect() as connection:
transaction = connection.begin()
yield connection
if transaction.is_active:
transaction.rollback()


@pytest.fixture
def expdb_test() -> Connection:
with automatic_rollback(expdb_database()) as connection:
yield connection


@pytest.fixture
def user_test() -> Connection:
with automatic_rollback(user_database()) as connection:
yield connection


@pytest.fixture
def py_api(expdb_test: Connection, user_test: Connection) -> TestClient:
app = create_api()
# We use the lambda definitions because fixtures may not be called directly.
app.dependency_overrides[expdb_connection] = lambda: expdb_test
app.dependency_overrides[userdb_connection] = lambda: user_test
return TestClient(app)
99 changes: 99 additions & 0 deletions tests/fixtures_testcontainer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import contextlib
import os
import re
from collections.abc import Generator, Iterator

import pytest
import sqlalchemy
from dotenv import load_dotenv
from fastapi.testclient import TestClient
from sqlalchemy import Connection, Engine
from testcontainers.mysql import LogMessageWaitStrategy, MySqlContainer

from main import create_api
from routers.dependencies import expdb_connection, userdb_connection

load_dotenv()


@pytest.fixture(scope="session", autouse=True)
def override_testcontainers_connect() -> None:
"""
Override MySqlContainer._connect once per test session.
Applied automatically everywhere.
"""

def _connect(self: MySqlContainer) -> None:
wait_strategy = LogMessageWaitStrategy(
re.compile(
r".*: ready for connections",
flags=re.DOTALL | re.MULTILINE,
)
)
wait_strategy.wait_until_ready(self)

MySqlContainer._connect = _connect # noqa: SLF001


@pytest.fixture(scope="session")
def mysql_container() -> MySqlContainer:
container = MySqlContainer(
os.environ.get(
"OPENML_DATABASES_OPENML_URL",
"openml/test-database:v0.1.20260204",
),
username=os.environ.get("OPENML_DATABASES_OPENML_USERNAME", ""),
password=os.environ.get("OPENML_DATABASES_OPENML_PASSWORD", ""),
dbname="openml_expdb",
)

container.start()
try:
yield container
finally:
container.stop()


@pytest.fixture
def expdb_test(mysql_container: MySqlContainer) -> Connection:
url = mysql_container.get_connection_url().replace("mysql://", "mysql+pymysql://")
engine = sqlalchemy.create_engine(url)

with engine.begin() as connection: # This starts a transaction
try:
yield connection
finally:
connection.rollback() # Rollback ALL test changes
Comment on lines +57 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

expdb_test uses engine.begin() instead of automatic_rollback — test isolation risk.

engine.begin() commits on normal context-manager exit. The finally: connection.rollback() runs first, but then the with engine.begin() __exit__ still executes its own cleanup logic, which can lead to unexpected behavior depending on SQLAlchemy version. Meanwhile, user_test (Line 90-99) correctly uses automatic_rollback. Use the same pattern here for consistency and reliable isolation.

Proposed fix
 `@pytest.fixture`
-def expdb_test(mysql_container: MySqlContainer) -> Connection:
+def expdb_test(mysql_container: MySqlContainer) -> Iterator[Connection]:
     url = mysql_container.get_connection_url().replace("mysql://", "mysql+pymysql://")
     engine = sqlalchemy.create_engine(url)
-
-    with engine.begin() as connection:  # This starts a transaction
-        try:
-            yield connection
-        finally:
-            connection.rollback()  # Rollback ALL test changes
+    with automatic_rollback(engine) as connection:
+        yield connection
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture
def expdb_test(mysql_container: MySqlContainer) -> Connection:
url = mysql_container.get_connection_url().replace("mysql://", "mysql+pymysql://")
engine = sqlalchemy.create_engine(url)
with engine.begin() as connection: # This starts a transaction
try:
yield connection
finally:
connection.rollback() # Rollback ALL test changes
`@pytest.fixture`
def expdb_test(mysql_container: MySqlContainer) -> Iterator[Connection]:
url = mysql_container.get_connection_url().replace("mysql://", "mysql+pymysql://")
engine = sqlalchemy.create_engine(url)
with automatic_rollback(engine) as connection:
yield connection
🤖 Prompt for AI Agents
In `@tests/fixtures_testcontainer.py` around lines 57 - 66, The expdb_test fixture
uses with engine.begin() which can commit on exit and conflicts with the
explicit rollback; change it to use the same automatic_rollback context as
user_test to ensure reliable test isolation: replace the with engine.begin()
block with a with automatic_rollback(engine) (or the project's
automatic_rollback helper) so the transaction is managed consistently and all
changes are rolled back; update the expdb_test fixture signature if needed to
import or reference automatic_rollback and keep the yield connection / final
rollback handling consistent with user_test.



@contextlib.contextmanager
def automatic_rollback(engine: Engine) -> Iterator[Connection]:
with engine.connect() as connection:
transaction = connection.begin()
yield connection
if transaction.is_active:
transaction.rollback()


@pytest.fixture
def py_api(expdb_test: Connection, user_test: Connection) -> Generator[TestClient, None, None]:
app = create_api()
# We use the lambda definitions because fixtures may not be called directly.
app.dependency_overrides[expdb_connection] = lambda: expdb_test
app.dependency_overrides[userdb_connection] = lambda: user_test

client = TestClient(app)
yield client
client.close()


@pytest.fixture
def user_test(mysql_container: MySqlContainer) -> Connection:
"""Get a connection to the user database using the testcontainer."""
url = mysql_container.get_connection_url()
url = url.replace("mysql://", "mysql+pymysql://")
url = url.replace("openml_expdb", "openml")

engine = sqlalchemy.create_engine(url)
with automatic_rollback(engine) as connection:
yield connection
Loading