Refactor the related command of task report and export
1.Use report plugins instead of separated reports, however, we still save old report command. 2.New report command rally task report --html --uuid <uuid> --out <dest> we can use --html and --html-static, and deprecate --junit, we have move junit to `rally task export` command. Example: rally task report --html --uuid xxxxxx --out /home/report.html 3.Change `rally task export` format, and deprecate old Exporter plugin. rally task export --uuid <uuid> --type <type> --to <dest> Example: rally task export --uuid xxxx --type junit-xml --to xxxxx 4.Remove FileExporter plugin. Change-Id: I44cafccb8d6c6c3cc704fb6e3ff2f49a756209ef
This commit is contained in:
parent
801e624c9c
commit
9d26483c12
@ -31,10 +31,10 @@ _rally()
|
||||
OPTS["task_abort"]="--uuid --soft"
|
||||
OPTS["task_delete"]="--force --uuid"
|
||||
OPTS["task_detailed"]="--uuid --iterations-data"
|
||||
OPTS["task_export"]="--uuid --connection"
|
||||
OPTS["task_export"]="--uuid --type --to"
|
||||
OPTS["task_import"]="--file --deployment --tag"
|
||||
OPTS["task_list"]="--deployment --all-deployments --status --uuids-only"
|
||||
OPTS["task_report"]="--tasks --out --open --html --html-static --junit"
|
||||
OPTS["task_report"]="--out --open --html --html-static --uuid"
|
||||
OPTS["task_results"]="--uuid"
|
||||
OPTS["task_sla-check"]="--uuid --json"
|
||||
OPTS["task_sla_check"]="--uuid --json"
|
||||
@ -91,4 +91,4 @@ _rally()
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -o filenames -F _rally rally
|
||||
complete -o filenames -F _rally rally
|
||||
|
28
rally/api.py
28
rally/api.py
@ -38,6 +38,7 @@ from rally import consts
|
||||
from rally.deployment import engine as deploy_engine
|
||||
from rally import exceptions
|
||||
from rally.task import engine
|
||||
from rally.task import exporter as texporter
|
||||
from rally.verification import context as vcontext
|
||||
from rally.verification import manager as vmanager
|
||||
from rally.verification import reporter as vreporter
|
||||
@ -580,6 +581,33 @@ class _Task(APIGroup):
|
||||
|
||||
return task_inst.to_dict()
|
||||
|
||||
@api_wrapper(path=API_REQUEST_PREFIX + "/task/export",
|
||||
method="POST")
|
||||
def export(self, tasks_uuids, output_type, output_dest=None):
|
||||
"""Generate a report for a task or a few tasks.
|
||||
|
||||
:param tasks_uuids: List of tasks UUIDs
|
||||
:param output_type: Plugin name of task reporter
|
||||
:param output_dest: Destination for task report
|
||||
"""
|
||||
|
||||
tasks_results = []
|
||||
for task_uuid in tasks_uuids:
|
||||
tasks_results.extend(self.get_detailed(
|
||||
task_id=task_uuid)["results"])
|
||||
|
||||
reporter_cls = texporter.TaskExporter.get(output_type)
|
||||
reporter_cls.validate(output_dest)
|
||||
|
||||
LOG.info("Building '%s' report for the following task(s): "
|
||||
"'%s'.", output_type, "', '".join(tasks_uuids))
|
||||
result = texporter.TaskExporter.make(reporter_cls,
|
||||
tasks_results,
|
||||
output_dest,
|
||||
api=self.api)
|
||||
LOG.info("The report has been successfully built.")
|
||||
return result
|
||||
|
||||
|
||||
class _Verifier(APIGroup):
|
||||
|
||||
|
@ -25,7 +25,6 @@ import webbrowser
|
||||
import jsonschema
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from rally.cli import cliutils
|
||||
from rally.cli import envutils
|
||||
@ -39,7 +38,6 @@ from rally.common import yamlutils as yaml
|
||||
from rally import consts
|
||||
from rally import exceptions
|
||||
from rally import plugins
|
||||
from rally.task import exporter
|
||||
from rally.task.processing import plot
|
||||
from rally.task.processing import utils as putils
|
||||
from rally.task import utils as tutils
|
||||
@ -450,7 +448,7 @@ class TaskCommands(object):
|
||||
print(_("* To plot HTML graphics with this data, run:"))
|
||||
print("\trally task report %s --out output.html\n" % task["uuid"])
|
||||
print(_("* To generate a JUnit report, run:"))
|
||||
print("\trally task report %s --junit --out output.xml\n" %
|
||||
print("\trally task export %s --type junit --to output.xml\n" %
|
||||
task["uuid"])
|
||||
print(_("* To get raw JSON output of task results, run:"))
|
||||
print("\trally task results %s\n" % task["uuid"])
|
||||
@ -624,31 +622,47 @@ class TaskCommands(object):
|
||||
else:
|
||||
print(result)
|
||||
|
||||
@cliutils.args("--tasks", dest="tasks", nargs="+",
|
||||
help="UUIDs of tasks, or JSON files with task results")
|
||||
@cliutils.deprecated_args("--tasks", dest="task_id", nargs="+",
|
||||
release="0.10.0", alternative="--uuid")
|
||||
@cliutils.args("--out", metavar="<path>",
|
||||
type=str, dest="out", required=False,
|
||||
help="Path to output file.")
|
||||
help="Report destination. Can be a path to a file (in case"
|
||||
" of HTML, HTML-STATIC, etc. types) to save the"
|
||||
" report to or a connection string.")
|
||||
@cliutils.args("--open", dest="open_it", action="store_true",
|
||||
help="Open the output in a browser.")
|
||||
@cliutils.args("--html", dest="out_format",
|
||||
action="store_const", const="html",
|
||||
help="Generate the report in HTML.")
|
||||
action="store_const", const="html")
|
||||
@cliutils.args("--html-static", dest="out_format",
|
||||
action="store_const", const="html_static",
|
||||
help=("Generate the report in HTML with embedded "
|
||||
"JS and CSS, so it will not depend on "
|
||||
"Internet availability."))
|
||||
@cliutils.args("--junit", dest="out_format",
|
||||
action="store_const", const="junit",
|
||||
help="Generate the report in the JUnit format.")
|
||||
@envutils.default_from_global("tasks", envutils.ENV_TASK, "tasks")
|
||||
action="store_const", const="html-static")
|
||||
@cliutils.deprecated_args("--junit", dest="out_format",
|
||||
action="store_const", const="junit-xml",
|
||||
release="0.10.0",
|
||||
alternative=("rally task export "
|
||||
"--type junit-xml"))
|
||||
@cliutils.args("--uuid", dest="task_id", nargs="+", type=str,
|
||||
help="UUIDs of tasks")
|
||||
@envutils.with_default_task_id
|
||||
@cliutils.suppress_warnings
|
||||
def report(self, api, tasks=None, out=None, open_it=False,
|
||||
out_format="html"):
|
||||
def report(self, api, task_id=None, out=None,
|
||||
open_it=False, out_format="html"):
|
||||
"""generate report file or string for specified task."""
|
||||
|
||||
if [task for task in task_id if os.path.exists(
|
||||
os.path.expanduser(task))]:
|
||||
self._old_report(api, tasks=task_id, out=out,
|
||||
open_it=open_it, out_format=out_format)
|
||||
else:
|
||||
self.export(api, task_id=task_id,
|
||||
output_type=out_format,
|
||||
output_dest=out,
|
||||
open_it=open_it)
|
||||
|
||||
def _old_report(self, api, tasks=None, out=None, open_it=False,
|
||||
out_format="html"):
|
||||
"""Generate report file for specified task.
|
||||
|
||||
:param tasks: list, UUIDs od tasks or pathes files with tasks results
|
||||
:param tasks: list, UUIDs of tasks or pathes files with tasks results
|
||||
:param out: str, output file name
|
||||
:param open_it: bool, whether to open output file in web browser
|
||||
:param out_format: output format (junit, html or html_static)
|
||||
@ -692,7 +706,7 @@ class TaskCommands(object):
|
||||
if out_format.startswith("html"):
|
||||
result = plot.plot(results,
|
||||
include_libs=(out_format == "html_static"))
|
||||
elif out_format == "junit":
|
||||
elif out_format == "junit-xml":
|
||||
test_suite = junit.JUnit("Rally test suite")
|
||||
for result in results:
|
||||
if isinstance(result["sla"], list):
|
||||
@ -801,51 +815,47 @@ class TaskCommands(object):
|
||||
api.task.get(task_id=task_id)
|
||||
fileutils.update_globals_file("RALLY_TASK", task_id)
|
||||
|
||||
@cliutils.args("--uuid", dest="uuid", type=str,
|
||||
@cliutils.args("--uuid", dest="task_id", nargs="+", type=str,
|
||||
help="UUIDs of tasks")
|
||||
@cliutils.args("--type", dest="output_type", type=str,
|
||||
required=True,
|
||||
help="UUID of a the task.")
|
||||
@cliutils.args("--connection", dest="connection_string", type=str,
|
||||
required=True,
|
||||
help="Connection url to the task export system.")
|
||||
help="Report type (Defaults to HTML). Out-of-the-box "
|
||||
"types: HTML, HTML-Static, JUnit-XML. "
|
||||
"HINT: You can list all types, executing `rally "
|
||||
"plugin list --plugin-base TaskExporter` "
|
||||
"command.")
|
||||
@cliutils.args("--to", dest="output_dest", type=str,
|
||||
metavar="<dest>", required=False,
|
||||
help="Report destination. Can be a path to a file (in case"
|
||||
" of HTML, HTML-Static, JUnit-XML, etc. types) to"
|
||||
" save the report to or a connection string."
|
||||
" It depends on the report type."
|
||||
)
|
||||
@envutils.with_default_task_id
|
||||
@plugins.ensure_plugins_are_loaded
|
||||
def export(self, api, uuid, connection_string):
|
||||
def export(self, api, task_id=None, output_type=None, output_dest=None,
|
||||
open_it=False):
|
||||
"""Export task results to the custom task's exporting system.
|
||||
|
||||
:param uuid: UUID of the task
|
||||
:param connection_string: string used to connect to the system
|
||||
:param task_id: UUID of the task
|
||||
:param output_type: str, output type
|
||||
:param output_dest: output format (html, html-static, junit-xml,etc)
|
||||
"""
|
||||
task_id = isinstance(task_id, list) and task_id or [task_id]
|
||||
report = api.task.export(tasks_uuids=task_id,
|
||||
output_type=output_type,
|
||||
output_dest=output_dest)
|
||||
if "files" in report:
|
||||
for path in report["files"]:
|
||||
output_file = os.path.expanduser(path)
|
||||
with open(output_file, "w+") as f:
|
||||
f.write(report["files"][path])
|
||||
if open_it:
|
||||
if "open" in report:
|
||||
webbrowser.open_new_tab(report["open"])
|
||||
|
||||
parsed_obj = urlparse.urlparse(connection_string)
|
||||
try:
|
||||
client = exporter.Exporter.get(parsed_obj.scheme)(
|
||||
connection_string)
|
||||
except exceptions.InvalidConnectionString as e:
|
||||
if logging.is_debug():
|
||||
LOG.exception(e)
|
||||
print(e)
|
||||
return 1
|
||||
except exceptions.PluginNotFound as e:
|
||||
if logging.is_debug():
|
||||
LOG.exception(e)
|
||||
msg = ("\nPlease check your connection string. The format of "
|
||||
"`connection` should be plugin-name://"
|
||||
"<user>:<pwd>@<full_address>:<port>/<path>.<type>")
|
||||
print(str(e) + msg)
|
||||
return 1
|
||||
|
||||
try:
|
||||
client.export(uuid)
|
||||
except (IOError, exceptions.RallyException) as e:
|
||||
if logging.is_debug():
|
||||
LOG.exception(e)
|
||||
print(e)
|
||||
return 1
|
||||
print(_("Task %(uuid)s results was successfully exported to %("
|
||||
"connection)s using %(name)s plugin.") % {
|
||||
"uuid": uuid,
|
||||
"connection": connection_string,
|
||||
"name": parsed_obj.scheme
|
||||
})
|
||||
if "print" in report:
|
||||
print(report["print"])
|
||||
|
||||
@staticmethod
|
||||
def _print_task_errors(task_id, task_errors):
|
||||
|
@ -1,108 +0,0 @@
|
||||
# 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 json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from rally import api
|
||||
from rally.common import logging
|
||||
from rally import exceptions
|
||||
from rally.task import exporter
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@exporter.configure(name="file")
|
||||
class FileExporter(exporter.Exporter):
|
||||
"""Export task results in the file."""
|
||||
|
||||
def validate(self):
|
||||
"""Validate connection string.
|
||||
|
||||
The format of connection string in file plugin is
|
||||
file:///<path>.<type-of-output>
|
||||
"""
|
||||
|
||||
parse_obj = urlparse.urlparse(self.connection_string)
|
||||
|
||||
available_formats = ("json",)
|
||||
available_formats_str = ", ".join(available_formats)
|
||||
if self.connection_string is None or parse_obj.path == "":
|
||||
raise exceptions.InvalidConnectionString(
|
||||
"It should be `file:///<path>.<type-of-output>`.")
|
||||
if self.type not in available_formats:
|
||||
raise exceptions.InvalidConnectionString(
|
||||
"Type of the exported task is not available. The available "
|
||||
"formats are %s." %
|
||||
available_formats_str)
|
||||
|
||||
def __init__(self, connection_string):
|
||||
super(FileExporter, self).__init__(connection_string)
|
||||
self.path = os.path.expanduser(urlparse.urlparse(
|
||||
connection_string).path[1:])
|
||||
self.type = connection_string.split(".")[-1]
|
||||
self.validate()
|
||||
|
||||
def export(self, uuid):
|
||||
"""Export results of the task to the file.
|
||||
|
||||
:param uuid: uuid of the task object
|
||||
"""
|
||||
rapi = api.API(config_args=sys.argv[1:], skip_db_check=True)
|
||||
task = rapi.task.get_detailed(task_id=uuid)
|
||||
|
||||
LOG.debug("Got the task object by it's uuid %s. " % uuid)
|
||||
|
||||
task_results = [{"key": x["key"], "result": x["data"]["raw"],
|
||||
"sla": x["data"]["sla"],
|
||||
"hooks": x["data"].get("hooks"),
|
||||
"load_duration": x["data"]["load_duration"],
|
||||
"full_duration": x["data"]["full_duration"]}
|
||||
for x in task["results"]]
|
||||
|
||||
if self.type == "json":
|
||||
if task_results:
|
||||
res = json.dumps(task_results, sort_keys=False, indent=4,
|
||||
separators=(",", ": "))
|
||||
LOG.debug("Got the task %s results." % uuid)
|
||||
else:
|
||||
msg = ("Task %s results would be available when it will "
|
||||
"finish." % uuid)
|
||||
raise exceptions.RallyException(msg)
|
||||
|
||||
if os.path.dirname(self.path) and (not os.path.exists(os.path.dirname(
|
||||
self.path))):
|
||||
raise IOError("There is no such directory: %s" %
|
||||
os.path.dirname(self.path))
|
||||
with open(self.path, "w") as f:
|
||||
LOG.debug("Writing task %s results to the %s." % (
|
||||
uuid, self.connection_string))
|
||||
f.write(res)
|
||||
LOG.debug("Task %s results was written to the %s." % (
|
||||
uuid, self.connection_string))
|
||||
|
||||
|
||||
@exporter.configure(name="file-exporter")
|
||||
class DeprecatedFileExporter(FileExporter):
|
||||
"""DEPRECATED."""
|
||||
def __init__(self, connection_string):
|
||||
super(DeprecatedFileExporter, self).__init__(connection_string)
|
||||
LOG.warning("'file-exporter' plugin is deprecated. Use 'file' "
|
||||
"instead.")
|
202
rally/plugins/common/exporter/reporters.py
Normal file
202
rally/plugins/common/exporter/reporters.py
Normal file
@ -0,0 +1,202 @@
|
||||
# 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 os
|
||||
|
||||
from rally.common.io import junit
|
||||
from rally.task import exporter
|
||||
from rally.task.processing import plot
|
||||
|
||||
|
||||
class OldJSONResultsMixin(object):
|
||||
"""Generates task report in old JSON format.
|
||||
|
||||
An example of the report (All dates, numbers, names appearing in this
|
||||
example are fictitious. Any resemblance to real things is purely
|
||||
coincidental):
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"hooks": [],
|
||||
"created_at": "2017-06-04T05:14:44",
|
||||
"load_duration": 2.03029203414917,
|
||||
"result": [
|
||||
{
|
||||
"timestamp": 1496553301.578394,
|
||||
"error": [],
|
||||
"duration": 1.0232760906219482,
|
||||
"output": {
|
||||
"additive": [],
|
||||
"complete": []
|
||||
},
|
||||
"idle_duration": 0.0,
|
||||
"atomic_actions": [
|
||||
{
|
||||
"finished_at": 1496553302.601537,
|
||||
"started_at": 1496553301.57868,
|
||||
"name": "cinder_v2.list_volumes",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"timestamp": 1496553302.608502,
|
||||
"error": [],
|
||||
"duration": 1.0001840591430664,
|
||||
"output": {
|
||||
"additive": [],
|
||||
"complete": []
|
||||
},
|
||||
"idle_duration": 0.0,
|
||||
"atomic_actions": [
|
||||
{
|
||||
"finished_at": 1496553303.608628,
|
||||
"started_at": 1496553302.608545,
|
||||
"name": "cinder_v2.list_volumes",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"key": {
|
||||
"kw": {
|
||||
"runner": {
|
||||
"type": "constant",
|
||||
"times": 2,
|
||||
"concurrency": 1
|
||||
},
|
||||
"hooks": [],
|
||||
"args": {
|
||||
"detailed": true
|
||||
},
|
||||
"sla": {},
|
||||
"context": {
|
||||
"volumes": {
|
||||
"size": 1,
|
||||
"volumes_per_tenant": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"pos": 0,
|
||||
"name": "CinderVolumes.list_volumes",
|
||||
"description": "List all volumes."
|
||||
},
|
||||
"full_duration": 29.969523191452026,
|
||||
"sla": []
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
def _generate_tasks_results(self):
|
||||
"""Prepare raw report."""
|
||||
results = [{"key": x["key"], "result": x["data"]["raw"],
|
||||
"sla": x["data"]["sla"],
|
||||
"hooks": x["data"].get("hooks", []),
|
||||
"load_duration": x["data"]["load_duration"],
|
||||
"full_duration": x["data"]["full_duration"],
|
||||
"created_at": x["created_at"]}
|
||||
for x in self.tasks_results]
|
||||
return results
|
||||
|
||||
|
||||
@exporter.configure("html")
|
||||
class HTMLExporter(exporter.TaskExporter, OldJSONResultsMixin):
|
||||
"""Generates task report in HTML format."""
|
||||
INCLUDE_LIBS = False
|
||||
|
||||
@classmethod
|
||||
def validate(cls, output_destination):
|
||||
"""Validate destination of report.
|
||||
|
||||
:param output_destination: Destination of report
|
||||
"""
|
||||
# nothing to check :)
|
||||
pass
|
||||
|
||||
def _generate(self):
|
||||
results = []
|
||||
processed_names = {}
|
||||
tasks_results = self._generate_tasks_results()
|
||||
for task_result in tasks_results:
|
||||
if task_result["key"]["name"] in processed_names:
|
||||
processed_names[task_result["key"]["name"]] += 1
|
||||
task_result["key"]["pos"] = processed_names[
|
||||
task_result["key"]["name"]]
|
||||
else:
|
||||
processed_names[task_result["key"]["name"]] = 0
|
||||
results.append(task_result)
|
||||
return results
|
||||
|
||||
def generate(self):
|
||||
results = self._generate()
|
||||
report = plot.plot(results,
|
||||
include_libs=self.INCLUDE_LIBS)
|
||||
|
||||
if self.output_destination:
|
||||
return {"files": {self.output_destination: report},
|
||||
"open": "file://" + os.path.abspath(
|
||||
self.output_destination)}
|
||||
else:
|
||||
return {"print": report}
|
||||
|
||||
|
||||
@exporter.configure("html-static")
|
||||
class HTMLStaticExporter(HTMLExporter):
|
||||
"""Generates task report in HTML format with embedded JS/CSS."""
|
||||
INCLUDE_LIBS = True
|
||||
|
||||
|
||||
@exporter.configure("junit-xml")
|
||||
class JUnitXMLExporter(HTMLExporter):
|
||||
"""Generates task report in JUnit-XML format.
|
||||
|
||||
An example of the report (All dates, numbers, names appearing in this
|
||||
example are fictitious. Any resemblance to real things is purely
|
||||
coincidental):
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<testsuite errors="0"
|
||||
failures="0"
|
||||
name="Rally test suite"
|
||||
tests="1"
|
||||
time="29.97">
|
||||
<testcase classname="CinderVolumes"
|
||||
name="list_volumes"
|
||||
time="29.97" />
|
||||
</testsuite>
|
||||
"""
|
||||
|
||||
def generate(self):
|
||||
results = self._generate()
|
||||
test_suite = junit.JUnit("Rally test suite")
|
||||
for result in results:
|
||||
if isinstance(result["sla"], list):
|
||||
message = ",".join([sla["detail"] for sla in
|
||||
result["sla"] if not sla["success"]])
|
||||
if message:
|
||||
outcome = junit.JUnit.FAILURE
|
||||
else:
|
||||
outcome = junit.JUnit.SUCCESS
|
||||
test_suite.add_test(result["key"]["name"],
|
||||
result["full_duration"], outcome, message)
|
||||
result = test_suite.to_xml()
|
||||
|
||||
if self.output_destination:
|
||||
return {"files": {self.output_destination: result},
|
||||
"open": "file://" + os.path.abspath(
|
||||
self.output_destination)}
|
||||
else:
|
||||
return {"print": result}
|
@ -21,19 +21,46 @@ system by connection string.
|
||||
|
||||
import abc
|
||||
|
||||
import jsonschema
|
||||
import six
|
||||
|
||||
from rally.common import logging
|
||||
from rally.common.plugin import plugin
|
||||
from rally import consts
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
configure = plugin.configure
|
||||
|
||||
REPORT_RESPONSE_SCHEMA = {
|
||||
"type": "object",
|
||||
"$schema": consts.JSON_SCHEMA,
|
||||
"properties": {
|
||||
"files": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".{1,}": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"open": {
|
||||
"type": "string",
|
||||
},
|
||||
"print": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": False
|
||||
}
|
||||
|
||||
|
||||
@plugin.base()
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Exporter(plugin.Plugin):
|
||||
|
||||
def __init__(self, connection_string):
|
||||
LOG.warning("Sorry, we have not support old Exporter plugin since"
|
||||
"Rally 0.10.0, please use TaskExporter instead.")
|
||||
self.connection_string = connection_string
|
||||
|
||||
@abc.abstractmethod
|
||||
@ -47,4 +74,60 @@ class Exporter(plugin.Plugin):
|
||||
def validate(self):
|
||||
"""Used to validate connection string."""
|
||||
|
||||
TaskExporter = Exporter
|
||||
|
||||
@plugin.base()
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class TaskExporter(plugin.Plugin):
|
||||
"""Base class for all exporters for Tasks."""
|
||||
|
||||
def __init__(self, tasks_results, output_destination, api=None):
|
||||
"""Init reporter
|
||||
|
||||
:param tasks_results: list of results to generate report for
|
||||
:param output_destination: destination of export
|
||||
:param api: an instance of rally.api.API object
|
||||
"""
|
||||
super(TaskExporter, self).__init__()
|
||||
self.tasks_results = tasks_results
|
||||
self.output_destination = output_destination
|
||||
self.api = api
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def validate(cls, output_destination):
|
||||
"""Validate destination of report.
|
||||
|
||||
:param output_destination: Destination of report
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate(self):
|
||||
"""Generate report
|
||||
|
||||
:returns: a dict with 3 optional elements:
|
||||
|
||||
- key "files" with a dictionary of files to save on disk.
|
||||
keys are paths, values are contents;
|
||||
- key "print" - data to print at CLI level
|
||||
- key "open" - path to file which should be open in case of
|
||||
--open flag
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def make(exporter_cls, task_results, output_destination, api=None):
|
||||
"""Initialize exporter, generate and validate result.
|
||||
|
||||
It is a base method which is called from API layer. It cannot be
|
||||
overridden. Do not even try! :)
|
||||
|
||||
:param exporter_cls: class of TaskExporter to be used
|
||||
:param task_results: list of results to generate report for
|
||||
:param output_destination: destination of export
|
||||
:param api: an instance of rally.api.API object
|
||||
"""
|
||||
report = exporter_cls(task_results, output_destination,
|
||||
api).generate()
|
||||
|
||||
jsonschema.validate(report, REPORT_RESPONSE_SCHEMA)
|
||||
|
||||
return report
|
||||
|
@ -57,6 +57,7 @@
|
||||
<li><a href="rally-plot/detailed_with_iterations.txt.gz">Text report detailed</a> <code>$ rally task detailed --iterations-data</code>
|
||||
<li><a href="rally-plot/sla.txt">Success criteria (SLA)</a> <code>$ rally task sla_check</code>
|
||||
<li><a href="rally-plot/results.json.gz">Raw results (JSON)</a> <code>$ rally task results</code>
|
||||
<li><a href="rally-plot/junit.xml.gz">JUNIT-XML report</a> <code>$ rally task export --type junit-xml</code>
|
||||
</ul>
|
||||
|
||||
<h2>About Rally</h2>
|
||||
|
@ -135,8 +135,10 @@ function run () {
|
||||
gzip -9 rally-plot/detailed.txt
|
||||
rally task detailed --iterations-data > rally-plot/detailed_with_iterations.txt
|
||||
gzip -9 rally-plot/detailed_with_iterations.txt
|
||||
rally task report --out rally-plot/results.html
|
||||
rally task report --html --out rally-plot/results.html
|
||||
gzip -9 rally-plot/results.html
|
||||
rally task export --type junit-xml --to rally-plot/junit.xml
|
||||
gzip -9 rally-plot/junit.xml
|
||||
|
||||
# NOTE(stpierre): if the sla check fails, we still want osresources.py
|
||||
# to run, so we turn off -e and save the return value
|
||||
|
@ -204,6 +204,8 @@ class TaskTestCase(unittest.TestCase):
|
||||
rally = utils.Rally()
|
||||
self.assertRaises(utils.RallyCliError,
|
||||
rally, "task report --tasks %s" % FAKE_TASK_UUID)
|
||||
self.assertRaises(utils.RallyCliError,
|
||||
rally, "task report --uuid %s" % FAKE_TASK_UUID)
|
||||
|
||||
def test_sla_check_with_wrong_task_id(self):
|
||||
rally = utils.Rally()
|
||||
@ -233,16 +235,22 @@ class TaskTestCase(unittest.TestCase):
|
||||
cfg = self._get_sample_task_config()
|
||||
config = utils.TaskConfig(cfg)
|
||||
rally("task start --task %s" % config.filename)
|
||||
rally("task report --out %s" % rally.gen_report_path(extension="html"))
|
||||
html_report = rally.gen_report_path(extension="html")
|
||||
rally("task report --out %s" % html_report)
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
self._assert_html_report_libs_are_embedded(html_report, False)
|
||||
self.assertRaises(utils.RallyCliError,
|
||||
rally, "task report --report %s" % FAKE_TASK_UUID)
|
||||
rally("task report --junit --out %s" %
|
||||
rally.gen_report_path(extension="junit"))
|
||||
self.assertTrue(os.path.exists(
|
||||
rally.gen_report_path(extension="junit")))
|
||||
|
||||
def test_new_report_one_uuid(self):
|
||||
rally = utils.Rally()
|
||||
cfg = self._get_sample_task_config()
|
||||
config = utils.TaskConfig(cfg)
|
||||
rally("task start --task %s" % config.filename)
|
||||
html_report = rally.gen_report_path(extension="html")
|
||||
rally("task report --out %s" % html_report)
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
self._assert_html_report_libs_are_embedded(html_report, False)
|
||||
self.assertRaises(utils.RallyCliError,
|
||||
rally, "task report --report %s" % FAKE_TASK_UUID)
|
||||
|
||||
@ -262,6 +270,21 @@ class TaskTestCase(unittest.TestCase):
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
self._assert_html_report_libs_are_embedded(html_report, False)
|
||||
|
||||
def test_new_report_bunch_uuids(self):
|
||||
rally = utils.Rally()
|
||||
cfg = self._get_sample_task_config()
|
||||
config = utils.TaskConfig(cfg)
|
||||
task_uuids = []
|
||||
for i in range(3):
|
||||
res = rally("task start --task %s" % config.filename)
|
||||
for line in res.splitlines():
|
||||
if "finished" in line:
|
||||
task_uuids.append(line.split(" ")[1][:-1])
|
||||
html_report = rally.gen_report_path(extension="html")
|
||||
rally("task report --uuid %s --out %s" % (" ".join(task_uuids),
|
||||
html_report))
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
|
||||
def test_report_bunch_files(self):
|
||||
rally = utils.Rally()
|
||||
cfg = self._get_sample_task_config()
|
||||
@ -289,7 +312,8 @@ class TaskTestCase(unittest.TestCase):
|
||||
task_result_file = "/tmp/report_42.json"
|
||||
if os.path.exists(task_result_file):
|
||||
os.remove(task_result_file)
|
||||
rally("task results", report_path=task_result_file, raw=True)
|
||||
rally("task results", report_path=task_result_file,
|
||||
raw=True)
|
||||
|
||||
task_run_output = rally(
|
||||
"task start --task %s" % config.filename).splitlines()
|
||||
@ -319,6 +343,16 @@ class TaskTestCase(unittest.TestCase):
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
self._assert_html_report_libs_are_embedded(html_report)
|
||||
|
||||
def test_new_report_one_uuid_with_static_libs(self):
|
||||
rally = utils.Rally()
|
||||
cfg = self._get_sample_task_config()
|
||||
config = utils.TaskConfig(cfg)
|
||||
rally("task start --task %s" % config.filename)
|
||||
html_report = rally.gen_report_path(extension="html")
|
||||
rally("task report --out %s --html-static" % html_report)
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
self._assert_html_report_libs_are_embedded(html_report)
|
||||
|
||||
def test_trends(self):
|
||||
cfg1 = {
|
||||
"Dummy.dummy": [
|
||||
@ -865,58 +899,38 @@ class TaskTestCase(unittest.TestCase):
|
||||
r"(?P<task_id>[0-9a-f\-]{36}): started", output)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_export(self):
|
||||
def test_export_one_uuid(self):
|
||||
rally = utils.Rally()
|
||||
cfg = {
|
||||
"Dummy.dummy": [
|
||||
{
|
||||
"runner": {
|
||||
"type": "constant",
|
||||
"times": 100,
|
||||
"concurrency": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
cfg = self._get_sample_task_config()
|
||||
config = utils.TaskConfig(cfg)
|
||||
output = rally("task start --task %s" % config.filename)
|
||||
uuid = re.search(
|
||||
r"(?P<uuid>[0-9a-f\-]{36}): started", output).group("uuid")
|
||||
connection = (
|
||||
"file-exporter:///" + rally.gen_report_path(extension="json"))
|
||||
output = rally("task export --uuid %s --connection %s" % (
|
||||
uuid, connection))
|
||||
expected = (
|
||||
"Task %(uuid)s results was successfully exported to %("
|
||||
"connection)s using file-exporter plugin." % {
|
||||
"uuid": uuid,
|
||||
"connection": connection,
|
||||
})
|
||||
self.assertIn(expected, output)
|
||||
rally("task start --task %s" % config.filename)
|
||||
html_report = rally.gen_report_path(extension="html")
|
||||
rally("task export --type html --to %s" % html_report)
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
self._assert_html_report_libs_are_embedded(html_report, False)
|
||||
|
||||
def test_export_with_wrong_connection(self):
|
||||
rally("task export --type html-static --to %s" % html_report)
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
self._assert_html_report_libs_are_embedded(html_report)
|
||||
|
||||
junit_report = rally.gen_report_path(extension="junit")
|
||||
rally("task export --type junit-xml --to %s" % junit_report)
|
||||
self.assertTrue(os.path.exists(junit_report))
|
||||
|
||||
def test_export_bunch_uuids(self):
|
||||
rally = utils.Rally()
|
||||
cfg = {
|
||||
"Dummy.dummy": [
|
||||
{
|
||||
"runner": {
|
||||
"type": "constant",
|
||||
"times": 100,
|
||||
"concurrency": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
cfg = self._get_sample_task_config()
|
||||
config = utils.TaskConfig(cfg)
|
||||
output = rally("task start --task %s" % config.filename)
|
||||
uuid = re.search(
|
||||
r"(?P<uuid>[0-9a-f\-]{36}): started", output).group("uuid")
|
||||
connection = (
|
||||
"fake:///" + rally.gen_report_path(extension="json"))
|
||||
self.assertRaises(utils.RallyCliError,
|
||||
rally,
|
||||
"task export --uuid %s --connection %s" % (
|
||||
uuid, connection))
|
||||
task_uuids = []
|
||||
for i in range(3):
|
||||
res = rally("task start --task %s" % config.filename)
|
||||
for line in res.splitlines():
|
||||
if "finished" in line:
|
||||
task_uuids.append(line.split(" ")[1][:-1])
|
||||
html_report = rally.gen_report_path(extension="html")
|
||||
rally("task export --uuid %s --type html --to %s" % (
|
||||
" ".join(task_uuids), html_report))
|
||||
self.assertTrue(os.path.exists(html_report))
|
||||
|
||||
|
||||
class SLATestCase(unittest.TestCase):
|
||||
|
@ -631,8 +631,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
side_effect=mock.mock_open(), create=True)
|
||||
@mock.patch("rally.cli.commands.task.plot")
|
||||
@mock.patch("rally.cli.commands.task.webbrowser")
|
||||
def test_report_one_uuid(self, mock_webbrowser,
|
||||
mock_plot, mock_open, mock_realpath):
|
||||
def test_old_report_one_uuid(self, mock_webbrowser,
|
||||
mock_plot, mock_open, mock_realpath):
|
||||
task_id = "eb290c30-38d8-4c8f-bbcc-fc8f74b004ae"
|
||||
data = [
|
||||
{"key": {"name": "class.test", "pos": 0},
|
||||
@ -663,8 +663,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
for m in (self.fake_api.task.get_detailed, mock_webbrowser,
|
||||
mock_plot, mock_open):
|
||||
m.reset_mock()
|
||||
self.task.report(self.fake_api, tasks=task_id,
|
||||
out="/tmp/%s.html" % task_id)
|
||||
self.task._old_report(self.fake_api, tasks=task_id,
|
||||
out="/tmp/%s.html" % task_id)
|
||||
mock_open.assert_called_once_with("/tmp/%s.html" % task_id, "w+")
|
||||
mock_plot.plot.assert_called_once_with(results, include_libs=False)
|
||||
|
||||
@ -674,23 +674,24 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
# JUnit
|
||||
reset_mocks()
|
||||
self.task.report(self.fake_api, tasks=task_id,
|
||||
out="/tmp/%s.html" % task_id, out_format="junit")
|
||||
self.task._old_report(self.fake_api, tasks=task_id,
|
||||
out="/tmp/%s.html" % task_id,
|
||||
out_format="junit-xml")
|
||||
mock_open.assert_called_once_with("/tmp/%s.html" % task_id, "w+")
|
||||
self.assertFalse(mock_plot.plot.called)
|
||||
|
||||
# HTML
|
||||
reset_mocks()
|
||||
self.task.report(self.fake_api, task_id, out="output.html",
|
||||
open_it=True, out_format="html")
|
||||
self.task._old_report(self.fake_api, task_id, out="output.html",
|
||||
open_it=True, out_format="html")
|
||||
mock_webbrowser.open_new_tab.assert_called_once_with(
|
||||
"file://realpath_output.html")
|
||||
mock_plot.plot.assert_called_once_with(results, include_libs=False)
|
||||
|
||||
# HTML with embedded JS/CSS
|
||||
reset_mocks()
|
||||
self.task.report(self.fake_api, task_id, open_it=False,
|
||||
out="output.html", out_format="html_static")
|
||||
self.task._old_report(self.fake_api, task_id, open_it=False,
|
||||
out="output.html", out_format="html_static")
|
||||
self.assertFalse(mock_webbrowser.open_new_tab.called)
|
||||
mock_plot.plot.assert_called_once_with(results, include_libs=True)
|
||||
|
||||
@ -700,8 +701,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
side_effect=mock.mock_open(), create=True)
|
||||
@mock.patch("rally.cli.commands.task.plot")
|
||||
@mock.patch("rally.cli.commands.task.webbrowser")
|
||||
def test_report_bunch_uuids(self, mock_webbrowser,
|
||||
mock_plot, mock_open, mock_realpath):
|
||||
def test_old_report_bunch_uuids(self, mock_webbrowser,
|
||||
mock_plot, mock_open, mock_realpath):
|
||||
tasks = ["eb290c30-38d8-4c8f-bbcc-fc8f74b004ae",
|
||||
"eb290c30-38d8-4c8f-bbcc-fc8f74b004af"]
|
||||
data = [
|
||||
@ -737,7 +738,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
for m in (self.fake_api.task.get_detailed, mock_webbrowser,
|
||||
mock_plot, mock_open):
|
||||
m.reset_mock()
|
||||
self.task.report(self.fake_api, tasks=tasks, out="/tmp/1_test.html")
|
||||
self.task._old_report(self.fake_api, tasks=tasks,
|
||||
out="/tmp/1_test.html")
|
||||
mock_open.assert_called_once_with("/tmp/1_test.html", "w+")
|
||||
mock_plot.plot.assert_called_once_with(results, include_libs=False)
|
||||
|
||||
@ -751,8 +753,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
side_effect=lambda p: "realpath_%s" % p)
|
||||
@mock.patch("rally.cli.commands.task.open", create=True)
|
||||
@mock.patch("rally.cli.commands.task.plot")
|
||||
def test_report_one_file(self, mock_plot, mock_open, mock_realpath,
|
||||
mock_path_exists):
|
||||
def test_old_report_one_file(self, mock_plot, mock_open, mock_realpath,
|
||||
mock_path_exists):
|
||||
|
||||
task_file = "/tmp/some_file.json"
|
||||
data = [
|
||||
@ -783,8 +785,8 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
return_value=results
|
||||
)
|
||||
|
||||
self.task.report(self.real_api, tasks=task_file,
|
||||
out="/tmp/1_test.html")
|
||||
self.task._old_report(self.real_api, tasks=task_file,
|
||||
out="/tmp/1_test.html")
|
||||
|
||||
self.task._load_task_results_file.assert_called_once_with(
|
||||
self.real_api, task_file)
|
||||
@ -795,11 +797,35 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
|
||||
@mock.patch("rally.cli.commands.task.os.path.exists", return_value=False)
|
||||
@mock.patch("rally.cli.commands.task.tutils.open", create=True)
|
||||
def test_report_exceptions(self, mock_open, mock_path_exists):
|
||||
ret = self.task.report(self.real_api, tasks="/tmp/task.json",
|
||||
out="/tmp/tmp.hsml")
|
||||
def test_old_report_exceptions(self, mock_open, mock_path_exists):
|
||||
ret = self.task._old_report(self.real_api, tasks="/tmp/task.json",
|
||||
out="/tmp/tmp.hsml")
|
||||
self.assertEqual(ret, 1)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.os.path.exists", return_value=True)
|
||||
def test_report(self, mock_path_exists):
|
||||
self.task._old_report = mock.MagicMock()
|
||||
self.task.export = mock.MagicMock()
|
||||
|
||||
self.task.report(self.fake_api, task_id="file",
|
||||
out="out", open_it=False, out_format="html")
|
||||
|
||||
self.task._old_report.assert_called_once_with(
|
||||
self.fake_api, tasks="file", out="out", open_it=False,
|
||||
out_format="html"
|
||||
)
|
||||
|
||||
self.task._old_report.reset_mock()
|
||||
self.task.export.reset_mock()
|
||||
mock_path_exists.return_value = False
|
||||
|
||||
self.task.report(self.fake_api, task_id="uuid",
|
||||
out="out", open_it=False, out_format="junit-xml")
|
||||
self.task.export.assert_called_once_with(
|
||||
self.fake_api, task_id="uuid", output_type="junit-xml",
|
||||
output_dest="out", open_it=False
|
||||
)
|
||||
|
||||
@mock.patch("rally.cli.commands.task.cliutils.print_list")
|
||||
@mock.patch("rally.cli.commands.task.envutils.get_global",
|
||||
return_value="123456789")
|
||||
@ -957,38 +983,40 @@ class TaskCommandsTestCase(test.TestCase):
|
||||
self.assertRaises(exceptions.TaskNotFound, self.task.use,
|
||||
self.fake_api, task_id)
|
||||
|
||||
@mock.patch("rally.task.exporter.Exporter.get")
|
||||
def test_export(self, mock_exporter_get):
|
||||
mock_client = mock.Mock()
|
||||
mock_exporter_class = mock.Mock(return_value=mock_client)
|
||||
mock_exporter_get.return_value = mock_exporter_class
|
||||
self.task.export(self.fake_api, "fake_uuid", "file:///fake_path.json")
|
||||
mock_exporter_get.assert_called_once_with("file")
|
||||
mock_client.export.assert_called_once_with("fake_uuid")
|
||||
@mock.patch("rally.cli.commands.task.os.path")
|
||||
@mock.patch("rally.cli.commands.task.webbrowser.open_new_tab")
|
||||
@mock.patch("rally.cli.commands.task.open", create=True)
|
||||
@mock.patch("rally.cli.commands.task.print")
|
||||
def test_export(self, mock_print, mock_open, mock_open_new_tab,
|
||||
mock_path):
|
||||
|
||||
@mock.patch("rally.task.exporter.Exporter.get")
|
||||
def test_export_exception(self, mock_exporter_get):
|
||||
mock_client = mock.Mock()
|
||||
mock_exporter_class = mock.Mock(return_value=mock_client)
|
||||
mock_exporter_get.return_value = mock_exporter_class
|
||||
mock_client.export.side_effect = IOError
|
||||
self.task.export(self.fake_api, "fake_uuid", "file:///fake_path.json")
|
||||
mock_exporter_get.assert_called_once_with("file")
|
||||
mock_client.export.assert_called_once_with("fake_uuid")
|
||||
# file
|
||||
self.fake_api.task.export.return_value = {
|
||||
"files": {"output_dest": "content"}, "open": "output_dest"}
|
||||
mock_path.expanduser.return_value = "output_file"
|
||||
mock_path.realpath.return_value = "real_path"
|
||||
mock_fd = mock.mock_open()
|
||||
mock_open.side_effect = mock_fd
|
||||
|
||||
@mock.patch("rally.cli.commands.task.sys.stdout")
|
||||
@mock.patch("rally.task.exporter.Exporter.get")
|
||||
def test_export_InvalidConnectionString(self, mock_exporter_get,
|
||||
mock_stdout):
|
||||
mock_exporter_class = mock.Mock(
|
||||
side_effect=exceptions.InvalidConnectionString)
|
||||
mock_exporter_get.return_value = mock_exporter_class
|
||||
self.task.export(self.fake_api, "fake_uuid", "file:///fake_path.json")
|
||||
mock_stdout.write.assert_has_calls([
|
||||
mock.call("The connection string is not valid: None. "
|
||||
"Please check your connection string."),
|
||||
mock.call("\n")])
|
||||
mock_exporter_get.assert_called_once_with("file")
|
||||
self.task.export(self.fake_api, task_id="uuid",
|
||||
output_type="json", output_dest="output_dest",
|
||||
open_it=True)
|
||||
|
||||
self.fake_api.task.export.assert_called_once_with(
|
||||
tasks_uuids=["uuid"], output_type="json",
|
||||
output_dest="output_dest"
|
||||
)
|
||||
mock_open.assert_called_once_with("output_file", "w+")
|
||||
mock_fd.return_value.write.assert_called_once_with("content")
|
||||
|
||||
# print
|
||||
self.fake_api.task.export.reset_mock()
|
||||
self.fake_api.task.export.return_value = {"print": "content"}
|
||||
self.task.export(self.fake_api, task_id="uuid", output_type="json")
|
||||
self.fake_api.task.export.assert_called_once_with(
|
||||
tasks_uuids=["uuid"], output_type="json", output_dest=None
|
||||
)
|
||||
mock_print.assert_called_once_with("content")
|
||||
|
||||
@mock.patch("rally.cli.commands.task.plot.charts")
|
||||
@mock.patch("rally.cli.commands.task.sys.stdout")
|
||||
|
@ -1,94 +0,0 @@
|
||||
# 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 mock
|
||||
import six
|
||||
import six.moves.builtins as __builtin__
|
||||
|
||||
from rally import exceptions
|
||||
from rally.plugins.common.exporter import file_system
|
||||
from tests.unit import test
|
||||
|
||||
if six.PY3:
|
||||
import io
|
||||
file = io.BytesIO
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class FileExporterTestCase(test.TestCase):
|
||||
|
||||
@mock.patch("rally.plugins.common.exporter.file_system.os.path.exists")
|
||||
@mock.patch.object(__builtin__, "open", autospec=True)
|
||||
@mock.patch("rally.plugins.common.exporter.file_system.json.dumps")
|
||||
@mock.patch("rally.api.API")
|
||||
def test_file_exporter_export(self, mock_api, mock_dumps,
|
||||
mock_open, mock_exists):
|
||||
rapi = mock_api.return_value
|
||||
mock_exists.return_value = True
|
||||
rapi.task.get_detailed.return_value = {"results": [{
|
||||
"key": "fake_key",
|
||||
"data": {
|
||||
"raw": "bar_raw",
|
||||
"sla": "baz_sla",
|
||||
"hooks": "baz_hooks",
|
||||
"load_duration": "foo_load_duration",
|
||||
"full_duration": "foo_full_duration",
|
||||
}}]}
|
||||
mock_dumps.return_value = "fake_results"
|
||||
input_mock = mock.MagicMock(spec=file)
|
||||
mock_open.return_value = input_mock
|
||||
|
||||
exporter = file_system.FileExporter("file-exporter:///fake_path.json")
|
||||
exporter.export("fake_uuid")
|
||||
|
||||
mock_open().__enter__().write.assert_called_once_with("fake_results")
|
||||
rapi.task.get_detailed.assert_called_once_with(task_id="fake_uuid")
|
||||
expected_dict = [
|
||||
{
|
||||
"load_duration": "foo_load_duration",
|
||||
"full_duration": "foo_full_duration",
|
||||
"result": "bar_raw",
|
||||
"key": "fake_key",
|
||||
"hooks": "baz_hooks",
|
||||
"sla": "baz_sla"
|
||||
}
|
||||
]
|
||||
mock_dumps.assert_called_once_with(expected_dict, sort_keys=False,
|
||||
indent=4, separators=(",", ": "))
|
||||
|
||||
@mock.patch("rally.api.API")
|
||||
def test_file_exporter_export_running_task(self, mock_api):
|
||||
mock_api.task.get_detailed.return_value = {"results": []}
|
||||
|
||||
exporter = file_system.FileExporter("file-exporter:///fake_path.json")
|
||||
self.assertRaises(exceptions.RallyException, exporter.export,
|
||||
"fake_uuid")
|
||||
|
||||
@ddt.data(
|
||||
{"connection": "",
|
||||
"raises": exceptions.InvalidConnectionString},
|
||||
{"connection": "file-exporter:///fake_path.json",
|
||||
"raises": None},
|
||||
{"connection": "file-exporter:///fake_path.fake",
|
||||
"raises": exceptions.InvalidConnectionString},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_file_exporter_validate(self, connection, raises):
|
||||
print(connection)
|
||||
if raises:
|
||||
self.assertRaises(raises, file_system.FileExporter, connection)
|
||||
else:
|
||||
file_system.FileExporter(connection)
|
180
tests/unit/plugins/common/exporter/test_reporters.py
Normal file
180
tests/unit/plugins/common/exporter/test_reporters.py
Normal file
@ -0,0 +1,180 @@
|
||||
# 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 os
|
||||
|
||||
import mock
|
||||
|
||||
from rally.plugins.common.exporter import reporters
|
||||
from tests.unit import test
|
||||
|
||||
PATH = "rally.plugins.common.exporter.reporters"
|
||||
|
||||
|
||||
def get_tasks_results():
|
||||
return [{"created_at": "2017-06-04T05:14:44",
|
||||
"updated_at": "2017-06-04T05:15:14",
|
||||
"task_uuid": "2fa4f5ff-7d23-4bb0-9b1f-8ee235f7f1c8",
|
||||
"key": {
|
||||
"kw": {},
|
||||
"pos": 0,
|
||||
"name": "CinderVolumes.list_volumes",
|
||||
"description": "List all volumes."
|
||||
},
|
||||
"data": {
|
||||
"raw": [],
|
||||
"full_duration": 29.969523191452026,
|
||||
"sla": [],
|
||||
"load_duration": 2.03029203414917,
|
||||
"hooks": []
|
||||
},
|
||||
"id": 3}]
|
||||
|
||||
|
||||
class OldJSONResultsMixinTestCase(test.TestCase):
|
||||
|
||||
def test__generate_tasks_results(self):
|
||||
|
||||
class DummyReport(reporters.OldJSONResultsMixin):
|
||||
def __init__(self, raw_tasks_results):
|
||||
self.tasks_results = raw_tasks_results
|
||||
|
||||
reporter = DummyReport(get_tasks_results())
|
||||
results = reporter._generate_tasks_results()
|
||||
self.assertEqual(
|
||||
[
|
||||
{
|
||||
"hooks": [],
|
||||
"created_at": "2017-06-04T05:14:44",
|
||||
"load_duration": 2.03029203414917,
|
||||
"result": [],
|
||||
"key": {
|
||||
"kw": {},
|
||||
"pos": 0,
|
||||
"name": "CinderVolumes.list_volumes",
|
||||
"description": "List all volumes."
|
||||
},
|
||||
"full_duration": 29.969523191452026,
|
||||
"sla": []
|
||||
}
|
||||
],
|
||||
results
|
||||
)
|
||||
|
||||
|
||||
class HTMLExporterTestCase(test.TestCase):
|
||||
|
||||
def test_validate(self):
|
||||
# nothing should fail
|
||||
reporters.HTMLExporter.validate(mock.Mock())
|
||||
reporters.HTMLExporter.validate("")
|
||||
reporters.HTMLExporter.validate(None)
|
||||
|
||||
def test__generate(self):
|
||||
tasks_results = get_tasks_results()
|
||||
tasks_results.extend(get_tasks_results())
|
||||
reporter = reporters.HTMLExporter(tasks_results, None)
|
||||
results = reporter._generate()
|
||||
self.assertEqual(
|
||||
[
|
||||
{
|
||||
"hooks": [],
|
||||
"created_at": "2017-06-04T05:14:44",
|
||||
"load_duration": 2.03029203414917,
|
||||
"result": [],
|
||||
"key": {
|
||||
"kw": {},
|
||||
"pos": 0,
|
||||
"name": "CinderVolumes.list_volumes",
|
||||
"description": "List all volumes."
|
||||
},
|
||||
"full_duration": 29.969523191452026,
|
||||
"sla": []
|
||||
},
|
||||
{
|
||||
"hooks": [],
|
||||
"created_at": "2017-06-04T05:14:44",
|
||||
"load_duration": 2.03029203414917,
|
||||
"result": [],
|
||||
"key": {
|
||||
"kw": {},
|
||||
"pos": 1,
|
||||
"name": "CinderVolumes.list_volumes",
|
||||
"description": "List all volumes."
|
||||
},
|
||||
"full_duration": 29.969523191452026,
|
||||
"sla": []
|
||||
}], results)
|
||||
|
||||
@mock.patch("%s.HTMLExporter._generate" % PATH,
|
||||
return_value="task_results")
|
||||
@mock.patch("%s.plot.plot" % PATH, return_value="html")
|
||||
def test_generate(self, mock_plot, mock__generate):
|
||||
reporter = reporters.HTMLExporter([], output_destination=None)
|
||||
self.assertEqual({"print": "html"}, reporter.generate())
|
||||
mock__generate.assert_called_once_with()
|
||||
mock_plot.assert_called_once_with("task_results",
|
||||
include_libs=False)
|
||||
|
||||
mock__generate.reset_mock()
|
||||
mock_plot.reset_mock()
|
||||
reporter = reporters.HTMLExporter([], output_destination="path")
|
||||
reporter.INCLUDE_LIBS = True
|
||||
self.assertEqual({"files": {"path": "html"},
|
||||
"open": "file://" + os.path.abspath("path")},
|
||||
reporter.generate())
|
||||
mock__generate.assert_called_once_with()
|
||||
mock_plot.assert_called_once_with("task_results",
|
||||
include_libs=True)
|
||||
|
||||
|
||||
class JUnitXMLExporterTestCase(test.TestCase):
|
||||
def test_generate(self):
|
||||
content = ("<testsuite errors=\"0\""
|
||||
" failures=\"0\""
|
||||
" name=\"Rally test suite\""
|
||||
" tests=\"1\""
|
||||
" time=\"29.97\">"
|
||||
"<testcase classname=\"CinderVolumes\""
|
||||
" name=\"list_volumes\""
|
||||
" time=\"29.97\" />"
|
||||
"</testsuite>")
|
||||
|
||||
reporter = reporters.JUnitXMLExporter(get_tasks_results(),
|
||||
output_destination=None)
|
||||
self.assertEqual({"print": content}, reporter.generate())
|
||||
|
||||
reporter = reporters.JUnitXMLExporter(get_tasks_results(),
|
||||
output_destination="path")
|
||||
self.assertEqual({"files": {"path": content},
|
||||
"open": "file://" + os.path.abspath("path")},
|
||||
reporter.generate())
|
||||
|
||||
def test_generate_fail(self):
|
||||
tasks_results = get_tasks_results()
|
||||
tasks_results[0]["data"]["sla"] = [{"success": False,
|
||||
"detail": "error"}]
|
||||
content = ("<testsuite errors=\"0\""
|
||||
" failures=\"1\""
|
||||
" name=\"Rally test suite\""
|
||||
" tests=\"1\""
|
||||
" time=\"29.97\">"
|
||||
"<testcase classname=\"CinderVolumes\""
|
||||
" name=\"list_volumes\""
|
||||
" time=\"29.97\">"
|
||||
"<failure message=\"error\" /></testcase>"
|
||||
"</testsuite>")
|
||||
reporter = reporters.JUnitXMLExporter(tasks_results,
|
||||
output_destination=None)
|
||||
self.assertEqual({"print": content}, reporter.generate())
|
@ -12,6 +12,9 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import jsonschema
|
||||
import mock
|
||||
|
||||
from rally.task import exporter
|
||||
from tests.unit import test
|
||||
|
||||
@ -33,3 +36,59 @@ class ExporterTestCase(test.TestCase):
|
||||
|
||||
def test_task_export_instantiate(self):
|
||||
TestExporter("fake_connection")
|
||||
|
||||
|
||||
class TaskExporterTestCase(test.TestCase):
|
||||
|
||||
def test_make(self):
|
||||
reporter_cls = mock.Mock()
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {}
|
||||
exporter.TaskExporter.make(reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"files": {}}
|
||||
exporter.TaskExporter.make(reporter_cls, None, None, None)
|
||||
reporter_cls.return_value.generate.return_value = {
|
||||
"files": {"/path/foo": "content"}}
|
||||
exporter.TaskExporter.make(reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"open": "/path/foo"}
|
||||
exporter.TaskExporter.make(reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"print": "foo"}
|
||||
exporter.TaskExporter.make(reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {
|
||||
"files": {"/path/foo": "content"}, "open": "/path/foo",
|
||||
"print": "foo"}
|
||||
exporter.TaskExporter.make(reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"files": []}
|
||||
self.assertRaises(jsonschema.ValidationError,
|
||||
exporter.TaskExporter.make,
|
||||
reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"files": ""}
|
||||
self.assertRaises(jsonschema.ValidationError,
|
||||
exporter.TaskExporter.make,
|
||||
reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"files": {"a": {}}}
|
||||
self.assertRaises(jsonschema.ValidationError,
|
||||
exporter.TaskExporter.make,
|
||||
reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"open": []}
|
||||
self.assertRaises(jsonschema.ValidationError,
|
||||
exporter.TaskExporter.make,
|
||||
reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"print": []}
|
||||
self.assertRaises(jsonschema.ValidationError,
|
||||
exporter.TaskExporter.make,
|
||||
reporter_cls, None, None, None)
|
||||
|
||||
reporter_cls.return_value.generate.return_value = {"additional": ""}
|
||||
self.assertRaises(jsonschema.ValidationError,
|
||||
exporter.TaskExporter.make,
|
||||
reporter_cls, None, None, None)
|
||||
|
@ -419,6 +419,31 @@ class TaskAPITestCase(test.TestCase):
|
||||
self.task_uuid,
|
||||
status=expected_status)
|
||||
|
||||
@mock.patch("rally.api.texporter.TaskExporter")
|
||||
@mock.patch("rally.api.objects.Task.get_detailed",
|
||||
return_value={"results": ["detail"]})
|
||||
def test_export(self, mock_task_get_detailed, mock_task_exporter):
|
||||
task_id = ["uuid-1", "uuid-2"]
|
||||
output_type = mock.Mock()
|
||||
output_dest = mock.Mock()
|
||||
|
||||
reporter = mock_task_exporter.get.return_value
|
||||
|
||||
self.assertEqual(mock_task_exporter.make.return_value,
|
||||
self.task_inst.export(
|
||||
tasks_uuids=task_id,
|
||||
output_type=output_type,
|
||||
output_dest=output_dest))
|
||||
mock_task_exporter.get.assert_called_once_with(output_type)
|
||||
|
||||
reporter.validate.assert_called_once_with(output_dest)
|
||||
|
||||
mock_task_exporter.make.assert_called_once_with(
|
||||
reporter, ["detail", "detail"], output_dest,
|
||||
api=self.task_inst.api)
|
||||
self.assertEqual([mock.call(u) for u in task_id],
|
||||
mock_task_get_detailed.call_args_list)
|
||||
|
||||
@mock.patch("rally.api.objects.Task")
|
||||
def test_get_detailed(self, mock_task):
|
||||
mock_task.get_detailed.return_value = "detailed_task_data"
|
||||
|
Loading…
Reference in New Issue
Block a user