[task] Move TaskConfig to separate module

The validation and processing of input task config to the unified
version-aware object is done by special class TaskConfig. It supports
both v1 and v2 formats. The size of this helper class is quite big and
since we are planning to extend it, it is reasonable to move it to
separate module - rally.task.task_cfg .

NOTE: we do not need to deprecate the old place, since TaskConfig is our
inner class and should not be used outside our code base.

Change-Id: I875b677a689e002413d0f07d43bacc960e965df4
This commit is contained in:
Andrey Kurilin 2018-04-13 13:49:19 +03:00
parent b5ba6d5ea9
commit b2ce5da5d8
7 changed files with 535 additions and 502 deletions

@ -36,6 +36,7 @@ from rally import consts
from rally import exceptions
from rally.task import engine
from rally.task import exporter as texporter
from rally.task import task_cfg
from rally.verification import context as vcontext
from rally.verification import manager as vmanager
from rally.verification import reporter as vreporter
@ -350,7 +351,7 @@ class _Task(APIGroup):
deployment = objects.Deployment.get(deployment)
try:
config = engine.TaskConfig(config)
config = task_cfg.TaskConfig(config)
except Exception as e:
if logging.is_debug():
LOG.exception("Invalid Task")
@ -394,7 +395,7 @@ class _Task(APIGroup):
status=deployment["status"])
try:
config = engine.TaskConfig(config)
config = task_cfg.TaskConfig(config)
except Exception as e:
if logging.is_debug():
LOG.exception("Invalid Task")

@ -19,8 +19,6 @@ import threading
import time
import traceback
import jsonschema
from rally.common import cfg
from rally.common import logging
from rally.common import objects
@ -239,7 +237,7 @@ class TaskEngine(object):
def __init__(self, config, task, env, abort_on_sla_failure=False):
"""TaskEngine constructor.
:param config: An instance of a TaskConfig
:param config: An instance of a rally.task.config.TaskConfig
:param task: Instance of Task,
the current task which is being performed
:param env: Instance of Environment,
@ -510,344 +508,3 @@ class TaskEngine(object):
except Exception:
LOG.exception("Unexpected exception during the workload execution")
# TODO(astudenov): save error to DB
class TaskConfig(object):
"""Version-aware wrapper around task.
"""
CONFIG_SCHEMA_V1 = {
"type": "object",
"$schema": consts.JSON_SCHEMA,
"patternProperties": {
".*": {
"type": "array",
"items": {
"type": "object",
"properties": {
"args": {"type": "object"},
"description": {
"type": "string"
},
"runner": {
"type": "object",
"properties": {"type": {"type": "string"}},
"required": ["type"]
},
"context": {"type": "object"},
"sla": {"type": "object"},
"hooks": {
"type": "array",
"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,
}
}
}
CONFIG_SCHEMA_V2 = {
"type": "object",
"$schema": consts.JSON_SCHEMA,
"properties": {
"version": {"type": "number"},
"title": {"type": "string", "maxLength": 128},
"description": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string"}
},
"subtasks": {
"type": "array",
"minItems": 1,
"items": {
"oneOf": [
{"$ref": "#/definitions/subtask-workload"},
{"$ref": "#/definitions/subtask-workloads"}
]
}
}
},
"additionalProperties": False,
"required": ["title", "subtasks"],
"definitions": {
"singleEntity": {
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"patternProperties": {
".*": {"type": "object"}
}
},
"subtask-workload": {
"type": "object",
"properties": {
"title": {"type": "string", "maxLength": 128},
"group": {"type": "string"},
"description": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string", "maxLength": 255}
},
"scenario": {"$ref": "#/definitions/singleEntity"},
"runner": {"$ref": "#/definitions/singleEntity"},
"sla": {"type": "object"},
"hooks": {
"type": "array",
"items": {"$ref": "#/definitions/hook"},
},
"contexts": {"type": "object"}
},
"additionalProperties": False,
"required": ["title", "scenario", "runner"]
},
"subtask-workloads": {
"type": "object",
"properties": {
"title": {"type": "string"},
"group": {"type": "string"},
"description": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string", "maxLength": 255}
},
"run_in_parallel": {"type": "boolean"},
"workloads": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"scenario": {
"$ref": "#/definitions/singleEntity"},
"description": {"type": "string"},
"runner": {
"$ref": "#/definitions/singleEntity"},
"sla": {"type": "object"},
"hooks": {
"type": "array",
"items": {"$ref": "#/definitions/hook"},
},
"contexts": {"type": "object"}
},
"additionalProperties": False,
"required": ["scenario"]
}
}
},
"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
},
]
}
}
}
CONFIG_SCHEMAS = {1: CONFIG_SCHEMA_V1, 2: CONFIG_SCHEMA_V2}
def __init__(self, config):
"""TaskConfig constructor.
Validates and represents different versions of task configuration in
unified form.
:param config: Dict with configuration of specified task
:raises Exception: in case of validation error. (This gets reraised as
InvalidTaskException. if we raise it here as InvalidTaskException,
then "Task config is invalid: " gets prepended to the message twice
"""
if config is None:
raise Exception("Input task is empty")
self.version = self._get_version(config)
self._validate_version()
self._validate_json(config)
if self.version == 1:
config = self._adopt_task_format_v1(config)
self.title = config.get("title", "Task")
self.tags = config.get("tags", [])
self.description = config.get("description")
self.subtasks = []
for sconf in config["subtasks"]:
sconf = copy.deepcopy(sconf)
# fill all missed properties of a SubTask
sconf.setdefault("tags", [])
sconf.setdefault("description", "")
# port the subtask to a single format before validating
if "workloads" not in sconf and "scenario" in sconf:
workload = sconf
sconf = {"title": workload.pop("title"),
"description": workload.pop("description"),
"tags": workload.pop("tags"),
"workloads": [workload]}
# it is not supported feature yet, but the code expects this
# variable
sconf.setdefault("contexts", {})
workloads = []
for position, wconf in enumerate(sconf["workloads"]):
# fill all missed properties of a Workload
wconf["name"], wconf["args"] = list(
wconf["scenario"].items())[0]
del wconf["scenario"]
wconf["position"] = position
if not wconf.get("description", ""):
try:
wconf["description"] = scenario.Scenario.get(
wconf["name"]).get_info()["title"]
except (exceptions.PluginNotFound,
exceptions.MultiplePluginsFound):
# let's fail an issue with loading plugin at a
# validation step
pass
wconf.setdefault("contexts", {})
if "runner" in wconf:
runner = list(wconf["runner"].items())[0]
wconf["runner_type"], wconf["runner"] = runner
else:
wconf["runner_type"] = "serial"
wconf["runner"] = {}
wconf.setdefault("sla", {"failure_rate": {"max": 0}})
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({
"description": hook_cfg["description"],
"action": (hook_cfg["name"], hook_cfg["args"]),
"trigger": (
trigger_cfg["name"], trigger_cfg["args"])})
else:
hook_cfg["action"] = list(
hook_cfg["action"].items())[0]
hook_cfg["trigger"] = list(
hook_cfg["trigger"].items())[0]
wconf["hooks"].append(hook_cfg)
workloads.append(wconf)
sconf["workloads"] = workloads
self.subtasks.append(sconf)
# if self.version == 1:
# TODO(ikhudoshyn): Warn user about deprecated format
@staticmethod
def _get_version(config):
return config.get("version", 1)
def _validate_version(self):
if self.version not in self.CONFIG_SCHEMAS:
allowed = ", ".join([str(k) for k in self.CONFIG_SCHEMAS])
msg = ("Task configuration version %s is not supported. "
"Supported versions: %s") % (self.version, allowed)
raise exceptions.InvalidTaskException(msg)
def _validate_json(self, config):
try:
jsonschema.validate(config, self.CONFIG_SCHEMAS[self.version])
except Exception as e:
raise exceptions.InvalidTaskException(str(e))
@staticmethod
def _adopt_task_format_v1(config):
subtasks = []
for name, v1_workloads in config.items():
for v1_workload in v1_workloads:
subtask = copy.deepcopy(v1_workload)
subtask["scenario"] = {name: subtask.pop("args", {})}
subtask["contexts"] = subtask.pop("context", {})
subtask["title"] = name
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.get("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}

367
rally/task/task_cfg.py Normal file

@ -0,0 +1,367 @@
# 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 copy
import jsonschema
from rally.common import cfg
from rally.common import logging
from rally import consts
from rally import exceptions
from rally.task import scenario
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class TaskConfig(object):
"""Version-aware wrapper around task config."""
CONFIG_SCHEMA_V1 = {
"type": "object",
"$schema": consts.JSON_SCHEMA,
"patternProperties": {
".*": {
"type": "array",
"items": {
"type": "object",
"properties": {
"args": {"type": "object"},
"description": {
"type": "string"
},
"runner": {
"type": "object",
"properties": {"type": {"type": "string"}},
"required": ["type"]
},
"context": {"type": "object"},
"sla": {"type": "object"},
"hooks": {
"type": "array",
"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,
}
}
}
CONFIG_SCHEMA_V2 = {
"type": "object",
"$schema": consts.JSON_SCHEMA,
"properties": {
"version": {"type": "number"},
"title": {"type": "string", "maxLength": 128},
"description": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string"}
},
"subtasks": {
"type": "array",
"minItems": 1,
"items": {
"oneOf": [
{"$ref": "#/definitions/subtask-workload"},
{"$ref": "#/definitions/subtask-workloads"}
]
}
}
},
"additionalProperties": False,
"required": ["title", "subtasks"],
"definitions": {
"singleEntity": {
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"patternProperties": {
".*": {"type": "object"}
}
},
"subtask-workload": {
"type": "object",
"properties": {
"title": {"type": "string", "maxLength": 128},
"group": {"type": "string"},
"description": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string", "maxLength": 255}
},
"scenario": {"$ref": "#/definitions/singleEntity"},
"runner": {"$ref": "#/definitions/singleEntity"},
"sla": {"type": "object"},
"hooks": {
"type": "array",
"items": {"$ref": "#/definitions/hook"},
},
"contexts": {"type": "object"}
},
"additionalProperties": False,
"required": ["title", "scenario", "runner"]
},
"subtask-workloads": {
"type": "object",
"properties": {
"title": {"type": "string"},
"group": {"type": "string"},
"description": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string", "maxLength": 255}
},
"run_in_parallel": {"type": "boolean"},
"workloads": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"scenario": {
"$ref": "#/definitions/singleEntity"},
"description": {"type": "string"},
"runner": {
"$ref": "#/definitions/singleEntity"},
"sla": {"type": "object"},
"hooks": {
"type": "array",
"items": {"$ref": "#/definitions/hook"},
},
"contexts": {"type": "object"}
},
"additionalProperties": False,
"required": ["scenario"]
}
}
},
"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
},
]
}
}
}
CONFIG_SCHEMAS = {1: CONFIG_SCHEMA_V1, 2: CONFIG_SCHEMA_V2}
def __init__(self, config):
"""TaskConfig constructor.
Validates and represents different versions of task configuration in
unified form.
:param config: Dict with configuration of specified task
:raises Exception: in case of validation error. (This gets reraised as
InvalidTaskException. if we raise it here as InvalidTaskException,
then "Task config is invalid: " gets prepended to the message twice
"""
if config is None:
raise Exception("Input task is empty")
self.version = self._get_version(config)
self._validate_version()
self._validate_json(config)
if self.version == 1:
config = self._adopt_task_format_v1(config)
self.title = config.get("title", "Task")
self.tags = config.get("tags", [])
self.description = config.get("description")
self.subtasks = []
for sconf in config["subtasks"]:
sconf = copy.deepcopy(sconf)
# fill all missed properties of a SubTask
sconf.setdefault("tags", [])
sconf.setdefault("description", "")
# port the subtask to a single format before validating
if "workloads" not in sconf and "scenario" in sconf:
workload = sconf
sconf = {"title": workload.pop("title"),
"description": workload.pop("description"),
"tags": workload.pop("tags"),
"workloads": [workload]}
# it is not supported feature yet, but the code expects this
# variable
sconf.setdefault("contexts", {})
workloads = []
for position, wconf in enumerate(sconf["workloads"]):
# fill all missed properties of a Workload
wconf["name"], wconf["args"] = list(
wconf["scenario"].items())[0]
del wconf["scenario"]
wconf["position"] = position
if not wconf.get("description", ""):
try:
wconf["description"] = scenario.Scenario.get(
wconf["name"]).get_info()["title"]
except (exceptions.PluginNotFound,
exceptions.MultiplePluginsFound):
# let's fail an issue with loading plugin at a
# validation step
pass
wconf.setdefault("contexts", {})
if "runner" in wconf:
runner = list(wconf["runner"].items())[0]
wconf["runner_type"], wconf["runner"] = runner
else:
wconf["runner_type"] = "serial"
wconf["runner"] = {}
wconf.setdefault("sla", {"failure_rate": {"max": 0}})
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({
"description": hook_cfg["description"],
"action": (hook_cfg["name"], hook_cfg["args"]),
"trigger": (
trigger_cfg["name"], trigger_cfg["args"])})
else:
hook_cfg["action"] = list(
hook_cfg["action"].items())[0]
hook_cfg["trigger"] = list(
hook_cfg["trigger"].items())[0]
wconf["hooks"].append(hook_cfg)
workloads.append(wconf)
sconf["workloads"] = workloads
self.subtasks.append(sconf)
# if self.version == 1:
# TODO(ikhudoshyn): Warn user about deprecated format
@staticmethod
def _get_version(config):
return config.get("version", 1)
def _validate_version(self):
if self.version not in self.CONFIG_SCHEMAS:
allowed = ", ".join([str(k) for k in self.CONFIG_SCHEMAS])
msg = ("Task configuration version %s is not supported. "
"Supported versions: %s") % (self.version, allowed)
raise exceptions.InvalidTaskException(msg)
def _validate_json(self, config):
try:
jsonschema.validate(config, self.CONFIG_SCHEMAS[self.version])
except Exception as e:
raise exceptions.InvalidTaskException(str(e))
@staticmethod
def _adopt_task_format_v1(config):
subtasks = []
for name, v1_workloads in config.items():
for v1_workload in v1_workloads:
subtask = copy.deepcopy(v1_workload)
subtask["scenario"] = {name: subtask.pop("args", {})}
subtask["contexts"] = subtask.pop("context", {})
subtask["title"] = name
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.get("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}

@ -26,6 +26,7 @@ from rally import api
from rally.task import context
from rally.task import engine
from rally.task import scenario
from rally.task import task_cfg
from tests.unit import test
RALLY_PATH = os.path.dirname(os.path.dirname(rally.__file__))
@ -85,7 +86,7 @@ class TaskSampleTestCase(test.TestCase):
except Exception:
print(traceback.format_exc())
self.fail("Invalid JSON file: %s" % path)
eng = engine.TaskEngine(engine.TaskConfig(task_config),
eng = engine.TaskEngine(task_cfg.TaskConfig(task_config),
mock.MagicMock(), mock.Mock())
eng.validate(only_syntax=True)
except Exception:

@ -26,6 +26,7 @@ from rally import exceptions
from rally.task import context
from rally.task import engine
from rally.task import scenario
from rally.task import task_cfg
from tests.unit import test
@ -73,8 +74,7 @@ class TaskEngineTestCase(test.TestCase):
mock_validate.platforms.assert_called_once_with(config)
mock_validate.semantic.assert_called_once_with(config)
@mock.patch("rally.task.engine.TaskConfig")
def test_validate__wrong_syntax(self, mock_task_config):
def test_validate__wrong_syntax(self):
task = mock.MagicMock()
eng = engine.TaskEngine(mock.MagicMock(), task, mock.Mock())
eng._validate_config_syntax = mock.MagicMock(
@ -87,8 +87,7 @@ class TaskEngineTestCase(test.TestCase):
# the next validation step should not be processed
self.assertFalse(eng._validate_config_platforms.called)
@mock.patch("rally.task.engine.TaskConfig")
def test_validate__wrong_semantic(self, mock_task_config):
def test_validate__wrong_semantic(self):
task = mock.MagicMock()
eng = engine.TaskEngine(mock.MagicMock(), task, mock.Mock())
eng._validate_config_syntax = mock.MagicMock()
@ -108,13 +107,11 @@ class TaskEngineTestCase(test.TestCase):
@mock.patch("rally.task.sla.SLA.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")
def test__validate_workload(
self, mock_context_validate,
mock_scenario_runner_validate,
mock_task_config,
mock_hook_action_validate,
mock_hook_trigger_validate,
mock_sla_validate,
@ -173,10 +170,9 @@ class TaskEngineTestCase(test.TestCase):
@mock.patch("rally.task.engine.json.dumps")
@mock.patch("rally.task.engine.scenario.Scenario.get")
@mock.patch("rally.task.engine.TaskConfig")
@mock.patch("rally.task.engine.runner.ScenarioRunner.validate")
def test___validate_workload__wrong_runner(
self, mock_scenario_runner_validate, mock_task_config,
self, mock_scenario_runner_validate,
mock_scenario_get, mock_dumps):
mock_dumps.return_value = "<JSON>"
mock_scenario_runner_validate.return_value = [
@ -196,10 +192,9 @@ class TaskEngineTestCase(test.TestCase):
@mock.patch("rally.task.engine.json.dumps")
@mock.patch("rally.task.engine.scenario.Scenario.get")
@mock.patch("rally.task.engine.TaskConfig")
@mock.patch("rally.task.engine.context.Context.validate")
def test__validate_config_syntax__wrong_context(
self, mock_context_validate, mock_task_config, mock_scenario_get,
self, mock_context_validate, mock_scenario_get,
mock_dumps):
mock_dumps.return_value = "<JSON>"
mock_context_validate.return_value = ["context_error"]
@ -223,10 +218,8 @@ class TaskEngineTestCase(test.TestCase):
@mock.patch("rally.task.engine.json.dumps")
@mock.patch("rally.task.engine.scenario.Scenario.get")
@mock.patch("rally.task.sla.SLA.validate")
@mock.patch("rally.task.engine.TaskConfig")
def test__validate_config_syntax__wrong_sla(
self, mock_task_config, mock_sla_validate, mock_scenario_get,
mock_dumps):
self, mock_sla_validate, mock_scenario_get, mock_dumps):
mock_dumps.return_value = "<JSON>"
mock_sla_validate.return_value = ["sla_error"]
scenario_cls = mock_scenario_get.return_value
@ -250,9 +243,8 @@ class TaskEngineTestCase(test.TestCase):
@mock.patch("rally.task.engine.scenario.Scenario.get")
@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_hook_trigger_validate,
self, mock_hook_trigger_validate,
mock_hook_action_validate,
mock_scenario_get, mock_dumps):
mock_dumps.return_value = "<JSON>"
@ -283,9 +275,8 @@ class TaskEngineTestCase(test.TestCase):
@mock.patch("rally.task.engine.scenario.Scenario.get")
@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_action_validate,
self, mock_hook_action_validate,
mock_hook_trigger_validate,
mock_scenario_get, mock_dumps):
mock_dumps.return_value = "<JSON>"
@ -314,9 +305,7 @@ class TaskEngineTestCase(test.TestCase):
@mock.patch("rally.task.engine.context.ContextManager.cleanup")
@mock.patch("rally.task.engine.context.ContextManager.setup")
@mock.patch("rally.task.engine.TaskConfig")
def test__validate_config_semantic(self, mock_task_config,
mock_context_manager_setup,
def test__validate_config_semantic(self, mock_context_manager_setup,
mock_context_manager_cleanup):
env = mock.MagicMock(uuid="env_uuid")
env.check_health.return_value = {
@ -354,10 +343,9 @@ class TaskEngineTestCase(test.TestCase):
eng._validate_config_semantic,
mock_task_instance)
@mock.patch("rally.task.engine.TaskConfig")
@mock.patch("rally.task.engine.TaskEngine._validate_workload")
def test__validate_config_platforms(
self, mock__validate_workload, mock_task_config):
self, mock__validate_workload):
foo_cred = {"admin": "admin", "users": ["user1"]}
env = mock.MagicMock(data={
@ -385,7 +373,6 @@ class TaskEngineTestCase(test.TestCase):
mock__validate_workload.call_args_list)
@mock.patch("rally.common.objects.Task.get_status")
@mock.patch("rally.task.engine.TaskConfig")
@mock.patch("rally.task.engine.ResultConsumer")
@mock.patch("rally.task.engine.context.ContextManager.cleanup")
@mock.patch("rally.task.engine.context.ContextManager.setup")
@ -394,7 +381,7 @@ class TaskEngineTestCase(test.TestCase):
def test_run__update_status(
self, mock_scenario_runner, mock_scenario,
mock_context_manager_setup, mock_context_manager_cleanup,
mock_result_consumer, mock_task_config, mock_task_get_status):
mock_result_consumer, mock_task_get_status):
task = mock.MagicMock()
mock_task_get_status.return_value = consts.TaskStatus.ABORTING
@ -461,7 +448,7 @@ class TaskEngineTestCase(test.TestCase):
mock_result_consumer.is_task_in_aborting_status.side_effect = [False,
False,
True]
config = engine.TaskConfig({
config = task_cfg.TaskConfig({
"a.task": [{"runner": {"type": "a", "b": 1},
"description": "foo"}],
"b.task": [{"runner": {"type": "a", "b": 1},
@ -498,7 +485,7 @@ class TaskEngineTestCase(test.TestCase):
mock_context_manager_setup, mock_context_manager_cleanup,
mock_result_consumer, mock_task_get_status):
task = mock.MagicMock(spec=objects.Task)
config = engine.TaskConfig({
config = task_cfg.TaskConfig({
"a.task": [{"runner": {"type": "a", "b": 1}}],
"b.task": [{"runner": {"type": "a", "b": 1}}],
"c.task": [{"runner": {"type": "a", "b": 1}}]
@ -530,7 +517,7 @@ class TaskEngineTestCase(test.TestCase):
subtask_obj = task.add_subtask.return_value
subtask_obj.add_workload.side_effect = MyException()
mock_result_consumer.is_task_in_aborting_status.return_value = False
config = engine.TaskConfig({
config = task_cfg.TaskConfig({
"a.task": [{"runner": {"type": "a", "b": 1}}],
"b.task": [{"runner": {"type": "a", "b": 1}}],
"c.task": [{"runner": {"type": "a", "b": 1}}]
@ -549,8 +536,7 @@ class TaskEngineTestCase(test.TestCase):
subtask_obj.update_status.assert_called_once_with(
consts.SubtaskStatus.CRASHED)
@mock.patch("rally.task.engine.TaskConfig")
def test__prepare_context(self, mock_task_config):
def test__prepare_context(self):
@context.configure("test1", 1, platform="testing")
class TestContext1(context.Context):
@ -972,126 +958,3 @@ class ResultConsumerTestCase(test.TestCase):
self.assertFalse(runner.abort.called)
# test task.get_status is checked until is_done is not set
self.assertEqual(4, mock_task_get_status.call_count)
class TaskConfigTestCase(test.TestCase):
def test_init_empty_config(self):
config = None
exception = self.assertRaises(Exception, # noqa
engine.TaskConfig, config)
self.assertIn("Input task is empty", str(exception))
@mock.patch("jsonschema.validate")
def test_validate_json(self, mock_validate):
config = {}
engine.TaskConfig(config)
mock_validate.assert_has_calls([
mock.call(config, engine.TaskConfig.CONFIG_SCHEMA_V1)])
@mock.patch("jsonschema.validate")
def test_validate_json_v2(self, mock_validate):
config = {"version": 2, "subtasks": []}
engine.TaskConfig(config)
mock_validate.assert_has_calls([
mock.call(config, engine.TaskConfig.CONFIG_SCHEMA_V2)])
@mock.patch("rally.task.engine.TaskConfig._get_version")
@mock.patch("rally.task.engine.TaskConfig._validate_json")
def test_validate_version(self, mock_task_config__validate_json,
mock_task_config__get_version):
mock_task_config__get_version.return_value = 1
engine.TaskConfig(mock.MagicMock())
@mock.patch("rally.task.engine.TaskConfig._get_version")
@mock.patch("rally.task.engine.TaskConfig._validate_json")
def test_validate_version_wrong_version(
self, mock_task_config__validate_json,
mock_task_config__get_version):
mock_task_config__get_version.return_value = "wrong"
self.assertRaises(exceptions.InvalidTaskException, engine.TaskConfig,
mock.MagicMock)
def test__adopt_task_format_v1(self):
# mock all redundant checks :)
class TaskConfig(engine.TaskConfig):
def __init__(self):
pass
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": {}
},
{
"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])

@ -0,0 +1,144 @@
# 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 collections
import mock
from rally import exceptions
from rally.task import task_cfg
from tests.unit import test
class TaskConfigTestCase(test.TestCase):
def test_init_empty_config(self):
config = None
exception = self.assertRaises(Exception, # noqa
task_cfg.TaskConfig, config)
self.assertIn("Input task is empty", str(exception))
@mock.patch("jsonschema.validate")
def test_validate_json(self, mock_validate):
config = {}
task_cfg.TaskConfig(config)
mock_validate.assert_has_calls([
mock.call(config, task_cfg.TaskConfig.CONFIG_SCHEMA_V1)])
@mock.patch("jsonschema.validate")
def test_validate_json_v2(self, mock_validate):
config = {"version": 2, "subtasks": []}
task_cfg.TaskConfig(config)
mock_validate.assert_has_calls([
mock.call(config, task_cfg.TaskConfig.CONFIG_SCHEMA_V2)])
@mock.patch("rally.task.task_cfg.TaskConfig._get_version")
@mock.patch("rally.task.task_cfg.TaskConfig._validate_json")
def test_validate_version(self, mock_task_config__validate_json,
mock_task_config__get_version):
mock_task_config__get_version.return_value = 1
task_cfg.TaskConfig(mock.MagicMock())
@mock.patch("rally.task.task_cfg.TaskConfig._get_version")
@mock.patch("rally.task.task_cfg.TaskConfig._validate_json")
def test_validate_version_wrong_version(
self, mock_task_config__validate_json,
mock_task_config__get_version):
mock_task_config__get_version.return_value = "wrong"
self.assertRaises(exceptions.InvalidTaskException, task_cfg.TaskConfig,
mock.MagicMock)
def test__adopt_task_format_v1(self):
# mock all redundant checks :)
class TaskConfig(task_cfg.TaskConfig):
def __init__(self):
pass
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": {}
},
{
"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 = task_cfg.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])

@ -67,7 +67,7 @@ class TaskAPITestCase(test.TestCase):
mock_api.endpoint_url = None
self.task_inst = api._Task(mock_api)
@mock.patch("rally.task.engine.TaskConfig")
@mock.patch("rally.api.task_cfg.TaskConfig")
@mock.patch("rally.api.objects.Task")
@mock.patch("rally.api.objects.Deployment.get")
@mock.patch("rally.api.engine.TaskEngine")
@ -261,7 +261,7 @@ class TaskAPITestCase(test.TestCase):
self.task_inst.create, deployment=deployment_id,
tags=["a"])
@mock.patch("rally.task.engine.TaskConfig")
@mock.patch("rally.api.task_cfg.TaskConfig")
@mock.patch("rally.api.objects.Task")
@mock.patch("rally.api.objects.Deployment.get")
@mock.patch("rally.api.engine.TaskEngine")
@ -337,7 +337,7 @@ class TaskAPITestCase(test.TestCase):
config="config",
task=fake_task["uuid"])
@mock.patch("rally.task.engine.TaskConfig")
@mock.patch("rally.api.task_cfg.TaskConfig")
@mock.patch("rally.api.objects.Task")
@mock.patch("rally.api.objects.Deployment.get")
@mock.patch("rally.api.engine.TaskEngine")