From 0a2bff9a38b2bcaecd2e8ba5fb1de115a2b66234 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 19 Apr 2017 17:53:52 +0300 Subject: [PATCH] Add support for namespaces into context manager Add some filtering login based on namespaces to the ContextManager. It allows us to: * execute the proper context(in case of having several "users" contexts it is very important feature) * execute two and more contexts with equal names. Change-Id: I39fa864426869d95ee135e575ff2982b4545716c --- rally/task/context.py | 38 +++++++++-- tests/unit/task/test_context.py | 116 +++++++++++++++++++++++++------- 2 files changed, 126 insertions(+), 28 deletions(-) diff --git a/rally/task/context.py b/rally/task/context.py index fa14fb2a08..27329a173e 100644 --- a/rally/task/context.py +++ b/rally/task/context.py @@ -21,6 +21,7 @@ from rally.common import logging from rally.common.plugin import plugin from rally.common import utils from rally.common import validation +from rally import exceptions from rally.task import functional LOG = logging.getLogger(__name__) @@ -157,11 +158,38 @@ class ContextManager(object): self.context_obj = context_obj def _get_sorted_context_lst(self): - return sorted([ - Context.get(ctx_name, - namespace=self.context_obj["scenario_namespace"], - allow_hidden=True)(self.context_obj) - for ctx_name in self.context_obj["config"].keys()]) + context_list = [] + for ctx_name in self.context_obj["config"].keys(): + # TODO(andreykurilin): move this logic to some "find" method + if "@" in ctx_name: + ctx_name, ctx_namespace = ctx_name.split("@", 1) + context_list.append(Context.get(ctx_name, + namespace=ctx_namespace, + fallback_to_default=False, + allow_hidden=True)) + else: + potential_result = Context.get_all(name=ctx_name, + allow_hidden=True) + if len(potential_result) == 1: + context_list.append(potential_result[0]) + continue + elif len(potential_result) > 1: + scen_namespace = self.context_obj["scenario_namespace"] + another_attempt = [c for c in potential_result + if c.get_namespace() == scen_namespace] + if another_attempt: + context_list.append(another_attempt[0]) + continue + another_attempt = [c for c in potential_result + if c.get_namespace() == "default"] + if another_attempt: + context_list.append(another_attempt[0]) + continue + + raise exceptions.PluginNotFound(name=ctx_name, + namespace="any of") + + return sorted([ctx(self.context_obj) for ctx in context_list]) def setup(self): """Creates benchmark environment from config.""" diff --git a/tests/unit/task/test_context.py b/tests/unit/task/test_context.py index 36c711f69a..95847a79a1 100644 --- a/tests/unit/task/test_context.py +++ b/tests/unit/task/test_context.py @@ -13,9 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import ddt import mock +from rally import exceptions from rally.task import context from tests.unit import fakes from tests.unit import test @@ -148,12 +150,12 @@ class BaseContextTestCase(test.TestCase): class ContextManagerTestCase(test.TestCase): + @mock.patch("rally.task.context.ContextManager._get_sorted_context_lst") + def test_setup(self, mock__get_sorted_context_lst): + foo_context = mock.MagicMock() + bar_context = mock.MagicMock() + mock__get_sorted_context_lst.return_value = [foo_context, bar_context] - @mock.patch("rally.task.context.Context.get") - def test_setup(self, mock_context_get): - mock_context = mock.MagicMock() - mock_context.return_value = mock.MagicMock(__lt__=lambda x, y: True) - mock_context_get.return_value = mock_context ctx_object = {"config": {"a": [], "b": []}, "scenario_namespace": "foo"} @@ -161,29 +163,96 @@ class ContextManagerTestCase(test.TestCase): result = manager.setup() self.assertEqual(result, ctx_object) - mock_context_get.assert_has_calls( - [mock.call("a", namespace="foo", allow_hidden=True), - mock.call("b", namespace="foo", allow_hidden=True)], - any_order=True) - mock_context.assert_has_calls( - [mock.call(ctx_object), mock.call(ctx_object)], any_order=True) - self.assertEqual([mock_context(), mock_context()], manager._visited) - mock_context.return_value.assert_has_calls( - [mock.call.setup(), mock.call.setup()], any_order=True) + foo_context.setup.assert_called_once_with() + bar_context.setup.assert_called_once_with() + + @mock.patch("rally.task.context.Context.get_all") + @mock.patch("rally.task.context.Context.get") + def test_get_sorted_context_lst(self, mock_context_get, + mock_context_get_all): + + # use ordereddict to predict the order of calls + ctx_object = {"config": collections.OrderedDict([("a@foo", []), + ("b", []), + ("c", []), + ("d", [])]), + "scenario_namespace": "foo"} + + def OrderableMock(**kwargs): + return mock.Mock(__lt__=(lambda x, y: x), **kwargs) + + a_ctx = mock.Mock(return_value=OrderableMock()) + mock_context_get.return_value = a_ctx + + b_ctx = mock.Mock(return_value=OrderableMock()) + c_ctx = mock.Mock(get_namespace=lambda: "foo", + return_value=OrderableMock()) + d_ctx = mock.Mock(get_namespace=lambda: "default", + return_value=OrderableMock()) + all_plugins = { + # it is a case when search is performed for any namespace and only + # one possible match is found + "b": [b_ctx], + # it is a case when plugin should be filtered by the scenario + # namespace + "c": [mock.Mock(get_namespace=lambda: "default"), c_ctx], + # it is a case when plugin should be filtered by the scenario + # namespace + "d": [mock.Mock(get_namespace=lambda: "bar"), d_ctx] + } + + def fake_get_all(name, allow_hidden=True): + # use pop to ensure that get_all is called only one time per ctx + result = all_plugins.pop(name, None) + if result is None: + self.fail("Unexpected call of Context.get_all for %s plugin" % + name) + return result + + mock_context_get_all.side_effect = fake_get_all + + manager = context.ContextManager(ctx_object) + + self.assertEqual({a_ctx.return_value, b_ctx.return_value, + c_ctx.return_value, d_ctx.return_value}, + set(manager._get_sorted_context_lst())) + + mock_context_get.assert_called_once_with("a", namespace="foo", + fallback_to_default=False, + allow_hidden=True) + a_ctx.assert_called_once_with(ctx_object) + self.assertEqual([mock.call(name=name, allow_hidden=True) + for name in ("b", "c", "d")], + mock_context_get_all.call_args_list) + + @mock.patch("rally.task.context.Context.get_all") + def test_get_sorted_context_lst_fails(self, mock_context_get_all): + ctx_object = {"config": {"foo": "bar"}, + "scenario_namespace": "foo"} + + mock_context_get_all.return_value = [] + manager = context.ContextManager(ctx_object) + + self.assertRaises(exceptions.PluginNotFound, + manager._get_sorted_context_lst) + + mock_context_get_all.assert_called_once_with(name="foo", + allow_hidden=True) @mock.patch("rally.task.context.Context.get") def test_cleanup(self, mock_context_get): mock_context = mock.MagicMock() mock_context.return_value = mock.MagicMock(__lt__=lambda x, y: True) mock_context_get.return_value = mock_context - ctx_object = {"config": {"a": [], "b": []}, - "scenario_namespace": "foo"} + ctx_object = {"config": {"a@foo": [], "b@foo": []}} manager = context.ContextManager(ctx_object) manager.cleanup() mock_context_get.assert_has_calls( - [mock.call("a", namespace="foo", allow_hidden=True), - mock.call("b", namespace="foo", allow_hidden=True)], + [mock.call("a", namespace="foo", allow_hidden=True, + fallback_to_default=False), + mock.call("b", namespace="foo", allow_hidden=True, + fallback_to_default=False)], any_order=True) mock_context.assert_has_calls( [mock.call(ctx_object), mock.call(ctx_object)], any_order=True) @@ -196,14 +265,15 @@ class ContextManagerTestCase(test.TestCase): mock_context.return_value = mock.MagicMock(__lt__=lambda x, y: True) mock_context.cleanup.side_effect = Exception() mock_context_get.return_value = mock_context - ctx_object = {"config": {"a": [], "b": []}, - "scenario_namespace": "foo"} + ctx_object = {"config": {"a@foo": [], "b@foo": []}} manager = context.ContextManager(ctx_object) manager.cleanup() mock_context_get.assert_has_calls( - [mock.call("a", namespace="foo", allow_hidden=True), - mock.call("b", namespace="foo", allow_hidden=True)], + [mock.call("a", namespace="foo", allow_hidden=True, + fallback_to_default=False), + mock.call("b", namespace="foo", allow_hidden=True, + fallback_to_default=False)], any_order=True) mock_context.assert_has_calls( [mock.call(ctx_object), mock.call(ctx_object)], any_order=True) @@ -223,7 +293,7 @@ class ContextManagerTestCase(test.TestCase): @mock.patch("rally.task.context.ContextManager.cleanup") @mock.patch("rally.task.context.ContextManager.setup") - def test_with_statement_excpetion_during_setup( + def test_with_statement_exception_during_setup( self, mock_context_manager_setup, mock_context_manager_cleanup): mock_context_manager_setup.side_effect = Exception("abcdef")