mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
refactor(tests): centralize fake database import-state cleanup
Test-only refactor continuing #2523. Centralizes the repeated guarded fake core.database/src.database import-state cleanup into a focused helper.
This commit is contained in:
committed by
GitHub
parent
370ae5d451
commit
301d1109b5
@@ -8,6 +8,10 @@ had before the block — present, absent, or carrying a parent-package attribute
|
|||||||
Use ``clear_module`` to drop a single module from both ``sys.modules`` and its
|
Use ``clear_module`` to drop a single module from both ``sys.modules`` and its
|
||||||
parent-package attribute (e.g. before forcing a fresh import inside the block).
|
parent-package attribute (e.g. before forcing a fresh import inside the block).
|
||||||
|
|
||||||
|
Use ``clear_fake_database_modules`` to evict a *stubbed* ``core.database`` (and
|
||||||
|
its companion ``src.database``) that another test left in import state, without
|
||||||
|
touching a real ``core.database`` loaded from disk.
|
||||||
|
|
||||||
Background: importing ``routes.session_routes`` also sets ``session_routes`` on
|
Background: importing ``routes.session_routes`` also sets ``session_routes`` on
|
||||||
the parent ``routes`` package object. A ``from routes import session_routes``
|
the parent ``routes`` package object. A ``from routes import session_routes``
|
||||||
or ``import routes.session_routes as X`` statement resolves through that parent
|
or ``import routes.session_routes as X`` statement resolves through that parent
|
||||||
@@ -60,6 +64,35 @@ def clear_module(dotted_name):
|
|||||||
_restore_one(dotted_name, _ABSENT, _ABSENT)
|
_restore_one(dotted_name, _ABSENT, _ABSENT)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_fake_database_modules():
|
||||||
|
"""Evict a *stubbed* ``core.database`` (and ``src.database``) from import state.
|
||||||
|
|
||||||
|
Test-only. Some tests install a fake ``core.database`` — a stub module with
|
||||||
|
no on-disk ``__file__`` — into ``sys.modules`` and onto the ``core`` package.
|
||||||
|
A later test that needs the real database module must evict that stub first,
|
||||||
|
or its ``import core.database`` resolves to the fake.
|
||||||
|
|
||||||
|
This is deliberately conservative and mirrors the per-file helpers it
|
||||||
|
replaces:
|
||||||
|
|
||||||
|
* It acts only when ``core.database`` is a fake/stub, detected by a missing
|
||||||
|
string ``__file__``. A real ``core.database`` loaded from disk is left
|
||||||
|
untouched, as is the case where nothing is cached.
|
||||||
|
* When it does act, it also drops the cached ``src.database`` entry.
|
||||||
|
* It removes the ``core.database`` parent-package attribute only when that
|
||||||
|
attribute is the same fake object being evicted.
|
||||||
|
"""
|
||||||
|
parent = sys.modules.get("core")
|
||||||
|
attr = getattr(parent, "database", None) if parent is not None else None
|
||||||
|
mod = sys.modules.get("core.database") or attr
|
||||||
|
if mod is None or isinstance(getattr(mod, "__file__", None), str):
|
||||||
|
return
|
||||||
|
sys.modules.pop("core.database", None)
|
||||||
|
sys.modules.pop("src.database", None)
|
||||||
|
if parent is not None and attr is mod:
|
||||||
|
delattr(parent, "database")
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def preserve_import_state(*module_names):
|
def preserve_import_state(*module_names):
|
||||||
"""Save and restore sys.modules entries and parent-package attributes.
|
"""Save and restore sys.modules entries and parent-package attributes.
|
||||||
|
|||||||
@@ -15,20 +15,9 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
|
from tests.helpers.import_state import clear_fake_database_modules
|
||||||
|
|
||||||
def _drop_fake_core_database():
|
clear_fake_database_modules()
|
||||||
parent = sys.modules.get("core")
|
|
||||||
attr = getattr(parent, "database", None) if parent is not None else None
|
|
||||||
mod = sys.modules.get("core.database") or attr
|
|
||||||
if mod is None or isinstance(getattr(mod, "__file__", None), str):
|
|
||||||
return
|
|
||||||
sys.modules.pop("core.database", None)
|
|
||||||
sys.modules.pop("src.database", None)
|
|
||||||
if parent is not None and attr is mod:
|
|
||||||
delattr(parent, "database")
|
|
||||||
|
|
||||||
|
|
||||||
_drop_fake_core_database()
|
|
||||||
|
|
||||||
import core.database as cdb
|
import core.database as cdb
|
||||||
from core.database import CalendarEvent
|
from core.database import CalendarEvent
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ while completing reliably everywhere.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import sys
|
|
||||||
import uuid
|
import uuid
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
@@ -22,20 +21,9 @@ from sqlalchemy.orm import sessionmaker
|
|||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from tests.helpers.import_state import clear_fake_database_modules
|
||||||
|
|
||||||
def _drop_fake_core_database():
|
clear_fake_database_modules()
|
||||||
parent = sys.modules.get("core")
|
|
||||||
attr = getattr(parent, "database", None) if parent is not None else None
|
|
||||||
mod = sys.modules.get("core.database") or attr
|
|
||||||
if mod is None or isinstance(getattr(mod, "__file__", None), str):
|
|
||||||
return
|
|
||||||
sys.modules.pop("core.database", None)
|
|
||||||
sys.modules.pop("src.database", None)
|
|
||||||
if parent is not None and attr is mod:
|
|
||||||
delattr(parent, "database")
|
|
||||||
|
|
||||||
|
|
||||||
_drop_fake_core_database()
|
|
||||||
|
|
||||||
import core.database as cdb
|
import core.database as cdb
|
||||||
import routes.document_routes as droutes
|
import routes.document_routes as droutes
|
||||||
|
|||||||
@@ -4,10 +4,18 @@ import types
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.helpers.import_state import clear_module, preserve_import_state
|
from tests.helpers.import_state import (
|
||||||
|
clear_fake_database_modules,
|
||||||
|
clear_module,
|
||||||
|
preserve_import_state,
|
||||||
|
)
|
||||||
|
|
||||||
_SENTINEL = "tests._import_state_test_sentinel"
|
_SENTINEL = "tests._import_state_test_sentinel"
|
||||||
|
|
||||||
|
# Names touched by clear_fake_database_modules — snapshot/restore these so the
|
||||||
|
# tests never leak into the real core/src packages.
|
||||||
|
_DB_NAMES = ("core", "core.database", "src", "src.database")
|
||||||
|
|
||||||
|
|
||||||
def test_absent_module_is_removed_after_block():
|
def test_absent_module_is_removed_after_block():
|
||||||
assert _SENTINEL not in sys.modules
|
assert _SENTINEL not in sys.modules
|
||||||
@@ -139,3 +147,106 @@ def test_parent_attr_restored_correctly_when_parent_also_preserved():
|
|||||||
finally:
|
finally:
|
||||||
sys.modules.pop("_fake_istate_parent", None)
|
sys.modules.pop("_fake_istate_parent", None)
|
||||||
sys.modules.pop("_fake_istate_parent.child", None)
|
sys.modules.pop("_fake_istate_parent.child", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_fake_database_removes_stub_core_database():
|
||||||
|
with preserve_import_state(*_DB_NAMES):
|
||||||
|
fake_core = types.ModuleType("core")
|
||||||
|
fake_db = types.ModuleType("core.database") # no __file__ => a stub
|
||||||
|
fake_core.database = fake_db
|
||||||
|
sys.modules["core"] = fake_core
|
||||||
|
sys.modules["core.database"] = fake_db
|
||||||
|
|
||||||
|
clear_fake_database_modules()
|
||||||
|
|
||||||
|
assert "core.database" not in sys.modules
|
||||||
|
assert not hasattr(fake_core, "database")
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_fake_database_preserves_real_core_database():
|
||||||
|
with preserve_import_state(*_DB_NAMES):
|
||||||
|
fake_core = types.ModuleType("core")
|
||||||
|
real_db = types.ModuleType("core.database")
|
||||||
|
real_db.__file__ = "/somewhere/core/database.py" # looks on-disk
|
||||||
|
fake_core.database = real_db
|
||||||
|
sys.modules["core"] = fake_core
|
||||||
|
sys.modules["core.database"] = real_db
|
||||||
|
|
||||||
|
clear_fake_database_modules()
|
||||||
|
|
||||||
|
assert sys.modules["core.database"] is real_db
|
||||||
|
assert fake_core.database is real_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_fake_database_drops_src_database_when_core_is_fake():
|
||||||
|
with preserve_import_state(*_DB_NAMES):
|
||||||
|
fake_core = types.ModuleType("core")
|
||||||
|
fake_db = types.ModuleType("core.database")
|
||||||
|
fake_core.database = fake_db
|
||||||
|
sys.modules["core"] = fake_core
|
||||||
|
sys.modules["core.database"] = fake_db
|
||||||
|
sys.modules["src.database"] = types.ModuleType("src.database")
|
||||||
|
|
||||||
|
clear_fake_database_modules()
|
||||||
|
|
||||||
|
assert "src.database" not in sys.modules
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_fake_database_leaves_src_database_when_core_is_real():
|
||||||
|
with preserve_import_state(*_DB_NAMES):
|
||||||
|
fake_core = types.ModuleType("core")
|
||||||
|
real_db = types.ModuleType("core.database")
|
||||||
|
real_db.__file__ = "/somewhere/core/database.py"
|
||||||
|
fake_core.database = real_db
|
||||||
|
sys.modules["core"] = fake_core
|
||||||
|
sys.modules["core.database"] = real_db
|
||||||
|
src_db = types.ModuleType("src.database")
|
||||||
|
sys.modules["src.database"] = src_db
|
||||||
|
|
||||||
|
clear_fake_database_modules()
|
||||||
|
|
||||||
|
assert sys.modules["src.database"] is src_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_fake_database_keeps_parent_attr_pointing_elsewhere():
|
||||||
|
"""When the cached core.database is a stub but the `database` attr on the
|
||||||
|
core package points at a *different* object, the attr is left intact —
|
||||||
|
only the same fake object is unlinked."""
|
||||||
|
with preserve_import_state(*_DB_NAMES):
|
||||||
|
fake_core = types.ModuleType("core")
|
||||||
|
cached_fake = types.ModuleType("core.database") # the stub in sys.modules
|
||||||
|
other = types.ModuleType("core.database") # parent attr points here
|
||||||
|
fake_core.database = other
|
||||||
|
sys.modules["core"] = fake_core
|
||||||
|
sys.modules["core.database"] = cached_fake
|
||||||
|
|
||||||
|
clear_fake_database_modules()
|
||||||
|
|
||||||
|
assert "core.database" not in sys.modules
|
||||||
|
assert fake_core.database is other
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_fake_database_uses_parent_attr_when_not_in_sys_modules():
|
||||||
|
"""A stub reachable only via the core package's `database` attribute (not in
|
||||||
|
sys.modules) is still detected and unlinked from the parent."""
|
||||||
|
with preserve_import_state(*_DB_NAMES):
|
||||||
|
sys.modules.pop("core.database", None)
|
||||||
|
fake_core = types.ModuleType("core")
|
||||||
|
fake_db = types.ModuleType("core.database")
|
||||||
|
fake_core.database = fake_db
|
||||||
|
sys.modules["core"] = fake_core
|
||||||
|
|
||||||
|
clear_fake_database_modules()
|
||||||
|
|
||||||
|
assert not hasattr(fake_core, "database")
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_fake_database_noop_when_nothing_cached():
|
||||||
|
with preserve_import_state(*_DB_NAMES):
|
||||||
|
sys.modules.pop("core.database", None)
|
||||||
|
fake_core = types.ModuleType("core") # no `database` attr
|
||||||
|
sys.modules["core"] = fake_core
|
||||||
|
|
||||||
|
clear_fake_database_modules() # must not raise
|
||||||
|
|
||||||
|
assert "core.database" not in sys.modules
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import sys
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from tests.helpers.import_state import clear_fake_database_modules
|
||||||
|
|
||||||
def _drop_fake_core_database():
|
clear_fake_database_modules()
|
||||||
parent = sys.modules.get("core")
|
|
||||||
attr = getattr(parent, "database", None) if parent is not None else None
|
|
||||||
mod = sys.modules.get("core.database") or attr
|
|
||||||
if mod is None or isinstance(getattr(mod, "__file__", None), str):
|
|
||||||
return
|
|
||||||
sys.modules.pop("core.database", None)
|
|
||||||
sys.modules.pop("src.database", None)
|
|
||||||
if parent is not None and attr is mod:
|
|
||||||
delattr(parent, "database")
|
|
||||||
|
|
||||||
|
|
||||||
_drop_fake_core_database()
|
|
||||||
|
|
||||||
from core.database import Base, Session, ChatMessage
|
from core.database import Base, Session, ChatMessage
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
@@ -12,20 +12,9 @@ if not isinstance(sqlalchemy, _types.ModuleType):
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from tests.helpers.import_state import clear_fake_database_modules
|
||||||
|
|
||||||
def _drop_fake_core_database():
|
clear_fake_database_modules()
|
||||||
parent = sys.modules.get("core")
|
|
||||||
attr = getattr(parent, "database", None) if parent is not None else None
|
|
||||||
mod = sys.modules.get("core.database") or attr
|
|
||||||
if mod is None or isinstance(getattr(mod, "__file__", None), str):
|
|
||||||
return
|
|
||||||
sys.modules.pop("core.database", None)
|
|
||||||
sys.modules.pop("src.database", None)
|
|
||||||
if parent is not None and attr is mod:
|
|
||||||
delattr(parent, "database")
|
|
||||||
|
|
||||||
|
|
||||||
_drop_fake_core_database()
|
|
||||||
|
|
||||||
import core.database as cdb
|
import core.database as cdb
|
||||||
from core.database import Base, Session as DbSession
|
from core.database import Base, Session as DbSession
|
||||||
|
|||||||
@@ -1,24 +1,12 @@
|
|||||||
"""Tests for topic keyword matching (src/topic_analyzer.py)."""
|
"""Tests for topic keyword matching (src/topic_analyzer.py)."""
|
||||||
import sys
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from tests.helpers.import_state import clear_fake_database_modules
|
||||||
|
|
||||||
def _drop_fake_core_database():
|
clear_fake_database_modules()
|
||||||
parent = sys.modules.get("core")
|
|
||||||
attr = getattr(parent, "database", None) if parent is not None else None
|
|
||||||
mod = sys.modules.get("core.database") or attr
|
|
||||||
if mod is None or isinstance(getattr(mod, "__file__", None), str):
|
|
||||||
return
|
|
||||||
sys.modules.pop("core.database", None)
|
|
||||||
sys.modules.pop("src.database", None)
|
|
||||||
if parent is not None and attr is mod:
|
|
||||||
delattr(parent, "database")
|
|
||||||
|
|
||||||
|
|
||||||
_drop_fake_core_database()
|
|
||||||
|
|
||||||
from core.database import Base, Session as DbSession, ChatMessage as DbChatMessage
|
from core.database import Base, Session as DbSession, ChatMessage as DbChatMessage
|
||||||
from core.session_manager import SessionManager
|
from core.session_manager import SessionManager
|
||||||
|
|||||||
Reference in New Issue
Block a user