From d2a166a98eaaec40cdd994dfe3fddeefa6c34532 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 9 May 2023 13:25:45 +0200 Subject: [PATCH] Add fake resources generator In a variety of places we need to have an easy way to have resources populated with fake data. It costs quite a lot of lines of code and can be easily automated. With this change it can be as easy as `_fake = sdk_fakes.generate_fake_resource(Project)`. The code is implemented not to require estabilshed connection to ease use in tests. Change-Id: I47312f4036a0b389cd3689466ab220ba558aa39a --- openstack/test/__init__.py | 0 openstack/test/fakes.py | 122 ++++++++++++++++++ openstack/tests/unit/test_fakes.py | 73 +++++++++++ .../add-fakes-generator-72c53d34c995fcb2.yaml | 5 + 4 files changed, 200 insertions(+) create mode 100644 openstack/test/__init__.py create mode 100644 openstack/test/fakes.py create mode 100644 openstack/tests/unit/test_fakes.py create mode 100644 releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml diff --git a/openstack/test/__init__.py b/openstack/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py new file mode 100644 index 000000000..a4f7b809a --- /dev/null +++ b/openstack/test/fakes.py @@ -0,0 +1,122 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import inspect +from random import choice +from random import randint +from random import random +import uuid + +from openstack import format as _format +from openstack import resource + + +def generate_fake_resource(resource_type, **attrs): + """Generate fake resource + + :param type resource_type: Object class + :param dict attrs: Optional attributes to be set on resource + + :return: Instance of `resource_type` class populated with fake + values of expected types. + """ + base_attrs = dict() + for name, value in inspect.getmembers( + resource_type, + predicate=lambda x: isinstance(x, (resource.Body, resource.URI)), + ): + if isinstance(value, resource.Body): + target_type = value.type + if target_type is None: + if ( + name == "properties" + and hasattr( + resource_type, "_store_unknown_attrs_as_properties" + ) + and resource_type._store_unknown_attrs_as_properties + ): + # virtual "properties" attr which hosts all unknown attrs + # (i.e. Image) + base_attrs[name] = dict() + else: + # Type not defined - string + base_attrs[name] = uuid.uuid4().hex + elif issubclass(target_type, resource.Resource): + # Attribute is of another Resource type + base_attrs[name] = generate_fake_resource(target_type) + elif issubclass(target_type, list) and value.list_type is not None: + # List of ... + item_type = value.list_type + if issubclass(item_type, resource.Resource): + # item is of Resource type + base_attrs[name] = generate_fake_resource(item_type) + elif issubclass(item_type, dict): + base_attrs[name] = dict() + elif issubclass(item_type, str): + base_attrs[name] = [uuid.uuid4().hex] + else: + # Everything else + msg = "Fake value for %s.%s can not be generated" % ( + resource_type.__name__, + name, + ) + raise NotImplementedError(msg) + elif issubclass(target_type, list) and value.list_type is None: + # List of str + base_attrs[name] = [uuid.uuid4().hex] + elif issubclass(target_type, str): + # definitely string + base_attrs[name] = uuid.uuid4().hex + elif issubclass(target_type, int): + # int + base_attrs[name] = randint(1, 100) + elif issubclass(target_type, float): + # float + base_attrs[name] = random() + elif issubclass(target_type, bool) or issubclass( + target_type, _format.BoolStr + ): + # bool + base_attrs[name] = choice([True, False]) + elif issubclass(target_type, dict): + # some dict - without further details leave it empty + base_attrs[name] = dict() + else: + # Everything else + msg = "Fake value for %s.%s can not be generated" % ( + resource_type.__name__, + name, + ) + raise NotImplementedError(msg) + if isinstance(value, resource.URI): + # For URI we just generate something + base_attrs[name] = uuid.uuid4().hex + + base_attrs.update(**attrs) + fake = resource_type(**base_attrs) + return fake + + +def generate_fake_resources(resource_type, count=1, attrs=None): + """Generate given number of fake resource entities + + :param type resource_type: Object class + :param int count: Number of objects to return + :param dict attrs: Attribute values to set into each instance + + :return: Array of `resource_type` class instances populated with fake + values of expected types. + """ + if not attrs: + attrs = {} + for _ in range(count): + yield generate_fake_resource(resource_type, **attrs) diff --git a/openstack/tests/unit/test_fakes.py b/openstack/tests/unit/test_fakes.py new file mode 100644 index 000000000..e0e2fa803 --- /dev/null +++ b/openstack/tests/unit/test_fakes.py @@ -0,0 +1,73 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack import format as _format +from openstack import resource +from openstack.test import fakes +from openstack.tests.unit import base + + +class TestGetFake(base.TestCase): + def test_generate_fake_resource_one(self): + res = fakes.generate_fake_resource(resource.Resource) + self.assertIsInstance(res, resource.Resource) + + def test_generate_fake_resource_list(self): + res = list(fakes.generate_fake_resources(resource.Resource, 2)) + self.assertEqual(2, len(res)) + self.assertIsInstance(res[0], resource.Resource) + + def test_generate_fake_resource_types(self): + class Fake(resource.Resource): + a = resource.Body("a", type=str) + b = resource.Body("b", type=int) + c = resource.Body("c", type=bool) + d = resource.Body("d", type=_format.BoolStr) + e = resource.Body("e", type=dict) + f = resource.URI("path") + + res = fakes.generate_fake_resource(Fake) + self.assertIsInstance(res.a, str) + self.assertIsInstance(res.b, int) + self.assertIsInstance(res.c, bool) + self.assertIsInstance(res.d, bool) + self.assertIsInstance(res.e, dict) + self.assertIsInstance(res.f, str) + + def test_generate_fake_resource_attrs(self): + class Fake(resource.Resource): + a = resource.Body("a", type=str) + b = resource.Body("b", type=str) + + res = fakes.generate_fake_resource(Fake, b="bar") + self.assertIsInstance(res.a, str) + self.assertIsInstance(res.b, str) + self.assertEqual("bar", res.b) + + def test_generate_fake_resource_types_inherit(self): + class Fake(resource.Resource): + a = resource.Body("a", type=str) + + class FakeInherit(resource.Resource): + a = resource.Body("a", type=Fake) + + res = fakes.generate_fake_resource(FakeInherit) + self.assertIsInstance(res.a, Fake) + self.assertIsInstance(res.a.a, str) + + def test_unknown_attrs_as_props(self): + class Fake(resource.Resource): + properties = resource.Body("properties") + _store_unknown_attrs_as_properties = True + + res = fakes.generate_fake_resource(Fake) + self.assertIsInstance(res.properties, dict) diff --git a/releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml b/releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml new file mode 100644 index 000000000..06fa9c039 --- /dev/null +++ b/releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add fake resource generator to ease unit testing in packages that depend on + openstacksdk.