New plugin type - Trigger
Triggers determine when hooks should be executed. Added event trigger. Added utils required for implementation of hook_section spec. implements spec: hook_section Change-Id: I33d558468f221a7c731e4a3fd42deb4f76be671a
This commit is contained in:
parent
8ff0f2ee8c
commit
03c8740588
@ -78,6 +78,8 @@ class StdErrCapture(object):
|
|||||||
|
|
||||||
|
|
||||||
class Timer(object):
|
class Timer(object):
|
||||||
|
"""Timer based on context manager interface."""
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.error = None
|
self.error = None
|
||||||
self.start = time.time()
|
self.start = time.time()
|
||||||
@ -86,6 +88,9 @@ class Timer(object):
|
|||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
return self.start
|
return self.start
|
||||||
|
|
||||||
|
def finish_timestamp(self):
|
||||||
|
return self.finish
|
||||||
|
|
||||||
def __exit__(self, type, value, tb):
|
def __exit__(self, type, value, tb):
|
||||||
self.finish = time.time()
|
self.finish = time.time()
|
||||||
if type:
|
if type:
|
||||||
@ -652,4 +657,54 @@ def format_float_to_str(num):
|
|||||||
|
|
||||||
num_str = "%f" % num
|
num_str = "%f" % num
|
||||||
float_part = num_str.split(".")[1].rstrip("0") or "0"
|
float_part = num_str.split(".")[1].rstrip("0") or "0"
|
||||||
return num_str.split(".")[0] + "." + float_part
|
return num_str.split(".")[0] + "." + float_part
|
||||||
|
|
||||||
|
|
||||||
|
class DequeAsQueue(object):
|
||||||
|
"""Allows to use some of Queue methods on collections.deque."""
|
||||||
|
|
||||||
|
def __init__(self, deque):
|
||||||
|
self.deque = deque
|
||||||
|
|
||||||
|
def qsize(self):
|
||||||
|
return len(self.deque)
|
||||||
|
|
||||||
|
def put(self, value):
|
||||||
|
self.deque.append(value)
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return self.deque.popleft()
|
||||||
|
|
||||||
|
def empty(self):
|
||||||
|
return bool(self.deque)
|
||||||
|
|
||||||
|
|
||||||
|
class Stopwatch(object):
|
||||||
|
"""Allows to sleep till specified time since start."""
|
||||||
|
|
||||||
|
def __init__(self, stop_event=None):
|
||||||
|
"""Creates Stopwatch.
|
||||||
|
|
||||||
|
:param stop_event: optional threading.Event to use for waiting
|
||||||
|
allows to interrupt sleep. If not provided time.sleep
|
||||||
|
will be used instead.
|
||||||
|
"""
|
||||||
|
self._stop_event = stop_event
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self._start_time = time.time()
|
||||||
|
|
||||||
|
def sleep(self, sec):
|
||||||
|
"""Sleeps till specified second since start."""
|
||||||
|
target_time = self._start_time + sec
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time >= target_time:
|
||||||
|
return
|
||||||
|
time_to_sleep = target_time - current_time
|
||||||
|
self._sleep(time_to_sleep)
|
||||||
|
|
||||||
|
def _sleep(self, sec):
|
||||||
|
if self._stop_event:
|
||||||
|
self._stop_event.wait(sec)
|
||||||
|
else:
|
||||||
|
interruptable_sleep(sec)
|
||||||
|
0
rally/plugins/common/trigger/__init__.py
Normal file
0
rally/plugins/common/trigger/__init__.py
Normal file
67
rally/plugins/common/trigger/event.py
Normal file
67
rally/plugins/common/trigger/event.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Copyright 2016: Mirantis Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from rally import consts
|
||||||
|
from rally.task import trigger
|
||||||
|
|
||||||
|
|
||||||
|
@trigger.configure(name="event")
|
||||||
|
class EventTrigger(trigger.Trigger):
|
||||||
|
"""Triggers hook on specified event and list of values."""
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = {
|
||||||
|
"type": "object",
|
||||||
|
"$schema": consts.JSON_SCHEMA,
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"unit": {"enum": ["time"]},
|
||||||
|
"at": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": True,
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["unit", "at"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"unit": {"enum": ["iteration"]},
|
||||||
|
"at": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": True,
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["unit", "at"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_configured_event_type(self):
|
||||||
|
return self.config["unit"]
|
||||||
|
|
||||||
|
def is_runnable(self, value):
|
||||||
|
return value in self.config["at"]
|
45
rally/task/trigger.py
Normal file
45
rally/task/trigger.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Copyright 2016: Mirantis Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
|
import six
|
||||||
|
|
||||||
|
from rally.common.plugin import plugin
|
||||||
|
|
||||||
|
configure = plugin.configure
|
||||||
|
|
||||||
|
|
||||||
|
@plugin.base()
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class Trigger(plugin.Plugin):
|
||||||
|
"""Factory for trigger classes."""
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_configured_event_type(self):
|
||||||
|
"""Returns supported event type."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_runnable(self, value):
|
||||||
|
"""Returns True if trigger is active on specified event value."""
|
@ -13,6 +13,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
import collections
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@ -104,6 +105,8 @@ class TimerTestCase(test.TestCase):
|
|||||||
mock_time.time = mock.MagicMock(return_value=end_time)
|
mock_time.time = mock.MagicMock(return_value=end_time)
|
||||||
|
|
||||||
self.assertIsNone(timer.error)
|
self.assertIsNone(timer.error)
|
||||||
|
self.assertEqual(start_time, timer.timestamp())
|
||||||
|
self.assertEqual(end_time, timer.finish_timestamp())
|
||||||
self.assertEqual(end_time - start_time, timer.duration())
|
self.assertEqual(end_time - start_time, timer.duration())
|
||||||
|
|
||||||
def test_timer_exception(self):
|
def test_timer_exception(self):
|
||||||
@ -586,4 +589,78 @@ class FloatFormatterTestCase(test.TestCase):
|
|||||||
)
|
)
|
||||||
@ddt.unpack
|
@ddt.unpack
|
||||||
def test_format_float_to_str(self, num_float, num_str):
|
def test_format_float_to_str(self, num_float, num_str):
|
||||||
self.assertEquals(num_str, utils.format_float_to_str(num_float))
|
self.assertEqual(num_str, utils.format_float_to_str(num_float))
|
||||||
|
|
||||||
|
|
||||||
|
class DequeAsQueueTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DequeAsQueueTestCase, self).setUp()
|
||||||
|
self.deque = collections.deque()
|
||||||
|
self.deque_as_queue = utils.DequeAsQueue(self.deque)
|
||||||
|
|
||||||
|
def test_qsize(self):
|
||||||
|
self.assertEqual(0, self.deque_as_queue.qsize())
|
||||||
|
self.deque.append(10)
|
||||||
|
self.assertEqual(1, self.deque_as_queue.qsize())
|
||||||
|
|
||||||
|
def test_put(self):
|
||||||
|
self.deque_as_queue.put(10)
|
||||||
|
self.assertEqual(10, self.deque.popleft())
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
self.deque.append(33)
|
||||||
|
self.assertEqual(33, self.deque_as_queue.get())
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
self.assertFalse(self.deque_as_queue.empty())
|
||||||
|
self.deque.append(10)
|
||||||
|
self.assertTrue(self.deque_as_queue.empty())
|
||||||
|
|
||||||
|
|
||||||
|
class StopwatchTestCase(test.TestCase):
|
||||||
|
|
||||||
|
@mock.patch("rally.common.utils.interruptable_sleep")
|
||||||
|
@mock.patch("rally.common.utils.time")
|
||||||
|
def test_stopwatch(self, mock_time, mock_interruptable_sleep):
|
||||||
|
mock_time.time.side_effect = [0, 0, 1, 2, 3]
|
||||||
|
|
||||||
|
sw = utils.Stopwatch()
|
||||||
|
sw.start()
|
||||||
|
sw.sleep(1)
|
||||||
|
sw.sleep(2)
|
||||||
|
sw.sleep(3)
|
||||||
|
|
||||||
|
mock_interruptable_sleep.assert_has_calls([
|
||||||
|
mock.call(1),
|
||||||
|
mock.call(1),
|
||||||
|
mock.call(1),
|
||||||
|
])
|
||||||
|
|
||||||
|
@mock.patch("rally.common.utils.interruptable_sleep")
|
||||||
|
@mock.patch("rally.common.utils.time")
|
||||||
|
def test_no_sleep(self, mock_time, mock_interruptable_sleep):
|
||||||
|
mock_time.time.side_effect = [0, 1]
|
||||||
|
|
||||||
|
sw = utils.Stopwatch()
|
||||||
|
sw.start()
|
||||||
|
sw.sleep(1)
|
||||||
|
|
||||||
|
self.assertFalse(mock_interruptable_sleep.called)
|
||||||
|
|
||||||
|
@mock.patch("rally.common.utils.time")
|
||||||
|
def test_stopwatch_with_event(self, mock_time):
|
||||||
|
mock_time.time.side_effect = [0, 0, 1, 2, 3]
|
||||||
|
event = mock.Mock(spec=threading.Event)()
|
||||||
|
|
||||||
|
sw = utils.Stopwatch(stop_event=event)
|
||||||
|
sw.start()
|
||||||
|
sw.sleep(1)
|
||||||
|
sw.sleep(2)
|
||||||
|
sw.sleep(3)
|
||||||
|
|
||||||
|
event.wait.assert_has_calls([
|
||||||
|
mock.call(1),
|
||||||
|
mock.call(1),
|
||||||
|
mock.call(1),
|
||||||
|
])
|
||||||
|
0
tests/unit/plugins/common/trigger/__init__.py
Normal file
0
tests/unit/plugins/common/trigger/__init__.py
Normal file
69
tests/unit/plugins/common/trigger/test_event.py
Normal file
69
tests/unit/plugins/common/trigger/test_event.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Copyright 2016: Mirantis Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
from rally.task import trigger
|
||||||
|
from tests.unit import test
|
||||||
|
|
||||||
|
|
||||||
|
def create_config(**kwargs):
|
||||||
|
return {"name": "event", "args": kwargs}
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class EventTriggerTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(EventTriggerTestCase, self).setUp()
|
||||||
|
self.trigger = trigger.Trigger.get("event")({"unit": "iteration",
|
||||||
|
"at": [1, 4, 5]})
|
||||||
|
|
||||||
|
@ddt.data((create_config(unit="time", at=[0, 3, 5]), True),
|
||||||
|
(create_config(unit="time", at=[2, 2]), False),
|
||||||
|
(create_config(unit="time", at=[-1]), False),
|
||||||
|
(create_config(unit="time", at=[1.5]), False),
|
||||||
|
(create_config(unit="time", at=[]), False),
|
||||||
|
(create_config(unit="time", wrong_prop=None), False),
|
||||||
|
(create_config(unit="time"), False),
|
||||||
|
(create_config(unit="iteration", at=[1, 5, 13]), True),
|
||||||
|
(create_config(unit="iteration", at=[1, 1]), False),
|
||||||
|
(create_config(unit="iteration", at=[0]), False),
|
||||||
|
(create_config(unit="iteration", at=[-1]), False),
|
||||||
|
(create_config(unit="iteration", at=[1.5]), False),
|
||||||
|
(create_config(unit="iteration", at=[]), False),
|
||||||
|
(create_config(unit="iteration", wrong_prop=None), False),
|
||||||
|
(create_config(unit="iteration"), False),
|
||||||
|
(create_config(unit="wrong-unit", at=[1, 2, 3]), False),
|
||||||
|
(create_config(at=[1, 2, 3]), False))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_config_schema(self, config, valid):
|
||||||
|
if valid:
|
||||||
|
trigger.Trigger.validate(config)
|
||||||
|
else:
|
||||||
|
self.assertRaises(jsonschema.ValidationError,
|
||||||
|
trigger.Trigger.validate, config)
|
||||||
|
|
||||||
|
def test_get_configured_event_type(self):
|
||||||
|
event_type = self.trigger.get_configured_event_type()
|
||||||
|
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)
|
61
tests/unit/task/test_trigger.py
Normal file
61
tests/unit/task/test_trigger.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Copyright 2016: Mirantis Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Tests for Trigger base class."""
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
from rally.task import trigger
|
||||||
|
from tests.unit import test
|
||||||
|
|
||||||
|
|
||||||
|
@trigger.configure(name="dummy_trigger")
|
||||||
|
class DummyTrigger(trigger.Trigger):
|
||||||
|
CONFIG_SCHEMA = {"type": "integer"}
|
||||||
|
|
||||||
|
def get_configured_event_type(self):
|
||||||
|
return "dummy"
|
||||||
|
|
||||||
|
def is_runnable(self, value):
|
||||||
|
return value == self.config
|
||||||
|
|
||||||
|
|
||||||
|
@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.unpack
|
||||||
|
def test_validate(self, config, valid):
|
||||||
|
if valid:
|
||||||
|
trigger.Trigger.validate(config)
|
||||||
|
else:
|
||||||
|
self.assertRaises(jsonschema.ValidationError,
|
||||||
|
trigger.Trigger.validate, config)
|
||||||
|
|
||||||
|
def test_get_configured_event_type(self):
|
||||||
|
event_type = self.trigger.get_configured_event_type()
|
||||||
|
self.assertEqual("dummy", event_type)
|
||||||
|
|
||||||
|
@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)
|
Loading…
Reference in New Issue
Block a user