diff --git a/rally/plugins/openstack/scenarios/tempest/utils.py b/rally/plugins/openstack/scenarios/tempest/utils.py index e1236d7281..bf3a24f0b9 100644 --- a/rally/plugins/openstack/scenarios/tempest/utils.py +++ b/rally/plugins/openstack/scenarios/tempest/utils.py @@ -47,8 +47,7 @@ def tempest_log_wrapper(func): total, tests = scenario_obj.context["verifier"].parse_results( kwargs["log_file"]) if total and tests: - scenario_obj._add_atomic_actions("test_execution", - total.get("time")) + scenario_obj._atomic_actions["test_execution"] = total.get("time") if total.get("errors") or total.get("failures"): raise TempestBenchmarkFailure([ test for test in six.itervalues(tests) diff --git a/rally/task/atomic.py b/rally/task/atomic.py index 576fc8e84c..047aa31aa7 100644 --- a/rally/task/atomic.py +++ b/rally/task/atomic.py @@ -13,11 +13,75 @@ # License for the specific language governing permissions and limitations # under the License. +import functools -from rally.task import scenario +from rally.common import costilius +from rally.common import utils -# TODO(boris-42): In future we will move code from scenario to atomic action -# to simplify migration to new code we will make just links for -# now -ActionTimer = scenario.AtomicAction -action_timer = scenario.atomic_action_timer + +class ActionTimerMixin(object): + + def __init__(self): + self._atomic_actions = costilius.OrderedDict() + + def atomic_actions(self): + """Returns the content of each atomic action.""" + return self._atomic_actions + + +class ActionTimer(utils.Timer): + """A class to measure the duration of atomic operations + + This would simplify the way measure atomic operation duration + in certain cases. For example if we want to get the duration + for each operation which runs in an iteration + for i in range(repetitions): + with atomic.ActionTimer(instance_of_action_timer, "name_of_action"): + self.clients(). + """ + + def __init__(self, instance, name): + """Create a new instance of the AtomicAction. + + :param instance: instance of subclass of ActionTimerMixin + :param name: name of the ActionBuilder + """ + super(ActionTimer, self).__init__() + self.instance = instance + self.name = self._get_atomic_action_name(name) + self.instance._atomic_actions[self.name] = None + + def _get_atomic_action_name(self, name): + # TODO(boris-42): It was quite bad idea to store atomic actions + # inside {}. We should refactor this in 0.2.0 release + # and store them inside array, that will allow us to + # store atomic actions with the same name + if name not in self.instance._atomic_actions: + return name + + name_template = name + " (%i)" + i = 2 + while name_template % i in self.instance._atomic_actions: + i += 1 + return name_template % i + + def __exit__(self, type_, value, tb): + super(ActionTimer, self).__exit__(type_, value, tb) + if type_ is None: + self.instance._atomic_actions[self.name] = self.duration() + + +def action_timer(name): + """Provide measure of execution time. + + Decorates methods of the Scenario class. + This provides duration in seconds of each atomic action. + """ + def wrap(func): + @functools.wraps(func) + def func_atomic_actions(self, *args, **kwargs): + with ActionTimer(self, name): + f = func(self, *args, **kwargs) + return f + return func_atomic_actions + return wrap diff --git a/rally/task/scenario.py b/rally/task/scenario.py index cab944b746..524b08a59c 100644 --- a/rally/task/scenario.py +++ b/rally/task/scenario.py @@ -13,18 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. -import functools + import random import time import six -from rally.common import costilius from rally.common import log as logging from rally.common.plugin import plugin from rally.common import utils from rally import consts from rally import exceptions +from rally.task import atomic from rally.task import functional @@ -91,7 +91,9 @@ class ConfigurePluginMeta(type): @six.add_metaclass(ConfigurePluginMeta) -class Scenario(plugin.Plugin, functional.FunctionalMixin): +class Scenario(plugin.Plugin, + atomic.ActionTimerMixin, + functional.FunctionalMixin): """This is base class for any benchmark scenario. You should create subclass of this class. And your test scenarios will @@ -101,9 +103,9 @@ class Scenario(plugin.Plugin, functional.FunctionalMixin): RESOURCE_NAME_LENGTH = 10 def __init__(self, context=None): + super(Scenario, self).__init__() self.context = context self._idle_duration = 0 - self._atomic_actions = costilius.OrderedDict() # TODO(amaretskiy): consider about prefix part of benchmark uuid @classmethod @@ -167,76 +169,3 @@ class Scenario(plugin.Plugin, functional.FunctionalMixin): def idle_duration(self): """Returns duration of all sleep_between.""" return self._idle_duration - - def _register_atomic_action(self, name): - """Registers an atomic action by its name.""" - self._atomic_actions[name] = None - - def _atomic_action_registered(self, name): - """Checks whether an atomic action has been already registered.""" - return name in self._atomic_actions - - def _add_atomic_actions(self, name, duration): - """Adds the duration of an atomic action by its name.""" - self._atomic_actions[name] = duration - - def atomic_actions(self): - """Returns the content of each atomic action.""" - return self._atomic_actions - - -def atomic_action_timer(name): - """Provide measure of execution time. - - Decorates methods of the Scenario class. - This provides duration in seconds of each atomic action. - """ - def wrap(func): - @functools.wraps(func) - def func_atomic_actions(self, *args, **kwargs): - with AtomicAction(self, name): - f = func(self, *args, **kwargs) - return f - return func_atomic_actions - return wrap - - -class AtomicAction(utils.Timer): - """A class to measure the duration of atomic operations - - This would simplify the way measure atomic operation duration - in certain cases. For example if we want to get the duration - for each operation which runs in an iteration - for i in range(repetitions): - with scenario_utils.AtomicAction(instance_of_base_scenario_subclass, - "name_of_action"): - self.clients(). - """ - - def __init__(self, scenario_instance, name): - """Create a new instance of the AtomicAction. - - :param scenario_instance: instance of subclass of base scenario - :param name: name of the ActionBuilder - """ - super(AtomicAction, self).__init__() - self.scenario_instance = scenario_instance - self.name = self._get_atomic_action_name(name) - self.scenario_instance._register_atomic_action(self.name) - - def _get_atomic_action_name(self, name): - if not self.scenario_instance._atomic_action_registered(name): - return name - else: - name_template = name + " (%i)" - atomic_action_iteration = 2 - while self.scenario_instance._atomic_action_registered( - name_template % atomic_action_iteration): - atomic_action_iteration += 1 - return name_template % atomic_action_iteration - - def __exit__(self, type_, value, tb): - super(AtomicAction, self).__exit__(type_, value, tb) - if type_ is None: - self.scenario_instance._add_atomic_actions(self.name, - self.duration()) diff --git a/tests/unit/task/test_atomic.py b/tests/unit/task/test_atomic.py new file mode 100644 index 0000000000..5aab82f112 --- /dev/null +++ b/tests/unit/task/test_atomic.py @@ -0,0 +1,57 @@ +# Copyright 2015: Mirantis Inc. +# All Rights Reserved. +# +# 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 mock + +from rally.common import costilius +from rally.task import atomic +from tests.unit import test + + +class ActionTimerMixinTestCase(test.TestCase): + + def test_atomic_actions(self): + inst = atomic.ActionTimerMixin() + self.assertEqual(inst._atomic_actions, inst.atomic_actions()) + + +class AtomicActionTestCase(test.TestCase): + + @mock.patch("time.time", side_effect=[1, 3, 6, 10, 15, 21]) + def test_action_timer_context(self, mock_time): + inst = atomic.ActionTimerMixin() + + with atomic.ActionTimer(inst, "test"): + with atomic.ActionTimer(inst, "test"): + with atomic.ActionTimer(inst, "some"): + pass + + expected = [("test", 20), ("test (2)", 12), ("some", 4)] + self.assertEqual(costilius.OrderedDict(expected), + inst.atomic_actions()) + + @mock.patch("time.time", side_effect=[1, 3]) + def test_action_timer_decorator(self, mock_time): + + class Some(atomic.ActionTimerMixin): + + @atomic.action_timer("some") + def some_func(self, a, b): + return a + b + + inst = Some() + self.assertEqual(5, inst.some_func(2, 3)) + self.assertEqual(costilius.OrderedDict({"some": 2}), + inst.atomic_actions()) diff --git a/tests/unit/task/test_scenario.py b/tests/unit/task/test_scenario.py index 7479d9a427..d66edb2e46 100644 --- a/tests/unit/task/test_scenario.py +++ b/tests/unit/task/test_scenario.py @@ -271,22 +271,3 @@ class ScenarioTestCase(test.TestCase): self.assertEqual( len_by_prefix(result, scenario.Scenario.RESOURCE_NAME_PREFIX), range_num) - - -class AtomicActionTestCase(test.TestCase): - def test__init__(self): - fake_scenario_instance = fakes.FakeScenario() - c = scenario.AtomicAction(fake_scenario_instance, "asdf") - self.assertEqual(c.scenario_instance, fake_scenario_instance) - self.assertEqual(c.name, "asdf") - - @mock.patch("tests.unit.fakes.FakeScenario._add_atomic_actions") - @mock.patch("rally.common.utils.time") - def test__exit__(self, mock_time, mock_fake_scenario__add_atomic_actions): - fake_scenario_instance = fakes.FakeScenario() - self.start = mock_time.time() - with scenario.AtomicAction(fake_scenario_instance, "asdf"): - pass - duration = mock_time.time() - self.start - mock_fake_scenario__add_atomic_actions.assert_called_once_with( - "asdf", duration)