From 9d26483c12c039f31ff3b6eb34d4e24e75680b75 Mon Sep 17 00:00:00 2001 From: chenhb Date: Thu, 27 Apr 2017 11:17:09 +0800 Subject: [PATCH] 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 --out 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 --type --to Example: rally task export --uuid xxxx --type junit-xml --to xxxxx 4.Remove FileExporter plugin. Change-Id: I44cafccb8d6c6c3cc704fb6e3ff2f49a756209ef --- etc/rally.bash_completion | 6 +- rally/api.py | 28 +++ rally/cli/commands/task.py | 128 ++++++----- rally/plugins/common/exporter/file_system.py | 108 ---------- rally/plugins/common/exporter/reporters.py | 202 ++++++++++++++++++ rally/task/exporter.py | 85 +++++++- rally/ui/templates/ci/index.html | 1 + tests/ci/rally_gate_functions.sh | 4 +- tests/functional/test_cli_task.py | 120 ++++++----- tests/unit/cli/commands/test_task.py | 128 ++++++----- .../common/exporter/test_file_system.py | 94 -------- .../plugins/common/exporter/test_reporters.py | 180 ++++++++++++++++ tests/unit/task/test_exporter.py | 59 +++++ tests/unit/test_api.py | 25 +++ 14 files changed, 799 insertions(+), 369 deletions(-) delete mode 100644 rally/plugins/common/exporter/file_system.py create mode 100644 rally/plugins/common/exporter/reporters.py delete mode 100644 tests/unit/plugins/common/exporter/test_file_system.py create mode 100644 tests/unit/plugins/common/exporter/test_reporters.py diff --git a/etc/rally.bash_completion b/etc/rally.bash_completion index 76a0e50230..fb62dd6985 100644 --- a/etc/rally.bash_completion +++ b/etc/rally.bash_completion @@ -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 \ No newline at end of file +complete -o filenames -F _rally rally diff --git a/rally/api.py b/rally/api.py index b360ee21c5..00631feb23 100644 --- a/rally/api.py +++ b/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): diff --git a/rally/cli/commands/task.py b/rally/cli/commands/task.py index 618c74bf49..fbce213708 100644 --- a/rally/cli/commands/task.py +++ b/rally/cli/commands/task.py @@ -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="", 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="", 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://" - ":@:/.") - 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): diff --git a/rally/plugins/common/exporter/file_system.py b/rally/plugins/common/exporter/file_system.py deleted file mode 100644 index 4e40d4e054..0000000000 --- a/rally/plugins/common/exporter/file_system.py +++ /dev/null @@ -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:///. - """ - - 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:///.`.") - 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.") diff --git a/rally/plugins/common/exporter/reporters.py b/rally/plugins/common/exporter/reporters.py new file mode 100644 index 0000000000..eaa1a7e6fd --- /dev/null +++ b/rally/plugins/common/exporter/reporters.py @@ -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 + + + + + """ + + 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} diff --git a/rally/task/exporter.py b/rally/task/exporter.py index d1cad56d6d..db60b3f943 100755 --- a/rally/task/exporter.py +++ b/rally/task/exporter.py @@ -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 diff --git a/rally/ui/templates/ci/index.html b/rally/ui/templates/ci/index.html index 5060639060..d52f65924f 100644 --- a/rally/ui/templates/ci/index.html +++ b/rally/ui/templates/ci/index.html @@ -57,6 +57,7 @@
  • Text report detailed $ rally task detailed --iterations-data
  • Success criteria (SLA) $ rally task sla_check
  • Raw results (JSON) $ rally task results +
  • JUNIT-XML report $ rally task export --type junit-xml

    About Rally

    diff --git a/tests/ci/rally_gate_functions.sh b/tests/ci/rally_gate_functions.sh index e2dc6c9823..6a050e4970 100644 --- a/tests/ci/rally_gate_functions.sh +++ b/tests/ci/rally_gate_functions.sh @@ -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 diff --git a/tests/functional/test_cli_task.py b/tests/functional/test_cli_task.py index 5d261637d2..736e2b0c8a 100644 --- a/tests/functional/test_cli_task.py +++ b/tests/functional/test_cli_task.py @@ -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[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[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[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): diff --git a/tests/unit/cli/commands/test_task.py b/tests/unit/cli/commands/test_task.py index 6db3c4cd00..33ceea1d4a 100644 --- a/tests/unit/cli/commands/test_task.py +++ b/tests/unit/cli/commands/test_task.py @@ -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") diff --git a/tests/unit/plugins/common/exporter/test_file_system.py b/tests/unit/plugins/common/exporter/test_file_system.py deleted file mode 100644 index 812542ee3a..0000000000 --- a/tests/unit/plugins/common/exporter/test_file_system.py +++ /dev/null @@ -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) diff --git a/tests/unit/plugins/common/exporter/test_reporters.py b/tests/unit/plugins/common/exporter/test_reporters.py new file mode 100644 index 0000000000..aad81c263c --- /dev/null +++ b/tests/unit/plugins/common/exporter/test_reporters.py @@ -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 = ("" + "" + "") + + 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 = ("" + "" + "" + "") + reporter = reporters.JUnitXMLExporter(tasks_results, + output_destination=None) + self.assertEqual({"print": content}, reporter.generate()) diff --git a/tests/unit/task/test_exporter.py b/tests/unit/task/test_exporter.py index 9364d6af68..0446a495d7 100644 --- a/tests/unit/task/test_exporter.py +++ b/tests/unit/task/test_exporter.py @@ -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) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index a933d19dff..305fb712e1 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -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"