diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2016-08-03 17:20:49 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2016-08-08 14:25:58 -0400 |
commit | af27831a9e6eceda192f63c1368d0aa1cc70c17e (patch) | |
tree | 0bf9e76de249be8099d1c3fcbbe9910c697a99a4 | |
parent | 428dbe1eec5a77d70af192b7eccaff89495940db (diff) | |
download | oslo-db-af27831a9e6eceda192f63c1368d0aa1cc70c17e.tar.gz |
Display full reason for backend not available
This adds to the provisioning system the ability
to preserve and re-display the full reason for a
particular DB backend not available, and propagates
this all the way through the skip() emitted
by the OpportunisticTestCase and DbFixture.
As part of the change, the database type requested
is added to the messages themselves, which
also appears in the messages emitted by the
consuming is_backend_avail() function, which
has tests that check for these explicit messages.
New tests specific to the Backend object are
added which are not dependent on is_backend_avail();
as this function is deprecated, we will be
able to remove its tests at the same time
as the function itself with no loss in coverage.
Change-Id: I7c00e0770b02aa751e184edfecdec4306de6340c
-rw-r--r-- | oslo_db/sqlalchemy/provision.py | 20 | ||||
-rw-r--r-- | oslo_db/sqlalchemy/test_base.py | 21 | ||||
-rw-r--r-- | oslo_db/tests/sqlalchemy/test_fixtures.py | 83 | ||||
-rw-r--r-- | oslo_db/tests/sqlalchemy/test_provision.py | 58 | ||||
-rw-r--r-- | oslo_db/tests/sqlalchemy/test_utils.py | 8 |
5 files changed, 176 insertions, 14 deletions
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 210e180..1e9475f 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 64f8ef1..0de123e 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()) |