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:
parent
e22c247e1e
commit
f1f09f3ab5
@ -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
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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")
|
||||||
|
40
rally/plugins/common/exporters/trends.py
Normal file
40
rally/plugins/common/exporters/trends.py
Normal 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
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
96
tests/unit/plugins/common/exporters/test_trends.py
Normal file
96
tests/unit/plugins/common/exporters/test_trends.py
Normal 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())
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user