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
1 change: 1 addition & 0 deletions docs/changes/2089.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed CI-only DB unit-test flakiness by hardening conftest.py database-test detection so DatabaseHandler is no longer accidentally globally mocked for tests/unit_tests/db/*.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ ignore-words-list = "chec,arrang,livetime"
[tool.pytest]
ini_options.markers = [
"uses_model_database: test uses model parameter database.",
"db_unit_test: test belongs to unit_tests/db and must not use global DatabaseHandler patching.",
]
ini_options.minversion = "6.0"
ini_options.norecursedirs = [
Expand Down
55 changes: 42 additions & 13 deletions tests/unit_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,39 @@
from simtools.runners.corsika_runner import CorsikaRunner

logger = logging.getLogger()
REAL_DATABASE_HANDLER_CLASS = db_handler.DatabaseHandler

UNIT_TEST_DB = "unit_tests/db"

def pytest_collection_modifyitems(items):
"""Tag db unit tests with a stable marker for fixture routing."""

def _is_db_item(item):
path = getattr(item, "path", None)
if path is None:
return False
parts = tuple(path.parts)
db_parts = ("tests", "unit_tests", "db")
return any(parts[idx : idx + 3] == db_parts for idx in range(max(len(parts) - 2, 0)))
Comment on lines +43 to +46
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

pytest_collection_modifyitems relies on item.path, but the rest of this conftest (and older pytest versions) use request.node.fspath. If item.path isn’t present (e.g., pytest versions that only expose fspath), db tests won’t be auto-marked and fixture routing can regress. Consider falling back to item.fspath (convert to Path(str(...))) when item.path is missing.

Suggested change
return False
parts = tuple(path.parts)
db_parts = ("tests", "unit_tests", "db")
return any(parts[idx : idx + 3] == db_parts for idx in range(max(len(parts) - 2, 0)))
fspath = getattr(item, "fspath", None)
if fspath is None:
return False
path = Path(str(fspath))
parts = tuple(path.parts)
db_parts = ("tests", "unit_tests", "db")
return any(
parts[idx : idx + 3] == db_parts for idx in range(max(len(parts) - 2, 0))
)

Copilot uses AI. Check for mistakes.

for item in items:
if _is_db_item(item):
item.add_marker("db_unit_test")


def _is_db_unit_test(request):
"""Return True when the current test carries the db_unit_test marker."""
node = getattr(request, "node", None)
if node is None:
return False

if node.get_closest_marker("db_unit_test") is not None:
return True

test_file = ""
location = getattr(node, "location", None)
if location:
test_file = str(location[0]).replace("\\", "/")
return test_file.startswith("tests/unit_tests/db/")


@functools.lru_cache
Expand Down Expand Up @@ -355,11 +386,8 @@ def mock_db_handler(request):
Returns a MagicMock configured with typical DatabaseHandler methods.
Tests in tests/unit_tests/db/ receive a real DatabaseHandler instance.
"""
test_file_path = str(request.node.fspath)
if UNIT_TEST_DB in test_file_path:
db_instance = request.getfixturevalue("db")
db_instance.get_model_versions = MagicMock(return_value=["1.0.0", "5.0.0", "6.0.0"])
return db_instance
if _is_db_unit_test(request):
return request.getfixturevalue("db")

# Load mock data from JSON files
mock_parameters = _apply_mock_param_defaults(_load_mock_db_json("mock_parameters.json"))
Expand Down Expand Up @@ -442,11 +470,13 @@ def patch_database_handler(request, mocker):
Tests in tests/unit_tests/db/ are excluded from this mocking.
Also patches model parameter schema validation to avoid schema version mismatches.
"""
test_file_path = str(request.node.fspath)

# Skip mocking for tests in db/ directory
if UNIT_TEST_DB in test_file_path:
yield
if _is_db_unit_test(request):
with mock.patch(
"simtools.db.db_handler.DatabaseHandler",
new=REAL_DATABASE_HANDLER_CLASS,
):
yield
return

mock_db_handler = request.getfixturevalue("mock_db_handler")
Expand All @@ -462,8 +492,7 @@ def patch_database_handler(request, mocker):
def db(request):
"""Database object with configuration from settings.config.db_handler."""

test_file_path = str(request.node.fspath)
if UNIT_TEST_DB not in test_file_path:
if not _is_db_unit_test(request):
db_instance = db_handler.DatabaseHandler()
yield db_instance
return
Expand Down Expand Up @@ -503,7 +532,7 @@ def db(request):
mock_mongo_client.close = MagicMock()

with patch("simtools.db.mongo_db.MongoClient", return_value=mock_mongo_client):
db_instance = db_handler.DatabaseHandler()
db_instance = REAL_DATABASE_HANDLER_CLASS()
MongoDBHandler.db_client = MongoDBHandler.db_client or mock_mongo_client
yield db_instance
# Explicitly close the mock client to avoid un-raisable exception warnings
Expand Down
7 changes: 5 additions & 2 deletions tests/unit_tests/db/test_db_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
from simtools.db.mongo_db import MongoDBHandler
from simtools.utils import names

# Suppress warnings
pytestmark = pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
# Suppress warnings and mark this module as db unit tests.
pytestmark = [
pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning"),
pytest.mark.db_unit_test,
]

logger = logging.getLogger()

Expand Down
2 changes: 2 additions & 0 deletions tests/unit_tests/db/test_db_model_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import simtools.db.db_model_upload as db_model_upload

pytestmark = pytest.mark.db_unit_test


@patch("simtools.db.db_model_upload.ascii_handler.collect_data_from_file")
def test_add_values_from_json_to_db(mock_collect_data_from_file):
Expand Down
5 changes: 4 additions & 1 deletion tests/unit_tests/db/test_mongo_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

from simtools.db import mongo_db

pytestmark = pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
pytestmark = [
pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning"),
pytest.mark.db_unit_test,
]


@pytest.fixture(autouse=True)
Expand Down
Loading