diff options
Diffstat (limited to 'oslo_db/tests/sqlalchemy/test_handle_error.py')
-rw-r--r-- | oslo_db/tests/sqlalchemy/test_handle_error.py | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/oslo_db/tests/sqlalchemy/test_handle_error.py b/oslo_db/tests/sqlalchemy/test_handle_error.py new file mode 100644 index 0000000..83322ef --- /dev/null +++ b/oslo_db/tests/sqlalchemy/test_handle_error.py @@ -0,0 +1,194 @@ +# 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. + +"""Test the compatibility layer for the handle_error() event. + +This event is added as of SQLAlchemy 0.9.7; oslo_db provides a compatibility +layer for prior SQLAlchemy versions. + +""" + +import mock +from oslotest import base as test_base +import sqlalchemy as sqla +from sqlalchemy.sql import column +from sqlalchemy.sql import literal +from sqlalchemy.sql import select +from sqlalchemy.types import Integer +from sqlalchemy.types import TypeDecorator + +from oslo_db.sqlalchemy.compat import handle_error +from oslo_db.sqlalchemy.compat import utils +from oslo_db.tests import utils as test_utils + + +class MyException(Exception): + pass + + +class ExceptionReraiseTest(test_base.BaseTestCase): + + def setUp(self): + super(ExceptionReraiseTest, self).setUp() + + self.engine = engine = sqla.create_engine("sqlite://") + self.addCleanup(engine.dispose) + + def _fixture(self): + engine = self.engine + + def err(context): + if "ERROR ONE" in str(context.statement): + raise MyException("my exception") + handle_error(engine, err) + + def test_exception_event_altered(self): + self._fixture() + + with mock.patch.object(self.engine.dialect.execution_ctx_cls, + "handle_dbapi_exception") as patched: + + matchee = self.assertRaises( + MyException, + self.engine.execute, "SELECT 'ERROR ONE' FROM I_DONT_EXIST" + ) + self.assertEqual(1, patched.call_count) + self.assertEqual("my exception", matchee.args[0]) + + def test_exception_event_non_altered(self): + self._fixture() + + with mock.patch.object(self.engine.dialect.execution_ctx_cls, + "handle_dbapi_exception") as patched: + + self.assertRaises( + sqla.exc.DBAPIError, + self.engine.execute, "SELECT 'ERROR TWO' FROM I_DONT_EXIST" + ) + self.assertEqual(1, patched.call_count) + + def test_is_disconnect_not_interrupted(self): + self._fixture() + + with test_utils.nested( + mock.patch.object( + self.engine.dialect.execution_ctx_cls, + "handle_dbapi_exception" + ), + mock.patch.object( + self.engine.dialect, "is_disconnect", + lambda *args: True + ) + ) as (handle_dbapi_exception, is_disconnect): + with self.engine.connect() as conn: + self.assertRaises( + MyException, + conn.execute, "SELECT 'ERROR ONE' FROM I_DONT_EXIST" + ) + self.assertEqual(1, handle_dbapi_exception.call_count) + self.assertTrue(conn.invalidated) + + def test_no_is_disconnect_not_invalidated(self): + self._fixture() + + with test_utils.nested( + mock.patch.object( + self.engine.dialect.execution_ctx_cls, + "handle_dbapi_exception" + ), + mock.patch.object( + self.engine.dialect, "is_disconnect", + lambda *args: False + ) + ) as (handle_dbapi_exception, is_disconnect): + with self.engine.connect() as conn: + self.assertRaises( + MyException, + conn.execute, "SELECT 'ERROR ONE' FROM I_DONT_EXIST" + ) + self.assertEqual(1, handle_dbapi_exception.call_count) + self.assertFalse(conn.invalidated) + + def test_exception_event_ad_hoc_context(self): + engine = self.engine + + nope = MyException("nope") + + class MyType(TypeDecorator): + impl = Integer + + def process_bind_param(self, value, dialect): + raise nope + + listener = mock.Mock(return_value=None) + handle_error(engine, listener) + + self.assertRaises( + sqla.exc.StatementError, + engine.execute, + select([1]).where(column('foo') == literal('bar', MyType)) + ) + + ctx = listener.mock_calls[0][1][0] + self.assertTrue(ctx.statement.startswith("SELECT 1 ")) + self.assertIs(ctx.is_disconnect, False) + self.assertIs(ctx.original_exception, nope) + + def _test_alter_disconnect(self, orig_error, evt_value): + engine = self.engine + + def evt(ctx): + ctx.is_disconnect = evt_value + handle_error(engine, evt) + + # if we are under sqla 0.9.7, and we are expecting to take + # an "is disconnect" exception and make it not a disconnect, + # that isn't supported b.c. the wrapped handler has already + # done the invalidation. + expect_failure = not utils.sqla_097 and orig_error and not evt_value + + with mock.patch.object(engine.dialect, + "is_disconnect", + mock.Mock(return_value=orig_error)): + + with engine.connect() as c: + conn_rec = c.connection._connection_record + try: + c.execute("SELECT x FROM nonexistent") + assert False + except sqla.exc.StatementError as st: + self.assertFalse(expect_failure) + + # check the exception's invalidation flag + self.assertEqual(st.connection_invalidated, evt_value) + + # check the Connection object's invalidation flag + self.assertEqual(c.invalidated, evt_value) + + # this is the ConnectionRecord object; it's invalidated + # when its .connection member is None + self.assertEqual(conn_rec.connection is None, evt_value) + + except NotImplementedError as ne: + self.assertTrue(expect_failure) + self.assertEqual( + str(ne), + "Can't reset 'disconnect' status of exception once it " + "is set with this version of SQLAlchemy") + + def test_alter_disconnect_to_true(self): + self._test_alter_disconnect(False, True) + self._test_alter_disconnect(True, True) + + def test_alter_disconnect_to_false(self): + self._test_alter_disconnect(True, False) + self._test_alter_disconnect(False, False) |