"""Test db.adapters.base.""" from __future__ import annotations from types import ModuleType import pytest from sqlalchemy.exc import UnboundExecutionError from sqlalchemy.orm import Session as SASession, sessionmaker import abacus_common_logic.db.adapters.base as mod @pytest.fixture(autouse=True) def _isolate_adapter_loader_state(): mod.clear_adapter_override() mod.clear_adapter_cache() yield mod.clear_adapter_override() mod.clear_adapter_cache() # Helpers class _Counter: def __init__(self): self.count = 0 def inc(self): self.count += 1 def make_dummy_module( dialect_name: str, class_name: str, *, ctor_counter: _Counter | None = None, declared_name_override: str | None = None, ) -> ModuleType: """Create a ModuleType containing an adapter class. Increments ctor_counter on instantiation. """ m = ModuleType(f'dummy_{dialect_name}') class DialectObj: def __init__(self, name: str): self.name = name declared = declared_name_override or dialect_name class AdapterClass: def __init__(self): if ctor_counter is not None: ctor_counter.inc() self.dialect = DialectObj(declared) setattr(m, class_name, AdapterClass) return m # is_dialect def test_is_dialect_true(monkeypatch): """Test is_dialect with valid dialects.""" monkeypatch.setattr( mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter', 'sqlite': 'dummy:SQLiteAdapter'}, ) assert mod.is_dialect('mysql') is True assert mod.is_dialect('sqlite') is True def test_is_dialect_false(monkeypatch): """Test is_dialect with invalid dialects.""" monkeypatch.setattr(mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter'}) assert mod.is_dialect('foo') is False assert mod.is_dialect('bar') is False # get_dialect def test_get_dialect_from_string(monkeypatch): """Test get_dialect from string.""" monkeypatch.setattr( mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter', 'sqlite': 'dummy:SQLiteAdapter'}, ) assert mod.get_dialect('mysql') == 'mysql' assert mod.get_dialect('sqlite') == 'sqlite' # type: ignore def test_get_dialect_from_hasdialect_object(monkeypatch): """Test get_dialect from object with dialect.name.""" monkeypatch.setattr(mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter'}) class HasDialectObj: def __init__(self): self.dialect = type('D', (), {'name': 'mysql'})() assert mod.get_dialect(HasDialectObj()) == 'mysql' # type: ignore def test_get_dialect_unbound_session_raises(monkeypatch): """Test get_dialect with an unbound session (no engine/connection).""" SessionLocal = sessionmaker() # no bind sess: SASession = SessionLocal() with pytest.raises(UnboundExecutionError): mod.get_dialect(sess) def test_get_dialect_unknown_target_type_raises(): """Test get_dialect raises error.""" class Weird: pass with pytest.raises(TypeError, match='Unknown target type'): mod.get_dialect(Weird()) # type: ignore # get_adapter def test_load_adapter_success_and_cache(monkeypatch): """Test adapter cache.""" ctor_counter = _Counter() dummy_mod = make_dummy_module('mysql', 'MySQLAdapter', ctor_counter=ctor_counter) monkeypatch.setattr(mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter'}) monkeypatch.setattr(mod, '_import_module', lambda _: dummy_mod) # lru_cache should enforce the same instance mod.clear_adapter_cache() a1 = mod.get_adapter('mysql') a2 = mod.get_adapter('mysql') assert a1 is a2 assert ctor_counter.count == 1 # Clearing cache should force a new instance mod.clear_adapter_cache() a3 = mod.get_adapter('mysql') assert a3 is not a1 assert ctor_counter.count == 2 def test_set_and_clear_adapter_override(monkeypatch): """Test clear_adapter_override clears the override.""" ctor_counter = _Counter() dummy_mod = make_dummy_module('mysql', 'MySQLAdapter', ctor_counter=ctor_counter) monkeypatch.setattr(mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter'}) monkeypatch.setattr(mod, '_import_module', lambda p: dummy_mod) mod.clear_adapter_cache() mod.clear_adapter_override() class Override: def __init__(self): self.dialect = type('D', (), {'name': 'mysql'})() override = Override() mod.set_adapter_override('mysql', override) # type: ignore # Should return override, not the dummy assert mod.get_adapter('mysql') is override assert ctor_counter.count == 0 # Not loaded since override # Clearing override should fall back to loader mod.clear_adapter_override('mysql') assert mod.get_adapter('mysql') is not override assert ctor_counter.count == 1 # loaded once after clearing override # get_adapter errors def test_load_adapter_unknown_dialect_raises(): """Test _load_adapter raises error when invalid dialect.""" with pytest.raises(TypeError, match=r"Unknown dialect 'bogus'.*Supported:"): mod.get_adapter('bogus') # type: ignore def test_load_adapter_import_failure_raises(monkeypatch): """Test _load_adapter raises error when module is missing.""" monkeypatch.setattr(mod, '_ADAPTERS', {'mysql': 'badmod:MySQLAdapter'}) def boom(_): raise RuntimeError('kapow') monkeypatch.setattr(mod, '_import_module', boom) error_msg = "Failed to import adapter module 'badmod' for 'mysql'" with pytest.raises(RuntimeError, match=error_msg): mod.get_adapter('mysql') def test_load_adapter_missing_class_raises(monkeypatch): """Test _load_adapter raises error when class is missing.""" monkeypatch.setattr(mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter'}) dummy_mod = ModuleType('dummy') monkeypatch.setattr(mod, '_import_module', lambda _: dummy_mod) error_msg = "Adapter class 'MySQLAdapter' not found in 'dummy' for 'mysql'" with pytest.raises(RuntimeError, match=error_msg): mod.get_adapter('mysql') def test_load_adapter_declared_dialect_mismatch_raises(monkeypatch): """Test _load_adapter raises error when dialect mismatch.""" # Adapter says its dialect.name is 'sqlite' but key is 'mysql' -> should raise monkeypatch.setattr(mod, '_ADAPTERS', {'mysql': 'dummy:MySQLAdapter'}) dummy_mod = make_dummy_module( 'mysql', 'MySQLAdapter', declared_name_override='sqlite' ) monkeypatch.setattr(mod, '_import_module', lambda p: dummy_mod) error_msg = r"declares dialect\.name='sqlite'.*expected 'mysql'" with pytest.raises(RuntimeError, match=error_msg): mod.get_adapter('mysql')