diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0caf8af8df..84fa97d96a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,15 @@ Changelog .. Release notes for existing releases are MUTABLE! If there is something that was missed or can be improved, feel free to change it! +unreleased +-------------------- + +Changed +~~~~~~~ + +* Add the --html-static option to commands ``rally task trends``, it could generate + trends report with embedded js/css. + [1.3.0] - 2018-12-01 -------------------- diff --git a/etc/rally.bash_completion b/etc/rally.bash_completion index 8c183cea17..f7705c1c36 100644 --- a/etc/rally.bash_completion +++ b/etc/rally.bash_completion @@ -55,7 +55,7 @@ _rally() OPTS["task_sla_check"]="--uuid --json" OPTS["task_start"]="--deployment --task --task-args --task-args-file --tag --no-use --abort-on-sla-failure" OPTS["task_status"]="--uuid" - OPTS["task_trends"]="--out --open --tasks" + OPTS["task_trends"]="--out --open --tasks --html-static" OPTS["task_use"]="--uuid" OPTS["task_validate"]="--deployment --task --task-args --task-args-file" OPTS["verify_add-verifier-ext"]="--id --source --version --extra-settings" diff --git a/rally/cli/commands/task.py b/rally/cli/commands/task.py index c15499edae..1cfa990206 100644 --- a/rally/cli/commands/task.py +++ b/rally/cli/commands/task.py @@ -39,7 +39,6 @@ from rally import exceptions from rally import plugins from rally.task import atomic from rally.task.processing import charts -from rally.task.processing import plot from rally.task import utils as tutils from rally.utils import strutils @@ -821,40 +820,21 @@ class TaskCommands(object): help="Open the output in a browser.") @cliutils.args("--tasks", dest="tasks", nargs="+", help="UUIDs of tasks, or JSON files with task results") + @cliutils.args("--html-static", dest="out_format", + action="store_const", const="trends-html-static") @cliutils.suppress_warnings def trends(self, api, *args, **kwargs): """Generate workloads trends HTML report.""" - tasks = kwargs.get("tasks", []) or list(args) - if not tasks: print("ERROR: At least one task must be specified", file=sys.stderr) return 1 - results = [] - for task_id in tasks: - if os.path.exists(os.path.expanduser(task_id)): - results.extend(self._load_task_results_file(api, task_id)) - elif strutils.is_uuid_like(task_id): - results.append(api.task.get(task_id=task_id, detailed=True)) - else: - print("ERROR: Invalid UUID or file name passed: %s" % task_id, - file=sys.stderr) - return 1 - - result = plot.trends(results) - - out = kwargs.get("out") - if out: - output_file = os.path.expanduser(out) - - with open(output_file, "w+") as f: - f.write(result) - if kwargs.get("open_it"): - webbrowser.open_new_tab("file://" + os.path.realpath(out)) - else: - print(result) + self.export(api, tasks=tasks, + output_type=kwargs.get("out_format", "trends-html"), + output_dest=kwargs.get("out"), + open_it=kwargs.get("open_it", False)) @cliutils.deprecated_args("--tasks", dest="tasks", nargs="+", release="0.10.0", alternative="--uuid") diff --git a/rally/plugins/common/exporters/trends.py b/rally/plugins/common/exporters/trends.py new file mode 100644 index 0000000000..be19c54c8c --- /dev/null +++ b/rally/plugins/common/exporters/trends.py @@ -0,0 +1,40 @@ +# Copyright 2018: ZTE 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 os + +from rally.task import exporter +from rally.task.processing import plot + + +@exporter.configure("trends-html") +class TrendsExporter(exporter.TaskExporter): + """Generates task trends report in HTML format.""" + INCLUDE_LIBS = False + + def generate(self): + report = plot.trends(self.tasks_results, 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("trends-html-static") +class TrendsStaticExport(TrendsExporter): + """Generates task trends report in HTML format with embedded JS/CSS.""" + INCLUDE_LIBS = True diff --git a/rally/task/processing/plot.py b/rally/task/processing/plot.py index d8c0188172..0bf083ed86 100644 --- a/rally/task/processing/plot.py +++ b/rally/task/processing/plot.py @@ -243,7 +243,7 @@ def plot(tasks_results, include_libs=False): include_libs=include_libs) -def trends(tasks): +def trends(tasks, include_libs=False): trends = Trends() for task in tasks: for workload in itertools.chain( @@ -251,7 +251,8 @@ def trends(tasks): trends.add_result(task["uuid"], workload) template = ui_utils.get_template("task/trends.html") return template.render(version=version.version_string(), - data=json.dumps(trends.get_data())) + data=json.dumps(trends.get_data()), + include_libs=include_libs) class Trends(object): diff --git a/tests/unit/cli/commands/test_task.py b/tests/unit/cli/commands/test_task.py index 296d2fbaa9..a20099816e 100644 --- a/tests/unit/cli/commands/test_task.py +++ b/tests/unit/cli/commands/test_task.py @@ -649,77 +649,14 @@ class TaskCommandsTestCase(test.TestCase): consts.TaskStatus.ABORTED))) mock_stdout.write.assert_has_calls([mock.call(expected_out)]) - @mock.patch("rally.cli.commands.task.os.path") - @mock.patch("rally.cli.commands.task.open", create=True) - @mock.patch("rally.cli.commands.task.plot") - @mock.patch("rally.cli.commands.task.webbrowser") - def test_trends(self, mock_webbrowser, mock_plot, - mock_open, mock_os_path): - mock_os_path.exists = lambda p: p.startswith("path_to_") - mock_os_path.expanduser = lambda p: p + "_expanded" - mock_os_path.realpath.side_effect = lambda p: "realpath_" + p - self.task._load_task_results_file = mock.MagicMock( - return_value=["result_1_from_file", "result_2_from_file"] - ) - mock_fd = mock.mock_open() - mock_open.side_effect = mock_fd - - task_obj = self._make_task() - self.fake_api.task.get.return_value = task_obj - mock_plot.trends.return_value = "rendered_trends_report" - - ret = self.task.trends(self.fake_api, - tasks=["ab123456-38d8-4c8f-bbcc-fc8f74b004ae", - "cd654321-38d8-4c8f-bbcc-fc8f74b004ae", - "path_to_file"], - out="output.html", out_format="html") - expected = [task_obj, task_obj, - "result_1_from_file", "result_2_from_file"] - mock_plot.trends.assert_called_once_with(expected) - self.assertEqual([mock.call(self.fake_api, "path_to_file")], - self.task._load_task_results_file.mock_calls) - self.assertEqual([mock.call("output.html_expanded", "w+")], - mock_open.mock_calls) - self.assertIsNone(ret) - self.assertFalse(mock_webbrowser.open_new_tab.called) - mock_fd.return_value.write.assert_called_once_with( - "rendered_trends_report") - - @mock.patch("rally.cli.commands.task.os.path") - @mock.patch("rally.cli.commands.task.open", create=True) - @mock.patch("rally.cli.commands.task.plot") - @mock.patch("rally.cli.commands.task.webbrowser") - def test_trends_single_file_and_open_webbrowser( - self, mock_webbrowser, mock_plot, mock_open, mock_os_path): - mock_os_path.exists.return_value = True - mock_os_path.expanduser = lambda path: path - mock_os_path.realpath.side_effect = lambda p: "realpath_" + p - self.task._load_task_results_file = mock.MagicMock( - return_value=["result"] - ) - ret = self.task.trends(self.real_api, - tasks=["path_to_file"], open_it=True, - out="output.html", out_format="html") - self.assertIsNone(ret) - mock_webbrowser.open_new_tab.assert_called_once_with( - "file://realpath_output.html") - - @mock.patch("rally.cli.commands.task.os.path") - @mock.patch("rally.cli.commands.task.open", create=True) - @mock.patch("rally.cli.commands.task.plot") - def test_trends_task_id_is_not_uuid_like(self, mock_plot, - mock_open, mock_os_path): - mock_os_path.exists.return_value = False - - ret = self.task.trends(self.fake_api, - tasks=["ab123456-38d8-4c8f-bbcc-fc8f74b004ae"], - out="output.html", out_format="html") - self.assertIsNone(ret) - - ret = self.task.trends(self.fake_api, - tasks=["this-is-not-uuid"], - out="output.html", out_format="html") - self.assertEqual(1, ret) + def test_trends(self): + self.task.export = mock.MagicMock() + self.task.trends(self.fake_api, + tasks=["uuid"], + out="output.html") + self.task.export.assert_called_once_with( + self.fake_api, tasks=["uuid"], output_type="trends-html", + output_dest="output.html", open_it=False) def test_trends_no_tasks_given(self): ret = self.task.trends(self.fake_api, tasks=[], @@ -978,7 +915,6 @@ class TaskCommandsTestCase(test.TestCase): ) mock_print.assert_called_once_with("content") - @mock.patch("rally.cli.commands.task.plot.charts") @mock.patch("rally.cli.commands.task.sys.stdout") @ddt.data({"error_type": "test_no_trace_type", "error_message": "no_trace_error_message", @@ -990,9 +926,8 @@ class TaskCommandsTestCase(test.TestCase): }) @ddt.unpack def test_show_task_errors_no_trace(self, mock_stdout, - mock_charts, error_type, error_message, + error_type, error_message, error_traceback=None): - mock_charts.MainStatsTable.columns = ["Column 1", "Column 2"] test_uuid = "test_task_id" error_data = [error_type, error_message] if error_traceback: diff --git a/tests/unit/plugins/common/exporters/test_trends.py b/tests/unit/plugins/common/exporters/test_trends.py new file mode 100644 index 0000000000..cd7c65e996 --- /dev/null +++ b/tests/unit/plugins/common/exporters/test_trends.py @@ -0,0 +1,96 @@ +# 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.exporters import trends +from tests.unit import test + +PATH = "rally.plugins.common.exporters.html" + + +def get_tasks_results(): + task_id = "2fa4f5ff-7d23-4bb0-9b1f-8ee235f7f1c8" + workload = {"uuid": "uuid", + "name": "CinderVolumes.list_volumes", + "description": "List all volumes.", + "created_at": "2017-06-04T05:14:44", + "updated_at": "2017-06-04T05:15:14", + "task_uuid": task_id, + "position": 0, + "data": {"raw": []}, + "full_duration": 29.969523191452026, + "load_duration": 2.03029203414917, + "hooks": [], + "runner": {}, + "runner_type": "runner_type", + "args": {}, + "contexts": {}, + "contexts_results": [], + "min_duration": 0.0, + "max_duration": 1.0, + "start_time": 0, + "statistics": {}, + "failed_iteration_count": 0, + "total_iteration_count": 10, + "sla": {}, + "sla_results": {"sla": []}, + "pass_sla": True + } + task = { + "uuid": task_id, + "title": "task", + "description": "description", + "status": "finished", + "env_uuid": "env-uuid", + "env_name": "env-name", + "tags": [], + "created_at": "2017-06-04T05:14:44", + "updated_at": "2017-06-04T05:15:14", + "pass_sla": True, + "task_duration": 2.0, + "subtasks": [ + {"uuid": "subtask_uuid", + "title": "subtask", + "description": "description", + "status": "finished", + "run_in_parallel": True, + "created_at": "2017-06-04T05:14:44", + "updated_at": "2017-06-04T05:15:14", + "sla": {}, + "duration": 29.969523191452026, + "task_uuid": task_id, + "workloads": [workload]} + ]} + return [task] + + +class TrendsExporterTestCase(test.TestCase): + + @mock.patch("%s.plot.trends" % PATH, return_value="html") + def test_generate(self, mock_plot_trends): + reporter = trends.TrendsExporter("task_results", None) + + self.assertEqual({"print": "html"}, reporter.generate()) + + mock_plot_trends.assert_called_once_with( + "task_results", False) + + reporter = trends.TrendsExporter("task_results", + output_destination="path") + self.assertEqual({"files": {"path": "html"}, + "open": "file://" + os.path.abspath("path")}, + reporter.generate()) diff --git a/tests/unit/task/processing/test_plot.py b/tests/unit/task/processing/test_plot.py index 5e6b813922..280123488e 100644 --- a/tests/unit/task/processing/test_plot.py +++ b/tests/unit/task/processing/test_plot.py @@ -389,7 +389,8 @@ class PlotTestCase(test.TestCase): trends.add_result.mock_calls) mock_get_template.assert_called_once_with("task/trends.html") template.render.assert_called_once_with(version="42.0", - data="[\"foo\", \"bar\"]") + data="[\"foo\", \"bar\"]", + include_libs=False) @ddt.ddt