diff --git a/oslo_db/sqlalchemy/provision.py b/oslo_db/sqlalchemy/provision.py index ff6ebc0..7a1eaee 100644 --- a/oslo_db/sqlalchemy/provision.py +++ b/oslo_db/sqlalchemy/provision.py @@ -157,7 +157,6 @@ class Backend(object): self.engine = None self.impl = BackendImpl.impl(database_type) self.current_dbs = set() - Backend.backends_by_database_type[database_type] = self @classmethod def backend_for_database_type(cls, database_type): @@ -167,7 +166,8 @@ class Backend(object): try: backend = cls.backends_by_database_type[database_type] except KeyError: - raise exception.BackendNotAvailable(database_type) + raise exception.BackendNotAvailable( + "Backend '%s' is unavailable: No such backend" % database_type) else: return backend._verify() @@ -197,14 +197,15 @@ class Backend(object): if not self.verified: try: eng = self._ensure_backend_available(self.url) - except exception.BackendNotAvailable: + except exception.BackendNotAvailable as bne: + self._no_engine_reason = str(bne) raise else: self.engine = eng finally: self.verified = True if self.engine is None: - raise exception.BackendNotAvailable(self.database_type) + raise exception.BackendNotAvailable(self._no_engine_reason) return self @classmethod @@ -219,7 +220,9 @@ class Backend(object): LOG.info( _LI("The %(dbapi)s backend is unavailable: %(err)s"), dict(dbapi=url.drivername, err=i_e)) - raise exception.BackendNotAvailable("No DBAPI installed") + raise exception.BackendNotAvailable( + "Backend '%s' is unavailable: No DBAPI installed" % + url.drivername) else: try: conn = eng.connect() @@ -231,7 +234,9 @@ class Backend(object): _LI("The %(dbapi)s backend is unavailable: %(err)s"), dict(dbapi=url.drivername, err=d_e) ) - raise exception.BackendNotAvailable("Could not connect") + raise exception.BackendNotAvailable( + "Backend '%s' is unavailable: Could not connect" % + url.drivername) else: conn.close() return eng @@ -312,7 +317,8 @@ class Backend(object): url = sa_url.make_url(url_str) m = re.match(r'([^+]+?)(?:\+(.+))?$', url.drivername) database_type = m.group(1) - Backend(database_type, url) + Backend.backends_by_database_type[database_type] = \ + Backend(database_type, url) @six.add_metaclass(abc.ABCMeta) diff --git a/oslo_db/sqlalchemy/test_base.py b/oslo_db/sqlalchemy/test_base.py index 40b9c30..f25d266 100644 --- a/oslo_db/sqlalchemy/test_base.py +++ b/oslo_db/sqlalchemy/test_base.py @@ -65,9 +65,10 @@ class DbFixture(fixtures.Fixture): testresources.tearDownResources, self.test, self.test.resources, testresources._get_result() ) - if not hasattr(self.test, 'db'): - msg = "backend '%s' unavailable" % self.DRIVER - if self.skip_on_unavailable_db: + + if not self.test._has_db_resource(): + msg = self.test._get_db_resource_not_available_reason() + if self.test.SKIP_ON_UNAVAILABLE_DB: self.test.skip(msg) else: self.test.fail(msg) @@ -98,9 +99,17 @@ class DbTestCase(test_base.BaseTestCase): SCHEMA_SCOPE = None SKIP_ON_UNAVAILABLE_DB = True + _db_not_available = {} _schema_resources = {} _database_resources = {} + def _get_db_resource_not_available_reason(self): + return self._db_not_available.get(self.FIXTURE.DRIVER, None) + + def _has_db_resource(self): + return self._database_resources.get( + self.FIXTURE.DRIVER, None) is not None + def _resources_for_driver(self, driver, schema_scope, generate_schema): # testresources relies on the identity and state of the # TestResourceManager objects in play to correctly manage @@ -110,12 +119,14 @@ class DbTestCase(test_base.BaseTestCase): # so we have to code the TestResourceManager logic into the # .resources attribute and ensure that the same set of test # variables always produces the same TestResourceManager objects. + if driver not in self._database_resources: try: self._database_resources[driver] = \ provision.DatabaseResource(driver) - except exception.BackendNotAvailable: + except exception.BackendNotAvailable as bne: self._database_resources[driver] = None + self._db_not_available[driver] = str(bne) database_resource = self._database_resources[driver] if database_resource is None: @@ -200,7 +211,7 @@ def backend_specific(*dialects): if self.engine.name not in dialects: msg = ('The test "%s" can be run ' 'only on %s. Current engine is %s.') - args = (reflection.get_callable_name(f), ' '.join(dialects), + args = (reflection.get_callable_name(f), ', '.join(dialects), self.engine.name) self.skip(msg % args) else: diff --git a/oslo_db/tests/sqlalchemy/test_fixtures.py b/oslo_db/tests/sqlalchemy/test_fixtures.py new file mode 100644 index 0000000..5d63d69 --- /dev/null +++ b/oslo_db/tests/sqlalchemy/test_fixtures.py @@ -0,0 +1,83 @@ +# 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 mock + +from oslo_db.sqlalchemy import provision +from oslo_db.sqlalchemy import test_base +from oslotest import base as oslo_test_base + + +class BackendSkipTest(oslo_test_base.BaseTestCase): + + def test_skip_no_dbapi(self): + + class FakeDatabaseOpportunisticFixture(test_base.DbFixture): + DRIVER = 'postgresql' + + class SomeTest(test_base.DbTestCase): + FIXTURE = FakeDatabaseOpportunisticFixture + + def runTest(self): + pass + + st = SomeTest() + + # patch in replacement lookup dictionaries to avoid + # leaking from/to other tests + with mock.patch( + "oslo_db.sqlalchemy.provision." + "Backend.backends_by_database_type", { + "postgresql": + provision.Backend("postgresql", "postgresql://")}): + st._database_resources = {} + st._db_not_available = {} + st._schema_resources = {} + + with mock.patch( + "sqlalchemy.create_engine", + mock.Mock(side_effect=ImportError())): + + self.assertEqual([], st.resources) + + ex = self.assertRaises( + self.skipException, + st.setUp + ) + + self.assertEqual( + "Backend 'postgresql' is unavailable: No DBAPI installed", + str(ex) + ) + + def test_skip_no_such_backend(self): + + class FakeDatabaseOpportunisticFixture(test_base.DbFixture): + DRIVER = 'postgresql+nosuchdbapi' + + class SomeTest(test_base.DbTestCase): + FIXTURE = FakeDatabaseOpportunisticFixture + + def runTest(self): + pass + + st = SomeTest() + + ex = self.assertRaises( + self.skipException, + st.setUp + ) + + self.assertEqual( + "Backend 'postgresql+nosuchdbapi' is unavailable: No such backend", + str(ex) + ) diff --git a/oslo_db/tests/sqlalchemy/test_provision.py b/oslo_db/tests/sqlalchemy/test_provision.py index 1ad586d..53d2303 100644 --- a/oslo_db/tests/sqlalchemy/test_provision.py +++ b/oslo_db/tests/sqlalchemy/test_provision.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import mock from oslotest import base as oslo_test_base +from sqlalchemy import exc as sa_exc from sqlalchemy import inspect from sqlalchemy import schema from sqlalchemy import types @@ -73,6 +75,62 @@ class DropAllObjectsTest(test_base.DbTestCase): ) +class BackendNotAvailableTest(oslo_test_base.BaseTestCase): + def test_no_dbapi(self): + backend = provision.Backend( + "postgresql", "postgresql+nosuchdbapi://hostname/dsn") + + with mock.patch( + "sqlalchemy.create_engine", + mock.Mock(side_effect=ImportError("nosuchdbapi"))): + + # NOTE(zzzeek): Call and test the _verify function twice, as it + # exercises a different code path on subsequent runs vs. + # the first run + ex = self.assertRaises( + exception.BackendNotAvailable, + backend._verify) + self.assertEqual( + "Backend 'postgresql+nosuchdbapi' is unavailable: " + "No DBAPI installed", str(ex)) + + ex = self.assertRaises( + exception.BackendNotAvailable, + backend._verify) + self.assertEqual( + "Backend 'postgresql+nosuchdbapi' is unavailable: " + "No DBAPI installed", str(ex)) + + def test_cant_connect(self): + backend = provision.Backend( + "postgresql", "postgresql+nosuchdbapi://hostname/dsn") + + with mock.patch( + "sqlalchemy.create_engine", + mock.Mock(return_value=mock.Mock(connect=mock.Mock( + side_effect=sa_exc.OperationalError( + "can't connect", None, None)) + )) + ): + + # NOTE(zzzeek): Call and test the _verify function twice, as it + # exercises a different code path on subsequent runs vs. + # the first run + ex = self.assertRaises( + exception.BackendNotAvailable, + backend._verify) + self.assertEqual( + "Backend 'postgresql+nosuchdbapi' is unavailable: " + "Could not connect", str(ex)) + + ex = self.assertRaises( + exception.BackendNotAvailable, + backend._verify) + self.assertEqual( + "Backend 'postgresql+nosuchdbapi' is unavailable: " + "Could not connect", str(ex)) + + class MySQLDropAllObjectsTest( DropAllObjectsTest, test_base.MySQLOpportunisticTestCase): pass diff --git a/oslo_db/tests/sqlalchemy/test_utils.py b/oslo_db/tests/sqlalchemy/test_utils.py index b3723a7..19f78ce 100644 --- a/oslo_db/tests/sqlalchemy/test_utils.py +++ b/oslo_db/tests/sqlalchemy/test_utils.py @@ -785,7 +785,9 @@ class TestConnectionUtils(test_utils.BaseTestCase): exception.BackendNotAvailable, provision.Backend._ensure_backend_available, self.connect_string ) - self.assertEqual("Could not connect", str(exc)) + self.assertEqual( + "Backend 'postgresql' is unavailable: " + "Could not connect", str(exc)) self.assertEqual( "The postgresql backend is unavailable: %s" % err, log.output.strip()) @@ -802,7 +804,9 @@ class TestConnectionUtils(test_utils.BaseTestCase): exception.BackendNotAvailable, provision.Backend._ensure_backend_available, self.connect_string ) - self.assertEqual("No DBAPI installed", str(exc)) + self.assertEqual( + "Backend 'postgresql' is unavailable: " + "No DBAPI installed", str(exc)) self.assertEqual( "The postgresql backend is unavailable: Can't import " "DBAPI module foobar", log.output.strip())