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

View File

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

View File

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

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

View File

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

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