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:
Anton Studenov 2016-09-15 16:12:50 +03:00
parent 8ff0f2ee8c
commit 03c8740588
8 changed files with 376 additions and 2 deletions

View File

@ -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)

View File

View 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
View 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."""

View File

@ -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),
])

View 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)

View 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)