The original idea of enginefacade was that applications would call upon the global _TransactionContextManager given in oslo_db.sqlalchemy.enginefacade. However, as it turns out, virtually no Openstack projects seem to be using that technique, and instead, everyone is creating their own ad-hoc _TransactionContextManager objects and usually establishing it as a module-level global. Nova has two of them. Additionally, projects add configuration to these enginefacades (which IS part of the original idea), and this configuration in some cases is necessary to be present for tests that run as well, a key example being the sqlite_fks flag. The original DbFixture integration provided no way of reusing this configuration. Finally, projects very much tend to use custom fixtures in order to define their database communication. Test classes themselves don't really make use of oslo_db's DbTestCase anymore. This patch introduces a modernized fixture system which, in conjunction with the recent provisioning patch, addresses these use cases. Applications will typically create their own subclasses of these fixtures up front to suit the various testing cases they have, including SQLite fixed, SQLite ad-hoc, and opportunistic. In order to accommodate the fixture-based flow along with the use of testresources for opportunistic database provisioning, a mixin class OpportunisticDbTestMixin is still needed when a test needs to use "opportunistic" testing in order to provide the .resources attribute. The calculation of .resources is moved into the fixture system, but because this attribute is consulted before setUp(), the "opportunistic" fixture must be created early and stored. Closes-Bug: #1548960 Change-Id: I0163e637ffef6d45d2573ebe29b5438911d01fce
446 lines
14 KiB
Python
446 lines
14 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.
|
|
|
|
|
|
from oslotest import base as oslo_test_base
|
|
from sqlalchemy.ext import declarative
|
|
from sqlalchemy import schema
|
|
from sqlalchemy import sql
|
|
from sqlalchemy import types as sqltypes
|
|
|
|
from oslo_db.sqlalchemy import update_match
|
|
from oslo_db.tests.sqlalchemy import base as test_base
|
|
|
|
Base = declarative.declarative_base()
|
|
|
|
|
|
class MyModel(Base):
|
|
__tablename__ = 'my_table'
|
|
|
|
id = schema.Column(sqltypes.Integer, primary_key=True)
|
|
uuid = schema.Column(sqltypes.String(36), nullable=False, unique=True)
|
|
x = schema.Column(sqltypes.Integer)
|
|
y = schema.Column(sqltypes.String(40))
|
|
z = schema.Column(sqltypes.String(40))
|
|
|
|
|
|
class ManufactureCriteriaTest(oslo_test_base.BaseTestCase):
|
|
def test_instance_criteria_basic(self):
|
|
specimen = MyModel(
|
|
y='y1', z='z3',
|
|
uuid='136254d5-3869-408f-9da7-190e0072641a'
|
|
)
|
|
self.assertEqual(
|
|
"my_table.uuid = :uuid_1 AND my_table.y = :y_1 "
|
|
"AND my_table.z = :z_1",
|
|
str(update_match.manufacture_entity_criteria(specimen).compile())
|
|
)
|
|
|
|
def test_instance_criteria_basic_wnone(self):
|
|
specimen = MyModel(
|
|
y='y1', z=None,
|
|
uuid='136254d5-3869-408f-9da7-190e0072641a'
|
|
)
|
|
self.assertEqual(
|
|
"my_table.uuid = :uuid_1 AND my_table.y = :y_1 "
|
|
"AND my_table.z IS NULL",
|
|
str(update_match.manufacture_entity_criteria(specimen).compile())
|
|
)
|
|
|
|
def test_instance_criteria_tuples(self):
|
|
specimen = MyModel(
|
|
y='y1', z=('z1', 'z2'),
|
|
)
|
|
self.assertEqual(
|
|
"my_table.y = :y_1 AND my_table.z IN (:z_1, :z_2)",
|
|
str(update_match.manufacture_entity_criteria(specimen).compile())
|
|
)
|
|
|
|
def test_instance_criteria_tuples_wnone(self):
|
|
specimen = MyModel(
|
|
y='y1', z=('z1', 'z2', None),
|
|
)
|
|
self.assertEqual(
|
|
"my_table.y = :y_1 AND (my_table.z IS NULL OR "
|
|
"my_table.z IN (:z_1, :z_2))",
|
|
str(update_match.manufacture_entity_criteria(specimen).compile())
|
|
)
|
|
|
|
def test_instance_criteria_none_list(self):
|
|
specimen = MyModel(
|
|
y='y1', z=[None],
|
|
)
|
|
self.assertEqual(
|
|
"my_table.y = :y_1 AND my_table.z IS NULL",
|
|
str(update_match.manufacture_entity_criteria(specimen).compile())
|
|
)
|
|
|
|
|
|
class UpdateMatchTest(test_base.DbTestCase):
|
|
def setUp(self):
|
|
super(UpdateMatchTest, self).setUp()
|
|
Base.metadata.create_all(self.engine)
|
|
self.addCleanup(Base.metadata.drop_all, self.engine)
|
|
# self.engine.echo = 'debug'
|
|
self.session = self.sessionmaker(autocommit=False)
|
|
self.addCleanup(self.session.close)
|
|
self.session.add_all([
|
|
MyModel(
|
|
id=1,
|
|
uuid='23cb9224-9f8e-40fe-bd3c-e7577b7af37d',
|
|
x=5, y='y1', z='z1'),
|
|
MyModel(
|
|
id=2,
|
|
uuid='136254d5-3869-408f-9da7-190e0072641a',
|
|
x=6, y='y1', z='z2'),
|
|
MyModel(
|
|
id=3,
|
|
uuid='094eb162-d5df-494b-a458-a91a1b2d2c65',
|
|
x=7, y='y1', z='z1'),
|
|
MyModel(
|
|
id=4,
|
|
uuid='94659b3f-ea1f-4ffd-998d-93b28f7f5b70',
|
|
x=8, y='y2', z='z2'),
|
|
MyModel(
|
|
id=5,
|
|
uuid='bdf3893c-ee3c-40a0-bc79-960adb6cd1d4',
|
|
x=8, y='y2', z=None),
|
|
])
|
|
|
|
self.session.commit()
|
|
|
|
def _assert_row(self, pk, values):
|
|
row = self.session.execute(
|
|
sql.select([MyModel.__table__]).where(MyModel.__table__.c.id == pk)
|
|
).first()
|
|
values['id'] = pk
|
|
self.assertEqual(values, dict(row))
|
|
|
|
def test_update_specimen_successful(self):
|
|
uuid = '136254d5-3869-408f-9da7-190e0072641a'
|
|
|
|
specimen = MyModel(
|
|
y='y1', z='z2', uuid=uuid
|
|
)
|
|
|
|
result = self.session.query(MyModel).update_on_match(
|
|
specimen,
|
|
'uuid',
|
|
values={'x': 9, 'z': 'z3'}
|
|
)
|
|
|
|
self.assertEqual(uuid, result.uuid)
|
|
self.assertEqual(2, result.id)
|
|
self.assertEqual('z3', result.z)
|
|
self.assertIn(result, self.session)
|
|
|
|
self._assert_row(
|
|
2,
|
|
{
|
|
'uuid': '136254d5-3869-408f-9da7-190e0072641a',
|
|
'x': 9, 'y': 'y1', 'z': 'z3'
|
|
}
|
|
)
|
|
|
|
def test_update_specimen_include_only(self):
|
|
uuid = '136254d5-3869-408f-9da7-190e0072641a'
|
|
|
|
specimen = MyModel(
|
|
y='y9', z='z5', x=6, uuid=uuid
|
|
)
|
|
|
|
# Query the object first to test that we merge when the object is
|
|
# already cached in the session.
|
|
self.session.query(MyModel).filter(MyModel.uuid == uuid).one()
|
|
|
|
result = self.session.query(MyModel).update_on_match(
|
|
specimen,
|
|
'uuid',
|
|
values={'x': 9, 'z': 'z3'},
|
|
include_only=('x', )
|
|
)
|
|
|
|
self.assertEqual(uuid, result.uuid)
|
|
self.assertEqual(2, result.id)
|
|
self.assertEqual('z3', result.z)
|
|
self.assertIn(result, self.session)
|
|
self.assertNotIn(result, self.session.dirty)
|
|
|
|
self._assert_row(
|
|
2,
|
|
{
|
|
'uuid': '136254d5-3869-408f-9da7-190e0072641a',
|
|
'x': 9, 'y': 'y1', 'z': 'z3'
|
|
}
|
|
)
|
|
|
|
def test_update_specimen_no_rows(self):
|
|
specimen = MyModel(
|
|
y='y1', z='z3',
|
|
uuid='136254d5-3869-408f-9da7-190e0072641a'
|
|
)
|
|
|
|
exc = self.assertRaises(
|
|
update_match.NoRowsMatched,
|
|
self.session.query(MyModel).update_on_match,
|
|
specimen, 'uuid', values={'x': 9, 'z': 'z3'}
|
|
)
|
|
|
|
self.assertEqual("Zero rows matched for 3 attempts", exc.args[0])
|
|
|
|
def test_update_specimen_process_query_no_rows(self):
|
|
specimen = MyModel(
|
|
y='y1', z='z2',
|
|
uuid='136254d5-3869-408f-9da7-190e0072641a'
|
|
)
|
|
|
|
def process_query(query):
|
|
return query.filter_by(x=10)
|
|
|
|
exc = self.assertRaises(
|
|
update_match.NoRowsMatched,
|
|
self.session.query(MyModel).update_on_match,
|
|
specimen, 'uuid', values={'x': 9, 'z': 'z3'},
|
|
process_query=process_query
|
|
)
|
|
|
|
self.assertEqual("Zero rows matched for 3 attempts", exc.args[0])
|
|
|
|
def test_update_specimen_given_query_no_rows(self):
|
|
specimen = MyModel(
|
|
y='y1', z='z2',
|
|
uuid='136254d5-3869-408f-9da7-190e0072641a'
|
|
)
|
|
|
|
query = self.session.query(MyModel).filter_by(x=10)
|
|
|
|
exc = self.assertRaises(
|
|
update_match.NoRowsMatched,
|
|
query.update_on_match,
|
|
specimen, 'uuid', values={'x': 9, 'z': 'z3'},
|
|
)
|
|
|
|
self.assertEqual("Zero rows matched for 3 attempts", exc.args[0])
|
|
|
|
def test_update_specimen_multi_rows(self):
|
|
specimen = MyModel(
|
|
y='y1', z='z1',
|
|
)
|
|
|
|
exc = self.assertRaises(
|
|
update_match.MultiRowsMatched,
|
|
self.session.query(MyModel).update_on_match,
|
|
specimen, 'y', values={'x': 9, 'z': 'z3'}
|
|
)
|
|
|
|
self.assertEqual("2 rows matched; expected one", exc.args[0])
|
|
|
|
def test_update_specimen_query_mismatch_error(self):
|
|
specimen = MyModel(
|
|
y='y1'
|
|
)
|
|
q = self.session.query(MyModel.x, MyModel.y)
|
|
exc = self.assertRaises(
|
|
AssertionError,
|
|
q.update_on_match,
|
|
specimen, 'y', values={'x': 9, 'z': 'z3'},
|
|
)
|
|
|
|
self.assertEqual("Query does not match given specimen", exc.args[0])
|
|
|
|
def test_custom_handle_failure_raise_new(self):
|
|
class MyException(Exception):
|
|
pass
|
|
|
|
def handle_failure(query):
|
|
# ensure the query is usable
|
|
result = query.count()
|
|
self.assertEqual(0, result)
|
|
|
|
raise MyException("test: %d" % result)
|
|
|
|
specimen = MyModel(
|
|
y='y1', z='z3',
|
|
uuid='136254d5-3869-408f-9da7-190e0072641a'
|
|
)
|
|
|
|
exc = self.assertRaises(
|
|
MyException,
|
|
self.session.query(MyModel).update_on_match,
|
|
specimen, 'uuid', values={'x': 9, 'z': 'z3'},
|
|
handle_failure=handle_failure
|
|
)
|
|
|
|
self.assertEqual("test: 0", exc.args[0])
|
|
|
|
def test_custom_handle_failure_cancel_raise(self):
|
|
uuid = '136254d5-3869-408f-9da7-190e0072641a'
|
|
|
|
class MyException(Exception):
|
|
pass
|
|
|
|
def handle_failure(query):
|
|
# ensure the query is usable
|
|
result = query.count()
|
|
self.assertEqual(0, result)
|
|
|
|
return True
|
|
|
|
specimen = MyModel(
|
|
id=2, y='y1', z='z3', uuid=uuid
|
|
)
|
|
|
|
result = self.session.query(MyModel).update_on_match(
|
|
specimen, 'uuid', values={'x': 9, 'z': 'z3'},
|
|
handle_failure=handle_failure
|
|
)
|
|
self.assertEqual(uuid, result.uuid)
|
|
self.assertEqual(2, result.id)
|
|
self.assertEqual('z3', result.z)
|
|
self.assertEqual(9, result.x)
|
|
self.assertIn(result, self.session)
|
|
|
|
def test_update_specimen_on_none_successful(self):
|
|
uuid = 'bdf3893c-ee3c-40a0-bc79-960adb6cd1d4'
|
|
|
|
specimen = MyModel(
|
|
y='y2', z=None, uuid=uuid
|
|
)
|
|
|
|
result = self.session.query(MyModel).update_on_match(
|
|
specimen,
|
|
'uuid',
|
|
values={'x': 9, 'z': 'z3'},
|
|
)
|
|
|
|
self.assertIn(result, self.session)
|
|
self.assertEqual(uuid, result.uuid)
|
|
self.assertEqual(5, result.id)
|
|
self.assertEqual('z3', result.z)
|
|
self._assert_row(
|
|
5,
|
|
{
|
|
'uuid': 'bdf3893c-ee3c-40a0-bc79-960adb6cd1d4',
|
|
'x': 9, 'y': 'y2', 'z': 'z3'
|
|
}
|
|
)
|
|
|
|
def test_update_specimen_on_multiple_nonnone_successful(self):
|
|
uuid = '094eb162-d5df-494b-a458-a91a1b2d2c65'
|
|
|
|
specimen = MyModel(
|
|
y=('y1', 'y2'), x=(5, 7), uuid=uuid
|
|
)
|
|
|
|
result = self.session.query(MyModel).update_on_match(
|
|
specimen,
|
|
'uuid',
|
|
values={'x': 9, 'z': 'z3'},
|
|
)
|
|
|
|
self.assertIn(result, self.session)
|
|
self.assertEqual(uuid, result.uuid)
|
|
self.assertEqual(3, result.id)
|
|
self.assertEqual('z3', result.z)
|
|
self._assert_row(
|
|
3,
|
|
{
|
|
'uuid': '094eb162-d5df-494b-a458-a91a1b2d2c65',
|
|
'x': 9, 'y': 'y1', 'z': 'z3'
|
|
}
|
|
)
|
|
|
|
def test_update_specimen_on_multiple_wnone_successful(self):
|
|
uuid = 'bdf3893c-ee3c-40a0-bc79-960adb6cd1d4'
|
|
specimen = MyModel(
|
|
y=('y1', 'y2'), x=(8, 7), z=('z1', 'z2', None), uuid=uuid
|
|
)
|
|
|
|
result = self.session.query(MyModel).update_on_match(
|
|
specimen,
|
|
'uuid',
|
|
values={'x': 9, 'z': 'z3'},
|
|
)
|
|
|
|
self.assertIn(result, self.session)
|
|
self.assertEqual(uuid, result.uuid)
|
|
self.assertEqual(5, result.id)
|
|
self.assertEqual('z3', result.z)
|
|
self._assert_row(
|
|
5,
|
|
{
|
|
'uuid': 'bdf3893c-ee3c-40a0-bc79-960adb6cd1d4',
|
|
'x': 9, 'y': 'y2', 'z': 'z3'
|
|
}
|
|
)
|
|
|
|
def test_update_returning_pk_matched(self):
|
|
pk = self.session.query(MyModel).\
|
|
filter_by(y='y1', z='z2').update_returning_pk(
|
|
{'x': 9, 'z': 'z3'},
|
|
('uuid', '136254d5-3869-408f-9da7-190e0072641a')
|
|
)
|
|
|
|
self.assertEqual((2,), pk)
|
|
self._assert_row(
|
|
2,
|
|
{
|
|
'uuid': '136254d5-3869-408f-9da7-190e0072641a',
|
|
'x': 9, 'y': 'y1', 'z': 'z3'
|
|
}
|
|
)
|
|
|
|
def test_update_returning_wrong_uuid(self):
|
|
exc = self.assertRaises(
|
|
update_match.NoRowsMatched,
|
|
self.session.query(MyModel).
|
|
filter_by(y='y1', z='z2').update_returning_pk,
|
|
{'x': 9, 'z': 'z3'},
|
|
('uuid', '23cb9224-9f8e-40fe-bd3c-e7577b7af37d')
|
|
)
|
|
|
|
self.assertEqual("No rows matched the UPDATE", exc.args[0])
|
|
|
|
def test_update_returning_no_rows(self):
|
|
exc = self.assertRaises(
|
|
update_match.NoRowsMatched,
|
|
self.session.query(MyModel).
|
|
filter_by(y='y1', z='z3').update_returning_pk,
|
|
{'x': 9, 'z': 'z3'},
|
|
('uuid', '136254d5-3869-408f-9da7-190e0072641a')
|
|
)
|
|
|
|
self.assertEqual("No rows matched the UPDATE", exc.args[0])
|
|
|
|
def test_update_multiple_rows(self):
|
|
exc = self.assertRaises(
|
|
update_match.MultiRowsMatched,
|
|
self.session.query(MyModel).
|
|
filter_by(y='y1', z='z1').update_returning_pk,
|
|
{'x': 9, 'z': 'z3'},
|
|
('y', 'y1')
|
|
)
|
|
|
|
self.assertEqual("2 rows matched; expected one", exc.args[0])
|
|
|
|
|
|
class PGUpdateMatchTest(
|
|
UpdateMatchTest,
|
|
test_base.PostgreSQLOpportunisticTestCase):
|
|
pass
|
|
|
|
|
|
class MySQLUpdateMatchTest(
|
|
UpdateMatchTest,
|
|
test_base.MySQLOpportunisticTestCase):
|
|
pass
|