oslo.db/oslo_db/tests/sqlalchemy/test_enginefacade.py

1817 lines
59 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import contextlib
import copy
import pickle
import warnings
import mock
from oslo_config import cfg
from oslo_context import context as oslo_context
from oslotest import base as oslo_test_base
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy.orm import mapper
from sqlalchemy import select
from sqlalchemy import String
from sqlalchemy import Table
from oslo_db import exception
from oslo_db import options
from oslo_db.sqlalchemy import enginefacade
from oslo_db.sqlalchemy import engines as oslo_engines
from oslo_db.sqlalchemy import orm
from oslo_db.sqlalchemy import test_base
enginefacade.transaction_context_provider(oslo_context.RequestContext)
class SingletonOnName(mock.MagicMock):
def __init__(self, the_name, **kw):
super(SingletonOnName, self).__init__(
__eq__=lambda self, other: other._assert_name == self._assert_name,
_assert_name=the_name,
**kw
)
def __deepcopy__(self, memo):
return self
class SingletonConnection(SingletonOnName):
def __init__(self, **kw):
super(SingletonConnection, self).__init__(
"connection", **kw)
class SingletonEngine(SingletonOnName):
def __init__(self, connection, **kw):
super(SingletonEngine, self).__init__(
"engine",
connect=mock.Mock(return_value=connection),
url=connection,
_assert_connection=connection,
**kw
)
class NonDecoratedContext(object):
"""a Context object that's not run through transaction_context_provider."""
class AssertDataSource(collections.namedtuple(
"AssertDataSource", ["writer", "reader", "async_reader"])):
def element_for_writer(self, const):
if const is enginefacade._WRITER:
return self.writer
elif const is enginefacade._READER:
return self.reader
elif const is enginefacade._ASYNC_READER:
return self.async_reader
else:
assert False, "Unknown constant: %s" % const
class MockFacadeTest(oslo_test_base.BaseTestCase):
"""test by applying mocks to internal call-points.
This applies mocks to
oslo.db.sqlalchemy.engines.create_engine() and
oslo.db.sqlalchemy.orm.get_maker(), then mocking a
_TransactionFactory into
oslo.db.sqlalchemy.enginefacade._context_manager._root_factory.
Various scenarios are run against the enginefacade functions, and the
exact calls made against the mock create_engine(), get_maker(), and
associated objects are tested exactly against expected calls.
"""
synchronous_reader = True
engine_uri = 'some_connection'
slave_uri = None
def setUp(self):
super(MockFacadeTest, self).setUp()
writer_conn = SingletonConnection()
writer_engine = SingletonEngine(writer_conn)
writer_session = mock.Mock(
connection=mock.Mock(return_value=writer_conn))
writer_maker = mock.Mock(return_value=writer_session)
if self.slave_uri:
async_reader_conn = SingletonConnection()
async_reader_engine = SingletonEngine(async_reader_conn)
async_reader_session = mock.Mock(
connection=mock.Mock(return_value=async_reader_conn))
async_reader_maker = mock.Mock(return_value=async_reader_session)
else:
async_reader_conn = writer_conn
async_reader_engine = writer_engine
async_reader_session = writer_session
async_reader_maker = writer_maker
if self.synchronous_reader:
reader_conn = async_reader_conn
reader_engine = async_reader_engine
reader_session = async_reader_session
reader_maker = async_reader_maker
else:
reader_conn = writer_conn
reader_engine = writer_engine
reader_session = writer_session
reader_maker = writer_maker
self.connections = AssertDataSource(
writer_conn, reader_conn, async_reader_conn
)
self.engines = AssertDataSource(
writer_engine, reader_engine, async_reader_engine
)
self.sessions = AssertDataSource(
writer_session, reader_session, async_reader_session
)
self.makers = AssertDataSource(
writer_maker, reader_maker, async_reader_maker
)
def get_maker(engine, **kw):
if engine is writer_engine:
return self.makers.writer
elif engine is reader_engine:
return self.makers.reader
elif engine is async_reader_engine:
return self.makers.async_reader
else:
assert False
session_patch = mock.patch.object(
orm, "get_maker",
side_effect=get_maker)
self.get_maker = session_patch.start()
self.addCleanup(session_patch.stop)
def create_engine(sql_connection, **kw):
if sql_connection == self.engine_uri:
return self.engines.writer
elif sql_connection == self.slave_uri:
return self.engines.async_reader
else:
assert False
engine_patch = mock.patch.object(
oslo_engines, "create_engine", side_effect=create_engine)
self.create_engine = engine_patch.start()
self.addCleanup(engine_patch.stop)
self.factory = enginefacade._TransactionFactory()
self.factory.configure(
synchronous_reader=self.synchronous_reader
)
self.factory.configure(
connection=self.engine_uri,
slave_connection=self.slave_uri
)
facade_patcher = mock.patch.object(
enginefacade._context_manager, "_root_factory", self.factory)
facade_patcher.start()
self.addCleanup(facade_patcher.stop)
def _assert_ctx_connection(self, context, connection):
self.assertIs(context.connection, connection)
def _assert_ctx_session(self, context, session):
self.assertIs(context.session, session)
def _assert_non_decorated_ctx_connection(self, context, connection):
transaction_ctx = enginefacade._transaction_ctx_for_context(context)
self.assertIs(transaction_ctx.connection, connection)
def _assert_non_decorated_ctx_session(self, context, session):
transaction_ctx = enginefacade._transaction_ctx_for_context(context)
self.assertIs(transaction_ctx.session, session)
@contextlib.contextmanager
def _assert_engines(self):
"""produce a mock series of engine calls.
These are expected to match engine-related calls established
by the test subject.
"""
writer_conn = SingletonConnection()
writer_engine = SingletonEngine(writer_conn)
if self.slave_uri:
async_reader_conn = SingletonConnection()
async_reader_engine = SingletonEngine(async_reader_conn)
else:
async_reader_conn = writer_conn
async_reader_engine = writer_engine
if self.synchronous_reader:
reader_engine = async_reader_engine
else:
reader_engine = writer_engine
engines = AssertDataSource(
writer_engine, reader_engine, async_reader_engine)
def create_engine(sql_connection, **kw):
if sql_connection == self.engine_uri:
return engines.writer
elif sql_connection == self.slave_uri:
return engines.async_reader
else:
assert False
engine_factory = mock.Mock(side_effect=create_engine)
engine_factory(
sql_connection=self.engine_uri,
**dict((k, mock.ANY) for k in self.factory._engine_cfg.keys())
)
if self.slave_uri:
engine_factory(
sql_connection=self.slave_uri,
**dict((k, mock.ANY) for k in self.factory._engine_cfg.keys())
)
yield AssertDataSource(
writer_engine, reader_engine, async_reader_engine
)
self.assertEqual(
engine_factory.mock_calls,
self.create_engine.mock_calls
)
for sym in [
enginefacade._WRITER, enginefacade._READER,
enginefacade._ASYNC_READER
]:
self.assertEqual(
engines.element_for_writer(sym).mock_calls,
self.engines.element_for_writer(sym).mock_calls
)
def _assert_async_reader_connection(self, engines, session=None):
return self._assert_connection(
engines, enginefacade._ASYNC_READER, session)
def _assert_reader_connection(self, engines, session=None):
return self._assert_connection(engines, enginefacade._READER, session)
def _assert_writer_connection(self, engines, session=None):
return self._assert_connection(engines, enginefacade._WRITER, session)
@contextlib.contextmanager
def _assert_connection(self, engines, writer, session=None):
"""produce a mock series of connection calls.
These are expected to match connection-related calls established
by the test subject.
"""
if session:
connection = session.connection()
yield connection
else:
connection = engines.element_for_writer(writer).connect()
trans = connection.begin()
yield connection
if writer is enginefacade._WRITER:
trans.commit()
else:
trans.rollback()
connection.close()
self.assertEqual(
connection.mock_calls,
self.connections.element_for_writer(writer).mock_calls)
@contextlib.contextmanager
def _assert_makers(self, engines):
writer_session = mock.Mock(connection=mock.Mock(
return_value=engines.writer._assert_connection)
)
writer_maker = mock.Mock(return_value=writer_session)
if self.slave_uri:
async_reader_session = mock.Mock(connection=mock.Mock(
return_value=engines.async_reader._assert_connection)
)
async_reader_maker = mock.Mock(return_value=async_reader_session)
else:
async_reader_session = writer_session
async_reader_maker = writer_maker
if self.synchronous_reader:
reader_maker = async_reader_maker
else:
reader_maker = writer_maker
makers = AssertDataSource(
writer_maker,
reader_maker,
async_reader_maker,
)
def get_maker(engine, **kw):
if engine is engines.writer:
return makers.writer
elif engine is engines.reader:
return makers.reader
elif engine is engines.async_reader:
return makers.async_reader
else:
assert False
maker_factories = mock.Mock(side_effect=get_maker)
maker_factories(
autocommit=True, engine=engines.writer,
expire_on_commit=False)
if self.slave_uri:
maker_factories(
autocommit=True, engine=engines.async_reader,
expire_on_commit=False)
yield makers
self.assertEqual(
maker_factories.mock_calls,
self.get_maker.mock_calls)
for sym in [
enginefacade._WRITER, enginefacade._READER,
enginefacade._ASYNC_READER
]:
self.assertEqual(
makers.element_for_writer(sym).mock_calls,
self.makers.element_for_writer(sym).mock_calls)
def _assert_async_reader_session(
self, makers, connection=None, assert_calls=True):
return self._assert_session(
makers, enginefacade._ASYNC_READER, connection, assert_calls)
def _assert_reader_session(
self, makers, connection=None, assert_calls=True):
return self._assert_session(
makers, enginefacade._READER,
connection, assert_calls)
def _assert_writer_session(
self, makers, connection=None, assert_calls=True):
return self._assert_session(
makers, enginefacade._WRITER,
connection, assert_calls)
@contextlib.contextmanager
def _assert_session(
self, makers, writer, connection=None, assert_calls=True):
"""produce a mock series of session calls.
These are expected to match session-related calls established
by the test subject.
"""
if connection:
session = makers.element_for_writer(writer)(bind=connection)
else:
session = makers.element_for_writer(writer)()
session.begin()
yield session
if writer is enginefacade._WRITER:
session.commit()
elif enginefacade.\
_context_manager._factory._transaction_ctx_cfg[
'rollback_reader_sessions']:
session.rollback()
session.close()
if assert_calls:
self.assertEqual(
session.mock_calls,
self.sessions.element_for_writer(writer).mock_calls)
def test_session_reader_decorator(self):
context = oslo_context.RequestContext()
@enginefacade.reader
def go(context):
context.session.execute("test")
go(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(makers) as session:
session.execute("test")
def test_connection_reader_decorator(self):
context = oslo_context.RequestContext()
@enginefacade.reader.connection
def go(context):
context.connection.execute("test")
go(context)
with self._assert_engines() as engines:
with self._assert_reader_connection(engines) as connection:
connection.execute("test")
def test_session_reader_nested_in_connection_reader(self):
context = oslo_context.RequestContext()
@enginefacade.reader.connection
def go1(context):
context.connection.execute("test1")
go2(context)
@enginefacade.reader
def go2(context):
context.session.execute("test2")
go1(context)
with self._assert_engines() as engines:
with self._assert_reader_connection(engines) as connection:
connection.execute("test1")
with self._assert_makers(engines) as makers:
with self._assert_reader_session(
makers, connection) as session:
session.execute("test2")
def test_connection_reader_nested_in_session_reader(self):
context = oslo_context.RequestContext()
@enginefacade.reader
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.reader.connection
def go2(context):
context.connection.execute("test2")
go1(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(makers) as session:
session.execute("test1")
with self._assert_reader_connection(
engines, session) as connection:
connection.execute("test2")
def test_session_reader_decorator_nested(self):
context = oslo_context.RequestContext()
@enginefacade.reader
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.reader
def go2(context):
context.session.execute("test2")
go1(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(makers) as session:
session.execute("test1")
session.execute("test2")
def test_reader_nested_in_writer_ok(self):
context = oslo_context.RequestContext()
@enginefacade.writer
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.reader
def go2(context):
context.session.execute("test2")
go1(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_writer_session(makers) as session:
session.execute("test1")
session.execute("test2")
def test_writer_nested_in_reader_raises(self):
context = oslo_context.RequestContext()
@enginefacade.reader
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.writer
def go2(context):
context.session.execute("test2")
exc = self.assertRaises(
TypeError, go1, context
)
self.assertEqual(
"Can't upgrade a READER "
"transaction to a WRITER mid-transaction",
exc.args[0]
)
def test_async_on_writer_raises(self):
exc = self.assertRaises(
TypeError, getattr, enginefacade.writer, "async"
)
self.assertEqual(
"Setting async on a WRITER makes no sense",
exc.args[0]
)
def test_savepoint_and_independent_raises(self):
exc = self.assertRaises(
TypeError, getattr, enginefacade.writer.independent, "savepoint"
)
self.assertEqual(
"setting savepoint and independent makes no sense.",
exc.args[0]
)
def test_reader_nested_in_async_reader_raises(self):
context = oslo_context.RequestContext()
@enginefacade.reader.async
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.reader
def go2(context):
context.session.execute("test2")
exc = self.assertRaises(
TypeError, go1, context
)
self.assertEqual(
"Can't upgrade an ASYNC_READER transaction "
"to a READER mid-transaction",
exc.args[0]
)
def test_reader_allow_async_nested_in_async_reader(self):
context = oslo_context.RequestContext()
@enginefacade.reader.async
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.reader.allow_async
def go2(context):
context.session.execute("test2")
go1(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_async_reader_session(makers) as session:
session.execute("test1")
session.execute("test2")
def test_reader_allow_async_nested_in_reader(self):
context = oslo_context.RequestContext()
@enginefacade.reader.reader
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.reader.allow_async
def go2(context):
context.session.execute("test2")
go1(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(makers) as session:
session.execute("test1")
session.execute("test2")
def test_reader_allow_async_is_reader_by_default(self):
context = oslo_context.RequestContext()
@enginefacade.reader.allow_async
def go1(context):
context.session.execute("test1")
go1(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(makers) as session:
session.execute("test1")
def test_writer_nested_in_async_reader_raises(self):
context = oslo_context.RequestContext()
@enginefacade.reader.async
def go1(context):
context.session.execute("test1")
go2(context)
@enginefacade.writer
def go2(context):
context.session.execute("test2")
exc = self.assertRaises(
TypeError, go1, context
)
self.assertEqual(
"Can't upgrade an ASYNC_READER transaction to a "
"WRITER mid-transaction",
exc.args[0]
)
def test_reader_then_writer_ok(self):
context = oslo_context.RequestContext()
@enginefacade.reader
def go1(context):
context.session.execute("test1")
@enginefacade.writer
def go2(context):
context.session.execute("test2")
go1(context)
go2(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(
makers, assert_calls=False) as session:
session.execute("test1")
with self._assert_writer_session(makers) as session:
session.execute("test2")
def test_async_reader_then_reader_ok(self):
context = oslo_context.RequestContext()
@enginefacade.reader.async
def go1(context):
context.session.execute("test1")
@enginefacade.reader
def go2(context):
context.session.execute("test2")
go1(context)
go2(context)
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_async_reader_session(
makers, assert_calls=False) as session:
session.execute("test1")
with self._assert_reader_session(makers) as session:
session.execute("test2")
def test_using_reader(self):
context = oslo_context.RequestContext()
with enginefacade.reader.using(context) as session:
self._assert_ctx_session(context, session)
session.execute("test1")
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(makers) as session:
session.execute("test1")
def test_using_reader_rollback_reader_session(self):
enginefacade.configure(rollback_reader_sessions=True)
context = oslo_context.RequestContext()
with enginefacade.reader.using(context) as session:
self._assert_ctx_session(context, session)
session.execute("test1")
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_reader_session(makers) as session:
session.execute("test1")
def test_using_writer(self):
context = oslo_context.RequestContext()
with enginefacade.writer.using(context) as session:
self._assert_ctx_session(context, session)
session.execute("test1")
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_writer_session(makers) as session:
session.execute("test1")
def test_using_writer_no_descriptors(self):
context = NonDecoratedContext()
with enginefacade.writer.using(context) as session:
self._assert_non_decorated_ctx_session(context, session)
session.execute("test1")
with self._assert_engines() as engines:
with self._assert_makers(engines) as makers:
with self._assert_writer_session(makers) as session:
session.execute("test1")
def test_using_writer_connection_no_descriptors(self):
context = NonDecoratedContext()
with enginefacade.writer.connection.using(context) as connection:
self._assert_non_decorated_ctx_connection(context, connection)
connection.execute("test1")
with self._assert_engines() as engines:
with self._assert_writer_connection(engines) as conn:
conn.execute("test1")
def test_using_reader_connection(self):
context = oslo_context.RequestContext()
with enginefacade.reader.connection.using(context) as connection:
self._assert_ctx_connection(context, connection)
connection.execute("test1")
with self._assert_engines() as engines:
with self._assert_reader_connection(engines) as conn:
conn.execute("test1")
def test_using_writer_connection(self):
context = oslo_context.RequestContext()
with enginefacade.writer.connection.using(context) as connection:
self._assert_ctx_connection(context, connection)
connection.execute("test1")
with self._assert_engines() as engines:
with self._assert_writer_connection(engines) as conn:
conn.execute("test1")
def test_context_copied_using_existing_writer_connection(self):
context = oslo_context.RequestContext()
with enginefacade.writer.connection.using(context) as connection:
self._assert_ctx_connection(context, connection)
connection.execute("test1")
ctx2 = copy.deepcopy(context)
with enginefacade.reader.connection.using(ctx2) as conn2:
self.assertIs(conn2, connection)
self._assert_ctx_connection(ctx2, conn2)
conn2.execute("test2")
with self._assert_engines() as engines:
with self._assert_writer_connection(engines) as conn:
conn.execute("test1")
conn.execute("test2")
def test_context_nodesc_copied_using_existing_writer_connection(self):
context = NonDecoratedContext()
with enginefacade.writer.connection.using(context) as connection:
self._assert_non_decorated_ctx_connection(context, connection)
connection.execute("test1")
ctx2 = copy.deepcopy(context)
with enginefacade.reader.connection.using(ctx2) as conn2:
self.assertIs(conn2, connection)
self._assert_non_decorated_ctx_connection(ctx2, conn2)
conn2.execute("test2")
with self._assert_engines() as engines:
with self._assert_writer_connection(engines) as conn:
conn.execute("test1")
conn.execute("test2")
def test_session_context_notrequested_exception(self):
context = oslo_context.RequestContext()
with enginefacade.reader.connection.using(context):
exc = self.assertRaises(
exception.ContextNotRequestedError,
getattr, context, 'session'
)
self.assertRegexpMatches(
exc.args[0],
"The 'session' context attribute was requested but it has "
"not been established for this context."
)
def test_connection_context_notrequested_exception(self):
context = oslo_context.RequestContext()
with enginefacade.reader.using(context):
exc = self.assertRaises(
exception.ContextNotRequestedError,
getattr, context, 'connection'
)
self.assertRegexpMatches(
exc.args[0],
"The 'connection' context attribute was requested but it has "
"not been established for this context."
)
def test_session_context_exception(self):
context = oslo_context.RequestContext()
exc = self.assertRaises(
exception.NoEngineContextEstablished,
getattr, context, 'session'
)
self.assertRegexpMatches(
exc.args[0],
"No TransactionContext is established for "
"this .*RequestContext.* object within the current "
"thread; the 'session' attribute is unavailable."
)
def test_session_context_getattr(self):
context = oslo_context.RequestContext()
self.assertIsNone(getattr(context, 'session', None))
def test_connection_context_exception(self):
context = oslo_context.RequestContext()
exc = self.assertRaises(
exception.NoEngineContextEstablished,
getattr, context, 'connection'
)
self.assertRegexpMatches(
exc.args[0],
"No TransactionContext is established for "
"this .*RequestContext.* object within the current "
"thread; the 'connection' attribute is unavailable."
)
def test_connection_context_getattr(self):
context = oslo_context.RequestContext()
self.assertIsNone(getattr(context, 'connection', None))
def test_transaction_context_exception(self):
context = oslo_context.RequestContext()
exc = self.assertRaises(
exception.NoEngineContextEstablished,
getattr, context, 'transaction'
)
self.assertRegexpMatches(
exc.args[0],
"No TransactionContext is established for "
"this .*RequestContext.* object within the current "
"thread; the 'transaction' attribute is unavailable."
)
def test_transaction_context_getattr(self):
context = oslo_context.RequestContext()
self.assertIsNone(getattr(context, 'transaction', None))
def test_trans_ctx_context_exception(self):
context = oslo_context.RequestContext()
exc = self.assertRaises(
exception.NoEngineContextEstablished,
getattr, context, 'transaction_ctx'
)
self.assertRegexpMatches(
exc.args[0],
"No TransactionContext is established for "
"this .*RequestContext.* object within the current "
"thread."
)
def test_trans_ctx_context_getattr(self):
context = oslo_context.RequestContext()
self.assertIsNone(getattr(context, 'transaction_ctx', None))
def test_multiple_factories(self):
"""Test that the instrumentation applied to a context class is
independent of a specific _TransactionContextManager
/ _TransactionFactory.
"""
mgr1 = enginefacade.transaction_context()
mgr1.configure(
connection=self.engine_uri,
slave_connection=self.slave_uri
)
mgr2 = enginefacade.transaction_context()
mgr2.configure(
connection=self.engine_uri,
slave_connection=self.slave_uri
)
context = oslo_context.RequestContext()
self.assertRaises(
exception.NoEngineContextEstablished,
getattr, context, 'session'
)
with mgr1.writer.using(context):
self.assertIs(context.transaction_ctx.factory, mgr1._factory)
self.assertIsNot(context.transaction_ctx.factory, mgr2._factory)
self.assertIsNotNone(context.session)
self.assertRaises(
exception.NoEngineContextEstablished,
getattr, context, 'session'
)
with mgr2.writer.using(context):
self.assertIsNot(context.transaction_ctx.factory, mgr1._factory)
self.assertIs(context.transaction_ctx.factory, mgr2._factory)
self.assertIsNotNone(context.session)
def test_multiple_factories_nested(self):
"""Test that the instrumentation applied to a context class supports
nested calls among multiple _TransactionContextManager objects.
"""
mgr1 = enginefacade.transaction_context()
mgr1.configure(
connection=self.engine_uri,
slave_connection=self.slave_uri
)
mgr2 = enginefacade.transaction_context()
mgr2.configure(
connection=self.engine_uri,
slave_connection=self.slave_uri
)
context = oslo_context.RequestContext()
with mgr1.writer.using(context):
self.assertIs(context.transaction_ctx.factory, mgr1._factory)
self.assertIsNot(context.transaction_ctx.factory, mgr2._factory)
with mgr2.writer.using(context):
self.assertIsNot(
context.transaction_ctx.factory, mgr1._factory)
self.assertIs(context.transaction_ctx.factory, mgr2._factory)
self.assertIsNotNone(context.session)
# mgr1 is restored
self.assertIs(context.transaction_ctx.factory, mgr1._factory)
self.assertIsNot(context.transaction_ctx.factory, mgr2._factory)
self.assertIsNotNone(context.session)
self.assertRaises(
exception.NoEngineContextEstablished,
getattr, context, 'transaction_ctx'
)
class SynchronousReaderWSlaveMockFacadeTest(MockFacadeTest):
synchronous_reader = True
engine_uri = 'some_connection'
slave_uri = 'some_slave_connection'
class AsyncReaderWSlaveMockFacadeTest(MockFacadeTest):
synchronous_reader = False
engine_uri = 'some_connection'
slave_uri = 'some_slave_connection'
class LegacyIntegrationtest(test_base.DbTestCase):
def test_legacy_integration(self):
legacy_facade = enginefacade.get_legacy_facade()
self.assertTrue(
legacy_facade.get_engine() is
enginefacade._context_manager._factory._writer_engine
)
self.assertTrue(
enginefacade.get_legacy_facade() is legacy_facade
)
def test_get_sessionmaker(self):
legacy_facade = enginefacade.get_legacy_facade()
self.assertTrue(
legacy_facade.get_sessionmaker() is
enginefacade._context_manager._factory._writer_maker
)
def test_legacy_facades_from_different_context_managers(self):
transaction_context1 = enginefacade.transaction_context()
transaction_context2 = enginefacade.transaction_context()
transaction_context1.configure(connection='sqlite:///?conn1')
transaction_context2.configure(connection='sqlite:///?conn2')
legacy1 = transaction_context1.get_legacy_facade()
legacy2 = transaction_context2.get_legacy_facade()
self.assertNotEqual(legacy1, legacy2)
def test_legacy_not_started(self):
factory = enginefacade._TransactionFactory()
self.assertRaises(
exception.CantStartEngineError,
factory.get_legacy_facade
)
legacy_facade = factory.get_legacy_facade()
self.assertRaises(
exception.CantStartEngineError,
legacy_facade.get_session
)
self.assertRaises(
exception.CantStartEngineError,
legacy_facade.get_session
)
self.assertRaises(
exception.CantStartEngineError,
legacy_facade.get_engine
)
class ThreadingTest(test_base.DbTestCase):
"""Test copy/pickle on new threads using real connections and sessions."""
def _assert_ctx_connection(self, context, connection):
self.assertIs(context.connection, connection)
def _assert_ctx_session(self, context, session):
self.assertIs(context.session, session)
def _patch_thread_ident(self):
self.ident = 1
test_instance = self
class MockThreadingLocal(object):
def __init__(self):
self.__dict__['state'] = collections.defaultdict(dict)
def __deepcopy__(self, memo):
return self
def __getattr__(self, key):
ns = self.state[test_instance.ident]
try:
return ns[key]
except KeyError:
raise AttributeError(key)
def __setattr__(self, key, value):
ns = self.state[test_instance.ident]
ns[key] = value
def __delattr__(self, key):
ns = self.state[test_instance.ident]
try:
del ns[key]
except KeyError:
raise AttributeError(key)
return mock.patch.object(
enginefacade, "_TransactionContextTLocal", MockThreadingLocal)
def test_thread_ctxmanager_writer(self):
context = oslo_context.RequestContext()
with self._patch_thread_ident():
with enginefacade.writer.using(context) as session:
self._assert_ctx_session(context, session)
self.ident = 2
with enginefacade.reader.using(context) as sess2:
# new session
self.assertIsNot(sess2, session)
# thread local shows the new session
self._assert_ctx_session(context, sess2)
self.ident = 1
with enginefacade.reader.using(context) as sess3:
self.assertIs(sess3, session)
self._assert_ctx_session(context, session)
def test_thread_ctxmanager_writer_connection(self):
context = oslo_context.RequestContext()
with self._patch_thread_ident():
with enginefacade.writer.connection.using(context) as conn:
self._assert_ctx_connection(context, conn)
self.ident = 2
with enginefacade.reader.connection.using(context) as conn2:
# new connection
self.assertIsNot(conn2, conn)
# thread local shows the new connection
self._assert_ctx_connection(context, conn2)
with enginefacade.reader.connection.using(
context) as conn3:
# we still get the right connection even though
# this context is not the "copied" context
self.assertIsNot(conn3, conn)
self.assertIs(conn3, conn2)
self.ident = 1
with enginefacade.reader.connection.using(context) as conn3:
self.assertIs(conn3, conn)
self._assert_ctx_connection(context, conn)
def test_thread_ctxmanager_switch_styles(self):
@enginefacade.writer.connection
def go_one(context):
self.assertRaises(
exception.ContextNotRequestedError,
getattr, context, "session"
)
self.assertIsNotNone(context.connection)
self.ident = 2
go_two(context)
self.ident = 1
self.assertRaises(
exception.ContextNotRequestedError,
getattr, context, "session"
)
self.assertIsNotNone(context.connection)
@enginefacade.reader
def go_two(context):
self.assertRaises(
exception.ContextNotRequestedError,
getattr, context, "connection"
)
self.assertIsNotNone(context.session)
context = oslo_context.RequestContext()
with self._patch_thread_ident():
go_one(context)
def test_thread_decorator_writer(self):
sessions = set()
@enginefacade.writer
def go_one(context):
sessions.add(context.session)
self.ident = 2
go_two(context)
self.ident = 1
go_three(context)
@enginefacade.reader
def go_two(context):
assert context.session not in sessions
@enginefacade.reader
def go_three(context):
assert context.session in sessions
context = oslo_context.RequestContext()
with self._patch_thread_ident():
go_one(context)
def test_thread_decorator_writer_connection(self):
connections = set()
@enginefacade.writer.connection
def go_one(context):
connections.add(context.connection)
self.ident = 2
go_two(context)
self.ident = 1
go_three(context)
@enginefacade.reader.connection
def go_two(context):
assert context.connection not in connections
@enginefacade.reader
def go_three(context):
assert context.connection in connections
context = oslo_context.RequestContext()
with self._patch_thread_ident():
go_one(context)
def test_contexts_picklable(self):
context = oslo_context.RequestContext()
with enginefacade.writer.using(context) as session:
self._assert_ctx_session(context, session)
pickled = pickle.dumps(context)
unpickled = pickle.loads(pickled)
with enginefacade.writer.using(unpickled) as session2:
self._assert_ctx_session(unpickled, session2)
assert session is not session2
class LiveFacadeTest(test_base.DbTestCase):
"""test using live SQL with test-provisioned databases.
Several of these tests require that multiple transactions run
simultaenously; as the default SQLite :memory: connection can't achieve
this, opportunistic test implementations against MySQL and PostgreSQL are
supplied.
"""
def setUp(self):
super(LiveFacadeTest, self).setUp()
metadata = MetaData()
user_table = Table(
'user', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(30)),
mysql_engine='InnoDB'
)
self.user_table = user_table
metadata.create_all(self.engine)
self.addCleanup(metadata.drop_all, self.engine)
class User(object):
def __init__(self, name):
self.name = name
mapper(User, user_table)
self.User = User
def _assert_ctx_connection(self, context, connection):
self.assertIs(context.connection, connection)
def _assert_ctx_session(self, context, session):
self.assertIs(context.session, session)
def test_transaction_committed(self):
context = oslo_context.RequestContext()
with enginefacade.writer.using(context) as session:
session.add(self.User(name="u1"))
session = self.sessionmaker(autocommit=True)
self.assertEqual(
"u1",
session.query(self.User.name).scalar()
)
def test_transaction_rollback(self):
context = oslo_context.RequestContext()
class MyException(Exception):
pass
@enginefacade.writer
def go(context):
context.session.add(self.User(name="u1"))
context.session.flush()
raise MyException("a test")
self.assertRaises(MyException, go, context)
session = self.sessionmaker(autocommit=True)
self.assertEqual(
None,
session.query(self.User.name).scalar()
)
def test_context_deepcopy_on_session(self):
context = oslo_context.RequestContext()
with enginefacade.writer.using(context) as session:
ctx2 = copy.deepcopy(context)
self._assert_ctx_session(ctx2, session)
with enginefacade.writer.using(ctx2) as s2:
self.assertIs(session, s2)
self._assert_ctx_session(ctx2, s2)
s2.add(self.User(name="u1"))
s2.flush()
session = self.sessionmaker(autocommit=True)
self.assertEqual(
"u1",
session.query(self.User.name).scalar()
)
def test_context_deepcopy_on_connection(self):
context = oslo_context.RequestContext()
with enginefacade.writer.connection.using(context) as conn:
ctx2 = copy.deepcopy(context)
self._assert_ctx_connection(ctx2, conn)
with enginefacade.writer.connection.using(ctx2) as conn2:
self.assertIs(conn, conn2)
self._assert_ctx_connection(ctx2, conn2)
conn2.execute(self.user_table.insert().values(name="u1"))
self._assert_ctx_connection(ctx2, conn2)
session = self.sessionmaker(autocommit=True)
self.assertEqual(
"u1",
session.query(self.User.name).scalar()
)
@test_base.backend_specific("postgresql", "mysql")
def test_external_session_transaction(self):
context = oslo_context.RequestContext()
with enginefacade.writer.using(context) as session:
session.add(self.User(name="u1"))
session.flush()
with enginefacade.writer.independent.using(context) as s2:
# transaction() uses a new session
self.assertIsNot(s2, session)
self._assert_ctx_session(context, s2)
# rows within a distinct transaction
s2.add(self.User(name="u2"))
# it also takes over the global enginefacade
# within the context
with enginefacade.writer.using(context) as s3:
self.assertIs(s3, s2)
s3.add(self.User(name="u3"))
self._assert_ctx_session(context, session)
# rollback the "outer" transaction
session.rollback()
# add more state on the "outer" transaction
session.begin()
session.add(self.User(name="u4"))
session = self.sessionmaker(autocommit=True)
# inner transction + second part of "outer" transaction were committed
self.assertEqual(
[("u2",), ("u3",), ("u4", )],
session.query(
self.User.name).order_by(self.User.name).all()
)
def test_savepoint_transaction_decorator(self):
context = oslo_context.RequestContext()
@enginefacade.writer
def go1(context):
session = context.session
session.add(self.User(name="u1"))
session.flush()
try:
go2(context)
except Exception:
pass
go3(context)
session.add(self.User(name="u4"))
@enginefacade.writer.savepoint
def go2(context):
session = context.session
session.add(self.User(name="u2"))
raise Exception("nope")
@enginefacade.writer.savepoint
def go3(context):
session = context.session
session.add(self.User(name="u3"))
go1(context)
session = self.sessionmaker(autocommit=True)
# inner transction + second part of "outer" transaction were committed
self.assertEqual(
[("u1",), ("u3",), ("u4", )],
session.query(
self.User.name).order_by(self.User.name).all()
)
def test_savepoint_transaction(self):
context = oslo_context.RequestContext()
with enginefacade.writer.using(context) as session:
session.add(self.User(name="u1"))
session.flush()
try:
with enginefacade.writer.savepoint.using(context) as session:
session.add(self.User(name="u2"))
raise Exception("nope")
except Exception:
pass
with enginefacade.writer.savepoint.using(context) as session:
session.add(self.User(name="u3"))
session.add(self.User(name="u4"))
session = self.sessionmaker(autocommit=True)
# inner transction + second part of "outer" transaction were committed
self.assertEqual(
[("u1",), ("u3",), ("u4", )],
session.query(
self.User.name).order_by(self.User.name).all()
)
@test_base.backend_specific("postgresql", "mysql")
def test_external_session_transaction_decorator(self):
context = oslo_context.RequestContext()
@enginefacade.writer
def go1(context):
session = context.session
session.add(self.User(name="u1"))
session.flush()
go2(context, session)
self._assert_ctx_session(context, session)
# rollback the "outer" transaction
session.rollback()
# add more state on the "outer" transaction
session.begin()
session.add(self.User(name="u4"))
@enginefacade.writer.independent
def go2(context, session):
s2 = context.session
# uses a new session
self.assertIsNot(s2, session)
self._assert_ctx_session(context, s2)
# rows within a distinct transaction
s2.add(self.User(name="u2"))
# it also takes over the global enginefacade
# within the context
with enginefacade.writer.using(context) as s3:
self.assertIs(s3, s2)
s3.add(self.User(name="u3"))
go1(context)
session = self.sessionmaker(autocommit=True)
# inner transction + second part of "outer" transaction were committed
self.assertEqual(
[("u2",), ("u3",), ("u4", )],
session.query(
self.User.name).order_by(self.User.name).all()
)
@test_base.backend_specific("postgresql", "mysql")
def test_external_connection_transaction(self):
context = oslo_context.RequestContext()
with enginefacade.writer.connection.using(context) as connection:
connection.execute(self.user_table.insert().values(name="u1"))
# transaction() uses a new Connection
with enginefacade.writer.independent.connection.\
using(context) as c2:
self.assertIsNot(c2, connection)
self._assert_ctx_connection(context, c2)
# rows within a distinct transaction
c2.execute(self.user_table.insert().values(name="u2"))
# it also takes over the global enginefacade
# within the context
with enginefacade.writer.connection.using(context) as c3:
self.assertIs(c2, c3)
c3.execute(self.user_table.insert().values(name="u3"))
self._assert_ctx_connection(context, connection)
# rollback the "outer" transaction
transaction_ctx = context.transaction_ctx
transaction_ctx.transaction.rollback()
transaction_ctx.transaction = connection.begin()
# add more state on the "outer" transaction
connection.execute(self.user_table.insert().values(name="u4"))
session = self.sessionmaker(autocommit=True)
self.assertEqual(
[("u2",), ("u3",), ("u4", )],
session.query(
self.User.name).order_by(self.User.name).all()
)
@test_base.backend_specific("postgresql", "mysql")
def test_external_writer_in_reader(self):
context = oslo_context.RequestContext()
with enginefacade.reader.using(context) as session:
ping = session.scalar(select([1]))
self.assertEqual(1, ping)
# we're definitely a reader
@enginefacade.writer
def go(ctx):
pass
exc = self.assertRaises(TypeError, go, context)
self.assertEqual(
"Can't upgrade a READER transaction to a "
"WRITER mid-transaction",
exc.args[0])
# but we can do a writer on a new transaction
with enginefacade.writer.independent.using(context) as sess2:
self.assertIsNot(sess2, session)
self._assert_ctx_session(context, sess2)
session.add(self.User(name="u1_nocommit"))
sess2.add(self.User(name="u1_commit"))
user = session.query(self.User).first()
self.assertEqual("u1_commit", user.name)
session = self.sessionmaker(autocommit=True)
self.assertEqual(
session.query(
self.User.name).order_by(self.User.name).all(),
[("u1_commit",)]
)
def test_replace_scope(self):
# "timeout" is an argument accepted by
# the pysqlite dialect, which we set here to ensure
# that even in an all-sqlite test, we test that the URL
# is different in the context we are looking for
alt_connection = "sqlite:///?timeout=90"
alt_mgr1 = enginefacade.transaction_context()
alt_mgr1.configure(
connection=alt_connection,
)
@enginefacade.writer
def go1(context):
s1 = context.session
self.assertEqual(
s1.bind.url,
enginefacade._context_manager._factory._writer_engine.url)
self.assertIs(
s1.bind,
enginefacade._context_manager._factory._writer_engine)
self.assertEqual(s1.bind.url, self.engine.url)
with alt_mgr1.replace.using(context):
go2(context)
go4(context)
@enginefacade.writer
def go2(context):
s2 = context.session
# factory is not replaced globally...
self.assertIsNot(
enginefacade._context_manager._factory._writer_engine,
alt_mgr1._factory._writer_engine
)
# but it is replaced for us
self.assertIs(s2.bind, alt_mgr1._factory._writer_engine)
self.assertEqual(
str(s2.bind.url), alt_connection)
go3(context)
@enginefacade.reader
def go3(context):
s3 = context.session
# in a call of a call, we still have the alt URL
self.assertIs(s3.bind, alt_mgr1._factory._writer_engine)
self.assertEqual(
str(s3.bind.url), alt_connection)
@enginefacade.writer
def go4(context):
s4 = context.session
# outside the "replace" context, all is back to normal
self.assertIs(s4.bind, self.engine)
self.assertEqual(
s4.bind.url, self.engine.url)
context = oslo_context.RequestContext()
go1(context)
self.assertIsNot(
enginefacade._context_manager._factory._writer_engine,
alt_mgr1._factory._writer_engine
)
def test_replace_scope_only_global_eng(self):
# "timeout" is an argument accepted by
# the pysqlite dialect, which we set here to ensure
# that even in an all-sqlite test, we test that the URL
# is different in the context we are looking for
alt_connection1 = "sqlite:///?timeout=90"
alt_mgr1 = enginefacade.transaction_context()
alt_mgr1.configure(
connection=alt_connection1,
)
alt_connection2 = "sqlite:///?timeout=120"
alt_mgr2 = enginefacade.transaction_context()
alt_mgr2.configure(
connection=alt_connection2,
)
@enginefacade.writer
def go1(context):
s1 = context.session
# global engine
self.assertEqual(s1.bind.url, self.engine.url)
# now replace global engine...
with alt_mgr1.replace.using(context):
go2(context)
# and back
go6(context)
@enginefacade.writer
def go2(context):
s2 = context.session
# we have the replace-the-global engine
self.assertEqual(str(s2.bind.url), alt_connection1)
self.assertIs(s2.bind, alt_mgr1._factory._writer_engine)
go3(context)
@alt_mgr2.writer
def go3(context):
s3 = context.session
# we don't use the global engine in the first place.
# make sure our own factory still used.
self.assertEqual(str(s3.bind.url), alt_connection2)
self.assertIs(s3.bind, alt_mgr2._factory._writer_engine)
go4(context)
@enginefacade.writer
def go4(context):
s4 = context.session
# we *do* use the global, so we still want the replacement.
self.assertEqual(str(s4.bind.url), alt_connection1)
self.assertIs(s4.bind, alt_mgr1._factory._writer_engine)
@enginefacade.writer
def go5(context):
s5 = context.session
# ...and here also
self.assertEqual(str(s5.bind.url), alt_connection1)
self.assertIs(s5.bind, alt_mgr1._factory._writer_engine)
@enginefacade.writer
def go6(context):
s6 = context.session
# ...but not here!
self.assertEqual(str(s6.bind.url), str(self.engine.url))
self.assertIs(s6.bind, self.engine)
context = oslo_context.RequestContext()
go1(context)
class MySQLLiveFacadeTest(
test_base.MySQLOpportunisticTestCase, LiveFacadeTest):
pass
class PGLiveFacadeTest(
test_base.PostgreSQLOpportunisticTestCase, LiveFacadeTest):
pass
class ConfigOptionsTest(oslo_test_base.BaseTestCase):
def test_all_options(self):
"""test that everything in CONF.database.iteritems() is accepted.
There's a handful of options in oslo.db.options that seem to have
no meaning, but need to be accepted. In particular, Cinder and
maybe others are doing exactly this call.
"""
factory = enginefacade._TransactionFactory()
cfg.CONF.register_opts(options.database_opts, 'database')
factory.configure(**dict(cfg.CONF.database.items()))
def test_options_not_supported(self):
factory = enginefacade._TransactionFactory()
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
factory.configure(fake1='x', idle_timeout=200, wrong2='y')
self.assertEqual(1, len(w))
self.assertTrue(
issubclass(w[-1].category, exception.NotSupportedWarning))
self.assertEqual(
"Configuration option(s) ['fake1', 'wrong2'] not supported",
str(w[-1].message)
)
def test_no_engine(self):
factory = enginefacade._TransactionFactory()
self.assertRaises(
exception.CantStartEngineError,
factory._create_session, enginefacade._WRITER
)
self.assertRaises(
exception.CantStartEngineError,
factory._create_session, enginefacade._WRITER
)
# TODO(zzzeek): test configuration options, e.g. like
# test_sqlalchemy->test_creation_from_config