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
This commit is contained in:
Mike Bayer
2016-08-03 17:20:49 -04:00
parent 428dbe1eec
commit af27831a9e
5 changed files with 176 additions and 14 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)
)

View File

@@ -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

View File

@@ -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())