Export trends report with task export plugin

Export trends report with plugin and support report with static libraries.

Change-Id: I05bff52c30b50387b7ad880709e3ce09f6f6ae9d
This commit is contained in:
chenhb 2018-12-04 14:56:25 +08:00
parent e22c247e1e
commit f1f09f3ab5
8 changed files with 166 additions and 104 deletions

View File

@ -17,6 +17,15 @@ Changelog
.. Release notes for existing releases are MUTABLE! If there is something that .. Release notes for existing releases are MUTABLE! If there is something that
was missed or can be improved, feel free to change it! 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 [1.3.0] - 2018-12-01
-------------------- --------------------

View File

@ -55,7 +55,7 @@ _rally()
OPTS["task_sla_check"]="--uuid --json" 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_start"]="--deployment --task --task-args --task-args-file --tag --no-use --abort-on-sla-failure"
OPTS["task_status"]="--uuid" OPTS["task_status"]="--uuid"
OPTS["task_trends"]="--out --open --tasks" OPTS["task_trends"]="--out --open --tasks --html-static"
OPTS["task_use"]="--uuid" OPTS["task_use"]="--uuid"
OPTS["task_validate"]="--deployment --task --task-args --task-args-file" OPTS["task_validate"]="--deployment --task --task-args --task-args-file"
OPTS["verify_add-verifier-ext"]="--id --source --version --extra-settings" OPTS["verify_add-verifier-ext"]="--id --source --version --extra-settings"

View File

@ -39,7 +39,6 @@ from rally import exceptions
from rally import plugins from rally import plugins
from rally.task import atomic from rally.task import atomic
from rally.task.processing import charts from rally.task.processing import charts
from rally.task.processing import plot
from rally.task import utils as tutils from rally.task import utils as tutils
from rally.utils import strutils from rally.utils import strutils
@ -821,40 +820,21 @@ class TaskCommands(object):
help="Open the output in a browser.") help="Open the output in a browser.")
@cliutils.args("--tasks", dest="tasks", nargs="+", @cliutils.args("--tasks", dest="tasks", nargs="+",
help="UUIDs of tasks, or JSON files with task results") 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 @cliutils.suppress_warnings
def trends(self, api, *args, **kwargs): def trends(self, api, *args, **kwargs):
"""Generate workloads trends HTML report.""" """Generate workloads trends HTML report."""
tasks = kwargs.get("tasks", []) or list(args) tasks = kwargs.get("tasks", []) or list(args)
if not tasks: if not tasks:
print("ERROR: At least one task must be specified", print("ERROR: At least one task must be specified",
file=sys.stderr) file=sys.stderr)
return 1 return 1
results = [] self.export(api, tasks=tasks,
for task_id in tasks: output_type=kwargs.get("out_format", "trends-html"),
if os.path.exists(os.path.expanduser(task_id)): output_dest=kwargs.get("out"),
results.extend(self._load_task_results_file(api, task_id)) open_it=kwargs.get("open_it", False))
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)
@cliutils.deprecated_args("--tasks", dest="tasks", nargs="+", @cliutils.deprecated_args("--tasks", dest="tasks", nargs="+",
release="0.10.0", alternative="--uuid") release="0.10.0", alternative="--uuid")

View File

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

View File

@ -243,7 +243,7 @@ def plot(tasks_results, include_libs=False):
include_libs=include_libs) include_libs=include_libs)
def trends(tasks): def trends(tasks, include_libs=False):
trends = Trends() trends = Trends()
for task in tasks: for task in tasks:
for workload in itertools.chain( for workload in itertools.chain(
@ -251,7 +251,8 @@ def trends(tasks):
trends.add_result(task["uuid"], workload) trends.add_result(task["uuid"], workload)
template = ui_utils.get_template("task/trends.html") template = ui_utils.get_template("task/trends.html")
return template.render(version=version.version_string(), 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): class Trends(object):

View File

@ -649,77 +649,14 @@ class TaskCommandsTestCase(test.TestCase):
consts.TaskStatus.ABORTED))) consts.TaskStatus.ABORTED)))
mock_stdout.write.assert_has_calls([mock.call(expected_out)]) mock_stdout.write.assert_has_calls([mock.call(expected_out)])
@mock.patch("rally.cli.commands.task.os.path") def test_trends(self):
@mock.patch("rally.cli.commands.task.open", create=True) self.task.export = mock.MagicMock()
@mock.patch("rally.cli.commands.task.plot") self.task.trends(self.fake_api,
@mock.patch("rally.cli.commands.task.webbrowser") tasks=["uuid"],
def test_trends(self, mock_webbrowser, mock_plot, out="output.html")
mock_open, mock_os_path): self.task.export.assert_called_once_with(
mock_os_path.exists = lambda p: p.startswith("path_to_") self.fake_api, tasks=["uuid"], output_type="trends-html",
mock_os_path.expanduser = lambda p: p + "_expanded" output_dest="output.html", open_it=False)
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_no_tasks_given(self): def test_trends_no_tasks_given(self):
ret = self.task.trends(self.fake_api, tasks=[], ret = self.task.trends(self.fake_api, tasks=[],
@ -978,7 +915,6 @@ class TaskCommandsTestCase(test.TestCase):
) )
mock_print.assert_called_once_with("content") mock_print.assert_called_once_with("content")
@mock.patch("rally.cli.commands.task.plot.charts")
@mock.patch("rally.cli.commands.task.sys.stdout") @mock.patch("rally.cli.commands.task.sys.stdout")
@ddt.data({"error_type": "test_no_trace_type", @ddt.data({"error_type": "test_no_trace_type",
"error_message": "no_trace_error_message", "error_message": "no_trace_error_message",
@ -990,9 +926,8 @@ class TaskCommandsTestCase(test.TestCase):
}) })
@ddt.unpack @ddt.unpack
def test_show_task_errors_no_trace(self, mock_stdout, def test_show_task_errors_no_trace(self, mock_stdout,
mock_charts, error_type, error_message, error_type, error_message,
error_traceback=None): error_traceback=None):
mock_charts.MainStatsTable.columns = ["Column 1", "Column 2"]
test_uuid = "test_task_id" test_uuid = "test_task_id"
error_data = [error_type, error_message] error_data = [error_type, error_message]
if error_traceback: if error_traceback:

View File

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

View File

@ -389,7 +389,8 @@ class PlotTestCase(test.TestCase):
trends.add_result.mock_calls) trends.add_result.mock_calls)
mock_get_template.assert_called_once_with("task/trends.html") mock_get_template.assert_called_once_with("task/trends.html")
template.render.assert_called_once_with(version="42.0", template.render.assert_called_once_with(version="42.0",
data="[\"foo\", \"bar\"]") data="[\"foo\", \"bar\"]",
include_libs=False)
@ddt.ddt @ddt.ddt