Carlos L. Torres fbe5836d86 [report] Improve reports data and units
- Adds new median metric both on CLI and HTML reports.
- Adds supports for table header that consolidates
  units from each metric, and gives a title.
- Re-organizes metrics based on order statistics
  (min, percentiles, max) then its followed by
  aggregate statistics metrics.
- Includes unit tests.

Change-Id: Icd154c9f00b5e9d6df11797685fe8f92c4fdbb1b
2015-04-27 10:13:44 -05:00

380 lines
14 KiB
Python

# Copyright 2014: 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 sys
import mock
import testtools
from rally.benchmark.processing import plot
from tests.unit import test
PLOT = "rally.benchmark.processing.plot."
class PlotTestCase(test.TestCase):
@mock.patch(PLOT + "ui_utils")
@mock.patch(PLOT + "_process_results")
def test_plot(self, mock_proc_results, mock_utils):
mock_render = mock.Mock(return_value="plot_html")
mock_utils.get_template = mock.Mock(
return_value=mock.Mock(render=mock_render))
task_data = [{"name": "a"}, {"name": "b"}]
task_source = "JSON"
mock_proc_results.return_value = (task_source, task_data)
result = plot.plot(["abc"])
self.assertEqual(result, "plot_html")
mock_render.assert_called_once_with(
data=json.dumps(task_data),
source=json.dumps(task_source)
)
mock_utils.get_template.assert_called_once_with("task/report.mako")
@mock.patch(PLOT + "json.dumps")
@mock.patch(PLOT + "_prepare_data")
@mock.patch(PLOT + "_process_atomic")
@mock.patch(PLOT + "_get_atomic_action_durations")
@mock.patch(PLOT + "_process_main_duration")
def test__process_results(self, mock_main_duration, mock_get_atomic,
mock_atomic, mock_prepare, mock_dumps):
sla = [{"success": True}]
result = ["iter_1", "iter_2"]
iterations = len(result)
kw = {"runner": {"type": "foo_runner"}}
result_ = lambda i: {
"key": {"pos": i,
"name": "Class.method",
"kw": kw},
"result": result,
"sla": sla}
results = [result_(i) for i in (0, 1, 2)]
table_cols = ["Action",
"Min (sec)",
"Median (sec)",
"90%ile (sec)",
"95%ile (sec)",
"Max (sec)",
"Avg (sec)",
"Success",
"Count"]
atomic_durations = [["atomic_1"], ["atomic_2"]]
mock_prepare.side_effect = lambda i: {"errors": "errors_list",
"output": [],
"output_errors": [],
"sla": i["sla"],
"load_duration": 1234.5,
"full_duration": 6789.1}
mock_main_duration.return_value = "main_duration"
mock_get_atomic.return_value = atomic_durations
mock_atomic.return_value = "main_atomic"
mock_dumps.return_value = "JSON"
source, scenarios = plot._process_results(results)
source_dict = {"Class.method": [kw] * len(results)}
mock_dumps.assert_called_with(source_dict, indent=2,
sort_keys=True)
self.assertEqual(source, "JSON")
results = sorted(results, key=lambda r: "%s%s" % (r["key"]["name"],
r["key"]["pos"]))
for i, r in enumerate(results):
config = json.dumps({r["key"]["name"]: [r["key"]["kw"]]}, indent=2)
pos = int(r["key"]["pos"])
cls = r["key"]["name"].split(".")[0]
met = r["key"]["name"].split(".")[1]
name = "%s%s" % (met, (pos and " [%d]" % (pos + 1) or ""))
self.assertEqual(scenarios[i], {
"cls": cls,
"pos": r["key"]["pos"],
"met": met,
"name": name,
"config": config,
"iterations": mock_main_duration.return_value,
"atomic": mock_atomic.return_value,
"table_cols": table_cols,
"table_rows": atomic_durations,
"errors": "errors_list",
"output": [],
"output_errors": [],
"runner": "foo_runner",
"sla": sla,
"sla_success": True,
"iterations_num": iterations,
"load_duration": 1234.5,
"full_duration": 6789.1
})
@testtools.skipIf(sys.version_info > (2, 9), "Problems with floating data")
def test__process_main_time(self):
result = {
"result": [
{
"error": [],
"duration": 1,
"idle_duration": 2,
"atomic_actions": {},
"scenario_output": {"errors": [], "data": {}}
},
{
"error": ["some", "error", "occurred"],
"duration": 1,
"idle_duration": 1,
"atomic_actions": {},
"scenario_output": {"errors": [], "data": {}}
},
{
"error": [],
"duration": 2,
"idle_duration": 3,
"atomic_actions": {},
"scenario_output": {"errors": [], "data": {}}
}
],
"sla": "foo_sla",
"load_duration": 1234.5,
"full_duration": 6789.1
}
output = plot._process_main_duration(result,
plot._prepare_data(result))
self.assertEqual({
"pie": [
{"key": "success", "value": 2},
{"key": "errors", "value": 1}
],
"iter": [
{
"key": "duration",
"values": [(1, 1.0), (2, 0), (3, 2.0)]
},
{
"key": "idle_duration",
"values": [(1, 2.0), (2, 0), (3, 3.0)]
}
],
"histogram": [
{
"key": "task",
"method": "Square Root Choice",
"values": [{"x": 1.0, "y": 1.0}, {"x": 1.0, "y": 0.0}]
},
{
"key": "task",
"method": "Sturges Formula",
"values": [{"x": 1.0, "y": 1.0}, {"x": 1.0, "y": 0.0}]
},
{
"key": "task",
"method": "Rice Rule",
"values": [{"x": 1.0, "y": 1.0}, {"x": 1.0, "y": 0.0},
{"x": 1.0, "y": 0.0}]
},
{
"key": "task",
"method": "One Half",
"values": [{"x": 2.0, "y": 2.0}]
}
]
}, output)
@testtools.skipIf(sys.version_info > (2, 9), "Problems with floating data")
def test__process_atomic_time(self):
result = {
"result": [
{
"error": [],
"atomic_actions": {
"action1": 1,
"action2": 2
},
"scenario_output": {"errors": [], "data": {}}
},
{
"error": ["some", "error", "occurred"],
"atomic_actions": {
"action1": 1,
"action2": 2
},
"scenario_output": {"errors": [], "data": {}}
},
{
"error": [],
"atomic_actions": {
"action1": 3,
"action2": 4
},
"scenario_output": {"errors": [], "data": {}}
}
]
}
data = {
"atomic_durations": {
"action1": [(1, 1.0), (2, 0.0), (3, 3.0)],
"action2": [(1, 2.0), (2, 0.0), (3, 4.0)]}}
output = plot._process_atomic(result, data)
self.assertEqual({
"histogram": [
[
{
"key": "action1",
"disabled": 0,
"method": "Square Root Choice",
"values": [{"x": 2, "y": 1}, {"x": 3, "y": 1}]
},
{
"key": "action1",
"disabled": 0,
"method": "Sturges Formula",
"values": [{"x": 2, "y": 1}, {"x": 3, "y": 1}]
},
{
"key": "action1",
"disabled": 0,
"method": "Rice Rule",
"values": [{"x": 1, "y": 1}, {"x": 1, "y": 0},
{"x": 1, "y": 0}]
},
{
"key": "action1",
"disabled": 0,
"method": "One Half",
"values": [{"x": 3, "y": 2}]
},
],
[
{
"key": "action2",
"disabled": 1,
"method": "Square Root Choice",
"values": [{"x": 3, "y": 1}, {"x": 4, "y": 1}]
},
{
"key": "action2",
"disabled": 1,
"method": "Sturges Formula",
"values": [{"x": 3, "y": 1}, {"x": 4, "y": 1}]
},
{
"key": "action2",
"disabled": 1,
"method": "Rice Rule",
"values": [{"x": 2, "y": 1}, {"x": 2, "y": 0},
{"x": 2, "y": 0}]
},
{
"key": "action2",
"disabled": 1,
"method": "One Half",
"values": [{"x": 4, "y": 2}]
}
]
],
"pie": [
{"key": "action1", "value": 2.0},
{"key": "action2", "value": 3.0}
],
"iter": [
{
"key": "action1",
"values": [(1, 1.), (2, 0.), (3, 3.)]
},
{
"key": "action2",
"values": [(1, 2.), (2, 0.), (3, 4.)]
}
]
}, output)
@mock.patch("rally.benchmark.processing.utils.compress")
def test__prepare_data(self, mock_compress):
mock_compress.side_effect = lambda i, **kv: i
rows_num = 100
load_duration = 1234.5
full_duration = 6789.1
sla = [{"foo": "bar"}]
data = []
for i in range(rows_num):
atomic_actions = {
"a1": i + 0.1,
"a2": i + 0.8,
}
row = {
"duration": i * 3.1,
"idle_duration": i * 0.2,
"error": [],
"atomic_actions": atomic_actions,
"scenario_output": {"errors": ["err"],
"data": {"out_key": "out_value"}}
}
data.append(row)
data[42]["error"] = ["foo", "bar", "spam"]
data[52]["error"] = ["spam", "bar", "foo"]
values_atomic_a1 = [i + 0.1 for i in range(rows_num)]
values_atomic_a2 = [i + 0.8 for i in range(rows_num)]
values_duration = [i * 3.1 for i in range(rows_num)]
values_duration[42] = 0
values_duration[52] = 0
values_idle = [i * 0.2 for i in range(rows_num)]
values_idle[42] = 0
values_idle[52] = 0
prepared_data = plot._prepare_data({"result": data,
"load_duration": load_duration,
"full_duration": full_duration,
"sla": sla,
"key": "foo_key"})
self.assertEqual(2, len(prepared_data["errors"]))
calls = [mock.call(values_atomic_a1),
mock.call(values_atomic_a2),
mock.call(values_duration),
mock.call(values_idle)]
mock_compress.assert_has_calls(calls)
expected_output = [{"key": "out_key",
"values": ["out_value"] * rows_num}]
expected_output_errors = [(i, [e])
for i, e in enumerate(["err"] * rows_num)]
self.assertEqual({
"total_durations": {"duration": values_duration,
"idle_duration": values_idle},
"atomic_durations": {"a1": values_atomic_a1,
"a2": values_atomic_a2},
"errors": [{"iteration": 42,
"message": "bar",
"traceback": "spam",
"type": "foo"},
{"iteration": 52,
"message": "bar",
"traceback": "foo",
"type": "spam"}],
"output": expected_output,
"output_errors": expected_output_errors,
"load_duration": load_duration,
"full_duration": full_duration,
"sla": sla,
}, prepared_data)