Add a test fixture

This adds a fixture that can be used by consuming projects to
simulate a set of limits in keystone, without requiring actual
keystone. Currently, consumers have to mock oslo.limit internals (at
least) in order to do testing.

Change-Id: If72050e90ca8b03e26d128c7bbcef6bbea92b501
This commit is contained in:
Dan Smith 2021-06-09 08:04:01 -07:00
parent 1175b0f7c1
commit caa75c1bab
6 changed files with 200 additions and 0 deletions

View File

@ -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

View File

@ -4,3 +4,4 @@ Using oslo.limit
.. toctree::
usage.rst
testing.rst

View File

@ -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

75
oslo_limit/fixture.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -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