Fix compare_obj() to obey missing/unset fields

When comparing objects to dicts, if a field is unset on both, they
should still be counted as equal. If something is unset on one, but not
on the other, they should be counted as unequal. In any of these
situations, if the field is set in allow_missing, all of the equality
checks should just be skipped.

Change-Id: I3e5143bc872ab4cb645d09c7e969fd1cf9c7985c
Closes-Bug: #1566398
This commit is contained in:
Ryan Rossiter 2016-04-05 15:38:19 +00:00
parent 788e3d08f7
commit 52545273d9
2 changed files with 95 additions and 22 deletions

View File

@ -53,33 +53,46 @@ def compare_obj(test, obj, db_obj, subs=None, allow_missing=None,
:param comparators: Map of comparator functions to use for certain fields
"""
if subs is None:
subs = {}
if allow_missing is None:
allow_missing = []
if comparators is None:
comparators = {}
subs = subs or {}
allow_missing = allow_missing or []
comparators = comparators or {}
for key in obj.fields:
# We'll raise a NotImplementedError if we try to compare against
# against something that isn't set in the object, but is not
# in the allow_missing. This will replace that exception
# with an AssertionError (because that is a better way of saying
# "these objects arent the same").
if not obj.obj_attr_is_set(key):
if key in allow_missing:
continue
else:
raise AssertionError(("%s is not set on the object, so "
"the objects are not equal") % key)
if key in allow_missing and not obj.obj_attr_is_set(key):
continue
obj_val = getattr(obj, key)
db_key = subs.get(key, key)
# If this is an allow_missing key and it's missing in either obj or
# db_obj, just skip it
if key in allow_missing:
if key not in obj or db_key not in db_obj:
continue
# If the value isn't set on the object, and also isn't set on the
# db_obj, we'll skip the value check, unset in both is equal
if not obj.obj_attr_is_set(key) and db_key not in db_obj:
continue
# If it's set on the object and not on the db_obj, they aren't equal
elif obj.obj_attr_is_set(key) and db_key not in db_obj:
raise AssertionError(("%s (db_key: %s) is set on the object, but "
"not on the db_obj, so the objects are not "
"equal")
% (key, db_key))
# If it's set on the db_obj and not the object, they aren't equal
elif not obj.obj_attr_is_set(key) and db_key in db_obj:
raise AssertionError(("%s (db_key: %s) is set on the db_obj, but "
"not on the object, so the objects are not "
"equal")
% (key, db_key))
# All of the checks above have safeguarded us, so we know we will
# get an obj_val and db_val without issue
obj_val = getattr(obj, key)
db_val = db_obj[db_key]
if isinstance(obj_val, datetime.datetime):
obj_val = obj_val.replace(tzinfo=None)
if isinstance(db_val, datetime.datetime):
db_val = obj_val.replace(tzinfo=None)
if key in comparators:
comparator = comparators[key]
comparator(db_val, obj_val)

View File

@ -80,12 +80,72 @@ class TestObjectComparators(test.TestCase):
self.assertIn(call, actual_calls)
def test_compare_obj_with_unset(self):
# If the object has nothing set, and also the db object has the same
# thing not set, it's OK.
mock_test = mock.Mock()
mock_test.assertEqual = mock.Mock()
my_obj = self.MyComparedObject()
my_db_obj = {}
self.assertRaises(AssertionError, fixture.compare_obj,
mock_test, my_obj, my_db_obj)
fixture.compare_obj(mock_test, my_obj, my_db_obj)
self.assertFalse(mock_test.assertEqual.called, "assertEqual should "
"not have been called, there is nothing to compare.")
def test_compare_obj_with_unset_in_obj(self):
# If the db dict has something set, but the object doesn't, that's !=
mock_test = mock.Mock()
mock_test.assertEqual = mock.Mock()
my_obj = self.MyComparedObject(foo=1)
my_db_obj = {'foo': 1, 'bar': 2}
self.assertRaises(AssertionError, fixture.compare_obj, mock_test,
my_obj, my_db_obj)
def test_compare_obj_with_unset_in_db_dict(self):
# If the object has something set, but the db dict doesn't, that's !=
mock_test = mock.Mock()
mock_test.assertEqual = mock.Mock()
my_obj = self.MyComparedObject(foo=1, bar=2)
my_db_obj = {'foo': 1}
self.assertRaises(AssertionError, fixture.compare_obj, mock_test,
my_obj, my_db_obj)
def test_compare_obj_with_unset_in_obj_ignored(self):
# If the db dict has something set, but the object doesn't, but we
# ignore that key, we are equal
my_obj = self.MyComparedObject(foo=1)
my_db_obj = {'foo': 1, 'bar': 2}
ignore = ['bar']
fixture.compare_obj(self, my_obj, my_db_obj, allow_missing=ignore)
def test_compare_obj_with_unset_in_db_dict_ignored(self):
# If the object has something set, but the db dict doesn't, but we
# ignore that key, we are equal
my_obj = self.MyComparedObject(foo=1, bar=2)
my_db_obj = {'foo': 1}
ignore = ['bar']
fixture.compare_obj(self, my_obj, my_db_obj, allow_missing=ignore)
def test_compare_obj_with_allow_missing_unequal(self):
# If the tested key is in allow_missing, but both the obj and db_obj
# have the value set, we should still check it for equality
mock_test = mock.Mock()
mock_test.assertEqual = mock.Mock()
my_obj = self.MyComparedObject(foo=1, bar=2)
my_db_obj = {'foo': 1, 'bar': 1}
ignore = ['bar']
fixture.compare_obj(mock_test, my_obj, my_db_obj,
allow_missing=ignore)
expected_calls = [(1, 1), (1, 2)]
actual_calls = [c[0] for c in mock_test.assertEqual.call_args_list]
for call in expected_calls:
self.assertIn(call, actual_calls)
def test_compare_obj_with_subs(self):
mock_test = mock.Mock()