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:
chenhb 2017-04-27 11:17:09 +08:00
parent 801e624c9c
commit 9d26483c12
14 changed files with 799 additions and 369 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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