[Core] Introduce class-based scenarios

This adds support of scenarios implemented as classes
in addition to existing method-based scenarios.

First class-based scenario will be added in next patch.

Implements: class-based-scenarios
Change-Id: Ia383226a356cf1b24ac7125c028f547d9de74c0d
This commit is contained in:
Alexander Maretskiy 2016-05-25 15:05:52 +03:00
parent 8d13116294
commit 8257b4a339
7 changed files with 92 additions and 34 deletions

View File

@ -152,12 +152,17 @@ class ScenarioRunner(plugin.Plugin):
"""
def run(self, name, context, args):
cls_name, method_name = name.split(".", 1)
cls = scenario.Scenario.get(name)._meta_get("cls_ref")
scenario_plugin = scenario.Scenario.get(name)
# NOTE(boris-42): processing @types decorators
args = types.preprocess(name, context, args)
if scenario_plugin.is_classbased:
cls, method_name = scenario_plugin, "run"
else:
cls, method_name = (scenario_plugin._meta_get("cls_ref"),
name.split(".", 1).pop())
with rutils.Timer() as timer:
self._run_scenario(cls, method_name, context, args)

View File

@ -18,6 +18,7 @@ import random
import six
from rally.common.i18n import _
from rally.common import logging
from rally.common.objects import task # noqa
from rally.common.plugin import plugin
@ -33,23 +34,32 @@ LOG = logging.getLogger(__name__)
def configure(name=None, namespace="default", context=None):
"""Make from plain python method task scenario plugin.
"""Configure scenario by setting proper meta data.
:param name: Plugin name
:param namespace: Plugin namespace
:param context: Default task context that is created for this scenario.
If there are custom user specified contexts this one
will be updated by provided contexts.
This can also transform plain function into scenario plugin, however
this approach is deprecated - now scenarios must be represented by classes
based on rally.task.scenario.Scenario.
:param name: str scenario name
:param namespace: str plugin namespace
:param context: default task context that is created for this scenario.
If there are custom user specified contexts this one
will be updated by provided contexts.
"""
def wrapper(func):
plugin.from_func(Scenario)(func)
func._meta_init()
def wrapper(scen):
scen.is_classbased = hasattr(scen, "run") and callable(scen.run)
if not scen.is_classbased:
plugin.from_func(Scenario)(scen)
scen._meta_init()
if name:
func._set_name_and_namespace(name, namespace)
if "." not in name.strip("."):
msg = (_("Scenario name must include a dot: '%s'") % name)
raise exceptions.RallyException(msg)
scen._set_name_and_namespace(name, namespace)
else:
func._meta_set("namespace", namespace)
func._meta_set("default_context", context or {})
return func
scen._meta_set("namespace", namespace)
scen._meta_set("default_context", context or {})
return scen
return wrapper

View File

@ -1662,6 +1662,14 @@ class FakeScenario(scenario.Scenario):
raise multiprocessing.TimeoutError()
@scenario.configure(name="classbased.fooscenario")
class FakeClassBasedScenario(FakeScenario):
"""Fake class-based scenario."""
def run(self, *args, **kwargs):
pass
class FakeTimer(rally_utils.Timer):
def duration(self):

View File

@ -169,6 +169,29 @@ class ScenarioRunnerTestCase(test.TestCase):
runner_obj._run_scenario.assert_called_once_with(
cls, method_name, context_obj, expected_config_kwargs)
@mock.patch(BASE + "rutils.Timer.duration", return_value=10)
def test_run_classbased(self, mock_timer_duration):
scenario_class = fakes.FakeClassBasedScenario
runner_obj = serial.SerialScenarioRunner(
mock.MagicMock(),
mock.MagicMock())
runner_obj._run_scenario = mock.Mock()
context_obj = {"task": runner_obj.task,
"scenario_name": "classbased.fooscenario",
"admin": {"credential": "foo_credentials"},
"config": {}}
result = runner_obj.run("classbased.fooscenario", context_obj,
{"foo": 11, "bar": "spam"})
self.assertIsNone(result)
self.assertEqual(runner_obj.run_duration,
mock_timer_duration.return_value)
self.assertEqual([], list(runner_obj.result_queue))
runner_obj._run_scenario.assert_called_once_with(
scenario_class, "run", context_obj, {"foo": 11, "bar": "spam"})
def test_abort(self):
runner_obj = serial.SerialScenarioRunner(
mock.MagicMock(),

View File

@ -30,12 +30,13 @@ class ScenarioConfigureTestCase(test.TestCase):
def test_configure(self):
@scenario.configure("test_configure", "testing")
@scenario.configure("fooscenario.name", "testing")
def some_func():
pass
self.assertEqual("test_configure", some_func.get_name())
self.assertEqual("fooscenario.name", some_func.get_name())
self.assertEqual("testing", some_func.get_namespace())
self.assertFalse(some_func.is_classbased)
some_func.unregister()
def test_configure_default_name(self):
@ -47,6 +48,7 @@ class ScenarioConfigureTestCase(test.TestCase):
self.assertIsNone(some_func._meta_get("name"))
self.assertEqual("testing", some_func.get_namespace())
self.assertEqual({"any": 42}, some_func._meta_get("default_context"))
self.assertFalse(some_func.is_classbased)
some_func.unregister()
def test_configure_cls(self):
@ -62,8 +64,20 @@ class ScenarioConfigureTestCase(test.TestCase):
self.assertEqual("any", ScenarioPluginCls.some.get_namespace())
self.assertEqual({"any": 43},
ScenarioPluginCls.some._meta_get("default_context"))
self.assertFalse(ScenarioPluginCls.some.is_classbased)
ScenarioPluginCls.some.unregister()
def test_configure_classbased(self):
@scenario.configure(name="fooscenario.name", namespace="testing")
class SomeScenario(scenario.Scenario):
def run(self):
pass
self.assertEqual("fooscenario.name", SomeScenario.get_name())
self.assertTrue(SomeScenario.is_classbased)
SomeScenario.unregister()
class ScenarioTestCase(test.TestCase):

View File

@ -200,9 +200,6 @@ class FakeClientsScenarioTestCase(ScenarioTestCase):
self._fake_clients = fakes.FakeClients()
def get_test_context():
return {
"task": {
"uuid": str(uuid.uuid4())
}
}
def get_test_context(**kwargs):
kwargs["task"] = {"uuid": str(uuid.uuid4())}
return kwargs

View File

@ -56,17 +56,18 @@ class DocstringsTestCase(test.TestCase):
"One-line description for %s "
"should be declarative and not start "
"with 'Test(s) ...'" % scenario_inst.get_name())
params_count = scenario_inst.__code__.co_argcount
params = scenario_inst.__code__.co_varnames[:params_count]
documented_params = [p["name"] for p in doc["params"]]
for param in params:
if param not in ignored_params:
self.assertIn(param, documented_params,
"Docstring for %(scenario)s should "
"describe the '%(param)s' parameter "
"in the :param <name>: clause." %
{"scenario": scenario_inst.get_name(),
"param": param})
if not scenario_inst.is_classbased:
params_count = scenario_inst.__code__.co_argcount
params = scenario_inst.__code__.co_varnames[:params_count]
documented_params = [p["name"] for p in doc["params"]]
for param in params:
if param not in ignored_params:
self.assertIn(param, documented_params,
"Docstring for %(scenario)s should "
"describe the '%(param)s' parameter "
"in the :param <name>: clause." %
{"scenario": scenario_inst.get_name(),
"param": param})
def test_all_deploy_engines_have_docstrings(self):
for deploy_engine in engine.Engine.get_all():