1#    Copyright 2013 IBM Corp.
2#
3#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4#    not use this file except in compliance with the License. You may obtain
5#    a copy of the License at
6#
7#         http://www.apache.org/licenses/LICENSE-2.0
8#
9#    Unless required by applicable law or agreed to in writing, software
10#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#    License for the specific language governing permissions and limitations
13#    under the License.
14
15import copy
16import datetime
17import jsonschema
18import logging
19import pytz
20from unittest import mock
21
22from oslo_context import context
23from oslo_serialization import jsonutils
24from oslo_utils import timeutils
25import testtools
26from testtools import matchers
27
28from oslo_versionedobjects import base
29from oslo_versionedobjects import exception
30from oslo_versionedobjects import fields
31from oslo_versionedobjects import fixture
32from oslo_versionedobjects import test
33
34
35LOG = logging.getLogger(__name__)
36
37
38def is_test_object(cls):
39    """Return True if class is defined in the tests.
40
41    :param cls: Class to inspect
42    """
43    return 'oslo_versionedobjects.tests' in cls.__module__
44
45
46@base.VersionedObjectRegistry.register
47class MyOwnedObject(base.VersionedObject):
48    VERSION = '1.0'
49    fields = {'baz': fields.Field(fields.Integer())}
50
51
52@base.VersionedObjectRegistry.register
53class MyObj(base.VersionedObject, base.VersionedObjectDictCompat):
54    VERSION = '1.6'
55    fields = {'foo': fields.Field(fields.Integer(), default=1),
56              'bar': fields.Field(fields.String()),
57              'missing': fields.Field(fields.String()),
58              'readonly': fields.Field(fields.Integer(), read_only=True),
59              'rel_object': fields.ObjectField('MyOwnedObject', nullable=True),
60              'rel_objects': fields.ListOfObjectsField('MyOwnedObject',
61                                                       nullable=True),
62              'mutable_default': fields.ListOfStringsField(default=[]),
63              'timestamp': fields.DateTimeField(nullable=True),
64              }
65
66    @staticmethod
67    def _from_db_object(context, obj, db_obj):
68        self = MyObj()
69        self.foo = db_obj['foo']
70        self.bar = db_obj['bar']
71        self.missing = db_obj['missing']
72        self.readonly = 1
73        return self
74
75    def obj_load_attr(self, attrname):
76        setattr(self, attrname, 'loaded!')
77
78    @base.remotable_classmethod
79    def query(cls, context):
80        obj = cls(context=context, foo=1, bar='bar')
81        obj.obj_reset_changes()
82        return obj
83
84    @base.remotable
85    def marco(self):
86        return 'polo'
87
88    @base.remotable
89    def _update_test(self):
90        project_id = getattr(context, 'tenant', None)
91        if project_id is None:
92            project_id = getattr(context, 'project_id', None)
93        if project_id == 'alternate':
94            self.bar = 'alternate-context'
95        else:
96            self.bar = 'updated'
97
98    @base.remotable
99    def save(self):
100        self.obj_reset_changes()
101
102    @base.remotable
103    def refresh(self):
104        self.foo = 321
105        self.bar = 'refreshed'
106        self.obj_reset_changes()
107
108    @base.remotable
109    def modify_save_modify(self):
110        self.bar = 'meow'
111        self.save()
112        self.foo = 42
113        self.rel_object = MyOwnedObject(baz=42)
114
115    def obj_make_compatible(self, primitive, target_version):
116        super(MyObj, self).obj_make_compatible(primitive, target_version)
117        # NOTE(danms): Simulate an older version that had a different
118        # format for the 'bar' attribute
119        if target_version == '1.1' and 'bar' in primitive:
120            primitive['bar'] = 'old%s' % primitive['bar']
121
122
123@base.VersionedObjectRegistry.register
124class MyComparableObj(MyObj, base.ComparableVersionedObject):
125    pass
126
127
128@base.VersionedObjectRegistry.register
129class MyObjDiffVers(MyObj):
130    VERSION = '1.5'
131
132    @classmethod
133    def obj_name(cls):
134        return 'MyObj'
135
136
137@base.VersionedObjectRegistry.register_if(False)
138class MyObj2(base.VersionedObject):
139    @classmethod
140    def obj_name(cls):
141        return 'MyObj'
142
143    @base.remotable_classmethod
144    def query(cls, *args, **kwargs):
145        pass
146
147
148@base.VersionedObjectRegistry.register_if(False)
149class MySensitiveObj(base.VersionedObject):
150    VERSION = '1.0'
151    fields = {
152        'data': fields.SensitiveStringField(nullable=True)
153    }
154
155    @base.remotable_classmethod
156    def query(cls, *args, **kwargs):
157        pass
158
159
160class RandomMixInWithNoFields(object):
161    """Used to test object inheritance using a mixin that has no fields."""
162    pass
163
164
165@base.VersionedObjectRegistry.register
166class TestSubclassedObject(RandomMixInWithNoFields, MyObj):
167    fields = {'new_field': fields.Field(fields.String())}
168    child_versions = {
169        '1.0': '1.0',
170        '1.1': '1.1',
171        '1.2': '1.1',
172        '1.3': '1.2',
173        '1.4': '1.3',
174        '1.5': '1.4',
175        '1.6': '1.5',
176        '1.7': '1.6',
177        }
178
179
180@base.VersionedObjectRegistry.register
181class MyCompoundObject(base.VersionedObject):
182    fields = {
183        "foo": fields.Field(fields.List(fields.Integer())),
184        "bar": fields.Field(fields.Dict(fields.Integer())),
185        "baz": fields.Field(fields.Set(fields.Integer()))
186    }
187
188
189class TestRegistry(test.TestCase):
190    def test_obj_tracking(self):
191
192        @base.VersionedObjectRegistry.register
193        class NewBaseClass(object):
194            VERSION = '1.0'
195            fields = {}
196
197            @classmethod
198            def obj_name(cls):
199                return cls.__name__
200
201        @base.VersionedObjectRegistry.register
202        class Fake1TestObj1(NewBaseClass):
203            @classmethod
204            def obj_name(cls):
205                return 'fake1'
206
207        @base.VersionedObjectRegistry.register
208        class Fake1TestObj2(Fake1TestObj1):
209            pass
210
211        @base.VersionedObjectRegistry.register
212        class Fake1TestObj3(Fake1TestObj1):
213            VERSION = '1.1'
214
215        @base.VersionedObjectRegistry.register
216        class Fake2TestObj1(NewBaseClass):
217            @classmethod
218            def obj_name(cls):
219                return 'fake2'
220
221        @base.VersionedObjectRegistry.register
222        class Fake1TestObj4(Fake1TestObj3):
223            VERSION = '1.2'
224
225        @base.VersionedObjectRegistry.register
226        class Fake2TestObj2(Fake2TestObj1):
227            VERSION = '1.1'
228
229        @base.VersionedObjectRegistry.register
230        class Fake1TestObj5(Fake1TestObj1):
231            VERSION = '1.1'
232
233        @base.VersionedObjectRegistry.register_if(False)
234        class ConditionalObj1(NewBaseClass):
235            fields = {'foo': fields.IntegerField()}
236
237        @base.VersionedObjectRegistry.register_if(True)
238        class ConditionalObj2(NewBaseClass):
239            fields = {'foo': fields.IntegerField()}
240
241        # Newest versions first in the list. Duplicate versions take the
242        # newest object.
243        expected = {'fake1': [Fake1TestObj4, Fake1TestObj5, Fake1TestObj2],
244                    'fake2': [Fake2TestObj2, Fake2TestObj1]}
245        self.assertEqual(expected['fake1'],
246                         base.VersionedObjectRegistry.obj_classes()['fake1'])
247        self.assertEqual(expected['fake2'],
248                         base.VersionedObjectRegistry.obj_classes()['fake2'])
249        self.assertEqual(
250            [],
251            base.VersionedObjectRegistry.obj_classes()['ConditionalObj1'])
252        self.assertTrue(hasattr(ConditionalObj1, 'foo'))
253        self.assertEqual(
254            [ConditionalObj2],
255            base.VersionedObjectRegistry.obj_classes()['ConditionalObj2'])
256        self.assertTrue(hasattr(ConditionalObj2, 'foo'))
257
258    def test_field_checking(self):
259        def create_class(field):
260            @base.VersionedObjectRegistry.register
261            class TestField(base.VersionedObject):
262                VERSION = '1.5'
263                fields = {'foo': field()}
264            return TestField
265
266        create_class(fields.DateTimeField)
267        self.assertRaises(exception.ObjectFieldInvalid,
268                          create_class, fields.DateTime)
269        self.assertRaises(exception.ObjectFieldInvalid,
270                          create_class, int)
271
272    def test_registration_hook(self):
273        class TestObject(base.VersionedObject):
274            VERSION = '1.0'
275
276        class TestObjectNewer(base.VersionedObject):
277            VERSION = '1.1'
278
279            @classmethod
280            def obj_name(cls):
281                return 'TestObject'
282
283        registry = base.VersionedObjectRegistry()
284        with mock.patch.object(registry, 'registration_hook') as mock_hook:
285            registry._register_class(TestObject)
286            mock_hook.assert_called_once_with(TestObject, 0)
287
288        with mock.patch.object(registry, 'registration_hook') as mock_hook:
289            registry._register_class(TestObjectNewer)
290            mock_hook.assert_called_once_with(TestObjectNewer, 0)
291
292    def test_subclassability(self):
293        class MyRegistryOne(base.VersionedObjectRegistry):
294
295            def registration_hook(self, cls, index):
296                cls.reg_to = "one"
297
298        class MyRegistryTwo(base.VersionedObjectRegistry):
299
300            def registration_hook(self, cls, index):
301                cls.reg_to = "two"
302
303        @MyRegistryOne.register
304        class AVersionedObject1(base.VersionedObject):
305            VERSION = '1.0'
306            fields = {'baz': fields.Field(fields.Integer())}
307
308        @MyRegistryTwo.register
309        class AVersionedObject2(base.VersionedObject):
310            VERSION = '1.0'
311            fields = {'baz': fields.Field(fields.Integer())}
312
313        self.assertIn('AVersionedObject1',
314                      MyRegistryOne.obj_classes())
315        self.assertIn('AVersionedObject2',
316                      MyRegistryOne.obj_classes())
317        self.assertIn('AVersionedObject1',
318                      MyRegistryTwo.obj_classes())
319        self.assertIn('AVersionedObject2',
320                      MyRegistryTwo.obj_classes())
321        self.assertIn('AVersionedObject1',
322                      base.VersionedObjectRegistry.obj_classes())
323        self.assertIn('AVersionedObject2',
324                      base.VersionedObjectRegistry.obj_classes())
325        self.assertEqual(AVersionedObject1.reg_to, "one")
326        self.assertEqual(AVersionedObject2.reg_to, "two")
327
328    @mock.patch.object(base.VersionedObjectRegistry, '__new__')
329    def test_register(self, mock_registry):
330        mock_reg_obj = mock.Mock()
331        mock_registry.return_value = mock_reg_obj
332        mock_reg_obj._register_class = mock.Mock()
333
334        class my_class(object):
335            pass
336
337        base.VersionedObjectRegistry.register(my_class)
338        mock_reg_obj._register_class.assert_called_once_with(my_class)
339
340    @mock.patch.object(base.VersionedObjectRegistry, 'register')
341    def test_register_if(self, mock_register):
342        class my_class(object):
343            pass
344
345        base.VersionedObjectRegistry.register_if(True)(my_class)
346        mock_register.assert_called_once_with(my_class)
347
348    @mock.patch.object(base, '_make_class_properties')
349    def test_register_if_false(self, mock_make_props):
350        class my_class(object):
351            pass
352
353        base.VersionedObjectRegistry.register_if(False)(my_class)
354        mock_make_props.assert_called_once_with(my_class)
355
356    @mock.patch.object(base.VersionedObjectRegistry, 'register_if')
357    def test_objectify(self, mock_register_if):
358        mock_reg_callable = mock.Mock()
359        mock_register_if.return_value = mock_reg_callable
360
361        class my_class(object):
362            pass
363
364        base.VersionedObjectRegistry.objectify(my_class)
365
366        mock_register_if.assert_called_once_with(False)
367        mock_reg_callable.assert_called_once_with(my_class)
368
369
370class TestObjMakeList(test.TestCase):
371
372    def test_obj_make_list(self):
373        @base.VersionedObjectRegistry.register
374        class MyList(base.ObjectListBase, base.VersionedObject):
375            fields = {
376                'objects': fields.ListOfObjectsField('MyObj'),
377            }
378
379        db_objs = [{'foo': 1, 'bar': 'baz', 'missing': 'banana'},
380                   {'foo': 2, 'bar': 'bat', 'missing': 'apple'},
381                   ]
382        mylist = base.obj_make_list('ctxt', MyList(), MyObj, db_objs)
383        self.assertEqual(2, len(mylist))
384        self.assertEqual('ctxt', mylist._context)
385        for index, item in enumerate(mylist):
386            self.assertEqual(db_objs[index]['foo'], item.foo)
387            self.assertEqual(db_objs[index]['bar'], item.bar)
388            self.assertEqual(db_objs[index]['missing'], item.missing)
389
390
391class TestGetSubobjectVersion(test.TestCase):
392    def setUp(self):
393        super(TestGetSubobjectVersion, self).setUp()
394        self.backport_mock = mock.MagicMock()
395        self.rels = [('1.1', '1.0'), ('1.3', '1.1')]
396
397    def test_get_subobject_version_not_existing(self):
398        # Verify that exception is raised if we try backporting
399        # to a version where we did not contain the subobject
400        self.assertRaises(exception.TargetBeforeSubobjectExistedException,
401                          base._get_subobject_version, '1.0', self.rels,
402                          self.backport_mock)
403
404    def test_get_subobject_version_explicit_version(self):
405        # Verify that we backport to the correct subobject version when the
406        # version we are going back to is explicitly said in the relationships
407        base._get_subobject_version('1.3', self.rels, self.backport_mock)
408        self.backport_mock.assert_called_once_with('1.1')
409
410    def test_get_subobject_version_implicit_version(self):
411        # Verify that we backport to the correct subobject version when the
412        # version backporting to is not explicitly stated in the relationships
413        base._get_subobject_version('1.2', self.rels, self.backport_mock)
414        self.backport_mock.assert_called_once_with('1.0')
415
416
417class TestDoSubobjectBackport(test.TestCase):
418    @base.VersionedObjectRegistry.register
419    class ParentObj(base.VersionedObject):
420        VERSION = '1.1'
421        fields = {'child': fields.ObjectField('ChildObj', nullable=True)}
422        obj_relationships = {'child': [('1.0', '1.0'), ('1.1', '1.1')]}
423
424    @base.VersionedObjectRegistry.register
425    class ParentObjList(base.VersionedObject, base.ObjectListBase):
426        VERSION = '1.1'
427        fields = {'objects': fields.ListOfObjectsField('ChildObj')}
428        obj_relationships = {'objects': [('1.0', '1.0'), ('1.1', '1.1')]}
429
430    @base.VersionedObjectRegistry.register
431    class ChildObj(base.VersionedObject):
432        VERSION = '1.1'
433        fields = {'foo': fields.IntegerField()}
434
435    def test_do_subobject_backport_without_manifest(self):
436        child = self.ChildObj(foo=1)
437        parent = self.ParentObj(child=child)
438        parent_primitive = parent.obj_to_primitive()['versioned_object.data']
439        primitive = child.obj_to_primitive()['versioned_object.data']
440        version = '1.0'
441
442        compat_func = 'obj_make_compatible_from_manifest'
443        with mock.patch.object(child, compat_func) as mock_compat:
444            base._do_subobject_backport(version, parent, 'child',
445                                        parent_primitive)
446            mock_compat.assert_called_once_with(primitive,
447                                                version,
448                                                version_manifest=None)
449
450    def test_do_subobject_backport_with_manifest(self):
451        child = self.ChildObj(foo=1)
452        parent = self.ParentObj(child=child)
453        parent_primitive = parent.obj_to_primitive()['versioned_object.data']
454        primitive = child.obj_to_primitive()['versioned_object.data']
455        version = '1.0'
456        manifest = {'ChildObj': '1.0'}
457        parent._obj_version_manifest = manifest
458
459        compat_func = 'obj_make_compatible_from_manifest'
460        with mock.patch.object(child, compat_func) as mock_compat:
461            base._do_subobject_backport(version, parent, 'child',
462                                        parent_primitive)
463            mock_compat.assert_called_once_with(primitive,
464                                                version,
465                                                version_manifest=manifest)
466
467    def test_do_subobject_backport_with_manifest_old_parent(self):
468        child = self.ChildObj(foo=1)
469        parent = self.ParentObj(child=child)
470        manifest = {'ChildObj': '1.0'}
471        parent_primitive = parent.obj_to_primitive(target_version='1.1',
472                                                   version_manifest=manifest)
473        child_primitive = parent_primitive['versioned_object.data']['child']
474        self.assertEqual('1.0', child_primitive['versioned_object.version'])
475
476    def test_do_subobject_backport_list_object(self):
477        child = self.ChildObj(foo=1)
478        parent = self.ParentObjList(objects=[child])
479        parent_primitive = parent.obj_to_primitive()['versioned_object.data']
480        primitive = child.obj_to_primitive()['versioned_object.data']
481        version = '1.0'
482
483        compat_func = 'obj_make_compatible_from_manifest'
484        with mock.patch.object(child, compat_func) as mock_compat:
485            base._do_subobject_backport(version, parent, 'objects',
486                                        parent_primitive)
487            mock_compat.assert_called_once_with(primitive,
488                                                version,
489                                                version_manifest=None)
490
491    def test_do_subobject_backport_list_object_with_manifest(self):
492        child = self.ChildObj(foo=1)
493        parent = self.ParentObjList(objects=[child])
494        manifest = {'ChildObj': '1.0', 'ParentObjList': '1.0'}
495        parent_primitive = parent.obj_to_primitive(target_version='1.0',
496                                                   version_manifest=manifest)
497        self.assertEqual('1.0', parent_primitive['versioned_object.version'])
498        child_primitive = parent_primitive['versioned_object.data']['objects']
499        self.assertEqual('1.0', child_primitive[0]['versioned_object.version'])
500
501    def test_do_subobject_backport_null_child(self):
502        parent = self.ParentObj(child=None)
503        parent_primitive = parent.obj_to_primitive()['versioned_object.data']
504        version = '1.0'
505
506        compat_func = 'obj_make_compatible_from_manifest'
507        with mock.patch.object(self.ChildObj, compat_func) as mock_compat:
508            base._do_subobject_backport(version, parent, 'child',
509                                        parent_primitive)
510            self.assertFalse(mock_compat.called,
511                             "obj_make_compatible_from_manifest() should not "
512                             "have been called because the subobject is "
513                             "None.")
514
515    def test_to_primitive_calls_make_compatible_manifest(self):
516        obj = self.ParentObj()
517        with mock.patch.object(obj, 'obj_make_compatible_from_manifest') as m:
518            obj.obj_to_primitive(target_version='1.0',
519                                 version_manifest=mock.sentinel.manifest)
520            m.assert_called_once_with(mock.ANY, '1.0', mock.sentinel.manifest)
521
522
523class _BaseTestCase(test.TestCase):
524    def setUp(self):
525        super(_BaseTestCase, self).setUp()
526        self.user_id = 'fake-user'
527        self.project_id = 'fake-project'
528        self.context = context.RequestContext(self.user_id, self.project_id)
529
530    def json_comparator(self, expected, obj_val):
531        # json-ify an object field for comparison with its db str
532        # equivalent
533        self.assertEqual(expected, jsonutils.dumps(obj_val))
534
535    def str_comparator(self, expected, obj_val):
536        """Compare a field to a string value
537
538        Compare an object field to a string in the db by performing
539        a simple coercion on the object field value.
540        """
541        self.assertEqual(expected, str(obj_val))
542
543    def assertNotIsInstance(self, obj, cls, msg=None):
544        """Python < v2.7 compatibility.  Assert 'not isinstance(obj, cls)."""
545        try:
546            f = super(_BaseTestCase, self).assertNotIsInstance
547        except AttributeError:
548            self.assertThat(obj,
549                            matchers.Not(matchers.IsInstance(cls)),
550                            message=msg or '')
551        else:
552            f(obj, cls, msg=msg)
553
554
555class TestFixture(_BaseTestCase):
556    def test_fake_indirection_takes_serializer(self):
557        ser = mock.MagicMock()
558        iapi = fixture.FakeIndirectionAPI(ser)
559        ser.serialize_entity.return_value = mock.sentinel.serial
560        iapi.object_action(mock.sentinel.context, mock.sentinel.objinst,
561                           mock.sentinel.objmethod, (), {})
562        ser.serialize_entity.assert_called_once_with(mock.sentinel.context,
563                                                     mock.sentinel.objinst)
564        ser.deserialize_entity.assert_called_once_with(mock.sentinel.context,
565                                                       mock.sentinel.serial)
566
567    def test_indirection_fixture_takes_indirection_api(self):
568        iapi = mock.sentinel.iapi
569        self.useFixture(fixture.IndirectionFixture(iapi))
570        self.assertEqual(iapi, base.VersionedObject.indirection_api)
571
572    def test_indirection_action(self):
573        self.useFixture(fixture.IndirectionFixture())
574        obj = MyObj(context=self.context)
575        with mock.patch.object(base.VersionedObject.indirection_api,
576                               'object_action') as mock_action:
577            mock_action.return_value = ({}, 'foo')
578            obj.marco()
579            mock_action.assert_called_once_with(self.context,
580                                                obj, 'marco',
581                                                (), {})
582
583    @mock.patch('oslo_versionedobjects.base.obj_tree_get_versions')
584    def test_indirection_class_action(self, mock_otgv):
585        mock_otgv.return_value = mock.sentinel.versions
586        self.useFixture(fixture.IndirectionFixture())
587        with mock.patch.object(base.VersionedObject.indirection_api,
588                               'object_class_action_versions') as mock_caction:
589            mock_caction.return_value = 'foo'
590            MyObj.query(self.context)
591            mock_caction.assert_called_once_with(self.context,
592                                                 'MyObj', 'query',
593                                                 mock.sentinel.versions,
594                                                 (), {})
595
596    def test_fake_indirection_serializes_arguments(self):
597        ser = mock.MagicMock()
598        iapi = fixture.FakeIndirectionAPI(serializer=ser)
599        arg1 = mock.MagicMock()
600        arg2 = mock.MagicMock()
601        iapi.object_action(mock.sentinel.context, mock.sentinel.objinst,
602                           mock.sentinel.objmethod, (arg1,), {'foo': arg2})
603        ser.serialize_entity.assert_any_call(mock.sentinel.context, arg1)
604        ser.serialize_entity.assert_any_call(mock.sentinel.context, arg2)
605
606    def test_get_hashes(self):
607        checker = fixture.ObjectVersionChecker()
608        hashes = checker.get_hashes()
609        # NOTE(danms): If this object's version or hash changes, this needs
610        # to change. Otherwise, leave it alone.
611        self.assertEqual('1.6-fb5f5379168bf08f7f2ce0a745e91027',
612                         hashes['TestSubclassedObject'])
613
614    def test_test_hashes(self):
615        checker = fixture.ObjectVersionChecker()
616        hashes = checker.get_hashes()
617        actual_hash = hashes['TestSubclassedObject']
618        hashes['TestSubclassedObject'] = 'foo'
619        expected, actual = checker.test_hashes(hashes)
620        self.assertEqual(['TestSubclassedObject'], list(expected.keys()))
621        self.assertEqual(['TestSubclassedObject'], list(actual.keys()))
622        self.assertEqual('foo', expected['TestSubclassedObject'])
623        self.assertEqual(actual_hash, actual['TestSubclassedObject'])
624
625    def test_get_dependency_tree(self):
626        checker = fixture.ObjectVersionChecker()
627        tree = checker.get_dependency_tree()
628
629        # NOTE(danms): If this object's dependencies change, this n eeds
630        # to change. Otherwise, leave it alone.
631        self.assertEqual({'MyOwnedObject': '1.0'},
632                         tree['TestSubclassedObject'])
633
634    def test_test_relationships(self):
635        checker = fixture.ObjectVersionChecker()
636        tree = checker.get_dependency_tree()
637        actual = tree['TestSubclassedObject']
638        tree['TestSubclassedObject']['Foo'] = '9.8'
639        expected, actual = checker.test_relationships(tree)
640        self.assertEqual(['TestSubclassedObject'], list(expected.keys()))
641        self.assertEqual(['TestSubclassedObject'], list(actual.keys()))
642        self.assertEqual({'MyOwnedObject': '1.0',
643                          'Foo': '9.8'},
644                         expected['TestSubclassedObject'])
645        self.assertEqual({'MyOwnedObject': '1.0'},
646                         actual['TestSubclassedObject'])
647
648    def test_test_compatibility(self):
649        fake_classes = {mock.sentinel.class_one: [mock.sentinel.impl_one_one,
650                                                  mock.sentinel.impl_one_two],
651                        mock.sentinel.class_two: [mock.sentinel.impl_two_one,
652                                                  mock.sentinel.impl_two_two],
653                        }
654        checker = fixture.ObjectVersionChecker(fake_classes)
655
656        @mock.patch.object(checker, '_test_object_compatibility')
657        def test(mock_compat):
658            checker.test_compatibility_routines()
659            mock_compat.assert_has_calls(
660                [mock.call(mock.sentinel.impl_one_one, manifest=None,
661                           init_args=[], init_kwargs={}),
662                 mock.call(mock.sentinel.impl_one_two, manifest=None,
663                           init_args=[], init_kwargs={}),
664                 mock.call(mock.sentinel.impl_two_one, manifest=None,
665                           init_args=[], init_kwargs={}),
666                 mock.call(mock.sentinel.impl_two_two, manifest=None,
667                           init_args=[], init_kwargs={})],
668                any_order=True)
669        test()
670
671    def test_test_compatibility_checks_obj_to_primitive(self):
672        fake = mock.MagicMock()
673        fake.VERSION = '1.3'
674
675        checker = fixture.ObjectVersionChecker()
676        checker._test_object_compatibility(fake)
677        fake().obj_to_primitive.assert_has_calls(
678            [mock.call(target_version='1.0'),
679             mock.call(target_version='1.1'),
680             mock.call(target_version='1.2'),
681             mock.call(target_version='1.3')])
682
683    def test_test_relationships_in_order(self):
684        fake_classes = {mock.sentinel.class_one: [mock.sentinel.impl_one_one,
685                                                  mock.sentinel.impl_one_two],
686                        mock.sentinel.class_two: [mock.sentinel.impl_two_one,
687                                                  mock.sentinel.impl_two_two],
688                        }
689        checker = fixture.ObjectVersionChecker(fake_classes)
690
691        @mock.patch.object(checker, '_test_relationships_in_order')
692        def test(mock_compat):
693            checker.test_relationships_in_order()
694            mock_compat.assert_has_calls(
695                [mock.call(mock.sentinel.impl_one_one),
696                 mock.call(mock.sentinel.impl_one_two),
697                 mock.call(mock.sentinel.impl_two_one),
698                 mock.call(mock.sentinel.impl_two_two)],
699                any_order=True)
700        test()
701
702    def test_test_relationships_in_order_good(self):
703        fake = mock.MagicMock()
704        fake.VERSION = '1.5'
705        fake.fields = {'foo': fields.ObjectField('bar')}
706        fake.obj_relationships = {'foo': [('1.2', '1.0'),
707                                          ('1.3', '1.2')]}
708
709        checker = fixture.ObjectVersionChecker()
710        checker._test_relationships_in_order(fake)
711
712    def _test_test_relationships_in_order_bad(self, fake_rels):
713        fake = mock.MagicMock()
714        fake.VERSION = '1.5'
715        fake.fields = {'foo': fields.ObjectField('bar')}
716        fake.obj_relationships = fake_rels
717        checker = fixture.ObjectVersionChecker()
718        self.assertRaises(AssertionError,
719                          checker._test_relationships_in_order, fake)
720
721    def test_test_relationships_in_order_bad_my_version(self):
722        self._test_test_relationships_in_order_bad(
723            {'foo': [('1.4', '1.1'), ('1.3', '1.2')]})
724
725    def test_test_relationships_in_order_bad_child_version(self):
726        self._test_test_relationships_in_order_bad(
727            {'foo': [('1.2', '1.3'), ('1.3', '1.2')]})
728
729    def test_test_relationships_in_order_bad_both_versions(self):
730        self._test_test_relationships_in_order_bad(
731            {'foo': [('1.5', '1.4'), ('1.3', '1.2')]})
732
733
734class _LocalTest(_BaseTestCase):
735    def setUp(self):
736        super(_LocalTest, self).setUp()
737        self.assertIsNone(base.VersionedObject.indirection_api)
738
739
740class _RemoteTest(_BaseTestCase):
741    def setUp(self):
742        super(_RemoteTest, self).setUp()
743        self.useFixture(fixture.IndirectionFixture())
744
745
746class _TestObject(object):
747    # def test_object_attrs_in_init(self):
748    #     # Spot check a few
749    #     objects.Instance
750    #     objects.InstanceInfoCache
751    #     objects.SecurityGroup
752    #     # Now check the test one in this file. Should be newest version
753    #     self.assertEqual('1.6', objects.MyObj.VERSION)
754
755    def test_hydration_type_error(self):
756        primitive = {'versioned_object.name': 'MyObj',
757                     'versioned_object.namespace': 'versionedobjects',
758                     'versioned_object.version': '1.5',
759                     'versioned_object.data': {'foo': 'a'}}
760        self.assertRaises(ValueError, MyObj.obj_from_primitive, primitive)
761
762    def test_hydration(self):
763        primitive = {'versioned_object.name': 'MyObj',
764                     'versioned_object.namespace': 'versionedobjects',
765                     'versioned_object.version': '1.5',
766                     'versioned_object.data': {'foo': 1}}
767        real_method = MyObj._obj_from_primitive
768
769        def _obj_from_primitive(*args):
770            return real_method(*args)
771
772        with mock.patch.object(MyObj, '_obj_from_primitive') as ofp:
773            ofp.side_effect = _obj_from_primitive
774            obj = MyObj.obj_from_primitive(primitive)
775            ofp.assert_called_once_with(None, '1.5', primitive)
776        self.assertEqual(obj.foo, 1)
777
778    def test_hydration_version_different(self):
779        primitive = {'versioned_object.name': 'MyObj',
780                     'versioned_object.namespace': 'versionedobjects',
781                     'versioned_object.version': '1.2',
782                     'versioned_object.data': {'foo': 1}}
783        obj = MyObj.obj_from_primitive(primitive)
784        self.assertEqual(obj.foo, 1)
785        self.assertEqual('1.2', obj.VERSION)
786
787    def test_hydration_bad_ns(self):
788        primitive = {'versioned_object.name': 'MyObj',
789                     'versioned_object.namespace': 'foo',
790                     'versioned_object.version': '1.5',
791                     'versioned_object.data': {'foo': 1}}
792        self.assertRaises(exception.UnsupportedObjectError,
793                          MyObj.obj_from_primitive, primitive)
794
795    def test_hydration_additional_unexpected_stuff(self):
796        primitive = {'versioned_object.name': 'MyObj',
797                     'versioned_object.namespace': 'versionedobjects',
798                     'versioned_object.version': '1.5.1',
799                     'versioned_object.data': {
800                         'foo': 1,
801                         'unexpected_thing': 'foobar'}}
802        obj = MyObj.obj_from_primitive(primitive)
803        self.assertEqual(1, obj.foo)
804        self.assertFalse(hasattr(obj, 'unexpected_thing'))
805        # NOTE(danms): If we call obj_from_primitive() directly
806        # with a version containing .z, we'll get that version
807        # in the resulting object. In reality, when using the
808        # serializer, we'll get that snipped off (tested
809        # elsewhere)
810        self.assertEqual('1.5.1', obj.VERSION)
811
812    def test_dehydration(self):
813        expected = {'versioned_object.name': 'MyObj',
814                    'versioned_object.namespace': 'versionedobjects',
815                    'versioned_object.version': '1.6',
816                    'versioned_object.data': {'foo': 1}}
817        obj = MyObj(foo=1)
818        obj.obj_reset_changes()
819        self.assertEqual(obj.obj_to_primitive(), expected)
820
821    def test_dehydration_invalid_version(self):
822        obj = MyObj(foo=1)
823        obj.obj_reset_changes()
824        self.assertRaises(exception.InvalidTargetVersion,
825                          obj.obj_to_primitive,
826                          target_version='1.7')
827
828    def test_dehydration_same_version(self):
829        expected = {'versioned_object.name': 'MyObj',
830                    'versioned_object.namespace': 'versionedobjects',
831                    'versioned_object.version': '1.6',
832                    'versioned_object.data': {'foo': 1}}
833        obj = MyObj(foo=1)
834        obj.obj_reset_changes()
835        with mock.patch.object(obj, 'obj_make_compatible') as mock_compat:
836            self.assertEqual(
837                obj.obj_to_primitive(target_version='1.6'), expected)
838            self.assertFalse(mock_compat.called)
839
840    def test_object_property(self):
841        obj = MyObj(foo=1)
842        self.assertEqual(obj.foo, 1)
843
844    def test_object_property_type_error(self):
845        obj = MyObj()
846
847        def fail():
848            obj.foo = 'a'
849        self.assertRaises(ValueError, fail)
850
851    def test_object_dict_syntax(self):
852        obj = MyObj(foo=123, bar=u'text')
853        self.assertEqual(obj['foo'], 123)
854        self.assertIn('bar', obj)
855        self.assertNotIn('missing', obj)
856        self.assertEqual(sorted(iter(obj)),
857                         ['bar', 'foo'])
858        self.assertEqual(sorted(obj.keys()),
859                         ['bar', 'foo'])
860        self.assertEqual(sorted(obj.values(), key=str),
861                         [123, u'text'])
862        self.assertEqual(sorted(obj.items()),
863                         [('bar', u'text'), ('foo', 123)])
864        self.assertEqual(dict(obj),
865                         {'foo': 123, 'bar': u'text'})
866
867    def test_non_dict_remotable(self):
868        @base.VersionedObjectRegistry.register
869        class TestObject(base.VersionedObject):
870            @base.remotable
871            def test_method(self):
872                return 123
873
874        obj = TestObject(context=self.context)
875        self.assertEqual(123, obj.test_method())
876
877    def test_load(self):
878        obj = MyObj()
879        self.assertEqual(obj.bar, 'loaded!')
880
881    def test_load_in_base(self):
882        @base.VersionedObjectRegistry.register
883        class Foo(base.VersionedObject):
884            fields = {'foobar': fields.Field(fields.Integer())}
885        obj = Foo()
886        with self.assertRaisesRegex(NotImplementedError, ".*foobar.*"):
887            obj.foobar
888
889    def test_loaded_in_primitive(self):
890        obj = MyObj(foo=1)
891        obj.obj_reset_changes()
892        self.assertEqual(obj.bar, 'loaded!')
893        expected = {'versioned_object.name': 'MyObj',
894                    'versioned_object.namespace': 'versionedobjects',
895                    'versioned_object.version': '1.6',
896                    'versioned_object.changes': ['bar'],
897                    'versioned_object.data': {'foo': 1,
898                                              'bar': 'loaded!'}}
899        self.assertEqual(obj.obj_to_primitive(), expected)
900
901    def test_changes_in_primitive(self):
902        obj = MyObj(foo=123)
903        self.assertEqual(obj.obj_what_changed(), set(['foo']))
904        primitive = obj.obj_to_primitive()
905        self.assertIn('versioned_object.changes', primitive)
906        obj2 = MyObj.obj_from_primitive(primitive)
907        self.assertEqual(obj2.obj_what_changed(), set(['foo']))
908        obj2.obj_reset_changes()
909        self.assertEqual(obj2.obj_what_changed(), set())
910
911    def test_obj_class_from_name(self):
912        obj = base.VersionedObject.obj_class_from_name('MyObj', '1.5')
913        self.assertEqual('1.5', obj.VERSION)
914
915    def test_obj_class_from_name_latest_compatible(self):
916        obj = base.VersionedObject.obj_class_from_name('MyObj', '1.1')
917        self.assertEqual('1.6', obj.VERSION)
918
919    def test_unknown_objtype(self):
920        self.assertRaises(exception.UnsupportedObjectError,
921                          base.VersionedObject.obj_class_from_name,
922                          'foo', '1.0')
923
924    def test_obj_class_from_name_supported_version(self):
925        self.assertRaises(exception.IncompatibleObjectVersion,
926                          base.VersionedObject.obj_class_from_name,
927                          'MyObj', '1.25')
928        try:
929            base.VersionedObject.obj_class_from_name('MyObj', '1.25')
930        except exception.IncompatibleObjectVersion as error:
931            self.assertEqual('1.6', error.kwargs['supported'])
932
933    def test_orphaned_object(self):
934        obj = MyObj.query(self.context)
935        obj._context = None
936        self.assertRaises(exception.OrphanedObjectError,
937                          obj._update_test)
938
939    def test_changed_1(self):
940        obj = MyObj.query(self.context)
941        obj.foo = 123
942        self.assertEqual(obj.obj_what_changed(), set(['foo']))
943        obj._update_test()
944        self.assertEqual(obj.obj_what_changed(), set(['foo', 'bar']))
945        self.assertEqual(obj.foo, 123)
946
947    def test_changed_2(self):
948        obj = MyObj.query(self.context)
949        obj.foo = 123
950        self.assertEqual(obj.obj_what_changed(), set(['foo']))
951        obj.save()
952        self.assertEqual(obj.obj_what_changed(), set([]))
953        self.assertEqual(obj.foo, 123)
954
955    def test_changed_3(self):
956        obj = MyObj.query(self.context)
957        obj.foo = 123
958        self.assertEqual(obj.obj_what_changed(), set(['foo']))
959        obj.refresh()
960        self.assertEqual(obj.obj_what_changed(), set([]))
961        self.assertEqual(obj.foo, 321)
962        self.assertEqual(obj.bar, 'refreshed')
963
964    def test_changed_4(self):
965        obj = MyObj.query(self.context)
966        obj.bar = 'something'
967        self.assertEqual(obj.obj_what_changed(), set(['bar']))
968        obj.modify_save_modify()
969        self.assertEqual(obj.obj_what_changed(), set(['foo', 'rel_object']))
970        self.assertEqual(obj.foo, 42)
971        self.assertEqual(obj.bar, 'meow')
972        self.assertIsInstance(obj.rel_object, MyOwnedObject)
973
974    def test_changed_with_sub_object(self):
975        @base.VersionedObjectRegistry.register
976        class ParentObject(base.VersionedObject):
977            fields = {'foo': fields.IntegerField(),
978                      'bar': fields.ObjectField('MyObj'),
979                      }
980        obj = ParentObject()
981        self.assertEqual(set(), obj.obj_what_changed())
982        obj.foo = 1
983        self.assertEqual(set(['foo']), obj.obj_what_changed())
984        bar = MyObj()
985        obj.bar = bar
986        self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed())
987        obj.obj_reset_changes()
988        self.assertEqual(set(), obj.obj_what_changed())
989        bar.foo = 1
990        self.assertEqual(set(['bar']), obj.obj_what_changed())
991
992    def test_changed_with_bogus_field(self):
993        obj = MyObj()
994        obj.foo = 123
995        # Add a bogus field name to the changed list, as could be the
996        # case if we're sent some broken primitive from another node.
997        obj._changed_fields.add('does_not_exist')
998        self.assertEqual(set(['foo']), obj.obj_what_changed())
999        self.assertEqual({'foo': 123}, obj.obj_get_changes())
1000
1001    def test_static_result(self):
1002        obj = MyObj.query(self.context)
1003        self.assertEqual(obj.bar, 'bar')
1004        result = obj.marco()
1005        self.assertEqual(result, 'polo')
1006
1007    def test_updates(self):
1008        obj = MyObj.query(self.context)
1009        self.assertEqual(obj.foo, 1)
1010        obj._update_test()
1011        self.assertEqual(obj.bar, 'updated')
1012
1013    def test_contains(self):
1014        obj = MyOwnedObject()
1015        self.assertNotIn('baz', obj)
1016        obj.baz = 1
1017        self.assertIn('baz', obj)
1018        self.assertNotIn('does_not_exist', obj)
1019
1020    def test_obj_attr_is_set(self):
1021        obj = MyObj(foo=1)
1022        self.assertTrue(obj.obj_attr_is_set('foo'))
1023        self.assertFalse(obj.obj_attr_is_set('bar'))
1024        self.assertRaises(AttributeError, obj.obj_attr_is_set, 'bang')
1025
1026    def test_obj_reset_changes_recursive(self):
1027        obj = MyObj(rel_object=MyOwnedObject(baz=123),
1028                    rel_objects=[MyOwnedObject(baz=456)])
1029        self.assertEqual(set(['rel_object', 'rel_objects']),
1030                         obj.obj_what_changed())
1031        obj.obj_reset_changes()
1032        self.assertEqual(set(['rel_object']), obj.obj_what_changed())
1033        self.assertEqual(set(['baz']), obj.rel_object.obj_what_changed())
1034        self.assertEqual(set(['baz']), obj.rel_objects[0].obj_what_changed())
1035        obj.obj_reset_changes(recursive=True, fields=['foo'])
1036        self.assertEqual(set(['rel_object']), obj.obj_what_changed())
1037        self.assertEqual(set(['baz']), obj.rel_object.obj_what_changed())
1038        self.assertEqual(set(['baz']), obj.rel_objects[0].obj_what_changed())
1039        obj.obj_reset_changes(recursive=True)
1040        self.assertEqual(set([]), obj.rel_object.obj_what_changed())
1041        self.assertEqual(set([]), obj.obj_what_changed())
1042
1043    def test_get(self):
1044        obj = MyObj(foo=1)
1045        # Foo has value, should not get the default
1046        self.assertEqual(obj.get('foo', 2), 1)
1047        # Foo has value, should return the value without error
1048        self.assertEqual(obj.get('foo'), 1)
1049        # Bar is not loaded, so we should get the default
1050        self.assertEqual(obj.get('bar', 'not-loaded'), 'not-loaded')
1051        # Bar without a default should lazy-load
1052        self.assertEqual(obj.get('bar'), 'loaded!')
1053        # Bar now has a default, but loaded value should be returned
1054        self.assertEqual(obj.get('bar', 'not-loaded'), 'loaded!')
1055        # Invalid attribute should raise AttributeError
1056        self.assertRaises(AttributeError, obj.get, 'nothing')
1057        # ...even with a default
1058        self.assertRaises(AttributeError, obj.get, 'nothing', 3)
1059
1060    def test_object_inheritance(self):
1061        base_fields = []
1062        myobj_fields = (['foo', 'bar', 'missing',
1063                         'readonly', 'rel_object',
1064                         'rel_objects', 'mutable_default', 'timestamp'] +
1065                        base_fields)
1066        myobj3_fields = ['new_field']
1067        self.assertTrue(issubclass(TestSubclassedObject, MyObj))
1068        self.assertEqual(len(myobj_fields), len(MyObj.fields))
1069        self.assertEqual(set(myobj_fields), set(MyObj.fields.keys()))
1070        self.assertEqual(len(myobj_fields) + len(myobj3_fields),
1071                         len(TestSubclassedObject.fields))
1072        self.assertEqual(set(myobj_fields) | set(myobj3_fields),
1073                         set(TestSubclassedObject.fields.keys()))
1074
1075    def test_obj_as_admin(self):
1076        self.skipTest('oslo.context does not support elevated()')
1077        obj = MyObj(context=self.context)
1078
1079        def fake(*args, **kwargs):
1080            self.assertTrue(obj._context.is_admin)
1081
1082        with mock.patch.object(obj, 'obj_reset_changes') as mock_fn:
1083            mock_fn.side_effect = fake
1084            with obj.obj_as_admin():
1085                obj.save()
1086            self.assertTrue(mock_fn.called)
1087
1088        self.assertFalse(obj._context.is_admin)
1089
1090    def test_get_changes(self):
1091        obj = MyObj()
1092        self.assertEqual({}, obj.obj_get_changes())
1093        obj.foo = 123
1094        self.assertEqual({'foo': 123}, obj.obj_get_changes())
1095        obj.bar = 'test'
1096        self.assertEqual({'foo': 123, 'bar': 'test'}, obj.obj_get_changes())
1097        obj.obj_reset_changes()
1098        self.assertEqual({}, obj.obj_get_changes())
1099
1100        timestamp = datetime.datetime(2001, 1, 1, tzinfo=pytz.utc)
1101        with mock.patch.object(timeutils, 'utcnow') as mock_utcnow:
1102            mock_utcnow.return_value = timestamp
1103            obj.timestamp = timeutils.utcnow()
1104            self.assertEqual({'timestamp': timestamp}, obj.obj_get_changes())
1105
1106        obj.obj_reset_changes()
1107        self.assertEqual({}, obj.obj_get_changes())
1108
1109        # Timestamp without tzinfo causes mismatch
1110        timestamp = datetime.datetime(2001, 1, 1)
1111        with mock.patch.object(timeutils, 'utcnow') as mock_utcnow:
1112            mock_utcnow.return_value = timestamp
1113            obj.timestamp = timeutils.utcnow()
1114            self.assertRaises(TypeError, obj.obj_get_changes())
1115
1116        obj.obj_reset_changes()
1117        self.assertEqual({}, obj.obj_get_changes())
1118
1119    def test_obj_fields(self):
1120        class TestObj(base.VersionedObject):
1121            fields = {'foo': fields.Field(fields.Integer())}
1122            obj_extra_fields = ['bar']
1123
1124            @property
1125            def bar(self):
1126                return 'this is bar'
1127
1128        obj = TestObj()
1129        self.assertEqual(['foo', 'bar'], obj.obj_fields)
1130
1131    def test_obj_context(self):
1132        class TestObj(base.VersionedObject):
1133            pass
1134
1135        # context is available through the public property
1136        context = mock.Mock()
1137        obj = TestObj(context)
1138        self.assertEqual(context, obj.obj_context)
1139
1140        # ..but it's not available for update
1141        new_context = mock.Mock()
1142        self.assertRaises(
1143            AttributeError,
1144            setattr, obj, 'obj_context', new_context)
1145
1146    def test_obj_constructor(self):
1147        obj = MyObj(context=self.context, foo=123, bar='abc')
1148        self.assertEqual(123, obj.foo)
1149        self.assertEqual('abc', obj.bar)
1150        self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed())
1151
1152    def test_obj_read_only(self):
1153        obj = MyObj(context=self.context, foo=123, bar='abc')
1154        obj.readonly = 1
1155        self.assertRaises(exception.ReadOnlyFieldError, setattr,
1156                          obj, 'readonly', 2)
1157
1158    def test_obj_mutable_default(self):
1159        obj = MyObj(context=self.context, foo=123, bar='abc')
1160        obj.mutable_default = None
1161        obj.mutable_default.append('s1')
1162        self.assertEqual(obj.mutable_default, ['s1'])
1163
1164        obj1 = MyObj(context=self.context, foo=123, bar='abc')
1165        obj1.mutable_default = None
1166        obj1.mutable_default.append('s2')
1167        self.assertEqual(obj1.mutable_default, ['s2'])
1168
1169    def test_obj_mutable_default_set_default(self):
1170        obj1 = MyObj(context=self.context, foo=123, bar='abc')
1171        obj1.obj_set_defaults('mutable_default')
1172        self.assertEqual(obj1.mutable_default, [])
1173        obj1.mutable_default.append('s1')
1174        self.assertEqual(obj1.mutable_default, ['s1'])
1175
1176        obj2 = MyObj(context=self.context, foo=123, bar='abc')
1177        obj2.obj_set_defaults('mutable_default')
1178        self.assertEqual(obj2.mutable_default, [])
1179        obj2.mutable_default.append('s2')
1180        self.assertEqual(obj2.mutable_default, ['s2'])
1181
1182    def test_obj_repr(self):
1183        obj = MyObj(foo=123)
1184        self.assertEqual('MyObj(bar=<?>,foo=123,missing=<?>,'
1185                         'mutable_default=<?>,readonly=<?>,'
1186                         'rel_object=<?>,rel_objects=<?>,timestamp=<?>)',
1187                         repr(obj))
1188
1189    def test_obj_repr_sensitive(self):
1190        obj = MySensitiveObj(data="""{'admin_password':'mypassword'}""")
1191        self.assertEqual(
1192            'MySensitiveObj(data=\'{\'admin_password\':\'***\'}\')', repr(obj))
1193
1194        obj2 = MySensitiveObj()
1195        self.assertEqual('MySensitiveObj(data=<?>)', repr(obj2))
1196
1197    def test_obj_repr_unicode(self):
1198        obj = MyObj(bar=u'\u0191\u01A1\u01A1')
1199        # verify the unicode string has been encoded as ASCII if on python 2
1200        self.assertEqual("MyObj(bar='\u0191\u01A1\u01A1',foo=<?>,"
1201                         "missing=<?>,mutable_default=<?>,readonly=<?>,"
1202                         "rel_object=<?>,rel_objects=<?>,timestamp=<?>)",
1203                         repr(obj))
1204
1205    def test_obj_make_obj_compatible_with_relationships(self):
1206        subobj = MyOwnedObject(baz=1)
1207        obj = MyObj(rel_object=subobj)
1208        obj.obj_relationships = {
1209            'rel_object': [('1.5', '1.1'), ('1.7', '1.2')],
1210        }
1211        primitive = obj.obj_to_primitive()['versioned_object.data']
1212        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1213            obj._obj_make_obj_compatible(copy.copy(primitive), '1.8',
1214                                         'rel_object')
1215            self.assertFalse(mock_compat.called)
1216
1217        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1218            obj._obj_make_obj_compatible(copy.copy(primitive),
1219                                         '1.7', 'rel_object')
1220            mock_compat.assert_called_once_with(
1221                primitive['rel_object']['versioned_object.data'], '1.2')
1222            self.assertEqual(
1223                '1.2', primitive['rel_object']['versioned_object.version'])
1224
1225        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1226            obj._obj_make_obj_compatible(copy.copy(primitive),
1227                                         '1.6', 'rel_object')
1228            mock_compat.assert_called_once_with(
1229                primitive['rel_object']['versioned_object.data'], '1.1')
1230            self.assertEqual(
1231                '1.1', primitive['rel_object']['versioned_object.version'])
1232
1233        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1234            obj._obj_make_obj_compatible(copy.copy(primitive), '1.5',
1235                                         'rel_object')
1236            mock_compat.assert_called_once_with(
1237                primitive['rel_object']['versioned_object.data'], '1.1')
1238            self.assertEqual(
1239                '1.1', primitive['rel_object']['versioned_object.version'])
1240
1241        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1242            _prim = copy.copy(primitive)
1243            obj._obj_make_obj_compatible(_prim, '1.4', 'rel_object')
1244            self.assertFalse(mock_compat.called)
1245            self.assertNotIn('rel_object', _prim)
1246
1247    def test_obj_make_compatible_hits_sub_objects_with_rels(self):
1248        subobj = MyOwnedObject(baz=1)
1249        obj = MyObj(foo=123, rel_object=subobj)
1250        obj.obj_relationships = {'rel_object': [('1.0', '1.0')]}
1251        with mock.patch.object(obj, '_obj_make_obj_compatible') as mock_compat:
1252            obj.obj_make_compatible({'rel_object': 'foo'}, '1.10')
1253            mock_compat.assert_called_once_with({'rel_object': 'foo'}, '1.10',
1254                                                'rel_object')
1255
1256    def test_obj_make_compatible_skips_unset_sub_objects_with_rels(self):
1257        obj = MyObj(foo=123)
1258        obj.obj_relationships = {'rel_object': [('1.0', '1.0')]}
1259        with mock.patch.object(obj, '_obj_make_obj_compatible') as mock_compat:
1260            obj.obj_make_compatible({'rel_object': 'foo'}, '1.10')
1261            self.assertFalse(mock_compat.called)
1262
1263    def test_obj_make_compatible_complains_about_missing_rel_rules(self):
1264        subobj = MyOwnedObject(baz=1)
1265        obj = MyObj(foo=123, rel_object=subobj)
1266        obj.obj_relationships = {}
1267        self.assertRaises(exception.ObjectActionError,
1268                          obj.obj_make_compatible, {}, '1.0')
1269
1270    def test_obj_make_compatible_handles_list_of_objects_with_rels(self):
1271        subobj = MyOwnedObject(baz=1)
1272        obj = MyObj(rel_objects=[subobj])
1273        obj.obj_relationships = {'rel_objects': [('1.0', '1.123')]}
1274
1275        def fake_make_compat(primitive, version, **k):
1276            self.assertEqual('1.123', version)
1277            self.assertIn('baz', primitive)
1278
1279        with mock.patch.object(subobj, 'obj_make_compatible') as mock_mc:
1280            mock_mc.side_effect = fake_make_compat
1281            obj.obj_to_primitive('1.0')
1282            self.assertTrue(mock_mc.called)
1283
1284    def test_obj_make_compatible_with_manifest(self):
1285        subobj = MyOwnedObject(baz=1)
1286        obj = MyObj(rel_object=subobj)
1287        obj.obj_relationships = {}
1288        orig_primitive = obj.obj_to_primitive()['versioned_object.data']
1289
1290        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1291            manifest = {'MyOwnedObject': '1.2'}
1292            primitive = copy.deepcopy(orig_primitive)
1293            obj.obj_make_compatible_from_manifest(primitive, '1.5', manifest)
1294            mock_compat.assert_called_once_with(
1295                primitive['rel_object']['versioned_object.data'], '1.2')
1296            self.assertEqual(
1297                '1.2',
1298                primitive['rel_object']['versioned_object.version'])
1299
1300        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1301            manifest = {'MyOwnedObject': '1.0'}
1302            primitive = copy.deepcopy(orig_primitive)
1303            obj.obj_make_compatible_from_manifest(primitive, '1.5', manifest)
1304            mock_compat.assert_called_once_with(
1305                primitive['rel_object']['versioned_object.data'], '1.0')
1306            self.assertEqual(
1307                '1.0',
1308                primitive['rel_object']['versioned_object.version'])
1309
1310        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1311            manifest = {}
1312            primitive = copy.deepcopy(orig_primitive)
1313            obj.obj_make_compatible_from_manifest(primitive, '1.5', manifest)
1314            self.assertFalse(mock_compat.called)
1315            self.assertEqual(
1316                '1.0',
1317                primitive['rel_object']['versioned_object.version'])
1318
1319    def test_obj_make_compatible_with_manifest_subobj(self):
1320        # Make sure that we call the subobject's "from_manifest" method
1321        # as well
1322        subobj = MyOwnedObject(baz=1)
1323        obj = MyObj(rel_object=subobj)
1324        obj.obj_relationships = {}
1325        manifest = {'MyOwnedObject': '1.2'}
1326        primitive = obj.obj_to_primitive()['versioned_object.data']
1327        method = 'obj_make_compatible_from_manifest'
1328        with mock.patch.object(subobj, method) as mock_compat:
1329            obj.obj_make_compatible_from_manifest(primitive, '1.5', manifest)
1330            mock_compat.assert_called_once_with(
1331                primitive['rel_object']['versioned_object.data'],
1332                '1.2', version_manifest=manifest)
1333
1334    def test_obj_make_compatible_with_manifest_subobj_list(self):
1335        # Make sure that we call the subobject's "from_manifest" method
1336        # as well
1337        subobj = MyOwnedObject(baz=1)
1338        obj = MyObj(rel_objects=[subobj])
1339        obj.obj_relationships = {}
1340        manifest = {'MyOwnedObject': '1.2'}
1341        primitive = obj.obj_to_primitive()['versioned_object.data']
1342        method = 'obj_make_compatible_from_manifest'
1343        with mock.patch.object(subobj, method) as mock_compat:
1344            obj.obj_make_compatible_from_manifest(primitive, '1.5', manifest)
1345            mock_compat.assert_called_once_with(
1346                primitive['rel_objects'][0]['versioned_object.data'],
1347                '1.2', version_manifest=manifest)
1348
1349    def test_obj_make_compatible_removes_field_cleans_changes(self):
1350        @base.VersionedObjectRegistry.register_if(False)
1351        class TestObject(base.VersionedObject):
1352            VERSION = '1.1'
1353            fields = {'foo': fields.StringField(),
1354                      'bar': fields.StringField()}
1355
1356            def obj_make_compatible(self, primitive, target_version):
1357                del primitive['bar']
1358
1359        obj = TestObject(foo='test1', bar='test2')
1360        prim = obj.obj_to_primitive('1.0')
1361        self.assertEqual(['foo'], prim['versioned_object.changes'])
1362
1363    def test_delattr(self):
1364        obj = MyObj(bar='foo')
1365        del obj.bar
1366
1367        # Should appear unset now
1368        self.assertFalse(obj.obj_attr_is_set('bar'))
1369
1370        # Make sure post-delete, references trigger lazy loads
1371        self.assertEqual('loaded!', getattr(obj, 'bar'))
1372
1373    def test_delattr_unset(self):
1374        obj = MyObj()
1375        self.assertRaises(AttributeError, delattr, obj, 'bar')
1376
1377    def test_obj_make_compatible_on_list_base(self):
1378        @base.VersionedObjectRegistry.register_if(False)
1379        class MyList(base.ObjectListBase, base.VersionedObject):
1380            VERSION = '1.1'
1381            fields = {'objects': fields.ListOfObjectsField('MyObj')}
1382
1383        childobj = MyObj(foo=1)
1384        listobj = MyList(objects=[childobj])
1385        compat_func = 'obj_make_compatible_from_manifest'
1386        with mock.patch.object(childobj, compat_func) as mock_compat:
1387            listobj.obj_to_primitive(target_version='1.0')
1388            mock_compat.assert_called_once_with({'foo': 1}, '1.0',
1389                                                version_manifest=None)
1390
1391    def test_comparable_objects(self):
1392        class NonVersionedObject(object):
1393            pass
1394
1395        obj1 = MyComparableObj(foo=1)
1396        obj2 = MyComparableObj(foo=1)
1397        obj3 = MyComparableObj(foo=2)
1398        obj4 = NonVersionedObject()
1399        self.assertTrue(obj1 == obj2)
1400        self.assertFalse(obj1 == obj3)
1401        self.assertFalse(obj1 == obj4)
1402        self.assertNotEqual(obj1, None)
1403
1404    def test_compound_clone(self):
1405        obj = MyCompoundObject()
1406        obj.foo = [1, 2, 3]
1407        obj.bar = {"a": 1, "b": 2, "c": 3}
1408        obj.baz = set([1, 2, 3])
1409        copy = obj.obj_clone()
1410        self.assertEqual(obj.foo, copy.foo)
1411        self.assertEqual(obj.bar, copy.bar)
1412        self.assertEqual(obj.baz, copy.baz)
1413        # ensure that the cloned object still coerces values in its compounds
1414        copy.foo.append("4")
1415        copy.bar.update(d="4")
1416        copy.baz.add("4")
1417        self.assertEqual([1, 2, 3, 4], copy.foo)
1418        self.assertEqual({"a": 1, "b": 2, "c": 3, "d": 4}, copy.bar)
1419        self.assertEqual(set([1, 2, 3, 4]), copy.baz)
1420
1421    def test_obj_list_fields_modifications(self):
1422        @base.VersionedObjectRegistry.register
1423        class ObjWithList(base.VersionedObject):
1424            fields = {
1425                'list_field': fields.Field(fields.List(fields.Integer())),
1426            }
1427        obj = ObjWithList()
1428
1429        def set_by_index(val):
1430            obj.list_field[0] = val
1431
1432        def append(val):
1433            obj.list_field.append(val)
1434
1435        def extend(val):
1436            obj.list_field.extend([val])
1437
1438        def add(val):
1439            obj.list_field = obj.list_field + [val]
1440
1441        def iadd(val):
1442            """Test += corner case
1443
1444            a=a+b and a+=b use different magic methods under the hood:
1445            first one calls __add__ which clones initial value before the
1446            assignment, second one call __iadd__ which modifies the initial
1447            list.
1448            Assignment should cause coercing in both cases, but __iadd__ may
1449            corrupt the initial value even if the assignment fails.
1450            So it should be overridden as well, and this test is needed to
1451            verify it
1452            """
1453            obj.list_field += [val]
1454
1455        def insert(val):
1456            obj.list_field.insert(0, val)
1457
1458        def simple_slice(val):
1459            obj.list_field[:] = [val]
1460
1461        def extended_slice(val):
1462            """Extended slice case
1463
1464            Extended slice (and regular slices in py3) are handled differently
1465            thus needing a separate test
1466            """
1467            obj.list_field[::2] = [val]
1468
1469        # positive tests to ensure that coercing works properly
1470        obj.list_field = ["42"]
1471        set_by_index("1")
1472        append("2")
1473        extend("3")
1474        add("4")
1475        iadd("5")
1476        insert("0")
1477        self.assertEqual([0, 1, 2, 3, 4, 5], obj.list_field)
1478        simple_slice("10")
1479        self.assertEqual([10], obj.list_field)
1480        extended_slice("42")
1481        self.assertEqual([42], obj.list_field)
1482        obj.obj_reset_changes()
1483        # negative tests with non-coerceable values
1484        self.assertRaises(ValueError, set_by_index, "abc")
1485        self.assertRaises(ValueError, append, "abc")
1486        self.assertRaises(ValueError, extend, "abc")
1487        self.assertRaises(ValueError, add, "abc")
1488        self.assertRaises(ValueError, iadd, "abc")
1489        self.assertRaises(ValueError, insert, "abc")
1490        self.assertRaises(ValueError, simple_slice, "abc")
1491        self.assertRaises(ValueError, extended_slice, "abc")
1492        # ensure that nothing has been changed
1493        self.assertEqual([42], obj.list_field)
1494        self.assertEqual({}, obj.obj_get_changes())
1495
1496    def test_obj_dict_field_modifications(self):
1497        @base.VersionedObjectRegistry.register
1498        class ObjWithDict(base.VersionedObject):
1499            fields = {
1500                'dict_field': fields.Field(fields.Dict(fields.Integer())),
1501            }
1502        obj = ObjWithDict()
1503        obj.dict_field = {"1": 1, "3": 3, "4": 4}
1504
1505        def set_by_key(key, value):
1506            obj.dict_field[key] = value
1507
1508        def add_by_key(key, value):
1509            obj.dict_field[key] = value
1510
1511        def update_w_dict(key, value):
1512            obj.dict_field.update({key: value})
1513
1514        def update_w_kwargs(key, value):
1515            obj.dict_field.update(**{key: value})
1516
1517        def setdefault(key, value):
1518            obj.dict_field.setdefault(key, value)
1519
1520        # positive tests to ensure that coercing works properly
1521        set_by_key("1", "10")
1522        add_by_key("2", "20")
1523        update_w_dict("3", "30")
1524        update_w_kwargs("4", "40")
1525        setdefault("5", "50")
1526        self.assertEqual({"1": 10, "2": 20, "3": 30, "4": 40, "5": 50},
1527                         obj.dict_field)
1528        obj.obj_reset_changes()
1529        # negative tests with non-coerceable values
1530        self.assertRaises(ValueError, set_by_key, "key", "abc")
1531        self.assertRaises(ValueError, add_by_key, "other", "abc")
1532        self.assertRaises(ValueError, update_w_dict, "key", "abc")
1533        self.assertRaises(ValueError, update_w_kwargs, "key", "abc")
1534        self.assertRaises(ValueError, setdefault, "other", "abc")
1535        # ensure that nothing has been changed
1536        self.assertEqual({"1": 10, "2": 20, "3": 30, "4": 40, "5": 50},
1537                         obj.dict_field)
1538        self.assertEqual({}, obj.obj_get_changes())
1539
1540    def test_obj_set_field_modifications(self):
1541        @base.VersionedObjectRegistry.register
1542        class ObjWithSet(base.VersionedObject):
1543            fields = {
1544                'set_field': fields.Field(fields.Set(fields.Integer()))
1545            }
1546        obj = ObjWithSet()
1547        obj.set_field = set([42])
1548
1549        def add(value):
1550            obj.set_field.add(value)
1551
1552        def update_w_set(value):
1553            obj.set_field.update(set([value]))
1554
1555        def update_w_list(value):
1556            obj.set_field.update([value, value, value])
1557
1558        def sym_diff_upd(value):
1559            obj.set_field.symmetric_difference_update(set([value]))
1560
1561        def union(value):
1562            obj.set_field = obj.set_field | set([value])
1563
1564        def iunion(value):
1565            obj.set_field |= set([value])
1566
1567        def xor(value):
1568            obj.set_field = obj.set_field ^ set([value])
1569
1570        def ixor(value):
1571            obj.set_field ^= set([value])
1572        # positive tests to ensure that coercing works properly
1573        sym_diff_upd("42")
1574        add("1")
1575        update_w_list("2")
1576        update_w_set("3")
1577        union("4")
1578        iunion("5")
1579        xor("6")
1580        ixor("7")
1581        self.assertEqual(set([1, 2, 3, 4, 5, 6, 7]), obj.set_field)
1582        obj.set_field = set([42])
1583        obj.obj_reset_changes()
1584        # negative tests with non-coerceable values
1585        self.assertRaises(ValueError, add, "abc")
1586        self.assertRaises(ValueError, update_w_list, "abc")
1587        self.assertRaises(ValueError, update_w_set, "abc")
1588        self.assertRaises(ValueError, sym_diff_upd, "abc")
1589        self.assertRaises(ValueError, union, "abc")
1590        self.assertRaises(ValueError, iunion, "abc")
1591        self.assertRaises(ValueError, xor, "abc")
1592        self.assertRaises(ValueError, ixor, "abc")
1593        # ensure that nothing has been changed
1594        self.assertEqual(set([42]), obj.set_field)
1595        self.assertEqual({}, obj.obj_get_changes())
1596
1597
1598class TestObject(_LocalTest, _TestObject):
1599    def test_set_defaults(self):
1600        obj = MyObj()
1601        obj.obj_set_defaults('foo')
1602        self.assertTrue(obj.obj_attr_is_set('foo'))
1603        self.assertEqual(1, obj.foo)
1604
1605    def test_set_defaults_no_default(self):
1606        obj = MyObj()
1607        self.assertRaises(exception.ObjectActionError,
1608                          obj.obj_set_defaults, 'bar')
1609
1610    def test_set_all_defaults(self):
1611        obj = MyObj()
1612        obj.obj_set_defaults()
1613        self.assertEqual(set(['mutable_default', 'foo']),
1614                         obj.obj_what_changed())
1615        self.assertEqual(1, obj.foo)
1616
1617    def test_set_defaults_not_overwrite(self):
1618        # NOTE(danms): deleted defaults to False, so verify that it does
1619        # not get reset by obj_set_defaults()
1620        obj = MyObj(deleted=True)
1621        obj.obj_set_defaults()
1622        self.assertEqual(1, obj.foo)
1623        self.assertTrue(obj.deleted)
1624
1625
1626class TestRemoteObject(_RemoteTest, _TestObject):
1627    @mock.patch('oslo_versionedobjects.base.obj_tree_get_versions')
1628    def test_major_version_mismatch(self, mock_otgv):
1629        mock_otgv.return_value = {'MyObj': '2.0'}
1630        self.assertRaises(exception.IncompatibleObjectVersion,
1631                          MyObj2.query, self.context)
1632
1633    @mock.patch('oslo_versionedobjects.base.obj_tree_get_versions')
1634    def test_minor_version_greater(self, mock_otgv):
1635        mock_otgv.return_value = {'MyObj': '1.7'}
1636        self.assertRaises(exception.IncompatibleObjectVersion,
1637                          MyObj2.query, self.context)
1638
1639    @mock.patch('oslo_versionedobjects.base.obj_tree_get_versions')
1640    def test_minor_version_less(self, mock_otgv):
1641        mock_otgv.return_value = {'MyObj': '1.2'}
1642        obj = MyObj2.query(self.context)
1643        self.assertEqual(obj.bar, 'bar')
1644
1645    @mock.patch('oslo_versionedobjects.base.obj_tree_get_versions')
1646    def test_compat(self, mock_otgv):
1647        mock_otgv.return_value = {'MyObj': '1.1'}
1648        obj = MyObj2.query(self.context)
1649        self.assertEqual('oldbar', obj.bar)
1650
1651    @mock.patch('oslo_versionedobjects.base.obj_tree_get_versions')
1652    def test_revision_ignored(self, mock_otgv):
1653        mock_otgv.return_value = {'MyObj': '1.1.456'}
1654        obj = MyObj2.query(self.context)
1655        self.assertEqual('bar', obj.bar)
1656
1657    def test_class_action_falls_back_compat(self):
1658        with mock.patch.object(base.VersionedObject, 'indirection_api') as ma:
1659            ma.object_class_action_versions.side_effect = NotImplementedError
1660            MyObj.query(self.context)
1661            ma.object_class_action.assert_called_once_with(
1662                self.context, 'MyObj', 'query', MyObj.VERSION, (), {})
1663
1664
1665class TestObjectListBase(test.TestCase):
1666    def test_list_like_operations(self):
1667        @base.VersionedObjectRegistry.register
1668        class MyElement(base.VersionedObject):
1669            fields = {'foo': fields.IntegerField()}
1670
1671            def __init__(self, foo):
1672                super(MyElement, self).__init__()
1673                self.foo = foo
1674
1675        class Foo(base.ObjectListBase, base.VersionedObject):
1676            fields = {'objects': fields.ListOfObjectsField('MyElement')}
1677
1678        objlist = Foo(context='foo',
1679                      objects=[MyElement(1), MyElement(2), MyElement(3)])
1680        self.assertEqual(list(objlist), objlist.objects)
1681        self.assertEqual(len(objlist), 3)
1682        self.assertIn(objlist.objects[0], objlist)
1683        self.assertEqual(list(objlist[:1]), [objlist.objects[0]])
1684        self.assertEqual(objlist[:1]._context, 'foo')
1685        self.assertEqual(objlist[2], objlist.objects[2])
1686        self.assertEqual(objlist.count(objlist.objects[0]), 1)
1687        self.assertEqual(objlist.index(objlist.objects[1]), 1)
1688        objlist.sort(key=lambda x: x.foo, reverse=True)
1689        self.assertEqual([3, 2, 1],
1690                         [x.foo for x in objlist])
1691
1692    def test_serialization(self):
1693        @base.VersionedObjectRegistry.register
1694        class Foo(base.ObjectListBase, base.VersionedObject):
1695            fields = {'objects': fields.ListOfObjectsField('Bar')}
1696
1697        @base.VersionedObjectRegistry.register
1698        class Bar(base.VersionedObject):
1699            fields = {'foo': fields.Field(fields.String())}
1700
1701        obj = Foo(objects=[])
1702        for i in 'abc':
1703            bar = Bar(foo=i)
1704            obj.objects.append(bar)
1705
1706        obj2 = base.VersionedObject.obj_from_primitive(obj.obj_to_primitive())
1707        self.assertFalse(obj is obj2)
1708        self.assertEqual([x.foo for x in obj],
1709                         [y.foo for y in obj2])
1710
1711    def _test_object_list_version_mappings(self, list_obj_class):
1712        # Figure out what sort of object this list is for
1713        list_field = list_obj_class.fields['objects']
1714        item_obj_field = list_field._type._element_type
1715        item_obj_name = item_obj_field._type._obj_name
1716
1717        # Look through all object classes of this type and make sure that
1718        # the versions we find are covered by the parent list class
1719        obj_classes = base.VersionedObjectRegistry.obj_classes()[item_obj_name]
1720        for item_class in obj_classes:
1721            if is_test_object(item_class):
1722                continue
1723            self.assertIn(
1724                item_class.VERSION,
1725                list_obj_class.child_versions.values(),
1726                'Version mapping is incomplete for %s' % (
1727                    list_obj_class.__name__))
1728
1729    def test_object_version_mappings(self):
1730        self.skipTest('this needs to be generalized')
1731        # Find all object list classes and make sure that they at least handle
1732        # all the current object versions
1733        for obj_classes in base.VersionedObjectRegistry.obj_classes().values():
1734            for obj_class in obj_classes:
1735                if issubclass(obj_class, base.ObjectListBase):
1736                    self._test_object_list_version_mappings(obj_class)
1737
1738    def test_obj_make_compatible_child_versions(self):
1739        @base.VersionedObjectRegistry.register
1740        class MyElement(base.VersionedObject):
1741            fields = {'foo': fields.IntegerField()}
1742
1743        @base.VersionedObjectRegistry.register
1744        class Foo(base.ObjectListBase, base.VersionedObject):
1745            VERSION = '1.1'
1746            fields = {'objects': fields.ListOfObjectsField('MyElement')}
1747            child_versions = {'1.0': '1.0', '1.1': '1.0'}
1748
1749        subobj = MyElement(foo=1)
1750        obj = Foo(objects=[subobj])
1751        primitive = obj.obj_to_primitive()['versioned_object.data']
1752
1753        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1754            obj.obj_make_compatible(copy.copy(primitive), '1.1')
1755            self.assertTrue(mock_compat.called)
1756
1757    def test_obj_make_compatible_obj_relationships(self):
1758        @base.VersionedObjectRegistry.register
1759        class MyElement(base.VersionedObject):
1760            fields = {'foo': fields.IntegerField()}
1761
1762        @base.VersionedObjectRegistry.register
1763        class Bar(base.ObjectListBase, base.VersionedObject):
1764            VERSION = '1.1'
1765            fields = {'objects': fields.ListOfObjectsField('MyElement')}
1766            obj_relationships = {
1767                'objects': [('1.0', '1.0'), ('1.1', '1.0')]
1768            }
1769
1770        subobj = MyElement(foo=1)
1771        obj = Bar(objects=[subobj])
1772        primitive = obj.obj_to_primitive()['versioned_object.data']
1773
1774        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1775            obj.obj_make_compatible(copy.copy(primitive), '1.1')
1776            self.assertTrue(mock_compat.called)
1777
1778    def test_obj_make_compatible_no_relationships(self):
1779        @base.VersionedObjectRegistry.register
1780        class MyElement(base.VersionedObject):
1781            fields = {'foo': fields.IntegerField()}
1782
1783        @base.VersionedObjectRegistry.register
1784        class Baz(base.ObjectListBase, base.VersionedObject):
1785            VERSION = '1.1'
1786            fields = {'objects': fields.ListOfObjectsField('MyElement')}
1787
1788        subobj = MyElement(foo=1)
1789        obj = Baz(objects=[subobj])
1790        primitive = obj.obj_to_primitive()['versioned_object.data']
1791
1792        with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
1793            obj.obj_make_compatible(copy.copy(primitive), '1.1')
1794            self.assertTrue(mock_compat.called)
1795
1796    def test_list_changes(self):
1797        @base.VersionedObjectRegistry.register
1798        class Foo(base.ObjectListBase, base.VersionedObject):
1799            fields = {'objects': fields.ListOfObjectsField('Bar')}
1800
1801        @base.VersionedObjectRegistry.register
1802        class Bar(base.VersionedObject):
1803            fields = {'foo': fields.StringField()}
1804
1805        obj = Foo(objects=[])
1806        self.assertEqual(set(['objects']), obj.obj_what_changed())
1807        obj.objects.append(Bar(foo='test'))
1808        self.assertEqual(set(['objects']), obj.obj_what_changed())
1809        obj.obj_reset_changes()
1810        # This should still look dirty because the child is dirty
1811        self.assertEqual(set(['objects']), obj.obj_what_changed())
1812        obj.objects[0].obj_reset_changes()
1813        # This should now look clean because the child is clean
1814        self.assertEqual(set(), obj.obj_what_changed())
1815
1816    def test_initialize_objects(self):
1817        class Foo(base.ObjectListBase, base.VersionedObject):
1818            fields = {'objects': fields.ListOfObjectsField('Bar')}
1819
1820        class Bar(base.VersionedObject):
1821            fields = {'foo': fields.StringField()}
1822
1823        obj = Foo()
1824        self.assertEqual([], obj.objects)
1825        self.assertEqual(set(), obj.obj_what_changed())
1826
1827    def test_obj_repr(self):
1828        @base.VersionedObjectRegistry.register
1829        class Foo(base.ObjectListBase, base.VersionedObject):
1830            fields = {'objects': fields.ListOfObjectsField('Bar')}
1831
1832        @base.VersionedObjectRegistry.register
1833        class Bar(base.VersionedObject):
1834            fields = {'uuid': fields.StringField()}
1835
1836        obj = Foo(objects=[Bar(uuid='fake-uuid')])
1837        self.assertEqual('Foo(objects=[Bar(fake-uuid)])', repr(obj))
1838
1839
1840class TestObjectSerializer(_BaseTestCase):
1841    def test_serialize_entity_primitive(self):
1842        ser = base.VersionedObjectSerializer()
1843        for thing in (1, 'foo', [1, 2], {'foo': 'bar'}):
1844            self.assertEqual(thing, ser.serialize_entity(None, thing))
1845
1846    def test_deserialize_entity_primitive(self):
1847        ser = base.VersionedObjectSerializer()
1848        for thing in (1, 'foo', [1, 2], {'foo': 'bar'}):
1849            self.assertEqual(thing, ser.deserialize_entity(None, thing))
1850
1851    def test_serialize_set_to_list(self):
1852        ser = base.VersionedObjectSerializer()
1853        self.assertEqual([1, 2], ser.serialize_entity(None, set([1, 2])))
1854
1855    @mock.patch('oslo_versionedobjects.base.VersionedObject.indirection_api')
1856    def _test_deserialize_entity_newer(self, obj_version, backported_to,
1857                                       mock_iapi,
1858                                       my_version='1.6'):
1859        ser = base.VersionedObjectSerializer()
1860        mock_iapi.object_backport_versions.return_value = 'backported'
1861
1862        @base.VersionedObjectRegistry.register
1863        class MyTestObj(MyObj):
1864            VERSION = my_version
1865
1866        obj = MyTestObj()
1867        obj.VERSION = obj_version
1868        primitive = obj.obj_to_primitive()
1869        result = ser.deserialize_entity(self.context, primitive)
1870        if backported_to is None:
1871            self.assertFalse(mock_iapi.object_backport_versions.called)
1872        else:
1873            self.assertEqual('backported', result)
1874            mock_iapi.object_backport_versions.assert_called_with(
1875                self.context, primitive, {'MyTestObj': my_version,
1876                                          'MyOwnedObject': '1.0'})
1877
1878    def test_deserialize_entity_newer_version_backports(self):
1879        self._test_deserialize_entity_newer('1.25', '1.6')
1880
1881    def test_deserialize_entity_newer_revision_does_not_backport_zero(self):
1882        self._test_deserialize_entity_newer('1.6.0', None)
1883
1884    def test_deserialize_entity_newer_revision_does_not_backport(self):
1885        self._test_deserialize_entity_newer('1.6.1', None)
1886
1887    def test_deserialize_entity_newer_version_passes_revision(self):
1888        self._test_deserialize_entity_newer('1.7', '1.6.1', my_version='1.6.1')
1889
1890    def test_deserialize_dot_z_with_extra_stuff(self):
1891        primitive = {'versioned_object.name': 'MyObj',
1892                     'versioned_object.namespace': 'versionedobjects',
1893                     'versioned_object.version': '1.6.1',
1894                     'versioned_object.data': {
1895                         'foo': 1,
1896                         'unexpected_thing': 'foobar'}}
1897        ser = base.VersionedObjectSerializer()
1898        obj = ser.deserialize_entity(self.context, primitive)
1899        self.assertEqual(1, obj.foo)
1900        self.assertFalse(hasattr(obj, 'unexpected_thing'))
1901        # NOTE(danms): The serializer is where the logic lives that
1902        # avoids backports for cases where only a .z difference in
1903        # the received object version is detected. As a result, we
1904        # end up with a version of what we expected, effectively the
1905        # .0 of the object.
1906        self.assertEqual('1.6', obj.VERSION)
1907
1908    def test_deserialize_entity_newer_version_no_indirection(self):
1909        ser = base.VersionedObjectSerializer()
1910        obj = MyObj()
1911        obj.VERSION = '1.25'
1912        primitive = obj.obj_to_primitive()
1913        self.assertRaises(exception.IncompatibleObjectVersion,
1914                          ser.deserialize_entity, self.context, primitive)
1915
1916    def _test_nested_backport(self, old):
1917        @base.VersionedObjectRegistry.register
1918        class Parent(base.VersionedObject):
1919            VERSION = '1.0'
1920
1921            fields = {
1922                'child': fields.ObjectField('MyObj'),
1923            }
1924
1925        @base.VersionedObjectRegistry.register  # noqa
1926        class Parent(base.VersionedObject):  # noqa
1927            VERSION = '1.1'
1928
1929            fields = {
1930                'child': fields.ObjectField('MyObj'),
1931            }
1932
1933        child = MyObj(foo=1)
1934        parent = Parent(child=child)
1935        prim = parent.obj_to_primitive()
1936        child_prim = prim['versioned_object.data']['child']
1937        child_prim['versioned_object.version'] = '1.10'
1938        ser = base.VersionedObjectSerializer()
1939        with mock.patch.object(base.VersionedObject, 'indirection_api') as a:
1940            if old:
1941                a.object_backport_versions.side_effect = NotImplementedError
1942            ser.deserialize_entity(self.context, prim)
1943            a.object_backport_versions.assert_called_once_with(
1944                self.context, prim, {'Parent': '1.1',
1945                                     'MyObj': '1.6',
1946                                     'MyOwnedObject': '1.0'})
1947            if old:
1948                # NOTE(danms): This should be the version of the parent object,
1949                # not the child. If wrong, this will be '1.6', which is the max
1950                # child version in our registry.
1951                a.object_backport.assert_called_once_with(
1952                    self.context, prim, '1.1')
1953
1954    def test_nested_backport_new_method(self):
1955        self._test_nested_backport(old=False)
1956
1957    def test_nested_backport_old_method(self):
1958        self._test_nested_backport(old=True)
1959
1960    def test_object_serialization(self):
1961        ser = base.VersionedObjectSerializer()
1962        obj = MyObj()
1963        primitive = ser.serialize_entity(self.context, obj)
1964        self.assertIn('versioned_object.name', primitive)
1965        obj2 = ser.deserialize_entity(self.context, primitive)
1966        self.assertIsInstance(obj2, MyObj)
1967        self.assertEqual(self.context, obj2._context)
1968
1969    def test_object_serialization_iterables(self):
1970        ser = base.VersionedObjectSerializer()
1971        obj = MyObj()
1972        for iterable in (list, tuple, set):
1973            thing = iterable([obj])
1974            primitive = ser.serialize_entity(self.context, thing)
1975            self.assertEqual(1, len(primitive))
1976            for item in primitive:
1977                self.assertNotIsInstance(item, base.VersionedObject)
1978            thing2 = ser.deserialize_entity(self.context, primitive)
1979            self.assertEqual(1, len(thing2))
1980            for item in thing2:
1981                self.assertIsInstance(item, MyObj)
1982        # dict case
1983        thing = {'key': obj}
1984        primitive = ser.serialize_entity(self.context, thing)
1985        self.assertEqual(1, len(primitive))
1986        for item in primitive.values():
1987            self.assertNotIsInstance(item, base.VersionedObject)
1988        thing2 = ser.deserialize_entity(self.context, primitive)
1989        self.assertEqual(1, len(thing2))
1990        for item in thing2.values():
1991            self.assertIsInstance(item, MyObj)
1992
1993        # object-action updates dict case
1994        thing = {'foo': obj.obj_to_primitive()}
1995        primitive = ser.serialize_entity(self.context, thing)
1996        self.assertEqual(thing, primitive)
1997        thing2 = ser.deserialize_entity(self.context, thing)
1998        self.assertIsInstance(thing2['foo'], base.VersionedObject)
1999
2000    def test_serializer_subclass_namespace(self):
2001        @base.VersionedObjectRegistry.register
2002        class MyNSObj(base.VersionedObject):
2003            OBJ_SERIAL_NAMESPACE = 'foo'
2004            fields = {'foo': fields.IntegerField()}
2005
2006        class MySerializer(base.VersionedObjectSerializer):
2007            OBJ_BASE_CLASS = MyNSObj
2008
2009        ser = MySerializer()
2010        obj = MyNSObj(foo=123)
2011        obj2 = ser.deserialize_entity(None, ser.serialize_entity(None, obj))
2012        self.assertIsInstance(obj2, MyNSObj)
2013        self.assertEqual(obj.foo, obj2.foo)
2014
2015    def test_serializer_subclass_namespace_mismatch(self):
2016        @base.VersionedObjectRegistry.register
2017        class MyNSObj(base.VersionedObject):
2018            OBJ_SERIAL_NAMESPACE = 'foo'
2019            fields = {'foo': fields.IntegerField()}
2020
2021        class MySerializer(base.VersionedObjectSerializer):
2022            OBJ_BASE_CLASS = MyNSObj
2023
2024        myser = MySerializer()
2025        voser = base.VersionedObjectSerializer()
2026        obj = MyObj(foo=123)
2027        obj2 = myser.deserialize_entity(None,
2028                                        voser.serialize_entity(None, obj))
2029
2030        # NOTE(danms): The new serializer should have ignored the objects
2031        # serialized by the base serializer, so obj2 here should be a dict
2032        # primitive and not a hydrated object
2033        self.assertNotIsInstance(obj2, MyNSObj)
2034        self.assertIn('versioned_object.name', obj2)
2035
2036    def test_serializer_subclass_base_object_indirection(self):
2037        @base.VersionedObjectRegistry.register
2038        class MyNSObj(base.VersionedObject):
2039            OBJ_SERIAL_NAMESPACE = 'foo'
2040            fields = {'foo': fields.IntegerField()}
2041            indirection_api = mock.MagicMock()
2042
2043        class MySerializer(base.VersionedObjectSerializer):
2044            OBJ_BASE_CLASS = MyNSObj
2045
2046        ser = MySerializer()
2047        prim = MyNSObj(foo=1).obj_to_primitive()
2048        prim['foo.version'] = '2.0'
2049        ser.deserialize_entity(mock.sentinel.context, prim)
2050        indirection_api = MyNSObj.indirection_api
2051        indirection_api.object_backport_versions.assert_called_once_with(
2052            mock.sentinel.context, prim, {'MyNSObj': '1.0'})
2053
2054    @mock.patch('oslo_versionedobjects.base.VersionedObject.indirection_api')
2055    def test_serializer_calls_old_backport_interface(self, indirection_api):
2056        @base.VersionedObjectRegistry.register
2057        class MyOldObj(base.VersionedObject):
2058            pass
2059
2060        ser = base.VersionedObjectSerializer()
2061        prim = MyOldObj(foo=1).obj_to_primitive()
2062        prim['versioned_object.version'] = '2.0'
2063        indirection_api.object_backport_versions.side_effect = (
2064            NotImplementedError('Old'))
2065        ser.deserialize_entity(mock.sentinel.context, prim)
2066        indirection_api.object_backport.assert_called_once_with(
2067            mock.sentinel.context, prim, '1.0')
2068
2069
2070class TestSchemaGeneration(test.TestCase):
2071    @base.VersionedObjectRegistry.register
2072    class FakeObject(base.VersionedObject):
2073        fields = {
2074            'a_boolean': fields.BooleanField(nullable=True),
2075        }
2076
2077    @base.VersionedObjectRegistry.register
2078    class FakeComplexObject(base.VersionedObject):
2079        fields = {
2080            'a_dict': fields.DictOfListOfStringsField(),
2081            'an_obj': fields.ObjectField('FakeObject', nullable=True),
2082            'list_of_objs': fields.ListOfObjectsField('FakeObject'),
2083        }
2084
2085    def test_to_json_schema(self):
2086        schema = self.FakeObject.to_json_schema()
2087        self.assertEqual({
2088            '$schema': 'http://json-schema.org/draft-04/schema#',
2089            'title': 'FakeObject',
2090            'type': ['object'],
2091            'properties': {
2092                'versioned_object.namespace': {
2093                    'type': 'string'
2094                },
2095                'versioned_object.name': {
2096                    'type': 'string'
2097                },
2098                'versioned_object.version': {
2099                    'type': 'string'
2100                },
2101                'versioned_object.changes': {
2102                    'type': 'array',
2103                    'items': {
2104                        'type': 'string'
2105                    }
2106                },
2107                'versioned_object.data': {
2108                    'type': 'object',
2109                    'description': 'fields of FakeObject',
2110                    'properties': {
2111                        'a_boolean': {
2112                            'readonly': False,
2113                            'type': ['boolean', 'null']},
2114                    },
2115                },
2116            },
2117            'required': ['versioned_object.namespace', 'versioned_object.name',
2118                         'versioned_object.version', 'versioned_object.data']
2119        }, schema)
2120
2121        jsonschema.validate(self.FakeObject(a_boolean=True).obj_to_primitive(),
2122                            self.FakeObject.to_json_schema())
2123
2124    def test_to_json_schema_complex_object(self):
2125        schema = self.FakeComplexObject.to_json_schema()
2126        expected_schema = {
2127            '$schema': 'http://json-schema.org/draft-04/schema#',
2128            'properties': {
2129                'versioned_object.changes':
2130                    {'items': {'type': 'string'}, 'type': 'array'},
2131                'versioned_object.data': {
2132                    'description': 'fields of FakeComplexObject',
2133                    'properties': {
2134                        'a_dict': {
2135                            'readonly': False,
2136                            'type': ['object'],
2137                            'additionalProperties': {
2138                                'type': ['array'],
2139                                'readonly': False,
2140                                'items': {
2141                                    'type': ['string'],
2142                                    'readonly': False}}},
2143                        'an_obj': {
2144                            'properties': {
2145                                'versioned_object.changes':
2146                                    {'items': {'type': 'string'},
2147                                     'type': 'array'},
2148                                'versioned_object.data': {
2149                                    'description': 'fields of FakeObject',
2150                                    'properties':
2151                                        {'a_boolean': {'readonly': False,
2152                                         'type': ['boolean', 'null']}},
2153                                    'type': 'object'},
2154                                'versioned_object.name': {'type': 'string'},
2155                                'versioned_object.namespace':
2156                                    {'type': 'string'},
2157                                'versioned_object.version':
2158                                    {'type': 'string'}},
2159                                'readonly': False,
2160                                'required': ['versioned_object.namespace',
2161                                             'versioned_object.name',
2162                                             'versioned_object.version',
2163                                             'versioned_object.data'],
2164                                'type': ['object', 'null']},
2165                        'list_of_objs': {
2166                            'items': {
2167                                'properties': {
2168                                    'versioned_object.changes':
2169                                        {'items': {'type': 'string'},
2170                                         'type': 'array'},
2171                                    'versioned_object.data': {
2172                                        'description': 'fields of FakeObject',
2173                                        'properties': {
2174                                            'a_boolean': {
2175                                                'readonly': False,
2176                                                'type': ['boolean', 'null']}},
2177                                            'type': 'object'},
2178                                    'versioned_object.name':
2179                                        {'type': 'string'},
2180                                    'versioned_object.namespace':
2181                                        {'type': 'string'},
2182                                    'versioned_object.version':
2183                                        {'type': 'string'}},
2184                                'readonly': False,
2185                                'required': ['versioned_object.namespace',
2186                                             'versioned_object.name',
2187                                             'versioned_object.version',
2188                                             'versioned_object.data'],
2189                                'type': ['object']},
2190                            'readonly': False,
2191                            'type': ['array']}},
2192                    'required': ['a_dict', 'list_of_objs'],
2193                    'type': 'object'},
2194                'versioned_object.name': {'type': 'string'},
2195                'versioned_object.namespace': {'type': 'string'},
2196                'versioned_object.version': {'type': 'string'}},
2197            'required': ['versioned_object.namespace',
2198                         'versioned_object.name',
2199                         'versioned_object.version',
2200                         'versioned_object.data'],
2201            'title': 'FakeComplexObject',
2202            'type': ['object']}
2203        self.assertEqual(expected_schema, schema)
2204
2205        fake_obj = self.FakeComplexObject(
2206            a_dict={'key1': ['foo', 'bar'],
2207                    'key2': ['bar', 'baz']},
2208            an_obj=self.FakeObject(a_boolean=True),
2209            list_of_objs=[self.FakeObject(a_boolean=False),
2210                          self.FakeObject(a_boolean=True),
2211                          self.FakeObject(a_boolean=False)])
2212
2213        primitives = fake_obj.obj_to_primitive()
2214        jsonschema.validate(primitives, schema)
2215
2216
2217class TestNamespaceCompatibility(test.TestCase):
2218    def setUp(self):
2219        super(TestNamespaceCompatibility, self).setUp()
2220
2221        @base.VersionedObjectRegistry.register_if(False)
2222        class TestObject(base.VersionedObject):
2223            OBJ_SERIAL_NAMESPACE = 'foo'
2224            OBJ_PROJECT_NAMESPACE = 'tests'
2225
2226        self.test_class = TestObject
2227
2228    def test_obj_primitive_key(self):
2229        self.assertEqual('foo.data',
2230                         self.test_class._obj_primitive_key('data'))
2231
2232    def test_obj_primitive_field(self):
2233        primitive = {
2234            'foo.data': mock.sentinel.data,
2235        }
2236        self.assertEqual(mock.sentinel.data,
2237                         self.test_class._obj_primitive_field(primitive,
2238                                                              'data'))
2239
2240    def test_obj_primitive_field_namespace(self):
2241        primitive = {
2242            'foo.name': 'TestObject',
2243            'foo.namespace': 'tests',
2244            'foo.version': '1.0',
2245            'foo.data': {},
2246        }
2247        with mock.patch.object(self.test_class, 'obj_class_from_name'):
2248            self.test_class.obj_from_primitive(primitive)
2249
2250    def test_obj_primitive_field_namespace_wrong(self):
2251        primitive = {
2252            'foo.name': 'TestObject',
2253            'foo.namespace': 'wrong',
2254            'foo.version': '1.0',
2255            'foo.data': {},
2256        }
2257        self.assertRaises(exception.UnsupportedObjectError,
2258                          self.test_class.obj_from_primitive, primitive)
2259
2260
2261class TestUtilityMethods(test.TestCase):
2262    def test_flat(self):
2263        @base.VersionedObjectRegistry.register
2264        class TestObject(base.VersionedObject):
2265            VERSION = '1.23'
2266            fields = {}
2267
2268        tree = base.obj_tree_get_versions('TestObject')
2269        self.assertEqual({'TestObject': '1.23'}, tree)
2270
2271    def test_parent_child(self):
2272        @base.VersionedObjectRegistry.register
2273        class TestChild(base.VersionedObject):
2274            VERSION = '2.34'
2275
2276        @base.VersionedObjectRegistry.register
2277        class TestObject(base.VersionedObject):
2278            VERSION = '1.23'
2279            fields = {
2280                'child': fields.ObjectField('TestChild'),
2281            }
2282
2283        tree = base.obj_tree_get_versions('TestObject')
2284        self.assertEqual({'TestObject': '1.23',
2285                          'TestChild': '2.34'},
2286                         tree)
2287
2288    def test_complex(self):
2289        @base.VersionedObjectRegistry.register
2290        class TestChild(base.VersionedObject):
2291            VERSION = '2.34'
2292
2293        @base.VersionedObjectRegistry.register
2294        class TestChildTwo(base.VersionedObject):
2295            VERSION = '4.56'
2296            fields = {
2297                'sibling': fields.ObjectField('TestChild'),
2298            }
2299
2300        @base.VersionedObjectRegistry.register
2301        class TestObject(base.VersionedObject):
2302            VERSION = '1.23'
2303            fields = {
2304                'child': fields.ObjectField('TestChild'),
2305                'childtwo': fields.ListOfObjectsField('TestChildTwo'),
2306            }
2307
2308        tree = base.obj_tree_get_versions('TestObject')
2309        self.assertEqual({'TestObject': '1.23',
2310                          'TestChild': '2.34',
2311                          'TestChildTwo': '4.56'},
2312                         tree)
2313
2314    def test_complex_loopy(self):
2315        @base.VersionedObjectRegistry.register
2316        class TestChild(base.VersionedObject):
2317            VERSION = '2.34'
2318            fields = {
2319                'sibling': fields.ObjectField('TestChildTwo'),
2320            }
2321
2322        @base.VersionedObjectRegistry.register
2323        class TestChildTwo(base.VersionedObject):
2324            VERSION = '4.56'
2325            fields = {
2326                'sibling': fields.ObjectField('TestChild'),
2327                'parents': fields.ListOfObjectsField('TestObject'),
2328            }
2329
2330        @base.VersionedObjectRegistry.register
2331        class TestObject(base.VersionedObject):
2332            VERSION = '1.23'
2333            fields = {
2334                'child': fields.ObjectField('TestChild'),
2335                'childtwo': fields.ListOfObjectsField('TestChildTwo'),
2336            }
2337
2338        tree = base.obj_tree_get_versions('TestObject')
2339        self.assertEqual({'TestObject': '1.23',
2340                          'TestChild': '2.34',
2341                          'TestChildTwo': '4.56'},
2342                         tree)
2343
2344    def test_missing_referenced(self):
2345        """Ensure a missing child object is highlighted."""
2346        @base.VersionedObjectRegistry.register
2347        class TestObjectFoo(base.VersionedObject):
2348            VERSION = '1.23'
2349            fields = {
2350                # note that this object does not exist
2351                'child': fields.ObjectField('TestChildBar'),
2352            }
2353
2354        exc = self.assertRaises(exception.UnregisteredSubobject,
2355                                base.obj_tree_get_versions,
2356                                'TestObjectFoo')
2357        self.assertIn('TestChildBar is referenced by TestObjectFoo',
2358                      exc.format_message())
2359
2360
2361class TestListObjectConcat(test.TestCase):
2362    def test_list_object_concat(self):
2363        @base.VersionedObjectRegistry.register_if(False)
2364        class MyList(base.ObjectListBase, base.VersionedObject):
2365            fields = {'objects': fields.ListOfObjectsField('MyOwnedObject')}
2366
2367        values = [1, 2, 42]
2368
2369        list1 = MyList(objects=[MyOwnedObject(baz=values[0]),
2370                                MyOwnedObject(baz=values[1])])
2371        list2 = MyList(objects=[MyOwnedObject(baz=values[2])])
2372
2373        concat_list = list1 + list2
2374        for idx, obj in enumerate(concat_list):
2375            self.assertEqual(values[idx], obj.baz)
2376
2377        # Assert that the original lists are unmodified
2378        self.assertEqual(2, len(list1.objects))
2379        self.assertEqual(1, list1.objects[0].baz)
2380        self.assertEqual(2, list1.objects[1].baz)
2381        self.assertEqual(1, len(list2.objects))
2382        self.assertEqual(42, list2.objects[0].baz)
2383
2384    def test_list_object_concat_fails_different_objects(self):
2385        @base.VersionedObjectRegistry.register_if(False)
2386        class MyList(base.ObjectListBase, base.VersionedObject):
2387            fields = {'objects': fields.ListOfObjectsField('MyOwnedObject')}
2388
2389        @base.VersionedObjectRegistry.register_if(False)
2390        class MyList2(base.ObjectListBase, base.VersionedObject):
2391            fields = {'objects': fields.ListOfObjectsField('MyOwnedObject')}
2392
2393        list1 = MyList(objects=[MyOwnedObject(baz=1)])
2394        list2 = MyList2(objects=[MyOwnedObject(baz=2)])
2395
2396        def add(x, y):
2397            return x + y
2398
2399        self.assertRaises(TypeError, add, list1, list2)
2400        # Assert that the original lists are unmodified
2401        self.assertEqual(1, len(list1.objects))
2402        self.assertEqual(1, len(list2.objects))
2403        self.assertEqual(1, list1.objects[0].baz)
2404        self.assertEqual(2, list2.objects[0].baz)
2405
2406    def test_list_object_concat_fails_extra_fields(self):
2407        @base.VersionedObjectRegistry.register_if(False)
2408        class MyList(base.ObjectListBase, base.VersionedObject):
2409            fields = {'objects': fields.ListOfObjectsField('MyOwnedObject'),
2410                      'foo': fields.IntegerField(nullable=True)}
2411
2412        list1 = MyList(objects=[MyOwnedObject(baz=1)])
2413        list2 = MyList(objects=[MyOwnedObject(baz=2)])
2414
2415        def add(x, y):
2416            return x + y
2417
2418        self.assertRaises(TypeError, add, list1, list2)
2419        # Assert that the original lists are unmodified
2420        self.assertEqual(1, len(list1.objects))
2421        self.assertEqual(1, len(list2.objects))
2422        self.assertEqual(1, list1.objects[0].baz)
2423        self.assertEqual(2, list2.objects[0].baz)
2424
2425    def test_builtin_list_add_fails(self):
2426        @base.VersionedObjectRegistry.register_if(False)
2427        class MyList(base.ObjectListBase, base.VersionedObject):
2428            fields = {'objects': fields.ListOfObjectsField('MyOwnedObject')}
2429
2430        list1 = MyList(objects=[MyOwnedObject(baz=1)])
2431
2432        def add(obj):
2433            return obj + []
2434
2435        self.assertRaises(TypeError, add, list1)
2436
2437    def test_builtin_list_radd_fails(self):
2438        @base.VersionedObjectRegistry.register_if(False)
2439        class MyList(base.ObjectListBase, base.VersionedObject):
2440            fields = {'objects': fields.ListOfObjectsField('MyOwnedObject')}
2441
2442        list1 = MyList(objects=[MyOwnedObject(baz=1)])
2443
2444        def add(obj):
2445            return [] + obj
2446
2447        self.assertRaises(TypeError, add, list1)
2448
2449
2450class TestTimestampedObject(test.TestCase):
2451    """Test TimestampedObject mixin.
2452
2453    Do this by creating an object that uses the mixin and confirm that the
2454    added fields are there and in fact behaves as the DateTimeFields we desire.
2455    """
2456
2457    def setUp(self):
2458        super(TestTimestampedObject, self).setUp()
2459
2460        @base.VersionedObjectRegistry.register_if(False)
2461        class MyTimestampedObject(base.VersionedObject,
2462                                  base.TimestampedObject):
2463            fields = {
2464                'field1': fields.Field(fields.String()),
2465            }
2466
2467        self.myclass = MyTimestampedObject
2468        self.my_object = self.myclass(field1='field1')
2469
2470    def test_timestamped_has_fields(self):
2471        self.assertEqual('field1', self.my_object.field1)
2472        self.assertIn('updated_at', self.my_object.fields)
2473        self.assertIn('created_at', self.my_object.fields)
2474
2475    def test_timestamped_holds_timestamps(self):
2476        now = timeutils.utcnow(with_timezone=True)
2477        self.my_object.updated_at = now
2478        self.my_object.created_at = now
2479        self.assertEqual(now, self.my_object.updated_at)
2480        self.assertEqual(now, self.my_object.created_at)
2481
2482    def test_timestamped_rejects_not_timestamps(self):
2483        with testtools.ExpectedException(ValueError, '.*parse date.*'):
2484            self.my_object.updated_at = 'a string'
2485        with testtools.ExpectedException(ValueError, '.*parse date.*'):
2486            self.my_object.created_at = 'a string'
2487