diff --git a/doc/requirements.txt b/doc/requirements.txt index 97c1879..1a5b4da 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,7 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. +fixtures>=3.0.0 # Apache-2.0/BSD openstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 685d593..410c66d 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -4,3 +4,4 @@ Using oslo.limit .. toctree:: usage.rst + testing.rst diff --git a/doc/source/user/testing.rst b/doc/source/user/testing.rst new file mode 100644 index 0000000..961ddd6 --- /dev/null +++ b/doc/source/user/testing.rst @@ -0,0 +1,30 @@ +======= +Testing +======= + +To test a project that uses oslo.limit, a fixture is provided. This +mocks out the connection to keystone and retrieval of registered and +project limits. + +Example +======= + +.. code-block:: python + + from oslo_limit import fixture + + class MyTest(unittest.TestCase): + def setUp(self): + super(MyTest, self).setUp() + + # Default limit of 10 widgets + registered_limits = {'widgets': 10} + + # project2 gets 20 widgets + project_limits = {'project2': {'widgets': 20}} + + self.useFixture(fixture.LimitFixture(registered_limits, + project_limits)) + + def test_thing(self): + # ... use limit.Enforcer() as usual diff --git a/oslo_limit/fixture.py b/oslo_limit/fixture.py new file mode 100644 index 0000000..032b7d0 --- /dev/null +++ b/oslo_limit/fixture.py @@ -0,0 +1,75 @@ +# Copyright 2021 Red Hat, Inc +# 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 unittest import mock + +import fixtures as fixtures + +from openstack.identity.v3 import endpoint +from openstack.identity.v3 import limit as keystone_limit + + +class LimitFixture(fixtures.Fixture): + def __init__(self, reglimits, projlimits): + """A fixture for testing code that relies on Keystone Unified Limits. + + :param reglimits: A dictionary of {resource_name: limit} values to + simulate registered limits in keystone. + :type reglimits: dict + :param projlimits: A dictionary of dictionaries defining per-project + limits like {project_id: {resource_name: limit}}. + As in reality, only per-project overrides need be + provided here; any unmentioned projects or + resources will take the registered limit defaults. + :type reglimits: dict + """ + self.reglimits = reglimits + self.projlimits = projlimits + + def setUp(self): + super(LimitFixture, self).setUp() + + # We mock our own cached connection to Keystone + self.mock_conn = mock.MagicMock() + self.useFixture(fixtures.MockPatch('oslo_limit.limit._SDK_CONNECTION', + new=self.mock_conn)) + + # Use a flat enforcement model + mock_gem = self.useFixture( + fixtures.MockPatch('oslo_limit.limit.Enforcer.' + '_get_enforcement_model')).mock + mock_gem.return_value = 'flat' + + # Fake keystone endpoint; no per-service limit distinction + fake_endpoint = endpoint.Endpoint() + fake_endpoint.service_id = "service_id" + fake_endpoint.region_id = "region_id" + self.mock_conn.get_endpoint.return_value = fake_endpoint + + def fake_limits(service_id, region_id, resource_name, project_id=None): + this_limit = keystone_limit.Limit() + this_limit.resource_name = resource_name + if project_id is None: + this_limit.default_limit = self.reglimits.get(resource_name) + if this_limit.default_limit is None: + return iter([None]) + else: + this_limit.resource_limit = \ + self.projlimits.get(project_id, {}).get(resource_name) + if this_limit.resource_limit is None: + return iter([None]) + + return iter([this_limit]) + + self.mock_conn.limits.side_effect = fake_limits + self.mock_conn.registered_limits.side_effect = fake_limits diff --git a/oslo_limit/tests/test_fixture.py b/oslo_limit/tests/test_fixture.py new file mode 100644 index 0000000..782a283 --- /dev/null +++ b/oslo_limit/tests/test_fixture.py @@ -0,0 +1,92 @@ +# Copyright 2021 Red Hat, Inc +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import base + +from oslo_limit import exception +from oslo_limit import fixture +from oslo_limit import limit + + +class TestFixture(base.BaseTestCase): + def setUp(self): + super(TestFixture, self).setUp() + + # Set up some default projects, registered limits, + # and project limits + reglimits = {'widgets': 100, + 'sprockets': 50} + projlimits = { + 'project2': {'widgets': 10}, + } + self.useFixture(fixture.LimitFixture(reglimits, projlimits)) + + # Some fake usage for projects + self.usage = { + 'project1': {'sprockets': 10, + 'widgets': 10}, + 'project2': {'sprockets': 3, + 'widgets': 3}, + } + + def proj_usage(project_id, resource_names): + return self.usage[project_id] + + # An enforcer to play with + self.enforcer = limit.Enforcer(proj_usage) + + def test_project_under_registered_limit_only(self): + # Project1 has quota of 50 and 100 each, so no problem with + # 10+1 usage. + self.enforcer.enforce('project1', {'sprockets': 1, + 'widgets': 1}) + + def test_project_over_registered_limit_only(self): + # Project1 has quota of 100 widgets, usage of 112 is over + # quota. + self.assertRaises(exception.ProjectOverLimit, + self.enforcer.enforce, + 'project1', {'sprockets': 1, + 'widgets': 102}) + + def test_project_over_registered_limit(self): + # delta=1 should be under the registered limit of 50 + self.enforcer.enforce('project2', {'sprockets': 1}) + + # delta=50 should be over the registered limit of 50 + self.assertRaises(exception.ProjectOverLimit, + self.enforcer.enforce, + 'project2', {'sprockets': 50}) + + def test_project_over_project_limits(self): + # delta=7 is usage=10, right at our project limit of 10 + self.enforcer.enforce('project2', {'widgets': 7}) + + # delta=10 is usage 13, over our project limit of 10 + self.assertRaises(exception.ProjectOverLimit, + self.enforcer.enforce, + 'project2', {'widgets': 10}) + + def test_calculate_usage(self): + # Make sure the usage calculator works with the fixture too + u = self.enforcer.calculate_usage('project2', ['widgets'])['widgets'] + self.assertEqual(3, u.usage) + self.assertEqual(10, u.limit) + + u = self.enforcer.calculate_usage('project1', ['widgets', 'sprockets']) + self.assertEqual(10, u['sprockets'].usage) + self.assertEqual(10, u['widgets'].usage) + # Since project1 has no project limits, make sure we get the + # registered limit values + self.assertEqual(50, u['sprockets'].limit) + self.assertEqual(100, u['widgets'].limit) diff --git a/test-requirements.txt b/test-requirements.txt index ecb5e3a..b9fffd4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. +fixtures>=3.0.0 # Apache-2.0/BSD hacking>=3.0.1,<3.1.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0