Remove old way to describe scenario plugin via method

Initally Rally scenario plugins were methods of speical class.
However to unify scenarios with other plugins we moved to model
where plugin is class.

We have a bunch of code that was used for transition and backward
compatiblity that we can finally remove.

Change-Id: Ia6841068eb1d5a39a8f64f160160f1fcc1e05ac5
This commit is contained in:
Boris Pavlovic 2017-05-14 21:43:57 -07:00
parent bae6b61bea
commit 9a225ac837
15 changed files with 66 additions and 353 deletions

View File

@ -24,6 +24,7 @@ from rally.task import scenario
from test_relative_import import zzz
@scenario.configure(name="FakePlugin.testplugin")
class FakePlugin(scenario.Scenario):
"""Fake plugin with a scenario."""
@ -35,8 +36,7 @@ class FakePlugin(scenario.Scenario):
def _test2(self, factor):
time.sleep(random.random() * factor)
@scenario.configure()
def testplugin(self, factor=1):
def run(self, factor=1):
"""Fake scenario.
:param factor: influences the argument value for a time.sleep() call

View File

@ -91,13 +91,12 @@ class InfoMixin(object):
@classmethod
def get_info(cls):
plugin_ = getattr(cls, "func_ref", cls)
doc = parse_docstring(cls._get_doc())
return {
"name": plugin_.get_name(),
"namespace": plugin_.get_namespace(),
"module": plugin_.__module__,
"name": cls.get_name(),
"namespace": cls.get_namespace(),
"module": cls.__module__,
"title": doc["short_description"],
"description": doc["long_description"],
"parameters": doc["params"],

View File

@ -77,78 +77,6 @@ def configure(name, namespace="default", hidden=False):
return decorator
def from_func(plugin_baseclass=None):
"""Add all plugin's methods to function object.
Rally benchmark scenarios are different from all other plugins in Rally.
Usually 1 plugin is 1 class and we can easily use Plugin() as base for
all of them to avoid code duplication. In case of benchmark scenarios
1 class can contain any amount of scenarios that are just methods
of this class.
To make Rally code cleaner, these methods should look/work like other
Plugins.
This decorator makes all dirty work for us, it creates dynamically new
class, adds plugin instance and aliases for all non-private methods of
Plugin instance to passed function.
For example,
@plugin.from_func()
def my_plugin_like_func(a, b):
pass
assert my_plugin_like_func.get_name() == "my_plugin_like_func"
assert my_plugin_like_func.get_all() == []
As a result, adding plugin behavior for benchmark scenarios fully unifies
work with benchmark scenarios and other kinds of plugins.
:param plugin_baseclass: if specified, subclass of this class will be used
to add behavior of plugin to function else,
subclass of Plugin will be used.
:returns: Function decorator that adds plugin behavior to function
"""
if plugin_baseclass:
if not issubclass(plugin_baseclass, Plugin):
raise TypeError("plugin_baseclass should be subclass of %s "
% Plugin)
class FuncPlugin(plugin_baseclass):
is_classbased = False
else:
class FuncPlugin(Plugin):
is_classbased = False
def decorator(func):
func._plugin = FuncPlugin
# NOTE(boris-42): This is required by Plugin.get_all method to
# return func instead of FuncPlugin that will be
# auto discovered.
FuncPlugin.func_ref = func
# NOTE(boris-42): Make aliases from func to all public Plugin fields
for field in dir(func._plugin):
if not field.startswith("__"):
obj = getattr(func._plugin, field)
if callable(obj):
setattr(func, field, obj)
return func
return decorator
class Plugin(meta.MetaMixin, info.InfoMixin):
"""Base class for all Plugins in Rally."""
@ -278,8 +206,7 @@ class Plugin(meta.MetaMixin, info.InfoMixin):
continue
if not allow_hidden and p.is_hidden():
continue
plugins.append(getattr(p, "func_ref", p))
plugins.append(p)
return plugins

View File

@ -60,9 +60,7 @@ class ArgsValidator(validation.Validator):
scenario = plugin_cls
name = scenario.get_name()
namespace = scenario.get_namespace()
if scenario.is_classbased:
# We need initialize scenario class to access instancemethods
scenario = scenario().run
scenario = scenario().run
args, _varargs, varkwargs, defaults = inspect.getargspec(scenario)
hint_msg = (" Use `rally plugin show --name %s --namespace %s` "

View File

@ -374,11 +374,8 @@ class TaskEngine(object):
# TODO(andreykurilin): Remove check for plugin namespace after
# Rally 0.10.0
if scenario_cls.is_classbased:
cls = scenario_cls
else:
cls = scenario_cls._plugin
if (issubclass(cls, os_scenario.OpenStackScenario)
if (issubclass(scenario_cls, os_scenario.OpenStackScenario)
and namespace == "default"):
LOG.warning(
"Scenario '%(scen)s' is located in 'default' "

View File

@ -164,14 +164,9 @@ class ScenarioRunner(plugin.Plugin, validation.ValidatablePluginMixin):
# 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)
# TODO(boris-42): remove method_name argument, now it's always run
self._run_scenario(scenario_plugin, "run", context, args)
self.run_duration = timer.duration()

View File

@ -15,8 +15,6 @@
import random
import six
from rally.common.i18n import _
from rally.common import logging
from rally.common.objects import task # noqa
@ -45,65 +43,22 @@ def configure(name=None, namespace="default", context=None):
If there are custom user specified contexts this one
will be updated by provided contexts.
"""
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()
def wrapper(cls):
# TODO(boris-42): Drop this check as soon as we refactor rally report
if name:
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:
scen._meta_set("namespace", namespace)
scen._meta_set("default_context", context or {})
return scen
cls = plugin.configure(name=name, namespace=namespace)(cls)
cls._meta_set("default_context", context or {})
return cls
return wrapper
class ConfigurePluginMeta(type):
"""Finish Scenario plugin configuration.
After @scenario.configure() is performed to cls.method, method.im_class is
pointing to FuncPlugin class instead of original cls. There is no way to
fix this, mostly because im_class is add to method when it's called via
cls, e.g. cls.method. Decorator is different case so there is no
information about cls. method._plugin is pointing to FuncPlugin that has
FuncPlugin pointer to method. What should be done is to set properly
FuncPluing.func_ref to the cls.method
This metaclass iterates over all cls methods and fix func_ref of FuncPlugin
class so func_ref will be cls.method instead of FuncPlugin.method.
Additionally this metaclass sets plugin names if they were not set explicit
via configure(). Default name is <cls_name>.<method_name>
As well we need to keep cls_ref inside of _meta because Python3 loves us.
Viva black magic and dirty hacks.
"""
def __init__(cls, name, bases, namespaces):
super(ConfigurePluginMeta, cls).__init__(name, bases, namespaces)
for name, field in namespaces.items():
if callable(field) and hasattr(field, "_plugin"):
field._plugin._meta_set("cls_ref", cls)
if not field._meta_get("name", None):
field._set_name_and_namespace(
"%s.%s" % (cls.__name__, field.__name__),
field.get_namespace())
field._plugin.func_ref = getattr(
cls, field._plugin.func_ref.__name__)
@validation.add_default("args-spec")
@plugin.base()
@six.add_metaclass(ConfigurePluginMeta)
class Scenario(plugin.Plugin,
atomic.ActionTimerMixin,
functional.FunctionalMixin,
@ -203,6 +158,4 @@ class Scenario(plugin.Plugin,
@classmethod
def _get_doc(cls):
if cls.is_classbased:
return cls.run.__doc__
return cls.__doc__
return cls.run.__doc__

View File

@ -16,10 +16,9 @@
from rally.task import scenario
@scenario.configure(name="FakeScenarioPlugin1.list")
class FakeScenarioPlugin1(scenario.Scenario):
"""Sample fake plugin."""
@scenario.configure()
def list(self):
"""Fake scenario."""
def run(self):
"""Sample fake scenario."""
pass

View File

@ -16,10 +16,9 @@
from rally.task import scenario
@scenario.configure(name="FakeScenarioPlugin2.list")
class FakeScenarioPlugin2(scenario.Scenario):
"""Sample fake plugin."""
@scenario.configure()
def list(self):
"""Fake scenario."""
def run(self):
"""Sample fake scenario."""
pass

View File

@ -20,30 +20,6 @@ from tests.unit import test
class PluginModuleTestCase(test.TestCase):
def test_deprecated_func(self):
@plugin.deprecated("some", "0.0.1")
@plugin.configure(name="deprecated_func_plugin_test")
@plugin.from_func()
def func():
return 42
self.assertEqual("deprecated_func_plugin_test", func.get_name())
self.assertEqual({"reason": "some", "rally_version": "0.0.1"},
func.is_deprecated())
self.assertEqual(42, func())
def test_configure(self):
@plugin.configure(name="configure_func_plugin_test")
@plugin.from_func()
def func(a):
return a
self.assertEqual("configure_func_plugin_test", func.get_name())
self.assertFalse(func.is_hidden())
self.assertEqual(42, func(42))
def test_deprecated_cls(self):
@plugin.deprecated("God why?", "0.0.2")
@ -106,38 +82,6 @@ class PluginModuleTestCase(test.TestCase):
self.assertRaises(exceptions.MultipleMatchesFound, plugin.Plugin.get,
name, name)
def test_from_func(self):
@plugin.from_func()
def func():
return 42
missing = [field for field in set(dir(plugin.Plugin)) - set(dir(func))
if not field.startswith("__")]
self.assertEqual([], missing)
self.assertTrue(issubclass(func._plugin, plugin.Plugin))
self.assertEqual(42, func())
def test_from_func_with_basecls(self):
class FakeFuncBasePlugin(plugin.Plugin):
pass
@plugin.from_func(FakeFuncBasePlugin)
def func():
return 43
self.assertTrue(issubclass(func._plugin, FakeFuncBasePlugin))
self.assertEqual(43, func())
def test_from_func_with_bad_basecls(self):
class FakeFuncBasePlugin(object):
pass
self.assertRaises(TypeError,
plugin.from_func, FakeFuncBasePlugin)
@plugin.configure(name="test_base_plugin")
class BasePlugin(plugin.Plugin):

View File

@ -22,7 +22,6 @@ import rally
from rally.common.plugin import plugin
from rally.common import validation
from rally.plugins.common import validators
from rally.task import scenario
from tests.unit import test
@ -67,7 +66,7 @@ class ArgsValidatorTestCase(test.TestCase):
@plugin.base()
class DummyPluginBase(plugin.Plugin,
validation.ValidatablePluginMixin):
is_classbased = True
pass
@validation.add(name="args-spec")
@plugin.configure(name="dummy_plugin")
@ -85,23 +84,6 @@ class ArgsValidatorTestCase(test.TestCase):
DummyPlugin.unregister()
class DummyPlugin2(DummyPluginBase):
@scenario.configure(name="dummy_plugin.func_based")
def func_based(self, a, b, c="spam"):
pass
result = scenario.Scenario.validate(
"dummy_plugin.func_based", None, config, None)
if err_msg is None:
self.assertEqual(0, len(result))
else:
self.assertEqual(1, len(result))
self.assertFalse(result[0].is_valid)
self.assertIn(err_msg, result[0].msg)
DummyPlugin2.func_based.unregister()
@ddt.ddt
class RequiredParameterValidatorTestCase(test.TestCase):

View File

@ -21,7 +21,6 @@ import mock
from rally.plugins.common.runners import serial
from rally.task import runner
from rally.task import scenario
from tests.unit import fakes
from tests.unit import test
@ -135,41 +134,6 @@ class ScenarioRunnerTestCase(test.TestCase):
@mock.patch(BASE + "rutils.Timer.duration", return_value=10)
def test_run(self, mock_timer_duration):
runner_obj = serial.SerialScenarioRunner(
mock.MagicMock(),
mock.MagicMock())
runner_obj._run_scenario = mock.MagicMock()
scenario_name = "NovaServers.boot_server_from_volume_and_delete"
config_kwargs = {"image": {"id": 1}, "flavor": {"id": 1}}
context_obj = {
"task": runner_obj.task,
"scenario_name": scenario_name,
"admin": {"credential": mock.MagicMock()},
"config": {
"cleanup": ["nova", "cinder"], "some_ctx": 2, "users": {}
}
}
result = runner_obj.run(scenario_name, context_obj, config_kwargs)
self.assertIsNone(result)
self.assertEqual(runner_obj.run_duration,
mock_timer_duration.return_value)
self.assertEqual(list(runner_obj.result_queue), [])
plugin_cls, method_name = scenario.Scenario.get(scenario_name), "run"
self.assertTrue(plugin_cls.is_classbased)
expected_config_kwargs = {"image": 1, "flavor": 1}
runner_obj._run_scenario.assert_called_once_with(
plugin_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(),

View File

@ -25,52 +25,13 @@ class ScenarioConfigureTestCase(test.TestCase):
def test_configure(self):
@scenario.configure("fooscenario.name", "testing")
def some_func():
pass
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):
@scenario.configure(namespace="testing", context={"any": 42})
def some_func():
pass
self.assertIsNone(some_func._meta_get("name"))
self.assertEqual("testing", some_func.get_namespace())
self.assertEqual({"any": 42}, some_func.get_default_context())
self.assertFalse(some_func.is_classbased)
some_func.unregister()
def test_configure_cls(self):
class ScenarioPluginCls(scenario.Scenario):
@scenario.configure(namespace="any", context={"any": 43})
def some(self):
pass
self.assertEqual("ScenarioPluginCls.some",
ScenarioPluginCls.some.get_name())
self.assertEqual("any", ScenarioPluginCls.some.get_namespace())
self.assertEqual({"any": 43},
ScenarioPluginCls.some.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)
self.assertEqual("testing", SomeScenario.get_namespace())
SomeScenario.unregister()

View File

@ -20,20 +20,23 @@ from rally.task import types
from tests.unit import test
class TestConvertPlugin(scenario.Scenario):
@types.convert(bar={"type": "test_bar"})
@scenario.configure()
def one_arg(self, bar):
@types.convert(bar={"type": "test_bar"})
@scenario.configure(name="TestConvertPlugin.one_arg")
class TestConvertOneArgPlugin(scenario.Scenario):
def run(self, bar):
"""Dummy docstring.
:param bar: dummy parameter
"""
pass
@types.convert(bar={"type": "test_bar"},
baz={"type": "test_baz"})
@scenario.configure()
def two_args(self, bar, baz):
@types.convert(bar={"type": "test_bar"},
baz={"type": "test_baz"})
@scenario.configure(name="TestConvertPlugin.two_args")
class TestConvertTwoArgsPlugin(scenario.Scenario):
def run(self, bar, baz):
"""Dummy docstring.
:param bar: dummy parameter

View File

@ -33,22 +33,27 @@ MODULE = "rally.task.validation."
class ValidationUtilsTestCase(test.TestCase):
def test_old_validator_admin(self):
@plugin.from_func()
def scenario():
def setUp(self):
super(ValidationUtilsTestCase, self).setUp()
class Plugin(plugin.Plugin):
pass
scenario._meta_init()
Plugin._meta_init()
self.Plugin = Plugin
def test_old_validator_admin(self):
validator_func = mock.Mock()
validator_func.return_value = None
validator = validation.validator(validator_func)
self.assertEqual(scenario, validator("a", "b", "c", d=1)(scenario))
self.assertEqual(1, len(scenario._meta_get("validators")))
self.assertEqual(self.Plugin,
validator("a", "b", "c", d=1)(self.Plugin))
self.assertEqual(1, len(self.Plugin._meta_get("validators")))
vname, args, kwargs = scenario._meta_get("validators")[0]
vname, args, kwargs = self.Plugin._meta_get("validators")[0]
validator_cls = common_validation.Validator.get(vname)
validator_inst = validator_cls(*args, **kwargs)
fake_admin = fakes.fake_credential()
@ -64,21 +69,17 @@ class ValidationUtilsTestCase(test.TestCase):
deployment.get_credentials_for("openstack"))
def test_old_validator_users(self):
@plugin.from_func()
def scenario():
pass
scenario._meta_init()
validator_func = mock.Mock()
validator_func.return_value = None
validator = validation.validator(validator_func)
self.assertEqual(scenario, validator("a", "b", "c", d=1)(scenario))
self.assertEqual(1, len(scenario._meta_get("validators")))
self.assertEqual(self.Plugin,
validator("a", "b", "c", d=1)(self.Plugin))
self.assertEqual(1, len(self.Plugin._meta_get("validators")))
vname, args, kwargs = scenario._meta_get("validators")[0]
vname, args, kwargs = self.Plugin._meta_get("validators")[0]
validator_cls = common_validation.Validator.get(vname)
validator_inst = validator_cls(*args, **kwargs)
fake_admin = fakes.fake_credential()
@ -104,21 +105,17 @@ class ValidationUtilsTestCase(test.TestCase):
deployment.get_credentials_for("openstack"))
def test_old_validator_users_error(self):
@plugin.from_func()
def scenario():
pass
scenario._meta_init()
validator_func = mock.Mock()
validator_func.return_value = common_validation.ValidationResult(False)
validator = validation.validator(validator_func)
self.assertEqual(scenario, validator("a", "b", "c", d=1)(scenario))
self.assertEqual(1, len(scenario._meta_get("validators")))
self.assertEqual(self.Plugin,
validator("a", "b", "c", d=1)(self.Plugin))
self.assertEqual(1, len(self.Plugin._meta_get("validators")))
vname, args, kwargs = scenario._meta_get("validators")[0]
vname, args, kwargs = self.Plugin._meta_get("validators")[0]
validator_cls = common_validation.Validator.get(vname)
validator_inst = validator_cls(*args, **kwargs)
fake_admin = fakes.fake_credential()
@ -141,22 +138,18 @@ class ValidationUtilsTestCase(test.TestCase):
@mock.patch("rally.task.validation.LOG.warning")
def test_deprecated_validator(self, mock_log_warning):
@plugin.from_func()
def my_plugin():
pass
my_plugin._meta_init()
my_plugin._meta_set("name", "my_plugin")
my_deprecated_validator = validation.deprecated_validator(
"new_validator", "deprecated_validator", "0.10.0")
my_plugin = my_deprecated_validator("foo", bar="baz")(my_plugin)
self.Plugin = my_deprecated_validator("foo", bar="baz")(self.Plugin)
self.assertEqual([("new_validator", ("foo",), {"bar": "baz"})],
my_plugin._meta_get("validators"))
self.Plugin._meta_get("validators"))
mock_log_warning.assert_called_once_with(
"Plugin '%s' uses validator 'rally.task.validation.%s' which is "
"deprecated in favor of '%s' (it should be used via new decorator "
"'rally.common.validation.add') in Rally v%s.",
"my_plugin", "deprecated_validator", "new_validator", "0.10.0")
self.Plugin.get_name(), "deprecated_validator", "new_validator",
"0.10.0")
@ddt.ddt
@ -164,14 +157,13 @@ class ValidatorsTestCase(test.TestCase):
def _unwrap_validator(self, validator, *args, **kwargs):
@plugin.from_func()
def func():
class Plugin(plugin.Plugin):
pass
func._meta_init()
validator(*args, **kwargs)(func)
Plugin._meta_init()
validator(*args, **kwargs)(Plugin)
fn = func._meta_get("validators")[0][1][0]
fn = Plugin._meta_get("validators")[0][1][0]
def wrap_validator(config, admin_clients, clients):
return (fn(config, admin_clients, clients, *args, **kwargs) or