diff --git a/rally-jobs/self-rally.yaml b/rally-jobs/self-rally.yaml index 2b445ed483..d84de4047e 100644 --- a/rally-jobs/self-rally.yaml +++ b/rally-jobs/self-rally.yaml @@ -253,28 +253,25 @@ times: 20 concurrency: 2 hooks: - - name: sys_call - description: Run script - args: sh rally-jobs/extra/hook_example_script.sh + - description: Run script + action: + sys_call: sh rally-jobs/extra/hook_example_script.sh trigger: - name: event - args: + event: unit: iteration at: [2, 5, 8, 13, 17] - - name: sys_call - description: Show time - args: date +%Y-%m-%dT%H:%M:%S + - description: Show time + action: + sys_call: date +%Y-%m-%dT%H:%M:%S trigger: - name: event - args: + event: unit: time at: [0, 2, 5, 6, 9] - - name: sys_call - description: Show system name - args: uname -a + - description: Show system name + action: + sys_call: uname -a trigger: - name: event - args: + event: unit: iteration at: [2, 3, 4, 5, 6, 8, 10, 12, 13, 15, 17, 18] sla: @@ -290,12 +287,11 @@ times: 10 concurrency: 2 hooks: - - name: sys_call - description: test hook - args: /bin/true + - description: test hook + action: + sys_call: /bin/true trigger: - name: periodic - args: + periodic: unit: iteration step: 2 start: 4 @@ -312,12 +308,11 @@ serial: times: 10 hooks: - - name: sys_call - description: Get system name - args: uname -a + - description: Show system name + action: + sys_call: uname -a trigger: - name: event - args: + event: unit: time at: [0, 2, 4, 6, 8, 10] sla: @@ -332,12 +327,11 @@ serial: times: 10 hooks: - - name: sys_call - description: test hook - args: /bin/true + - description: test hook + action: + sys_call: /bin/true trigger: - name: periodic - args: + periodic: unit: time step: 2 start: 0 diff --git a/rally/plugins/common/hook/sys_call.py b/rally/plugins/common/hook/sys_call.py index fa023635b5..a7f5f5f89d 100644 --- a/rally/plugins/common/hook/sys_call.py +++ b/rally/plugins/common/hook/sys_call.py @@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__) @hook.configure(name="sys_call") -class SysCallHook(hook.Hook): +class SysCallHook(hook.HookAction): """Performs system call.""" CONFIG_SCHEMA = { diff --git a/rally/plugins/common/trigger/__init__.py b/rally/plugins/common/hook/triggers/__init__.py similarity index 100% rename from rally/plugins/common/trigger/__init__.py rename to rally/plugins/common/hook/triggers/__init__.py diff --git a/rally/plugins/common/trigger/event.py b/rally/plugins/common/hook/triggers/event.py similarity index 96% rename from rally/plugins/common/trigger/event.py rename to rally/plugins/common/hook/triggers/event.py index dc5471bf89..a6b49e2d65 100644 --- a/rally/plugins/common/trigger/event.py +++ b/rally/plugins/common/hook/triggers/event.py @@ -14,11 +14,11 @@ # under the License. from rally import consts -from rally.task import trigger +from rally.task import hook -@trigger.configure(name="event") -class EventTrigger(trigger.Trigger): +@hook.configure(name="event") +class EventTrigger(hook.HookTrigger): """Triggers hook on specified event and list of values.""" CONFIG_SCHEMA = { diff --git a/rally/plugins/common/trigger/periodic.py b/rally/plugins/common/hook/triggers/periodic.py similarity index 96% rename from rally/plugins/common/trigger/periodic.py rename to rally/plugins/common/hook/triggers/periodic.py index 01135fec64..c590830042 100644 --- a/rally/plugins/common/trigger/periodic.py +++ b/rally/plugins/common/hook/triggers/periodic.py @@ -14,11 +14,11 @@ # under the License. from rally import consts -from rally.task import trigger +from rally.task import hook -@trigger.configure(name="periodic") -class PeriodicTrigger(trigger.Trigger): +@hook.configure(name="periodic") +class PeriodicTrigger(hook.HookTrigger): """Periodically triggers hook with specified range and step.""" CONFIG_SCHEMA = { diff --git a/rally/task/engine.py b/rally/task/engine.py index b2211821e4..6e632ea553 100644 --- a/rally/task/engine.py +++ b/rally/task/engine.py @@ -33,7 +33,6 @@ from rally.task import hook from rally.task import runner from rally.task import scenario from rally.task import sla -from rally.task import trigger LOG = logging.getLogger(__name__) @@ -312,19 +311,22 @@ class TaskEngine(object): vtype=vtype)) for hook_conf in workload["hooks"]: - results.extend(hook.Hook.validate( - name=hook_conf["config"]["name"], + action_name, action_cfg = list( + hook_conf["config"]["action"].items())[0] + results.extend(hook.HookAction.validate( + name=action_name, context=vcontext, config=None, - plugin_cfg=hook_conf["config"]["args"], + plugin_cfg=action_cfg, vtype=vtype)) - trigger_conf = hook_conf["config"]["trigger"] - results.extend(trigger.Trigger.validate( - name=trigger_conf["name"], + trigger_name, trigger_cfg = list( + hook_conf["config"]["trigger"].items())[0] + results.extend(hook.HookTrigger.validate( + name=trigger_name, context=vcontext, config=None, - plugin_cfg=trigger_conf["args"], + plugin_cfg=trigger_cfg, vtype=vtype)) if results: @@ -495,26 +497,6 @@ class TaskConfig(object): """ - HOOK_CONFIG = { - "type": "object", - "properties": { - "name": {"type": "string"}, - "description": {"type": "string"}, - "args": {}, - "trigger": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "args": {}, - }, - "required": ["name", "args"], - "additionalProperties": False, - } - }, - "required": ["name", "args", "trigger"], - "additionalProperties": False, - } - CONFIG_SCHEMA_V1 = { "type": "object", "$schema": consts.JSON_SCHEMA, @@ -537,12 +519,33 @@ class TaskConfig(object): "sla": {"type": "object"}, "hooks": { "type": "array", - "items": HOOK_CONFIG, + "items": {"$ref": "#/definitions/hook"}, } }, "additionalProperties": False } } + }, + "definitions": { + "hook": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + "args": {}, + "trigger": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "args": {}, + }, + "required": ["name", "args"], + "additionalProperties": False, + } + }, + "required": ["name", "args", "trigger"], + "additionalProperties": False, + } } } @@ -595,7 +598,7 @@ class TaskConfig(object): "sla": {"type": "object"}, "hooks": { "type": "array", - "items": HOOK_CONFIG, + "items": {"$ref": "#/definitions/hook"}, }, "contexts": {"type": "object"} }, @@ -627,7 +630,7 @@ class TaskConfig(object): "sla": {"type": "object"}, "hooks": { "type": "array", - "items": HOOK_CONFIG, + "items": {"$ref": "#/definitions/hook"}, }, "contexts": {"type": "object"} }, @@ -638,6 +641,43 @@ class TaskConfig(object): }, "additionalProperties": False, "required": ["title", "workloads"] + }, + "hook": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + "args": {}, + "trigger": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "args": {}, + }, + "required": ["name", "args"], + "additionalProperties": False, + } + }, + "required": ["name", "args", "trigger"], + "additionalProperties": False + }, + { + "properties": { + "action": { + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "patternProperties": {".*": {}} + }, + "trigger": {"$ref": "#/definitions/singleEntity"}, + "description": {"type": "string"}, + }, + "required": ["action", "trigger"], + "additionalProperties": False + }, + ] } } } @@ -718,8 +758,23 @@ class TaskConfig(object): else: wconf["runner"] = {"serial": {}} wconf.setdefault("sla", {"failure_rate": {"max": 0}}) - wconf.setdefault("hooks", []) - wconf["hooks"] = [{"config": h} for h in wconf["hooks"]] + + hooks = wconf.get("hooks", []) + wconf["hooks"] = [] + for hook_cfg in hooks: + if "name" in hook_cfg: + LOG.warning("The deprecated format of hook is found. " + "Check task format documentation for more " + "details.") + trigger_cfg = hook_cfg["trigger"] + wconf["hooks"].append({"config": { + "description": hook_cfg["description"], + "action": {hook_cfg["name"]: hook_cfg["args"]}, + "trigger": { + trigger_cfg["name"]: trigger_cfg["args"]}} + }) + else: + wconf["hooks"].append({"config": hook_cfg}) workloads.append(wconf) sconf["workloads"] = workloads @@ -757,6 +812,18 @@ class TaskConfig(object): if "runner" in subtask: runner_type = subtask["runner"].pop("type") subtask["runner"] = {runner_type: subtask["runner"]} + if "hooks" in subtask: + hooks = subtask["hooks"] + subtask["hooks"] = [] + for hook_cfg in hooks: + trigger_cfg = hook_cfg["trigger"] + subtask["hooks"].append( + {"description": hook_cfg["description"], + "action": { + hook_cfg["name"]: hook_cfg["args"]}, + "trigger": { + trigger_cfg["name"]: trigger_cfg["args"]}} + ) subtasks.append(subtask) return {"title": "Task (adopted from task format v1)", "subtasks": subtasks} diff --git a/rally/task/hook.py b/rally/task/hook.py index 0340136ee5..029e28410a 100644 --- a/rally/task/hook.py +++ b/rally/task/hook.py @@ -27,7 +27,6 @@ from rally.common import validation from rally import consts from rally import exceptions from rally.task.processing import charts -from rally.task import trigger from rally.task import utils @@ -47,9 +46,11 @@ class HookExecutor(object): self.triggers = collections.defaultdict(list) for hook in config.get("hooks", []): hook_cfg = hook["config"] - hook_cls = Hook.get(hook_cfg["name"]) - trigger_obj = trigger.Trigger.get( - hook_cfg["trigger"]["name"])(hook_cfg, self.task, hook_cls) + action_name = list(hook_cfg["action"].keys())[0] + trigger_name = list(hook_cfg["trigger"].keys())[0] + action_cls = HookAction.get(action_name) + trigger_obj = HookTrigger.get( + trigger_name)(hook_cfg, self.task, action_cls) event_type = trigger_obj.get_listening_event() self.triggers[event_type].append(trigger_obj) @@ -112,7 +113,7 @@ class HookExecutor(object): @validation.add_default("jsonschema") @plugin.base() @six.add_metaclass(abc.ABCMeta) -class Hook(plugin.Plugin, validation.ValidatablePluginMixin): +class HookAction(plugin.Plugin, validation.ValidatablePluginMixin): """Factory for hook classes.""" CONFIG_SCHEMA = {"type": "null"} @@ -205,3 +206,56 @@ class Hook(plugin.Plugin, validation.ValidatablePluginMixin): # hook is still running, wait for result self._thread.join() return self._result + + +@validation.add_default("jsonschema") +@plugin.base() +@six.add_metaclass(abc.ABCMeta) +class HookTrigger(plugin.Plugin, validation.ValidatablePluginMixin): + """Factory for hook trigger classes.""" + + CONFIG_SCHEMA = {"type": "null"} + + def __init__(self, hook_cfg, task, hook_cls): + self.hook_cfg = hook_cfg + self.config = self.hook_cfg["trigger"][self.get_name()] + self.task = task + self.hook_cls = hook_cls + self._runs = [] + + @abc.abstractmethod + def get_listening_event(self): + """Returns event type to listen.""" + + def on_event(self, event_type, value=None): + """Launch hook on specified event.""" + LOG.info(_("Hook action %s is triggered for Task %s by %s=%s") + % (self.hook_cls.get_name(), self.task["uuid"], + event_type, value)) + action_cfg = list(self.hook_cfg["action"].values())[0] + action = self.hook_cls(self.task, action_cfg, + {"event_type": event_type, "value": value}) + action.run_async() + self._runs.append(action) + + def get_results(self): + results = {"config": self.hook_cfg, + "results": [], + "summary": {}} + for action in self._runs: + action_result = action.result() + results["results"].append(action_result) + results["summary"].setdefault(action_result["status"], 0) + results["summary"][action_result["status"]] += 1 + return results + + +class Hook(HookAction): + """DEPRECATED! USE `rally.task.hook.HookAction` instead.""" + + def __init__(self, *args, **kwargs): + super(Hook, self).__init__(*args, **kwargs) + LOG.warning("Please contact Rally plugin maintainer. The plugin '%s' " + "inherits the deprecated base class(Hook), " + "`rally.task.hook.HookAction` should be used instead." + % self.get_name()) diff --git a/rally/task/processing/plot.py b/rally/task/processing/plot.py index 8079fcdb32..08ae80ec9f 100644 --- a/rally/task/processing/plot.py +++ b/rally/task/processing/plot.py @@ -33,7 +33,7 @@ def _process_hooks(hooks): """Prepare hooks data for report.""" hooks_ctx = [] for hook in hooks: - hook_ctx = {"name": hook["config"]["name"], + hook_ctx = {"name": list(hook["config"]["action"].keys())[0], "desc": hook["config"].get("description", ""), "additive": [], "complete": []} diff --git a/rally/task/trigger.py b/rally/task/trigger.py index 80fec655da..afe93fc978 100644 --- a/rally/task/trigger.py +++ b/rally/task/trigger.py @@ -13,56 +13,28 @@ # License for the specific language governing permissions and limitations # under the License. -import abc - -import six - -from rally.common.i18n import _ from rally.common import logging -from rally.common.plugin import plugin -from rally.common import validation - -configure = plugin.configure +from rally.task import hook LOG = logging.getLogger(__name__) -@validation.add_default("jsonschema") -@plugin.base() -@six.add_metaclass(abc.ABCMeta) -class Trigger(plugin.Plugin, validation.ValidatablePluginMixin): - """Factory for trigger classes.""" +class Trigger(hook.HookTrigger): + """DEPRECATED!!! USE `rally.task.hook.HookTrigger` instead.""" - CONFIG_SCHEMA = {"type": "null"} + def __init__(self, *args, **kwargs): + super(Trigger, self).__init__(*args, **kwargs) + LOG.warning("Please contact Rally plugin maintainer. The plugin '%s' " + "inherits the deprecated base class(Trigger), " + "`rally.task.hook.HookTrigger` should be used instead." + % self.get_name()) - def __init__(self, context, task, hook_cls): - self.context = context - self.config = self.context["trigger"]["args"] - self.task = task - self.hook_cls = hook_cls - self._runs = [] - - @abc.abstractmethod - def get_listening_event(self): - """Returns event type to listen.""" - - def on_event(self, event_type, value=None): - """Launch hook on specified event.""" - LOG.info(_("Hook %s is triggered for Task %s by %s=%s") - % (self.hook_cls.__name__, self.task["uuid"], - event_type, value)) - hook = self.hook_cls(self.task, self.context.get("args", {}), - {"event_type": event_type, "value": value}) - hook.run_async() - self._runs.append(hook) - - def get_results(self): - results = {"config": self.context, - "results": [], - "summary": {}} - for hook in self._runs: - hook_result = hook.result() - results["results"].append(hook_result) - results["summary"].setdefault(hook_result["status"], 0) - results["summary"][hook_result["status"]] += 1 - return results + @property + def context(self): + action_name, action_cfg = list(self.hook_cfg["action"].items())[0] + trigger_name, trigger_cfg = list(self.hook_cfg["trigger"].items())[0] + return {"description": self.hook_cfg["description"], + "name": action_name, + "args": action_cfg, + "trigger": {"name": trigger_name, + "args": trigger_cfg}} diff --git a/tests/unit/plugins/common/hook/test_sys_call.py b/tests/unit/plugins/common/hook/test_sys_call.py index 14b9cf36b9..0c4600dfe5 100644 --- a/tests/unit/plugins/common/hook/test_sys_call.py +++ b/tests/unit/plugins/common/hook/test_sys_call.py @@ -31,7 +31,7 @@ class SysCallHookTestCase(test.TestCase): @ddt.data(("ls", True), (50, False)) @ddt.unpack def test_validate(self, config, valid): - results = hook.Hook.validate( + results = hook.HookAction.validate( "sys_call", None, None, config) if valid: self.assertEqual([], results) diff --git a/tests/unit/plugins/common/trigger/__init__.py b/tests/unit/plugins/common/hook/triggers/__init__.py similarity index 100% rename from tests/unit/plugins/common/trigger/__init__.py rename to tests/unit/plugins/common/hook/triggers/__init__.py diff --git a/tests/unit/plugins/common/trigger/test_event.py b/tests/unit/plugins/common/hook/triggers/test_event.py similarity index 90% rename from tests/unit/plugins/common/trigger/test_event.py rename to tests/unit/plugins/common/hook/triggers/test_event.py index 7cffbc9044..0c25eade61 100644 --- a/tests/unit/plugins/common/trigger/test_event.py +++ b/tests/unit/plugins/common/hook/triggers/test_event.py @@ -16,8 +16,8 @@ import ddt import mock -from rally.plugins.common.trigger import event -from rally.task import trigger +from rally.plugins.common.hook.triggers import event +from rally.task import hook from tests.unit import test @@ -28,8 +28,8 @@ class EventTriggerTestCase(test.TestCase): super(EventTriggerTestCase, self).setUp() self.hook_cls = mock.MagicMock(__name__="name") self.trigger = event.EventTrigger( - {"trigger": {"name": "event", - "args": {"unit": "iteration", "at": [1, 4, 5]}}}, + {"trigger": {"event": {"unit": "iteration", "at": [1, 4, 5]}}, + "action": {"foo": {}}}, mock.MagicMock(), self.hook_cls) @ddt.data((dict(unit="time", at=[0, 3, 5]), True), @@ -51,7 +51,7 @@ class EventTriggerTestCase(test.TestCase): (dict(at=[1, 2, 3]), False)) @ddt.unpack def test_validate(self, config, valid): - results = trigger.Trigger.validate("event", None, None, config) + results = hook.HookTrigger.validate("event", None, None, config) if valid: self.assertEqual([], results) else: diff --git a/tests/unit/plugins/common/trigger/test_periodic.py b/tests/unit/plugins/common/hook/triggers/test_periodic.py similarity index 87% rename from tests/unit/plugins/common/trigger/test_periodic.py rename to tests/unit/plugins/common/hook/triggers/test_periodic.py index ffe58b2084..7bd55375cb 100644 --- a/tests/unit/plugins/common/trigger/test_periodic.py +++ b/tests/unit/plugins/common/hook/triggers/test_periodic.py @@ -16,8 +16,8 @@ import ddt import mock -from rally.plugins.common.trigger import periodic -from rally.task import trigger +from rally.plugins.common.hook.triggers import periodic +from rally.task import hook from tests.unit import test @@ -28,8 +28,8 @@ class PeriodicTriggerTestCase(test.TestCase): super(PeriodicTriggerTestCase, self).setUp() self.hook_cls = mock.MagicMock(__name__="name") self.trigger = periodic.PeriodicTrigger( - {"trigger": {"name": "periodic", - "args": {"unit": "iteration", "step": 2}}}, + {"trigger": {"periodic": {"unit": "iteration", "step": 2}}, + "action": {"foo": {}}}, mock.MagicMock(), self.hook_cls) @ddt.data((dict(unit="time", step=1), True), @@ -52,7 +52,7 @@ class PeriodicTriggerTestCase(test.TestCase): (dict(step=1), False)) @ddt.unpack def test_validate(self, config, valid): - results = trigger.Trigger.validate("periodic", None, None, config) + results = hook.HookTrigger.validate("periodic", None, None, config) if valid: self.assertEqual([], results) else: @@ -74,9 +74,9 @@ class PeriodicTriggerTestCase(test.TestCase): @ddt.unpack def test_on_event_start_end(self, value, should_call): trigger = periodic.PeriodicTrigger( - {"trigger": {"name": "periodic", - "args": {"unit": "time", - "step": 3, "start": 2, "end": 9}}}, + {"trigger": {"periodic": {"unit": "time", + "step": 3, "start": 2, "end": 9}}, + "action": {"foo": {}}}, mock.MagicMock(), self.hook_cls) trigger.on_event("time", value) self.assertEqual(should_call, self.hook_cls.called) diff --git a/tests/unit/plugins/openstack/hook/test_fault_injection.py b/tests/unit/plugins/openstack/hook/test_fault_injection.py index 0dbd021589..a53d7ed63d 100644 --- a/tests/unit/plugins/openstack/hook/test_fault_injection.py +++ b/tests/unit/plugins/openstack/hook/test_fault_injection.py @@ -40,7 +40,8 @@ class FaultInjectionHookTestCase(test.TestCase): (dict(), False)) @ddt.unpack def test_config_schema(self, config, valid): - results = hook.Hook.validate("fault_injection", None, None, config) + results = hook.HookAction.validate("fault_injection", None, None, + config) if valid: self.assertEqual([], results) else: diff --git a/tests/unit/task/processing/test_plot.py b/tests/unit/task/processing/test_plot.py index 1e57c038eb..3a144a8cd9 100644 --- a/tests/unit/task/processing/test_plot.py +++ b/tests/unit/task/processing/test_plot.py @@ -87,48 +87,65 @@ class PlotTestCase(test.TestCase): @ddt.data( {"hooks": [], "expected": []}, {"hooks": [ - {"config": { - "trigger": {"args": {"at": [2, 5], "unit": "iteration"}, - "name": "event"}, - "args": "foo cmd", "description": "Foo", "name": "sys_call"}, - "results": [ - {"status": "success", "finished_at": 1475589987.525735, - "triggered_by": {"event_type": "iteration", "value": 2}, - "started_at": 1475589987.433399, - "output": { - "additive": [ - {"chart_plugin": "StatsTable", "title": "Foo table", - "data": [["A", 158], ["B", 177]]}], - "complete": []}}, - {"status": "success", "finished_at": 1475589993.457818, - "triggered_by": {"event_type": "iteration", "value": 5}, - "started_at": 1475589993.432734, - "output": { - "additive": [ - {"chart_plugin": "StatsTable", "title": "Foo table", - "data": [["A", 243], ["B", 179]]}], - "complete": []}}], - "summary": {"success": 2}}, - {"config": {"trigger": {"args": {"at": [1, 2, 4], "unit": "time"}, - "name": "event"}, - "args": "bar cmd", "name": "sys_call"}, - "results": [ - {"status": "success", "finished_at": 1475589988.437791, - "triggered_by": {"event_type": "time", "value": 1}, - "started_at": 1475589988.434244, - "output": {"additive": [], - "complete": [ - {"chart_plugin": "Pie", "title": "Bar Pie", - "data": [["F", 4], ["G", 2]]}]}}, - {"status": "success", - "finished_at": 1475589989.437589, - "triggered_by": {"event_type": "time", "value": 2}, - "started_at": 1475589989.433964, - "output": {"additive": [], - "complete": [ - {"chart_plugin": "Pie", "title": "Bar Pie", - "data": [["F", 42], ["G", 24]]}]}}], - "summary": {"success": 2}}], + { + "config": { + "description": "Foo", + "action": {"sys_call": "foo cmd"}, + "trigger": {"event": {"at": [2, 5], "unit": "iteration"}}}, + "results": [ + { + "status": "success", + "started_at": 1475589987.433399, + "finished_at": 1475589987.525735, + "triggered_by": {"event_type": "iteration", + "value": 2}, + "output": { + "additive": [ + {"chart_plugin": "StatsTable", + "title": "Foo table", + "data": [["A", 158], ["B", 177]]}], + "complete": []}}, + { + "status": "success", + "started_at": 1475589993.432734, + "finished_at": 1475589993.457818, + "triggered_by": {"event_type": "iteration", + "value": 5}, + "output": { + "additive": [ + {"chart_plugin": "StatsTable", + "title": "Foo table", + "data": [["A", 243], ["B", 179]]}], + "complete": []}}], + "summary": {"success": 2}}, + { + "config": { + "action": {"sys_call": "bar cmd"}, + "trigger": {"event": {"at": [1, 2, 4], "unit": "time"}}}, + "results": [ + { + "status": "success", + "started_at": 1475589988.434244, + "finished_at": 1475589988.437791, + "triggered_by": {"event_type": "time", "value": 1}, + "output": { + "additive": [], + "complete": [ + {"chart_plugin": "Pie", + "title": "Bar Pie", + "data": [["F", 4], ["G", 2]]}]}}, + { + "status": "success", + "started_at": 1475589989.433964, + "finished_at": 1475589989.437589, + "triggered_by": {"event_type": "time", "value": 2}, + "output": { + "additive": [], + "complete": [ + {"chart_plugin": "Pie", + "title": "Bar Pie", + "data": [["F", 42], ["G", 24]]}]}}], + "summary": {"success": 2}}], "expected": [ {"additive": [ {"data": {"cols": ["Action", "Min (sec)", "Median (sec)", diff --git a/tests/unit/task/test_engine.py b/tests/unit/task/test_engine.py index 3be484b375..ec05a22cfb 100644 --- a/tests/unit/task/test_engine.py +++ b/tests/unit/task/test_engine.py @@ -128,8 +128,8 @@ class TaskEngineTestCase(test.TestCase): @mock.patch("rally.task.engine.scenario.Scenario.get") @mock.patch("rally.task.sla.SLA.validate") - @mock.patch("rally.task.trigger.Trigger.validate") - @mock.patch("rally.task.hook.Hook.validate") + @mock.patch("rally.task.hook.HookTrigger.validate") + @mock.patch("rally.task.hook.HookAction.validate") @mock.patch("rally.task.engine.TaskConfig") @mock.patch("rally.task.engine.runner.ScenarioRunner.validate") @mock.patch("rally.task.engine.context.Context.validate") @@ -137,15 +137,15 @@ class TaskEngineTestCase(test.TestCase): self, mock_context_validate, mock_scenario_runner_validate, mock_task_config, - mock_hook_validate, - mock_trigger_validate, + mock_hook_action_validate, + mock_hook_trigger_validate, mock_sla_validate, mock_scenario_get): mock_context_validate.return_value = [] mock_sla_validate.return_value = [] - mock_hook_validate.return_value = [] - mock_trigger_validate.return_value = [] + mock_hook_action_validate.return_value = [] + mock_hook_trigger_validate.return_value = [] default_context = {"foo": "foo_conf"} scenario_cls = mock_scenario_get.return_value scenario_cls.get_platform.return_value = "default" @@ -153,9 +153,8 @@ class TaskEngineTestCase(test.TestCase): scenario_name = "Foo.bar" runner_type = "MegaRunner" - hook_conf = {"name": "c", - "args": "c_args", - "trigger": {"name": "d", "args": "d_args"}} + hook_conf = {"action": {"c": "c_args"}, + "trigger": {"d": "d_args"}} workload = {"name": scenario_name, "runner": {"type": runner_type}, "context": {"a": "a_conf"}, @@ -186,10 +185,10 @@ class TaskEngineTestCase(test.TestCase): mock_sla_validate.assert_called_once_with( config=None, context=None, name="foo_sla", plugin_cfg="sla_conf", vtype=None) - mock_hook_validate.assert_called_once_with( + mock_hook_action_validate.assert_called_once_with( config=None, context=None, name="c", plugin_cfg="c_args", vtype=None) - mock_trigger_validate.assert_called_once_with( + mock_hook_trigger_validate.assert_called_once_with( config=None, context=None, name="d", plugin_cfg="d_args", vtype=None) @@ -270,21 +269,21 @@ class TaskEngineTestCase(test.TestCase): @mock.patch("rally.task.engine.json.dumps") @mock.patch("rally.task.engine.scenario.Scenario.get") - @mock.patch("rally.task.hook.Hook.validate") - @mock.patch("rally.task.trigger.Trigger.validate") + @mock.patch("rally.task.hook.HookAction.validate") + @mock.patch("rally.task.hook.HookTrigger.validate") @mock.patch("rally.task.engine.TaskConfig") def test__validate_config_syntax__wrong_hook( - self, mock_task_config, mock_trigger_validate, mock_hook_validate, + self, mock_task_config, mock_hook_trigger_validate, + mock_hook_action_validate, mock_scenario_get, mock_dumps): mock_dumps.return_value = "" - mock_trigger_validate.return_value = [] - mock_hook_validate.return_value = ["hook_error"] + mock_hook_trigger_validate.return_value = [] + mock_hook_action_validate.return_value = ["hook_error"] scenario_cls = mock_scenario_get.return_value scenario_cls.get_default_context.return_value = {} mock_task_instance = mock.MagicMock() - hook_conf = {"name": "c", - "args": "c_args", - "trigger": {"name": "d", "args": "d_args"}} + hook_conf = {"action": {"c": "c_args"}, + "trigger": {"d": "d_args"}} mock_task_instance.subtasks = [{"workloads": [ self._make_workload(name="sca"), self._make_workload(name="sca", position=1, @@ -303,21 +302,21 @@ class TaskEngineTestCase(test.TestCase): @mock.patch("rally.task.engine.json.dumps") @mock.patch("rally.task.engine.scenario.Scenario.get") - @mock.patch("rally.task.trigger.Trigger.validate") - @mock.patch("rally.task.hook.Hook.validate") + @mock.patch("rally.task.hook.HookTrigger.validate") + @mock.patch("rally.task.hook.HookAction.validate") @mock.patch("rally.task.engine.TaskConfig") def test__validate_config_syntax__wrong_trigger( - self, mock_task_config, mock_hook_validate, mock_trigger_validate, + self, mock_task_config, mock_hook_action_validate, + mock_hook_trigger_validate, mock_scenario_get, mock_dumps): mock_dumps.return_value = "" - mock_trigger_validate.return_value = ["trigger_error"] - mock_hook_validate.return_value = [] + mock_hook_trigger_validate.return_value = ["trigger_error"] + mock_hook_action_validate.return_value = [] scenario_cls = mock_scenario_get.return_value scenario_cls.get_default_context.return_value = {} mock_task_instance = mock.MagicMock() - hook_conf = {"name": "c", - "args": "c_args", - "trigger": {"name": "d", "args": "d_args"}} + hook_conf = {"action": {"c": "c_args"}, + "trigger": {"d": "d_args"}} mock_task_instance.subtasks = [{"workloads": [ self._make_workload(name="sca"), self._make_workload(name="sca", position=1, @@ -954,7 +953,7 @@ class ResultConsumerTestCase(test.TestCase): self.assertEqual(4, mock_task_get_status.call_count) -class TaskTestCase(test.TestCase): +class TaskConfigTestCase(test.TestCase): @mock.patch("jsonschema.validate") def test_validate_json(self, mock_validate): config = {} @@ -996,19 +995,75 @@ class TaskTestCase(test.TestCase): config = collections.OrderedDict() config["a.task"] = [{"s": 1, "context": {"foo": "bar"}}, {"s": 2}] config["b.task"] = [{"s": 3, "sla": {"key": "value"}}] + config["c.task"] = [{"s": 5, + "hooks": [{"name": "foo", + "args": "bar", + "description": "DESCR!!!", + "trigger": { + "name": "mega-trigger", + "args": {"some": "thing"} + }}] + }] self.assertEqual( {"title": "Task (adopted from task format v1)", - "subtasks": [{"title": "a.task", - "scenario": {"a.task": {}}, - "s": 1, - "contexts": {"foo": "bar"}}, - {"title": "a.task", - "s": 2, - "scenario": {"a.task": {}}, - "contexts": {}}, - {"title": "b.task", - "s": 3, - "scenario": {"b.task": {}}, - "sla": {"key": "value"}, - "contexts": {}}]}, + "subtasks": [ + { + "title": "a.task", + "scenario": {"a.task": {}}, + "s": 1, + "contexts": {"foo": "bar"} + }, + { + "title": "a.task", + "s": 2, + "scenario": {"a.task": {}}, + "contexts": {} + }, + { + "title": "b.task", + "s": 3, + "scenario": {"b.task": {}}, + "sla": {"key": "value"}, + "contexts": {} + }, + { + "title": "c.task", + "s": 5, + "scenario": {"c.task": {}}, + "contexts": {}, + "hooks": [ + {"description": "DESCR!!!", + "action": {"foo": "bar"}, + "trigger": {"mega-trigger": {"some": "thing"}}} + ] + }]}, TaskConfig._adopt_task_format_v1(config)) + + def test_hook_config_compatibility(self): + cfg = { + "title": "foo", + "version": 2, + "subtasks": [ + { + "title": "foo", + "scenario": {"xxx": {}}, + "runner": {"yyy": {}}, + "hooks": [ + {"description": "descr", + "name": "hook_action", + "args": {"k1": "v1"}, + "trigger": { + "name": "hook_trigger", + "args": {"k2": "v2"} + }} + ] + } + ] + } + task = engine.TaskConfig(cfg) + workload = task.subtasks[0]["workloads"][0] + self.assertEqual( + {"description": "descr", + "action": {"hook_action": {"k1": "v1"}}, + "trigger": {"hook_trigger": {"k2": "v2"}}}, + workload["hooks"][0]["config"]) diff --git a/tests/unit/task/test_hook.py b/tests/unit/task/test_hook.py index 3992cdfb7e..314f80dcff 100644 --- a/tests/unit/task/test_hook.py +++ b/tests/unit/task/test_hook.py @@ -25,7 +25,7 @@ from tests.unit import test @hook.configure(name="dummy_hook") -class DummyHook(hook.Hook): +class DummyHookAction(hook.HookAction): CONFIG_SCHEMA = { "type": "object", "properties": { @@ -56,14 +56,12 @@ class HookExecutorTestCase(test.TestCase): self.conf = { "hooks": [ {"config": { - "name": "dummy_hook", "description": "dummy_action", - "args": { - "status": consts.HookStatus.SUCCESS, + "action": { + "dummy_hook": {"status": consts.HookStatus.SUCCESS} }, "trigger": { - "name": "event", - "args": { + "event": { "unit": "iteration", "at": [1], } @@ -92,7 +90,7 @@ class HookExecutorTestCase(test.TestCase): @mock.patch("rally.task.hook.HookExecutor._timer_method") @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) def test_result_optional(self, mock_timer, mock__timer_method): - hook_args = self.conf["hooks"][0]["config"]["args"] + hook_args = list(self.conf["hooks"][0]["config"]["action"].values())[0] hook_args["error"] = ["Exception", "Description", "Traceback"] hook_args["output"] = {"additive": None, "complete": None} @@ -120,9 +118,10 @@ class HookExecutorTestCase(test.TestCase): hook_executor.results()) @mock.patch("rally.task.hook.HookExecutor._timer_method") - @mock.patch.object(DummyHook, "run", side_effect=Exception("My err msg")) + @mock.patch.object(DummyHookAction, "run", + side_effect=Exception("My err msg")) @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) - def test_failed_result(self, mock_timer, mock_dummy_hook_run, + def test_failed_result(self, mock_timer, mock_dummy_hook_action_run, mock__timer_method): hook_executor = hook.HookExecutor(self.conf, self.task) hook_executor.on_event(event_type="iteration", value=1) @@ -141,7 +140,8 @@ class HookExecutorTestCase(test.TestCase): @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) def test_time_event(self, mock_timer): - trigger_args = self.conf["hooks"][0]["config"]["trigger"]["args"] + trigger_args = list( + self.conf["hooks"][0]["config"]["trigger"].values())[0] trigger_args["unit"] = "time" hook_executor = hook.HookExecutor(self.conf, self.task) @@ -160,7 +160,7 @@ class HookExecutorTestCase(test.TestCase): @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) def test_time_periodic(self, mock_timer): self.conf["hooks"][0]["config"]["trigger"] = { - "name": "periodic", "args": {"unit": "time", "step": 2}} + "periodic": {"unit": "time", "step": 2}} hook_executor = hook.HookExecutor(self.conf, self.task) for i in range(1, 7): @@ -196,7 +196,8 @@ class HookExecutorTestCase(test.TestCase): @mock.patch("rally.common.utils.Stopwatch", autospec=True) @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) def test_timer_thread(self, mock_timer, mock_stopwatch): - trigger_args = self.conf["hooks"][0]["config"]["trigger"]["args"] + trigger_args = list( + self.conf["hooks"][0]["config"]["trigger"].values())[0] trigger_args["unit"] = "time" hook_executor = hook.HookExecutor(self.conf, self.task) @@ -235,7 +236,7 @@ class HookTestCase(test.TestCase): @ddt.data(({"status": "foo"}, True), (3, False)) @ddt.unpack def test_validate(self, config, valid): - results = hook.Hook.validate( + results = hook.HookAction.validate( "dummy_hook", None, None, config) if valid: self.assertEqual([], results) @@ -246,8 +247,8 @@ class HookTestCase(test.TestCase): def test_result(self, mock_timer): task = mock.MagicMock() triggered_by = {"event_type": "iteration", "value": 1} - dummy_hook = DummyHook(task, {"status": consts.HookStatus.SUCCESS}, - triggered_by) + dummy_hook = DummyHookAction( + task, {"status": consts.HookStatus.SUCCESS}, triggered_by) dummy_hook.run_sync() self.assertEqual( @@ -259,11 +260,80 @@ class HookTestCase(test.TestCase): def test_result_not_started(self): task = mock.MagicMock() triggered_by = {"event_type": "iteration", "value": 1} - dummy_hook = DummyHook(task, {"status": consts.HookStatus.SUCCESS}, - triggered_by) + dummy_hook = DummyHookAction(task, + {"status": consts.HookStatus.SUCCESS}, + triggered_by) self.assertEqual( {"started_at": 0.0, "finished_at": 0.0, "triggered_by": triggered_by, "status": consts.HookStatus.SUCCESS}, dummy_hook.result()) + + +@ddt.ddt +class TriggerTestCase(test.TestCase): + + def setUp(self): + super(TriggerTestCase, self).setUp() + + @hook.configure(name="dummy_trigger") + class DummyTrigger(hook.HookTrigger): + CONFIG_SCHEMA = {"type": "array", + "minItems": 1, + "uniqueItems": True, + "items": { + "type": "integer", + "minimum": 0, + }} + + def get_listening_event(self): + return "dummy" + + def on_event(self, event_type, value=None): + if value not in self.config: + return + super(DummyTrigger, self).on_event(event_type, value) + + self.DummyTrigger = DummyTrigger + + self.addCleanup(DummyTrigger.unregister) + + @ddt.data(([5], True), ("str", False)) + @ddt.unpack + def test_validate(self, config, valid): + results = hook.HookTrigger.validate( + "dummy_trigger", None, None, config) + if valid: + self.assertEqual([], results) + else: + self.assertEqual(1, len(results)) + + def test_on_event_and_get_results(self): + + # get_results requires launched hooks, so if we want to test it, we + # need to duplicate all calls on_event. It is redundant, so let's merge + # test_on_event and test_get_results in one test. + right_values = [5, 7, 12, 13] + + cfg = {"trigger": {self.DummyTrigger.get_name(): right_values}, + "action": {"fake": {}}} + task = mock.MagicMock() + hook_cls = mock.MagicMock(__name__="fake") + dummy_trigger = self.DummyTrigger(cfg, task, hook_cls) + for i in range(0, 20): + dummy_trigger.on_event("fake", i) + + self.assertEqual( + [mock.call(task, {}, {"event_type": "fake", "value": i}) + for i in right_values], + hook_cls.call_args_list) + self.assertEqual(len(right_values), + hook_cls.return_value.run_async.call_count) + hook_status = hook_cls.return_value.result.return_value["status"] + self.assertEqual( + {"config": cfg, + "results": [hook_cls.return_value.result.return_value] * + len(right_values), + "summary": {hook_status: len(right_values)}}, + dummy_trigger.get_results()) diff --git a/tests/unit/task/test_trigger.py b/tests/unit/task/test_trigger.py index 8e2ca14276..137eda5b7f 100644 --- a/tests/unit/task/test_trigger.py +++ b/tests/unit/task/test_trigger.py @@ -15,68 +15,50 @@ """Tests for Trigger base class.""" -import ddt import mock from rally.task import trigger from tests.unit import test -@trigger.configure(name="dummy_trigger") -class DummyTrigger(trigger.Trigger): - CONFIG_SCHEMA = {"type": "array", - "minItems": 1, - "uniqueItems": True, - "items": { - "type": "integer", - "minimum": 0, - }} - - def get_listening_event(self): - return "dummy" - - def on_event(self, event_type, value=None): - if value not in self.config: - return - super(DummyTrigger, self).on_event(event_type, value) - - -@ddt.ddt class TriggerTestCase(test.TestCase): - @ddt.data(([5], True), ("str", False)) - @ddt.unpack - def test_validate(self, config, valid): - results = trigger.Trigger.validate( - "dummy_trigger", None, None, config) - if valid: - self.assertEqual([], results) - else: - self.assertEqual(1, len(results)) + def setUp(self): + super(TriggerTestCase, self).setUp() - def test_on_event_and_get_results(self): - # get_results requires launched hooks, so if we want to test it, we - # need to duplicate all calls on_event. It is redundant, so let's merge - # test_on_event and test_get_results in one test. - right_values = [5, 7, 12, 13] + @trigger.hook.configure(self.id()) + class DummyTrigger(trigger.Trigger): + def get_listening_event(self): + return "dummy" - cfg = {"trigger": {"args": right_values}} - task = mock.MagicMock() - hook_cls = mock.MagicMock(__name__="fake") - dummy_trigger = DummyTrigger(cfg, task, hook_cls) - for i in range(0, 20): - dummy_trigger.on_event("fake", i) + self.addCleanup(DummyTrigger.unregister) + self.DummyTrigger = DummyTrigger + + @mock.patch("rally.task.trigger.LOG.warning") + def test_warning(self, mock_log_warning): + self.DummyTrigger({"trigger": {self.id(): {}}}, None, None) + + mock_log_warning.assert_called_once_with( + "Please contact Rally plugin maintainer. The plugin '%s'" + " inherits the deprecated base class(Trigger), " + "`rally.task.hook.HookTrigger` should be used instead." % + self.id()) + + def test_context(self): + action_name = "mega_action" + action_cfg = {"action_arg": "action_value"} + trigger_name = self.id() + trigger_cfg = {"trigger_arg": "trigger_value"} + descr = "descr" + + trigger_obj = self.DummyTrigger({ + "trigger": {trigger_name: trigger_cfg}, + "action": {action_name: action_cfg}, + "description": descr}, None, None) self.assertEqual( - [mock.call(task, {}, {"event_type": "fake", "value": i}) - for i in right_values], - hook_cls.call_args_list) - self.assertEqual(len(right_values), - hook_cls.return_value.run_async.call_count) - hook_status = hook_cls.return_value.result.return_value["status"] - self.assertEqual( - {"config": cfg, - "results": [hook_cls.return_value.result.return_value] * - len(right_values), - "summary": {hook_status: len(right_values)}}, - dummy_trigger.get_results()) + {"name": action_name, + "args": action_cfg, + "trigger": {"name": trigger_name, + "args": trigger_cfg}, + "description": descr}, trigger_obj.context)