Refactor hook stuff

- Triggers should care about triggering(launching) hooks, not only about
  checking when hooks should be launched. This logic is moved from
  HookExecutor to Triggers
- Remove "n/a" status of Hooks. There is no possible situation when such
  status can be configured. Also, "success" status is used now as default
  status.
- `sys.call` saves output for success call too.
- `get_configured_event_type` method of Trigger is renamed to
  `get_listening_event`.
- Task Engine should not launch hook executor and timers if hooks section
  is not configured
- HookExecutor doesn't create and start timer thread if "time" unit is not
  configured in any hook.
- Results of Hooks relate to each particular trigger, so they are stored in
  trigger object and HookExecutor obtains results from there.

Co-Authored-By: Andrey Kurilin <andr.kurilin@gmail.com>

Change-Id: I2ce6132e101790af75ed0899bc4d5e3ddb00d0d7
This commit is contained in:
Andrey Kurilin 2016-09-28 15:44:39 +03:00
parent 228d8c246f
commit ad52ccae24
18 changed files with 298 additions and 303 deletions

View File

@ -470,7 +470,7 @@ class TaskCommands(object):
results = [{"key": x["key"], "result": x["data"]["raw"],
"sla": x["data"]["sla"],
"hooks": x["data"]["hooks"],
"hooks": x["data"].get("hooks", []),
"load_duration": x["data"]["load_duration"],
"full_duration": x["data"]["full_duration"]}
for x in task.get_results()]
@ -580,7 +580,7 @@ class TaskCommands(object):
task_results = map(
lambda x: {"key": x["key"],
"sla": x["data"]["sla"],
"hooks": x["data"]["hooks"],
"hooks": x["data"].get("hooks", []),
"result": x["data"]["raw"],
"load_duration": x["data"]["load_duration"],
"full_duration": x["data"]["full_duration"]},
@ -660,7 +660,7 @@ class TaskCommands(object):
tasks_results = map(
lambda x: {"key": x["key"],
"sla": x["data"]["sla"],
"hooks": x["data"]["hooks"],
"hooks": x["data"].get("hooks", []),
"result": x["data"]["raw"],
"load_duration": x["data"]["load_duration"],
"full_duration": x["data"]["full_duration"]},

View File

@ -96,33 +96,18 @@ OUTPUT_SCHEMA = {
"additionalProperties": False
}
HOOK_RESULT_SCHEMA = {
HOOK_RUN_RESULT_SCHEMA = {
"type": "object",
"$schema": consts.JSON_SCHEMA,
"properties": {
"hook": {"type": "string"},
"started_at": {"type": "number"},
"finished_at": {"type": "number"},
"triggered_by": {
"type": "object",
"oneOf": [
{
"properties": {
"iteration": {"type": "integer"},
},
"required": ["iteration"],
"additionalProperties": False,
},
{
"properties": {
"time": {"type": "integer"},
},
"required": ["time"],
"additionalProperties": False,
},
]
"properties": {"event_type": {"type": "string"},
"value": {}},
"required": ["event_type", "value"],
"additionalProperties": False
},
"description": {"type": "string"},
"status": {"type": "string"},
"error": {
"type": "array",
@ -132,14 +117,19 @@ HOOK_RESULT_SCHEMA = {
},
"output": OUTPUT_SCHEMA,
},
"required": [
"hook",
"started_at",
"finished_at",
"triggered_by",
"description",
"status",
],
"required": ["finished_at", "triggered_by", "status"],
"additionalProperties": False
}
HOOK_RESULTS_SCHEMA = {
"type": "object",
"properties": {
"config": {"type": "object"},
"results": {"type": "array",
"items": HOOK_RUN_RESULT_SCHEMA},
"summary": {"type": "object"}
},
"required": ["config", "results", "summary"],
"additionalProperties": False,
}
@ -179,10 +169,7 @@ TASK_RESULT_SCHEMA = {
}
}
},
"hooks": {
"type": "array",
"items": HOOK_RESULT_SCHEMA,
},
"hooks": {"type": "array", "items": HOOK_RESULTS_SCHEMA},
"result": {
"type": "array",
"items": {
@ -228,8 +215,7 @@ TASK_RESULT_SCHEMA = {
"type": "number",
},
},
"required": ["key", "sla", "hooks", "result", "load_duration",
"full_duration"],
"required": ["key", "sla", "result", "load_duration", "full_duration"],
"additionalProperties": False
}
@ -270,10 +256,7 @@ TASK_EXTENDED_RESULT_SCHEMA = {
}
}
},
"hooks": {
"type": "array",
"items": HOOK_RESULT_SCHEMA,
},
"hooks": {"type": "array", "items": HOOK_RESULTS_SCHEMA},
"iterations": {
"type": "array",
"items": {
@ -327,7 +310,7 @@ TASK_EXTENDED_RESULT_SCHEMA = {
}
}
},
"required": ["key", "sla", "hooks", "iterations", "info"],
"required": ["key", "sla", "iterations", "info"],
"additionalProperties": False
}
@ -527,7 +510,7 @@ class Task(object):
else:
scenario["iterations"] = iter(iterations)
scenario["sla"] = scenario["data"]["sla"]
scenario["hooks"] = scenario["data"]["hooks"]
scenario["hooks"] = scenario["data"].get("hooks", [])
del scenario["data"]
del scenario["task_uuid"]
del scenario["id"]

View File

@ -183,7 +183,6 @@ class _ServiceType(utils.ImmutableMixin, utils.EnumMixin):
class _HookStatus(utils.ImmutableMixin, utils.EnumMixin):
"""Hook result statuses."""
UNKNOWN = "n/a"
SUCCESS = "success"
FAILED = "failed"
VALIDATION_FAILED = "validation_failed"

View File

@ -69,7 +69,7 @@ class FileExporter(exporter.Exporter):
task_results = [{"key": x["key"], "result": x["data"]["raw"],
"sla": x["data"]["sla"],
"hooks": x["data"]["hooks"],
"hooks": x["data"].get("hooks"),
"load_duration": x["data"]["load_duration"],
"full_duration": x["data"]["full_duration"]}
for x in task.get_results()]

View File

@ -41,9 +41,8 @@ class SysCallHook(hook.Hook):
proc.wait()
LOG.debug("sys_call hook: Command %s returned %s",
self.config, proc.returncode)
if proc.returncode == 0:
self.set_status(consts.HookStatus.SUCCESS)
self.set_output(proc.stdout.read().decode())
else:
self.set_error(
exception_name="n/a", # no exception class

View File

@ -296,7 +296,7 @@ class ConstantForDurationScenarioRunner(runner.ScenarioRunner):
def event_listener():
while not stop_event_listener.isSet():
while not event_queue.empty():
self._send_event(event_queue.get())
self.send_event(**event_queue.get())
else:
time.sleep(0.01)

View File

@ -60,8 +60,12 @@ class EventTrigger(trigger.Trigger):
]
}
def get_configured_event_type(self):
def get_listening_event(self):
return self.config["unit"]
def is_runnable(self, value):
return value in self.config["at"]
def on_event(self, event_type, value=None):
if not (event_type == self.get_listening_event()
and value in self.config["at"]):
# do nothing
return
super(EventTrigger, self).on_event(event_type, value)

View File

@ -71,16 +71,16 @@ class ResultConsumer(object):
self.is_done = threading.Event()
self.unexpected_failure = {}
self.results = []
self.thread = threading.Thread(
target=self._consume_results
)
self.thread = threading.Thread(target=self._consume_results)
self.aborting_checker = threading.Thread(target=self.wait_and_abort)
self.event_thread = threading.Thread(target=self._consume_events)
if "hooks" in self.key["kw"]:
self.event_thread = threading.Thread(target=self._consume_events)
def __enter__(self):
self.thread.start()
self.aborting_checker.start()
self.event_thread.start()
if "hooks" in self.key["kw"]:
self.event_thread.start()
self.start = time.time()
return self
@ -104,7 +104,7 @@ class ResultConsumer(object):
time.sleep(0.1)
def _consume_events(self):
while not self.is_done.isSet():
while not self.is_done.isSet() or self.runner.event_queue:
if self.runner.event_queue:
event = self.runner.event_queue.popleft()
self.hook_executor.on_event(
@ -117,7 +117,6 @@ class ResultConsumer(object):
self.is_done.set()
self.aborting_checker.join()
self.thread.join()
self.event_thread.join()
if exc_type:
self.sla_checker.set_unexpected_failure(exc_value)
@ -138,13 +137,18 @@ class ResultConsumer(object):
LOG.info("Full duration is %s" % utils.format_float_to_str(
self.finish - self.start))
self.task.append_results(self.key, {
results = {
"raw": self.results,
"load_duration": load_duration,
"full_duration": self.finish - self.start,
"sla": self.sla_checker.results(),
"hooks": self.hook_executor.results(),
})
}
self.runner.send_event(type="load_finished", value=results)
if "hooks" in self.key["kw"]:
self.event_thread.join()
results["hooks"] = self.hook_executor.results()
self.task.append_results(self.key, results)
@staticmethod
def is_task_in_aborting_status(task_uuid, check_soft=True):

View File

@ -22,7 +22,6 @@ import six
from rally.common.i18n import _, _LE
from rally.common import logging
from rally.common import objects
from rally.common.plugin import plugin
from rally.common import utils as rutils
from rally import consts
@ -42,22 +41,18 @@ class HookExecutor(object):
def __init__(self, config, task):
self.config = config
self.task = task
self._timer_thread = threading.Thread(target=self._timer_method)
self._timer_stop_event = threading.Event()
# map triggers to event types
self.triggers = collections.defaultdict(list)
for hook in config.get("hooks", []):
hook_cls = Hook.get(hook["name"])
trigger_obj = trigger.Trigger.get(
hook["trigger"]["name"])(hook["trigger"]["args"])
trigger_event_type = trigger_obj.get_configured_event_type()
self.triggers[trigger_event_type].append(
(trigger_obj, hook["name"], hook["args"],
hook.get("description", "n/a"))
)
hook["trigger"]["name"])(hook, self.task, hook_cls)
event_type = trigger_obj.get_listening_event()
self.triggers[event_type].append(trigger_obj)
# list of executed hooks
self.hooks = []
if "time" in self.triggers:
self._timer_thread = threading.Thread(target=self._timer_method)
self._timer_stop_event = threading.Event()
def _timer_method(self):
"""Timer thread method.
@ -88,26 +83,27 @@ class HookExecutor(object):
particular event occurred.
It runs hooks configured for event.
"""
# start timer on first iteration
if self.triggers["time"]:
if "time" in self.triggers:
# start timer on first iteration
if event_type == "iteration" and value == 1:
self._start_timer()
triggers = self.triggers[event_type]
for trigger_obj, hook_name, hook_args, hook_description in triggers:
if trigger_obj.is_runnable(value=value):
hook = Hook.get(hook_name)(task=self.task, config=hook_args,
triggered_by={event_type: value},
description=hook_description)
self.hooks.append(hook)
hook.run_async()
for trigger_obj in self.triggers[event_type]:
started = trigger_obj.on_event(event_type, value)
if started:
LOG.info(_("Hook %s is trigged for Task %s by %s=%s")
% (hook_name, self.task["uuid"], event_type, value))
% (trigger_obj.hook_cls.__name__, self.task["uuid"],
event_type, value))
def results(self):
"""Returns list of dicts with hook results."""
self._stop_timer()
return [hook.result() for hook in self.hooks]
if "time" in self.triggers:
self._stop_timer()
results = []
for triggers_group in self.triggers.values():
for trigger_obj in triggers_group:
results.append(trigger_obj.get_results())
return results
@plugin.base()
@ -115,55 +111,31 @@ class HookExecutor(object):
class Hook(plugin.Plugin):
"""Factory for hook classes."""
@classmethod
def validate(cls, config):
hook_schema = cls.get(config["name"]).CONFIG_SCHEMA
jsonschema.validate(config["args"], hook_schema)
CONFIG_SCHEMA = {}
trigger.Trigger.validate(config["trigger"])
def __init__(self, task, config, triggered_by, description):
def __init__(self, task, config, triggered_by):
self.task = task
self.config = config
self._triggered_by = triggered_by
self._description = description
self._thread = threading.Thread(target=self._thread_method)
self._started_at = 0.0
self._finished_at = 0.0
self._result = self._format_result(status=consts.HookStatus.UNKNOWN)
def _format_result(self, status, error=None):
"""Returns hook result dict."""
result = {
"hook": self.get_name(),
"status": status,
"description": self._description,
self._result = {
"status": consts.HookStatus.SUCCESS,
"started_at": self._started_at,
"finished_at": self._finished_at,
"triggered_by": self._triggered_by,
}
if error is not None:
result["error"] = error
return result
@classmethod
def validate(cls, config):
jsonschema.validate(config["args"], cls.CONFIG_SCHEMA)
trigger.Trigger.validate(config["trigger"])
def _thread_method(self):
# Run hook synchronously
self.run_sync()
self._validate_result_schema()
def _validate_result_schema(self):
"""Validates result format."""
try:
jsonschema.validate(self._result, objects.task.HOOK_RESULT_SCHEMA)
except jsonschema.ValidationError as validation_error:
LOG.error(_LE("Hook %s returned result "
"in wrong format.") % self.get_name())
LOG.exception(validation_error)
self._result = self._format_result(
status=consts.HookStatus.VALIDATION_FAILED,
error=utils.format_exc(validation_error),
)
def set_error(self, exception_name, description, details):
"""Set error related information to result.
@ -173,7 +145,8 @@ class Hook(plugin.Plugin):
:param details: any details as string
"""
self.set_status(consts.HookStatus.FAILED)
self._result["error"] = [exception_name, description, details]
self._result["error"] = {"etype": exception_name,
"msg": description, "details": details}
def set_status(self, status):
"""Set status to result."""
@ -184,7 +157,8 @@ class Hook(plugin.Plugin):
:param output: Diagram data in task.OUTPUT_SCHEMA format
"""
self._result["output"] = output
if output:
self._result["output"] = output
def run_async(self):
"""Run hook asynchronously."""

View File

@ -137,7 +137,7 @@ def _extend_results(results):
"task_uuid": None,
"key": result["key"],
"data": {"sla": result["sla"],
"hooks": result["hooks"],
"hooks": result.get("hooks"),
"raw": result["result"],
"full_duration": result["full_duration"],
"load_duration": result["load_duration"]},

View File

@ -217,7 +217,7 @@ class ScenarioRunner(plugin.Plugin):
time.sleep(0.01)
while not event_queue.empty():
self._send_event(event_queue.get())
self.send_event(**event_queue.get())
while not result_queue.empty():
self._send_result(result_queue.get())
@ -323,12 +323,14 @@ class ScenarioRunner(plugin.Plugin):
self.result_queue.append(sorted_batch)
del self.result_batch[:]
def _send_event(self, event):
def send_event(self, type, value=None):
"""Store event to send it to consumer later.
:param event: Event dict to be sent.
:param type: Event type
:param value: Optional event data
"""
self.event_queue.append(event)
self.event_queue.append({"type": type,
"value": value})
def _log_debug_info(self, **info):
"""Log runner parameters for debugging.

View File

@ -18,28 +18,54 @@ import abc
import jsonschema
import six
from rally.common.i18n import _
from rally.common import logging
from rally.common.plugin import plugin
configure = plugin.configure
LOG = logging.getLogger(__name__)
@plugin.base()
@six.add_metaclass(abc.ABCMeta)
class Trigger(plugin.Plugin):
"""Factory for trigger classes."""
CONFIG_SCHEMA = {}
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 = []
@classmethod
def validate(cls, config):
trigger_schema = cls.get(config["name"]).CONFIG_SCHEMA
jsonschema.validate(config["args"], trigger_schema)
def __init__(self, config):
self.config = config
jsonschema.validate(config["args"], cls.CONFIG_SCHEMA)
@abc.abstractmethod
def get_configured_event_type(self):
"""Returns supported event type."""
def get_listening_event(self):
"""Returns event type to listen."""
@abc.abstractmethod
def is_runnable(self, value):
"""Returns True if trigger is active on specified event value."""
def on_event(self, event_type, value=None):
"""Launch hook on specified event."""
LOG.info(_("Hook %s is trigged 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

View File

@ -1036,14 +1036,15 @@ class HookTestCase(unittest.TestCase):
self.started = time.time()
def _assert_results_time(self, results):
for result in results:
started_at = result["started_at"]
finished_at = result["finished_at"]
self.assertIsInstance(started_at, float)
self.assertGreater(started_at, self.started)
self.assertIsInstance(finished_at, float)
self.assertGreater(finished_at, self.started)
self.assertGreater(finished_at, started_at)
for trigger_results in results:
for result in trigger_results["results"]:
started_at = result["started_at"]
finished_at = result["finished_at"]
self.assertIsInstance(started_at, float)
self.assertGreater(started_at, self.started)
self.assertIsInstance(finished_at, float)
self.assertGreater(finished_at, self.started)
self.assertGreater(finished_at, started_at)
def _get_sample_task_config(self, cmd, description, runner):
return {
@ -1071,20 +1072,23 @@ class HookTestCase(unittest.TestCase):
]
}
def _get_result(self, description, iteration=None, second=None):
triggered_by = {}
if iteration is not None:
triggered_by["iteration"] = iteration
elif second is not None:
triggered_by["time"] = second
def _get_result(self, config, iterations=None, seconds=None):
result = {
"hook": "sys_call",
"description": description,
"finished_at": mock.ANY,
"started_at": mock.ANY,
"triggered_by": triggered_by,
"status": "success",
"config": config,
"results": [],
"summary": {"success": 0}
}
events = iterations if iterations else seconds
event_type = "iteration" if iterations else "time"
for i in range(len(events)):
result["results"].append({
"finished_at": mock.ANY,
"started_at": mock.ANY,
"triggered_by": {"event_type": event_type, "value": events[i]},
"status": "success"})
result["summary"]["success"] += 1
return result
def test_hook_result_with_constant_runner(self):
@ -1097,9 +1101,8 @@ class HookTestCase(unittest.TestCase):
rally("task start --task %s" % config.filename)
results = json.loads(rally("task results"))
hook_results = results[0]["hooks"]
expected = [
self._get_result("event_hook", iteration=5)
]
hooks_cfg = cfg["Dummy.dummy"][0]["hooks"]
expected = [self._get_result(hooks_cfg[0], iterations=[5])]
self.assertEqual(expected, hook_results)
self._assert_results_time(hook_results)
@ -1114,9 +1117,8 @@ class HookTestCase(unittest.TestCase):
rally("task start --task %s" % config.filename)
results = json.loads(rally("task results"))
hook_results = results[0]["hooks"]
expected = [
self._get_result("event_hook", iteration=5)
]
hooks_cfg = cfg["Dummy.dummy"][0]["hooks"]
expected = [self._get_result(hooks_cfg[0], iterations=[5])]
self.assertEqual(expected, hook_results)
self._assert_results_time(hook_results)
@ -1130,9 +1132,8 @@ class HookTestCase(unittest.TestCase):
rally("task start --task %s" % config.filename)
results = json.loads(rally("task results"))
hook_results = results[0]["hooks"]
expected = [
self._get_result("event_hook", iteration=5)
]
hooks_cfg = cfg["Dummy.dummy"][0]["hooks"]
expected = [self._get_result(hooks_cfg[0], iterations=[5])]
self.assertEqual(expected, hook_results)
self._assert_results_time(hook_results)
@ -1146,9 +1147,8 @@ class HookTestCase(unittest.TestCase):
rally("task start --task %s" % config.filename)
results = json.loads(rally("task results"))
hook_results = results[0]["hooks"]
expected = [
self._get_result("event_hook", iteration=5)
]
hooks_cfg = cfg["Dummy.dummy"][0]["hooks"]
expected = [self._get_result(hooks_cfg[0], iterations=[5])]
self.assertEqual(expected, hook_results)
self._assert_results_time(hook_results)
@ -1162,17 +1162,13 @@ class HookTestCase(unittest.TestCase):
rally("task start --task %s" % config.filename)
results = json.loads(rally("task results"))
hook_results = results[0]["hooks"]
expected = [
{
"description": "event_hook",
"finished_at": mock.ANY,
"started_at": mock.ANY,
"hook": "sys_call",
"triggered_by": {"iteration": 5},
"status": "failed",
"error": ["n/a", "Subprocess returned 1", ""],
}
]
hooks_cfg = cfg["Dummy.dummy"][0]["hooks"]
expected = [self._get_result(hooks_cfg[0], iterations=[5])]
expected[0]["results"][0]["status"] = "failed"
expected[0]["summary"] = {"failed": 1}
expected[0]["results"][0]["error"] = {"etype": "n/a",
"msg": "Subprocess returned 1",
"details": ""}
self.assertEqual(expected, hook_results)
self._assert_results_time(hook_results)
@ -1200,11 +1196,9 @@ class HookTestCase(unittest.TestCase):
rally("task start --task %s" % config.filename)
results = json.loads(rally("task results"))
hook_results = results[0]["hooks"]
expected = [
self._get_result("event_hook", iteration=5),
self._get_result("time_hook", second=3),
self._get_result("time_hook", second=6),
self._get_result("time_hook", second=9),
]
hooks_cfg = cfg["Dummy.dummy"][0]["hooks"]
expected = [self._get_result(hooks_cfg[0], iterations=[5]),
self._get_result(hooks_cfg[1], seconds=[3, 6, 9])]
self.assertEqual(expected, hook_results)
self._assert_results_time(hook_results)

View File

@ -20,7 +20,6 @@ import mock
from rally import consts
from rally.plugins.common.hook import sys_call
from rally.task import hook
from tests.unit import fakes
from tests.unit import test
@ -28,7 +27,7 @@ from tests.unit import test
class SysCallHookTestCase(test.TestCase):
def test_validate(self):
hook.Hook.validate(
sys_call.SysCallHook.validate(
{
"name": "sys_call",
"description": "list folder",
@ -59,29 +58,27 @@ class SysCallHookTestCase(test.TestCase):
}
}
self.assertRaises(
jsonschema.ValidationError, hook.Hook.validate, conf)
jsonschema.ValidationError, sys_call.SysCallHook.validate, conf)
@mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer)
@mock.patch("subprocess.Popen")
@mock.patch("rally.plugins.common.hook.sys_call.subprocess.Popen")
def test_run(self, mock_popen, mock_timer):
popen_instance = mock_popen.return_value
popen_instance.returncode = 0
task = mock.MagicMock()
sys_call_hook = sys_call.SysCallHook(task, "/bin/bash -c 'ls'",
{"iteration": 1}, "dummy_action")
{"iteration": 1})
sys_call_hook.run_sync()
sys_call_hook._validate_result_schema()
self.assertEqual(
{
"hook": "sys_call",
"description": "dummy_action",
"triggered_by": {"iteration": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.SUCCESS,
"output": mock_popen.return_value.stdout.read().decode()
}, sys_call_hook.result())
mock_popen.assert_called_once_with(
@ -90,7 +87,7 @@ class SysCallHookTestCase(test.TestCase):
stderr=subprocess.STDOUT)
@mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer)
@mock.patch("subprocess.Popen")
@mock.patch("rally.plugins.common.hook.sys_call.subprocess.Popen")
def test_run_error(self, mock_popen, mock_timer):
popen_instance = mock_popen.return_value
popen_instance.returncode = 1
@ -98,24 +95,21 @@ class SysCallHookTestCase(test.TestCase):
task = mock.MagicMock()
sys_call_hook = sys_call.SysCallHook(task, "/bin/bash -c 'ls'",
{"iteration": 1}, "dummy_action")
{"iteration": 1})
sys_call_hook.run_sync()
sys_call_hook._validate_result_schema()
self.assertEqual(
{
"hook": "sys_call",
"description": "dummy_action",
"triggered_by": {"iteration": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.FAILED,
"error": [
"n/a",
"Subprocess returned 1",
"No such file or directory",
]
"error": {
"etype": "n/a",
"msg": "Subprocess returned 1",
"details": "No such file or directory",
}
}, sys_call_hook.result())
mock_popen.assert_called_once_with(

View File

@ -15,8 +15,9 @@
import ddt
import jsonschema
import mock
from rally.task import trigger
from rally.plugins.common.trigger import event
from tests.unit import test
@ -29,8 +30,10 @@ class EventTriggerTestCase(test.TestCase):
def setUp(self):
super(EventTriggerTestCase, self).setUp()
self.trigger = trigger.Trigger.get("event")({"unit": "iteration",
"at": [1, 4, 5]})
self.hook_cls = mock.MagicMock(__name__="name")
self.trigger = event.EventTrigger({"trigger": {"args": {
"unit": "iteration", "at": [1, 4, 5]}}},
mock.MagicMock(), self.hook_cls)
@ddt.data((create_config(unit="time", at=[0, 3, 5]), True),
(create_config(unit="time", at=[2, 2]), False),
@ -52,18 +55,18 @@ class EventTriggerTestCase(test.TestCase):
@ddt.unpack
def test_config_schema(self, config, valid):
if valid:
trigger.Trigger.validate(config)
event.EventTrigger.validate(config)
else:
self.assertRaises(jsonschema.ValidationError,
trigger.Trigger.validate, config)
event.EventTrigger.validate, config)
def test_get_configured_event_type(self):
event_type = self.trigger.get_configured_event_type()
def test_get_listening_event(self):
event_type = self.trigger.get_listening_event()
self.assertEqual("iteration", event_type)
@ddt.data((1, True), (4, True), (5, True),
(0, False), (2, False), (3, False), (6, False), (7, False))
@ddt.unpack
def test_is_runnable(self, value, expected_result):
result = self.trigger.is_runnable(value)
self.assertIs(result, expected_result)
def test_on_event(self, value, should_call):
self.trigger.on_event("iteration", value)
self.assertEqual(should_call, self.hook_cls.called)

View File

@ -533,8 +533,6 @@ class ResultConsumerTestCase(test.TestCase):
mock_sla_results = mock.MagicMock()
mock_sla_checker.return_value = mock_sla_instance
mock_sla_instance.results.return_value = mock_sla_results
mock_hook_executor_instance = mock_hook_executor.return_value
mock_hook_results = mock_hook_executor_instance.results.return_value
mock_task_get_status.return_value = consts.TaskStatus.RUNNING
key = {"kw": {"fake": 2}, "name": "fake", "pos": 0}
task = mock.MagicMock()
@ -551,7 +549,6 @@ class ResultConsumerTestCase(test.TestCase):
"raw": [],
"full_duration": 1,
"sla": mock_sla_results,
"hooks": mock_hook_results,
"load_duration": 0
}
)], any_order=True)
@ -674,7 +671,7 @@ class ResultConsumerTestCase(test.TestCase):
mock_hook_results = mock_hook_executor_instance.results.return_value
mock_task_get_status.return_value = consts.TaskStatus.RUNNING
key = {"kw": {"fake": 2}, "name": "fake", "pos": 0}
key = {"kw": {"fake": 2, "hooks": []}, "name": "fake", "pos": 0}
task = mock.MagicMock()
runner = mock.MagicMock()
events = [

View File

@ -80,12 +80,14 @@ class HookExecutorTestCase(test.TestCase):
hook_executor.on_event(event_type="iteration", value=1)
self.assertEqual(
[{"description": "dummy_action",
"hook": "dummy_hook",
"triggered_by": {"iteration": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.SUCCESS}], hook_executor.results())
[{"config": self.conf["hooks"][0],
"results": [{
"triggered_by": {"event_type": "iteration", "value": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.SUCCESS}],
"summary": {consts.HookStatus.SUCCESS: 1}}],
hook_executor.results())
@mock.patch("rally.task.hook.HookExecutor._timer_method")
@mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer)
@ -98,18 +100,23 @@ class HookExecutorTestCase(test.TestCase):
hook_executor.on_event(event_type="iteration", value=1)
self.assertEqual(
[{"description": "dummy_action",
"hook": "dummy_hook",
"triggered_by": {"iteration": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"error": ["Exception", "Description", "Traceback"],
"output": {"additive": [], "complete": []},
"status": consts.HookStatus.FAILED}], hook_executor.results())
[{"config": self.conf["hooks"][0],
"results": [{
"triggered_by": {"event_type": "iteration", "value": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"error": {"details": "Traceback", "etype": "Exception",
"msg": "Description"},
"output": {"additive": [], "complete": []},
"status": consts.HookStatus.FAILED}],
"summary": {consts.HookStatus.FAILED: 1}}],
hook_executor.results())
def test_empty_result(self):
hook_executor = hook.HookExecutor(self.conf, self.task)
self.assertEqual([], hook_executor.results())
self.assertEqual([{"config": self.conf["hooks"][0], "results": [],
"summary": {}}],
hook_executor.results())
@mock.patch("rally.task.hook.HookExecutor._timer_method")
@mock.patch.object(DummyHook, "run", side_effect=Exception("My err msg"))
@ -120,30 +127,15 @@ class HookExecutorTestCase(test.TestCase):
hook_executor.on_event(event_type="iteration", value=1)
self.assertEqual(
[{"description": "dummy_action",
"hook": "dummy_hook",
"triggered_by": {"iteration": 1},
"error": ["Exception", "My err msg", mock.ANY],
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.FAILED}], hook_executor.results())
@mock.patch("rally.task.hook.HookExecutor._timer_method")
@mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer)
def test_result_wrong_format(self, mock_timer, mock__timer_method):
hook_args = self.conf["hooks"][0]["args"]
hook_args["status"] = 10
hook_executor = hook.HookExecutor(self.conf, self.task)
hook_executor.on_event(event_type="iteration", value=1)
self.assertEqual(
[{"description": "dummy_action",
"hook": "dummy_hook",
"triggered_by": {"iteration": 1},
"error": ["ValidationError", mock.ANY, mock.ANY],
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.VALIDATION_FAILED}],
[{"config": self.conf["hooks"][0],
"results": [{
"triggered_by": {"event_type": "iteration", "value": 1},
"error": {"etype": "Exception",
"msg": mock.ANY, "details": mock.ANY},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.FAILED}],
"summary": {consts.HookStatus.FAILED: 1}}],
hook_executor.results())
@mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer)
@ -155,12 +147,14 @@ class HookExecutorTestCase(test.TestCase):
hook_executor.on_event(event_type="time", value=1)
self.assertEqual(
[{"description": "dummy_action",
"hook": "dummy_hook",
"triggered_by": {"time": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.SUCCESS}], hook_executor.results())
[{"config": self.conf["hooks"][0],
"results": [{
"triggered_by": {"event_type": "time", "value": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.SUCCESS}],
"summary": {consts.HookStatus.SUCCESS: 1}}],
hook_executor.results())
@mock.patch("rally.common.utils.Stopwatch", autospec=True)
@mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer)
@ -180,12 +174,15 @@ class HookExecutorTestCase(test.TestCase):
self.assertTrue(hook_executor._timer_stop_event.wait(1))
self.assertEqual(
[{"description": "dummy_action",
"hook": "dummy_hook",
"triggered_by": {"time": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.SUCCESS}], hook_executor.results())
[{"config": self.conf["hooks"][0],
"results": [{
"triggered_by": {"event_type": "time", "value": 1},
"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"status": consts.HookStatus.SUCCESS}],
"summary": {consts.HookStatus.SUCCESS: 1}
}],
hook_executor.results())
stopwatch_inst.start.assert_called_once_with()
stopwatch_inst.sleep.assert_has_calls([
@ -198,7 +195,7 @@ class HookExecutorTestCase(test.TestCase):
class HookTestCase(test.TestCase):
def test_validate(self):
hook.Hook.validate(
DummyHook.validate(
{
"name": "dummy_hook",
"description": "dummy_action",
@ -228,33 +225,30 @@ class HookTestCase(test.TestCase):
}
}
}
self.assertRaises(jsonschema.ValidationError, hook.Hook.validate, conf)
self.assertRaises(jsonschema.ValidationError, DummyHook.validate, conf)
@mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer)
def test_result(self, mock_timer):
task = mock.MagicMock()
triggered_by = {"event_type": "iteration", "value": 1}
dummy_hook = DummyHook(task, {"status": consts.HookStatus.SUCCESS},
{"iteration": 1}, "dummy_action")
triggered_by)
dummy_hook.run_sync()
dummy_hook._validate_result_schema()
self.assertEqual(
{"description": "dummy_action",
"hook": "dummy_hook",
"started_at": fakes.FakeTimer().timestamp(),
{"started_at": fakes.FakeTimer().timestamp(),
"finished_at": fakes.FakeTimer().finish_timestamp(),
"triggered_by": {"iteration": 1},
"triggered_by": triggered_by,
"status": consts.HookStatus.SUCCESS}, dummy_hook.result())
def test_result_not_started(self):
task = mock.MagicMock()
triggered_by = {"event_type": "iteration", "value": 1}
dummy_hook = DummyHook(task, {"status": consts.HookStatus.SUCCESS},
{"iteration": 1}, "dummy_action")
triggered_by)
self.assertEqual(
{"description": "dummy_action",
"hook": "dummy_hook",
"started_at": 0.0,
{"started_at": 0.0,
"finished_at": 0.0,
"triggered_by": {"iteration": 1},
"status": consts.HookStatus.UNKNOWN}, dummy_hook.result())
"triggered_by": triggered_by,
"status": consts.HookStatus.SUCCESS}, dummy_hook.result())

View File

@ -17,6 +17,7 @@
import ddt
import jsonschema
import mock
from rally.task import trigger
from tests.unit import test
@ -24,38 +25,59 @@ from tests.unit import test
@trigger.configure(name="dummy_trigger")
class DummyTrigger(trigger.Trigger):
CONFIG_SCHEMA = {"type": "integer"}
CONFIG_SCHEMA = {"type": "array",
"minItems": 1,
"uniqueItems": True,
"items": {
"type": "integer",
"minimum": 0,
}}
def get_configured_event_type(self):
def get_listening_event(self):
return "dummy"
def is_runnable(self, value):
return value == self.config
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):
def setUp(self):
super(TriggerTestCase, self).setUp()
self.trigger = DummyTrigger(10)
@ddt.data(({"name": "dummy_trigger", "args": 5}, True),
({"name": "dummy_trigger", "args": "str"}, False))
@ddt.data(({"name": "dummy_trigger", "args": [5]}, True),
({"name": "dummy_trigger", "args": ["str"]}, False))
@ddt.unpack
def test_validate(self, config, valid):
if valid:
trigger.Trigger.validate(config)
DummyTrigger.validate(config)
else:
self.assertRaises(jsonschema.ValidationError,
trigger.Trigger.validate, config)
DummyTrigger.validate, config)
def test_get_configured_event_type(self):
event_type = self.trigger.get_configured_event_type()
self.assertEqual("dummy", event_type)
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]
@ddt.data((10, True), (1, False))
@ddt.unpack
def test_is_runnable(self, value, expected_result):
result = self.trigger.is_runnable(value)
self.assertIs(result, expected_result)
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.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())