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):
|
||||
"""Timer based on context manager interface."""
|
||||
|
||||
def __enter__(self):
|
||||
self.error = None
|
||||
self.start = time.time()
|
||||
@ -86,6 +88,9 @@ class Timer(object):
|
||||
def timestamp(self):
|
||||
return self.start
|
||||
|
||||
def finish_timestamp(self):
|
||||
return self.finish
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
self.finish = time.time()
|
||||
if type:
|
||||
@ -653,3 +658,53 @@ def format_float_to_str(num):
|
||||
num_str = "%f" % num
|
||||
float_part = num_str.split(".")[1].rstrip("0") or "0"
|
||||
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.
|
||||
|
||||
from __future__ import print_function
|
||||
import collections
|
||||
import string
|
||||
import sys
|
||||
import threading
|
||||
@ -104,6 +105,8 @@ class TimerTestCase(test.TestCase):
|
||||
mock_time.time = mock.MagicMock(return_value=end_time)
|
||||
|
||||
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())
|
||||
|
||||
def test_timer_exception(self):
|
||||
@ -586,4 +589,78 @@ class FloatFormatterTestCase(test.TestCase):
|
||||
)
|
||||
@ddt.unpack
|
||||
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