Base HTML templates and improvements for task report

This patch adds UI templates directory and utils,
which serve and unify HTML generation.

Also, there are some fixes and improvements for HTML task report.

In this patch:
  * Base mako templates (package rally.ui)
  * Rework tests/ci/rally-gate templates
    in order to use base template with generic header and styles
  * Show scenario errors (if any) in task report tab
  * Show scenario output (if any) in task report tab
  * Show SLA data in Overview tab
  * Show total scenario duration value (after the scenario name, above tabs)
  * If got some iteration error, save exception class name in the database
    instead of its repr()
  * Prevent layout from breaking and show proper message if JS libs
    can not be loaded for some reason
  * Fix bug 1387661 - the cause of the bug is wrong input json data,
    generated by plot.py. This happens when some atomic actions data
    missed (which is a result of scenario errors) - and we have different
    atomic actions sets between iterations. The fix saves atomic actions
    integrity by adding missed atomic actions (with 0 value).
  * Fix: if unexistend task uuid is specified in `task report' command,
    then proper exception is raised

Closes-Bug: 1387661

Change-Id: I4bcbf86e6fad844e6752306eb6c1ccfefa6c6909
This commit is contained in:
Alexander Maretskiy 2014-11-18 13:10:30 +02:00
parent c9e9ca124a
commit 4901d45bbf
19 changed files with 580 additions and 637 deletions

File diff suppressed because one or more lines are too long

View File

@ -15,45 +15,82 @@
import copy
import json
import os
import mako.template
from rally.benchmark.processing.charts import histogram as histo
from rally.benchmark.processing import utils
from rally.ui import utils as ui_utils
def _prepare_data(data, reduce_rows=1000):
def _prepare_data(data):
durations = []
idle_durations = []
atomic_durations = {}
num_errors = 0
output = {}
output_errors = []
output_stacked = []
errors = []
for i in data["result"]:
# TODO(maretskiy): store error value and scenario output
# NOTE(maretskiy): We need this extra iteration
# to determine something that we should know about the data
# before starting its processing.
atomic_names = set()
output_names = set()
for r in data["result"]:
atomic_names.update(r["atomic_actions"].keys())
output_names.update(r["scenario_output"]["data"].keys())
if i["error"]:
num_errors += 1
for idx, r in enumerate(data["result"]):
# NOTE(maretskiy): Sometimes we miss iteration data.
# So we care about data integrity by setting zero values
if len(r["atomic_actions"]) < len(atomic_names):
for atomic_name in atomic_names:
r["atomic_actions"].setdefault(atomic_name, 0)
durations.append(i["duration"])
idle_durations.append(i["idle_duration"])
if len(r["scenario_output"]["data"]) < len(output_names):
for output_name in output_names:
r["scenario_output"]["data"].setdefault(output_name, 0)
for met, duration in i["atomic_actions"].items():
if r["scenario_output"]["errors"]:
output_errors.append((idx, r["scenario_output"]["errors"]))
for param, value in r["scenario_output"]["data"].items():
try:
output[param].append(value)
except KeyError:
output[param] = [value]
if r["error"]:
type_, message, traceback = r["error"]
errors.append({"iteration": idx,
"type": type_,
"message": message,
"traceback": traceback})
durations.append(r["duration"])
idle_durations.append(r["idle_duration"])
for met, duration in r["atomic_actions"].items():
try:
atomic_durations[met].append(duration)
except KeyError:
atomic_durations[met] = [duration]
for k, v in atomic_durations.items():
atomic_durations[k] = utils.compress(v, limit=reduce_rows)
for k, v in output.iteritems():
output_stacked.append({"key": k, "values": utils.compress(v)})
for k, v in atomic_durations.iteritems():
atomic_durations[k] = utils.compress(v)
return {
"total_durations": {
"duration": utils.compress(durations, limit=reduce_rows),
"idle_duration": utils.compress(idle_durations,
limit=reduce_rows)},
"duration": utils.compress(durations),
"idle_duration": utils.compress(idle_durations)},
"atomic_durations": atomic_durations,
"num_errors": num_errors,
"output": output_stacked,
"output_errors": output_errors,
"errors": errors,
"sla": data["sla"],
"duration": data["duration"],
}
@ -79,7 +116,7 @@ def _process_main_duration(result, data):
return {
"pie": [
{"key": "success", "value": len(histogram_data)},
{"key": "errors", "value": data["num_errors"]},
{"key": "errors", "value": len(data["errors"])},
],
"iter": stacked_area,
"histogram": [
@ -214,14 +251,14 @@ def _get_atomic_action_durations(result):
def _process_results(results):
output = []
for result in results:
table_cols = ["action",
"min (sec)",
"avg (sec)",
"max (sec)",
table_cols = ["Action",
"Min (sec)",
"Avg (sec)",
"Max (sec)",
"90 percentile",
"95 percentile",
"success",
"count"]
"Success",
"Count"]
table_rows = _get_atomic_action_durations(result)
name, kw, pos = (result["key"]["name"],
result["key"]["kw"], result["key"]["pos"])
@ -239,15 +276,16 @@ def _process_results(results):
"atomic": _process_atomic(result, data),
"table_cols": table_cols,
"table_rows": table_rows,
"output": data["output"],
"output_errors": data["output_errors"],
"errors": data["errors"],
"total_duration": data["duration"],
"sla": data["sla"],
})
return sorted(output, key=lambda r: "%s%s" % (r["cls"], r["name"]))
def plot(results):
data = _process_results(results)
template_file = os.path.join(os.path.dirname(__file__),
"src", "index.mako")
with open(template_file) as index:
template = mako.template.Template(index.read())
return template.render(data=json.dumps(data))
template = ui_utils.get_template("task/report.mako")
return template.render(data=json.dumps(data))

View File

@ -134,7 +134,7 @@ def wait_for_delete(resource, update_resource=None, timeout=60,
def format_exc(exc):
return [str(type(exc)), str(exc), traceback.format_exc()]
return [exc.__class__.__name__, str(exc), traceback.format_exc()]
def infinite_run_args_generator(args_func):

View File

@ -32,6 +32,7 @@ from rally.cmd import envutils
from rally import db
from rally import exceptions
from rally.i18n import _
from rally.objects import task
from rally.openstack.common import cliutils as common_cliutils
from rally.orchestrator import api
from rally import utils as rutils
@ -313,9 +314,9 @@ class TaskCommands(object):
:param task_id: Task uuid
"""
results = map(lambda x: {"key": x["key"], 'result': x['data']['raw'],
results = map(lambda x: {"key": x["key"], "result": x["data"]["raw"],
"sla": x["data"]["sla"]},
db.task_result_get_all_by_uuid(task_id))
task.Task.get(task_id).get_results())
if results:
print(json.dumps(results, sort_keys=True, indent=4))
@ -350,8 +351,10 @@ class TaskCommands(object):
:param open_it: bool, whether to open output file in web browser
"""
results = map(lambda x: {"key": x["key"],
"result": x["data"]["raw"]},
db.task_result_get_all_by_uuid(task_id))
"sla": x["data"]["sla"],
"result": x["data"]["raw"],
"duration": x["data"]["scenario_duration"]},
task.Task.get(task_id).get_results())
if out:
out = os.path.expanduser(out)
output_file = out or ("%s.html" % task_id)
@ -403,20 +406,19 @@ class TaskCommands(object):
:param task_id: Task uuid.
:returns: Number of failed criteria.
"""
task = db.task_result_get_all_by_uuid(task_id)
results = task.Task.get(task_id).get_results()
failed_criteria = 0
results = []
for result in task:
data = []
for result in results:
key = result["key"]
for sla in result["data"]["sla"]:
sla["benchmark"] = key["name"]
sla["pos"] = key["pos"]
failed_criteria += 0 if sla['success'] else 1
results.append(sla if tojson else rutils.Struct(**sla))
data.append(sla if tojson else rutils.Struct(**sla))
if tojson:
print(json.dumps(results))
print(json.dumps(data))
else:
common_cliutils.print_list(results, ('benchmark', 'pos',
'criterion', 'success',
'detail'))
common_cliutils.print_list(data, ("benchmark", "pos", "criterion",
"success", "detail"))
return failed_criteria

View File

@ -53,6 +53,9 @@ class Task(object):
'status': consts.TaskStatus.FAILED,
'verification_log': json.dumps(log)})
def get_results(self):
return db.task_result_get_all_by_uuid(self.task["uuid"])
def append_results(self, key, value):
db.task_result_create(self.task['uuid'], key, value)

0
rally/ui/__init__.py Normal file
View File

View File

@ -0,0 +1,49 @@
<!doctype html>
<html<%block name="html_attr"/>>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rally | <%block name="title_text"/></title>
<%block name="libs"/>
<script type="text/javascript"><%block name="js_before"/></script>
<style>
body { margin:0 0 50px; padding:0; font-size:14px; font-family:Helvetica,Arial,sans-serif }
a, a:active, a:focus, a:visited { text-decoration:none; outline:none }
h1 { color:#666; margin:0 0 25px; font-size:30px; font-weight:normal }
h2 { color:#666; margin:30px 0 15px; font-size:26px; font-weight:normal }
table { border-collapse:collapse; border-spacing:0; width:100%; font-size:12px }
table th { text-align:left; padding:8px; color:#000; border:2px solid #ddd; border-width:0 0 2px 0 }
table td { text-align:left; border-top:1px solid #ddd; padding:8px; color:#333 }
table.striped tr:nth-child(odd) td { background:#f9f9f9 }
.richcolor td { color:#036; font-weight:bold }
.rich, .rich td { font-weight:bold }
.header { text-align:left; background:#333; font-size:18px; padding:13px 0; margin-bottom:20px; color:#fff; background-image:linear-gradient(to bottom, #444 0px, #222 100%) }
.header a, .header a:visited, .header a:focus { color:#999 }
.notify-error { padding:5px 10px; background:#fee; color:red }
.status-skip, .status-skip td { color:grey }
.status-pass, .status-pass td { color:green }
.status-fail, .status-fail td { color:red }
.capitalize { text-transform:capitalize }
<%block name="css"/>
.content-wrap {<%block name="css_content_wrap"> margin:0 auto; padding:0 5px </%block>}
<%block name="media_queries"/>
</style>
</head>
<body<%block name="body_attr"/>>
<div class="header">
<div class="content-wrap">
<a href="https://github.com/stackforge/rally">Rally</a>&nbsp;
<span><%block name="header_text"/></span>
</div>
</div>
<div class="content-wrap">
<%block name="content"/>
</div>
<script type="text/javascript"><%block name="js_after"/></script>
</body>
</html>

View File

@ -1,15 +1,21 @@
<!DOCTYPE html>
<html ng-app="BenchmarkApp">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rally | Benchmark Task Report</title>
## -*- coding: utf-8 -*-
<%inherit file="/base.mako"/>
<%block name="html_attr"> ng-app="BenchmarkApp"</%block>
<%block name="title_text">Benchmark Task Report</%block>
<%block name="libs">
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.15-beta/nv.d3.min.css">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-rc.5/angular.min.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.12/d3.min.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.15-beta/nv.d3.min.js"></script>
<script type="text/javascript">
app = angular.module("BenchmarkApp", []);
</%block>
<%block name="js_before">
var app = angular.module("BenchmarkApp", [])
app.controller("BenchmarkController", ["$scope", "$location", function($scope, $location) {
/* Navigation */
@ -29,6 +35,14 @@
id: "details",
name: "Details",
visible: function(){ return !! $scope.scenario.atomic.pie.length }
},{
id: "output",
name: "Output",
visible: function(){ return !! $scope.scenario.output.length }
},{
id: "failures",
name: "Failures",
visible: function(){ return !! $scope.scenario.errors.length }
},{
id: "config",
name: "Config",
@ -109,6 +123,7 @@
var chart = nv.models.multiBarChart()
.reduceXTicks(true)
.showControls(false)
.transitionDuration(0)
.groupSpacing(0.05);
chart.legend
.radioButtonMode(true)
@ -152,6 +167,12 @@
}
}
$scope.renderOutput = function() {
if ($scope.scenario) {
Charts.stack("#output-stack", $scope.scenario.output)
}
}
/* Scenario */
$scope.showScenario = function(nav_idx, scenario_idx) {
@ -165,7 +186,6 @@
angular.element(document).ready(function () {
$scope.scenarios = ${data};
$scope.histogramOptions = [];
$scope.totalHistogramModel = {label:'', value:0};
$scope.atomicHistogramModel = {label:'', value:0};
@ -231,79 +251,63 @@
$scope.$digest()
});
}])
</script>
<style>
body { margin:0 0 50px; padding:0; font-size:14px; font-family:Helvetica,Arial,sans-serif }
a, a:active, a:focus, a:visited { text-decoration: none; outline:none }
h1 { color:#666; margin:0 0 25px; font-size:32px; font-weight:normal }
h2 { color:#666; margin:30px 0 15px; font-size:26px; font-weight:normal }
</%block>
<%block name="css">
pre { padding:10px; font-size:13px; color:#333; background:#f5f5f5; border:1px solid #ccc; border-radius:4px }
table { border-collapse:collapse; border-spacing:0; width:100%; font-size:12px }
table th { text-align:left; padding:8px; color:#000; border:2px solid #ddd; border-width:0 0 2px 0 }
table td { text-align:left; border-top:1px solid #ddd; padding:8px; color:#333 }
table.striped tr:nth-child(odd) td { background:#f9f9f9 }
table .highlight td { color:#036; font-weight:bold }
.header { text-align:left; background:#333; font-size:18px; padding:13px 6px; margin-bottom: 20px; color:#fff; background-image: linear-gradient(to bottom, #444 0px, #222 100%) }
.header a, .header a:visited, .header a:focus { color:#999 }
.aside { margin:0 20px 0 0; display:block; width:255px; float:left }
.aside div:first-child { border-radius:4px 4px 0 0 }
.aside div:last-child { border-radius:0 0 4px 4px }
.nav-group { color:#678; background:#eee; border:1px solid #ddd; margin-bottom:-1px; display:block; padding:8px 9px; font-weight:bold; text-aligh:left; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:pointer }
.nav-group.active { color:#469 }
.nav-item { color:#555; background:#fff; border:1px solid #ddd; font-size: 12px; display:block; margin-bottom:-1px; padding:8px 10px; text-aligh:left; text-overflow:ellipsis; white-space:nowrap; overflow:hidden; cursor:pointer }
.nav-item { color:#555; background:#fff; border:1px solid #ddd; font-size:12px; display:block; margin-bottom:-1px; padding:8px 10px; text-aligh:left; text-overflow:ellipsis; white-space:nowrap; overflow:hidden; cursor:pointer }
.nav-item:hover { background:#f8f8f8 }
.nav-item.active, .nav-item.active:hover { background:#428bca; background-image: linear-gradient(to bottom, #428bca 0px, #3278b3 100%); border-color:#3278b3; color:#fff }
.nav-item.active, .nav-item.active:hover { background:#428bca; background-image:linear-gradient(to bottom, #428bca 0px, #3278b3 100%); border-color:#3278b3; color:#fff }
.tabs { list-style:outside none none; margin-bottom:0; padding-left:0; border-bottom:1px solid #ddd }
.tabs:after { clear:both }
.tabs li { float:left; margin-bottom:-1px; display:block; position:relative }
.tabs li div { border:1px solid transparent; border-radius:4px 4px 0 0; line-height:20px; margin-right:2px; padding:10px 15px; color:#428bca }
.tabs li div:hover { border-color:#eee #eee #ddd; background:#eee; cursor:pointer; }
.tabs li.active div { background:#fff; border-color: #ddd #ddd transparent; border-style: solid; border-width: 1px; color:#555; cursor:default }
.tabs li.active div { background:#fff; border-color:#ddd #ddd transparent; border-style:solid; border-width:1px; color:#555; cursor:default }
.failure-mesg { color:#900 }
.failure-trace { color:#333; white-space:pre; overflow:auto }
.chart { height:300px }
.chart .chart-dropdown { float:right; margin:0 35px 0 }
.chart.lesser { padding:0; margin:0; float:left; width:40% }
.chart.larger { padding:0; margin:0; float:left; width:59% }
.content-wrap { margin:0 auto; padding:0 5px }
.content-main { margin:0 5px; display:block; float:left }
.expandable { cursor:pointer }
.clearfix { clear:both }
.text-error { color:red }
.top-margin { margin-top:40px !important }
.content-main { margin:0 5px; display:block; float:left }
</%block>
<%block name="media_queries">
@media only screen and (min-width: 320px) { .content-wrap { width:900px } .content-main { width:600px } }
@media only screen and (min-width: 900px) { .content-wrap { width:880px } .content-main { width:590px } }
@media only screen and (min-width: 1000px) { .content-wrap { width:980px } .content-main { width:690px } }
@media only screen and (min-width: 1100px) { .content-wrap { width:1080px } .content-main { width:790px } }
@media only screen and (min-width: 1200px) { .content-wrap { width:1180px } .content-main { width:890px } }
</style>
</head>
<body>
</%block>
<div class="header">
<div class="content-wrap">
<a href="https://github.com/stackforge/rally">Rally</a>&nbsp;
<span>benchmark results</span>
</div>
</div>
<%block name="body_attr"> ng-controller="BenchmarkController"</%block>
<div class="content-wrap" ng-controller="BenchmarkController">
<%block name="header_text">benchmark results</%block>
<div ng-hide="scenario" class="text-error">Failed to render scenario data</div>
<%block name="content">
<p id="page-error" class="notify-error" style="display:none"></p>
<div class="aside" ng-show="scenario">
<div id="scenarios-list" class="aside" ng-show="scenario" ng-cloack>
<div class="nav-group" title="{{n.cls}}"
ng-repeat-start="n in nav track by $index"
ng-click="showNav(n.idx)"
ng-class="{active:n.idx == nav_idx}">
<span ng-hide="n.idx == nav_idx">&#9658;</span>
<span ng-show="n.idx == nav_idx">&#9660;</span>
{{n.cls}}
</div>
{{n.cls}}</div>
<div class="nav-item" title="{{m.name}}"
ng-show="n.idx == nav_idx"
ng-class="{active:m.idx == scenario_idx}"
@ -312,9 +316,9 @@
ng-repeat-end>{{m.name}}</div>
</div>
<div class="content-main" ng-show="scenario">
<div id="scenario-data" class="content-main" ng-show="scenario" ng-cloak>
<h1>{{scenario.cls}}.<wbr>{{scenario.name}}</h1>
<h1>{{scenario.cls}}.<wbr>{{scenario.name}} ({{scenario.total_duration | number:2}}s)</h1>
<ul class="tabs">
<li ng-repeat="tab in tabs"
ng-class="{active:tab.id == tabId}"
@ -329,21 +333,43 @@
<script type="text/ng-template" id="overview">
{{renderTotal()}}
<h2>Table for task results</h2>
<table class="striped lastrow">
<div ng-show="scenario.sla.length">
<h2>Service-level agreement</h2>
<table class="striped">
<thead>
<tr>
<th>Criterion
<th>Detail
<th>Success
<tr>
</thead>
<tbody>
<tr class="rich"
ng-repeat="row in scenario.sla track by $index"
ng-class="{'status-fail':!row.success, 'status-pass':row.success}">
<td>{{row.criterion}}
<td>{{row.detail}}
<td class="capitalize">{{row.success}}
<tr>
</tbody>
</table>
</div>
<h2>Total durations</h2>
<table class="striped">
<thead>
<tr>
<th ng-repeat="i in scenario.table_cols track by $index">{{i}}</th>
<th ng-repeat="i in scenario.table_cols track by $index">{{i}}
<tr>
</thead>
<tbody>
<tr ng-class="{highlight:$last}" ng-repeat="row in scenario.table_rows track by $index">
<td ng-repeat="i in row track by $index">{{i}}</td>
<tr ng-class="{richcolor:$last}" ng-repeat="row in scenario.table_rows track by $index">
<td ng-repeat="i in row track by $index">{{i}}
<tr>
</tbody>
</table>
<h2>Charts for the Total Duration</h2>
<h2>Charts for the Total durations</h2>
<div class="chart">
<svg id="total-stack"></svg>
</div>
@ -364,7 +390,7 @@
<script type="text/ng-template" id="details">
{{renderDetails()}}
<h2>Charts for every Atomic Action</h2>
<h2>Charts for each Atomic Action</h2>
<div class="chart">
<svg id="atomic-stack"></svg>
</div>
@ -382,6 +408,47 @@
</div>
</script>
<script type="text/ng-template" id="output">
{{renderOutput()}}
<h2>Scenario output</h2>
<div class="chart">
<svg id="output-stack"></svg>
</div>
</script>
<script type="text/ng-template" id="failures">
<h2>Benchmark failures (<ng-pluralize
count="scenario.errors.length"
when="{'1': '1 iteration', 'other': '{} iterations'}"></ng-pluralize> failed)
</h2>
<table class="striped">
<thead>
<tr>
<th>
<th>Iteration
<th>Exception type
<th>Exception message
</tr>
</thead>
<tbody>
<tr class="expandable"
ng-repeat-start="i in scenario.errors track by $index"
ng-click="i.expanded = ! i.expanded">
<td>
<span ng-hide="i.expanded">&#9658;</span>
<span ng-show="i.expanded">&#9660;</span>
<td>{{i.iteration}}
<td>{{i.type}}
<td class="failure-mesg">{{i.message}}
</tr>
<tr ng-show="i.expanded" ng-repeat-end>
<td colspan="4" class="failure-trace">{{i.traceback}}
</tr>
</tbody>
</table>
</script>
<script type="text/ng-template" id="config">
<h2>Scenario Configuration</h2>
<pre>{{scenario.config}}</pre>
@ -389,8 +456,13 @@
</div>
<div class="clearfix"></div>
</%block>
</div>
</body>
</html>
<%block name="js_after">
if (! window.angular) {
document.getElementById("page-error").style.display = "block";
document.getElementById("page-error").textContent = "Failed to load AngularJS framework";
document.getElementById("scenarios-list").style.display = "none";
document.getElementById("scenario-data").style.display = "none";
}
</%block>

48
rally/ui/utils.py Normal file
View File

@ -0,0 +1,48 @@
# 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.
from __future__ import print_function
import os.path
import sys
import mako.exceptions
import mako.lookup
import mako.template
templates_dir = os.path.join(os.path.dirname(__file__), "templates")
lookup_dirs = [templates_dir,
os.path.abspath(os.path.join(templates_dir, "..", "..", ".."))]
lookup = mako.lookup.TemplateLookup(directories=lookup_dirs)
def get_template(template_path):
return lookup.get_template(template_path)
def main(*args):
if len(args) != 2 or args[0] != "render":
exit("Usage: utils.py render <lookup/path/to/template.mako>")
try:
print(get_template(sys.argv[2]).render())
except mako.exceptions.TopLevelLookupException as e:
exit(e)
if __name__ == '__main__':
args = sys.argv[1:]
main(*args)

View File

@ -45,7 +45,8 @@ rally show keypairs
rally -v task start --task $SCENARIO
mkdir -p rally-plot/extra
cp $BASE/new/rally/tests/ci/rally-gate/index.html rally-plot/extra/index.html
python $BASE/new/rally/rally/ui/utils.py render\
tests/ci/rally-gate/index.mako > rally-plot/extra/index.html
cp $SCENARIO rally-plot/task.txt
tar -czf rally-plot/plugins.tar.gz -C $RALLY_PLUGINS_DIR .
rally task report --out rally-plot/results.html

View File

@ -1,62 +0,0 @@
<HTML>
<HEAD>
<title> Rally performance job results </title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<style>
body {
margin: 20px;
}
code {
color: gray;
}
</style>
</HEAD>
<BODY>
<h1> Rally performance job </h1>
<hr/>
<h2> Job results </h2>
<p> Logs and files: </p>
<ul>
<li> <a href="console.html"> Benchmarking logs </a> <code> console.html </code> </li>
<li> <a href="logs/"> Logs of all services</a> <code> logs/ </code> </li>
<li> <a href="rally-plot/"> Rally files </a><code> rally-plot/ </code> </li>
</ul>
<p> Results of this job can be obtained in different formats: </p>
<ul>
<li> <a href="rally-plot/task.txt"> Rally input task </a> </li>
<li> <a href="rally-plot/results.html.gz"> HTML report with graphs </a> <code>$ rally task report</code></li>
<li> <a href="rally-plot/detailed.txt.gz"> Plain text aggregated data </a> <code> $ rally task detailed </code></li>
<li> <a href="rally-plot/detailed_with_iterations.txt.gz"> Plain text aggregated data with detailed iterations </a> <code> $ rally task detailed --iterations-data </code></li>
<li> <a href="rally-plot/sla.txt"> SLA checks </a> <code> $ rally task sla_check </code></li>
<li> <a href="rally-plot/results.json.gz"> Full result in json </a> <code>$ rally task results</code></li>
</ul>
<h2> About Rally </h2>
<p> Rally is benchmark system for OpenStask: </p>
<ul>
<li> <a href="https://github.com/stackforge/rally"> Git repository </a> </li>
<li> <a href="https://rally.readthedocs.org/en/latest/"> Documentation </a> </li>
<li> <a href="https://wiki.openstack.org/wiki/Rally/HowTo"> How to use Rally (locally) </a> </li>
<li> <a href="https://wiki.openstack.org/wiki/Rally/RallyGates"> How to add Rally job to your project </a> </li>
</ul>
<h2> Steps to repeat locally: </h2>
<ol>
<li> Fetch rally task from <a href="rally-plot/task.txt">here</a> </li>
<li> Fetch rally plugins from <a href="rally-plot/plugins.tar.gz">here</a> </li>
<li> Install OpenStack and Rally using <a href="https://github.com/stackforge/rally/tree/master/contrib/devstack"> this instruction </a> </li>
<li> Unzip plugins and put to .rally/plugins/ directory </li>
<li> Run rally task <code>$ rally task start task.txt</code> </li>
</ol>
</BODY>
</HTML>

View File

@ -0,0 +1,61 @@
## -*- coding: utf-8 -*-
<%inherit file="/base.mako"/>
<%block name="title_text">Performance job results</%block>
<%block name="css">
li { margin:2px 0 }
a, a:visited { color:#039 }
code { padding:0 5px; color:#888 }
.columns li { position:relative }
.columns li > :first-child { display:block }
.columns li > :nth-child(2) { display:block; position:static; left:165px; top:0; white-space:nowrap }
</%block>
<%block name="css_content_wrap">margin:0 auto; padding:0 5px</%block>
<%block name="media_queries">
@media only screen and (min-width: 320px) { .content-wrap { width:400px } }
@media only screen and (min-width: 520px) { .content-wrap { width:500px } }
@media only screen and (min-width: 620px) { .content-wrap { width:90% } .columns li > :nth-child(2) { position:absolute } }
@media only screen and (min-width: 720px) { .content-wrap { width:70% } }
</%block>
<%block name="header_text">performance job results</%block>
<%block name="content">
<h2>Logs and files</h2>
<ul class="columns">
<li><a href="rally-plot/task.txt" class="rich">Rally input task</a>
<li><a href="console.html">Benchmarking logs</a> <code>console.html</code>
<li><a href="logs/">Logs of all services</a> <code>logs/</code>
<li><a href="rally-plot/">Rally files</a> <code>rally-plot/</code>
</ul>
<h2>Job results, in different formats</h2>
<ul class="columns">
<li><a href="rally-plot/results.html.gz" class="rich">HTML report</a> <code>$ rally task report</code>
<li><a href="rally-plot/detailed.txt.gz">Text report</a> <code>$ rally task detailed</code>
<li><a href="rally-plot/detailed_with_iterations.txt.gz">Text report detailed</a> <code>$ rally task detailed --iterations-data</code>
<li><a href="rally-plot/sla.txt" class="rich">Success criteria (SLA)</a> <code>$ rally task sla_check</code>
<li><a href="rally-plot/results.json.gz">Raw results (JSON)</a> <code>$ rally task results</code>
</ul>
<h2>About Rally</h2>
<p>Rally is benchmark system for OpenStack:</p>
<ul>
<li><a href="https://github.com/stackforge/rally">Git repository</a>
<li><a href="https://rally.readthedocs.org/en/latest/">Documentation</a>
<li><a href="https://wiki.openstack.org/wiki/Rally/HowTo">How to use Rally (locally)</a>
<li><a href="https://wiki.openstack.org/wiki/Rally/RallyGates">How to add Rally job to your project</a>
</ul>
<h2>Steps to repeat locally</h2>
<ol>
<li>Fetch rally task from <a href="rally-plot/task.txt">here</a></li>
<li>Fetch rally plugins from <a href="rally-plot/plugins.tar.gz">here</a></li>
<li>Install OpenStack and Rally using <a href="https://github.com/stackforge/rally/tree/master/contrib/devstack">this instruction</a></li>
<li>Unzip plugins and put to <code>.rally/plugins/</code> directory</li>
<li>Run rally task: <code>$ rally task start task.txt</code></li>
</ol>
</%block>

View File

@ -69,7 +69,7 @@ class TaskTestCase(unittest.TestCase):
rally("task start --task %s" % config.filename)
if os.path.exists(html_file):
os.remove(html_file)
rally("task report /tmp/test_plot")
rally("task report --out %s" % html_file)
self.assertTrue(os.path.exists(html_file))
def test_delete(self):

View File

@ -22,31 +22,22 @@ from tests.unit import test
class PlotTestCase(test.TestCase):
@mock.patch("rally.benchmark.processing.plot.open", create=True)
@mock.patch("rally.benchmark.processing.plot.mako.template.Template")
@mock.patch("rally.benchmark.processing.plot.os.path.dirname")
@mock.patch("rally.benchmark.processing.plot.ui_utils")
@mock.patch("rally.benchmark.processing.plot._process_results")
def test_plot(self, mock_proc_results, mock_dirname, mock_template,
mock_open):
mock_dirname.return_value = "abspath"
mock_open.return_value = mock_open
mock_open.__enter__.return_value = mock_open
mock_open.read.return_value = "some_template"
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))
templ = mock.MagicMock()
templ.render.return_value = "output"
mock_template.return_value = templ
mock_proc_results.return_value = [{"name": "a"}, {"name": "b"}]
result = plot.plot(["abc"])
self.assertEqual(result, templ.render.return_value)
templ.render.assert_called_once_with(
self.assertEqual(result, "plot_html")
mock_render.assert_called_once_with(
data=json.dumps(mock_proc_results.return_value)
)
mock_template.assert_called_once_with(mock_open.read.return_value)
mock_open.assert_called_once_with("%s/src/index.mako"
% mock_dirname.return_value)
mock_utils.get_template.assert_called_once_with("task/report.mako")
@mock.patch("rally.benchmark.processing.plot._prepare_data")
@mock.patch("rally.benchmark.processing.plot._process_atomic")
@ -58,15 +49,20 @@ class PlotTestCase(test.TestCase):
{"key": {"name": "Klass.method_foo", "pos": 1, "kw": "config2"}},
{"key": {"name": "Klass.method_bar", "pos": 0, "kw": "config3"}}
]
table_cols = ["action",
"min (sec)",
"avg (sec)",
"max (sec)",
table_cols = ["Action",
"Min (sec)",
"Avg (sec)",
"Max (sec)",
"90 percentile",
"95 percentile",
"success",
"count"]
"Success",
"Count"]
mock_prepare.side_effect = lambda i: {"errors": "errors_list",
"output": [],
"output_errors": [],
"sla": "foo_sla",
"duration": 12345.67}
mock_main_duration.return_value = "main_duration"
mock_atomic.return_value = "main_atomic"
@ -90,7 +86,12 @@ class PlotTestCase(test.TestCase):
"duration": mock_main_duration.return_value,
"atomic": mock_atomic.return_value,
"table_cols": table_cols,
"table_rows": [['total', None, None, None, None, None, 0, 0]]
"table_rows": [["total", None, None, None, None, None, 0, 0]],
"errors": "errors_list",
"output": [],
"output_errors": [],
"sla": "foo_sla",
"total_duration": 12345.67
})
def test__process_main_time(self):
@ -100,21 +101,26 @@ class PlotTestCase(test.TestCase):
"error": [],
"duration": 1,
"idle_duration": 2,
"atomic_actions": {}
"atomic_actions": {},
"scenario_output": {"errors": [], "data": {}}
},
{
"error": True,
"error": ["some", "error", "occurred"],
"duration": 1,
"idle_duration": 1,
"atomic_actions": {}
"atomic_actions": {},
"scenario_output": {"errors": [], "data": {}}
},
{
"error": [],
"duration": 2,
"idle_duration": 3,
"atomic_actions": {}
"atomic_actions": {},
"scenario_output": {"errors": [], "data": {}}
}
]
],
"sla": "foo_sla",
"duration": 12345.67
}
output = plot._process_main_duration(result,
@ -168,28 +174,32 @@ class PlotTestCase(test.TestCase):
"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)]}}
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)
@ -270,11 +280,11 @@ class PlotTestCase(test.TestCase):
def test__prepare_data(self, mock_compress):
mock_compress.side_effect = lambda i, **kv: i
rows_range = 100
limit = 10
rows_num = 100
total_duration = 12345.67
sla = [{"foo": "bar"}]
data = []
for i in range(rows_range):
for i in range(rows_num):
atomic_actions = {
"a1": i + 0.1,
"a2": i + 0.8,
@ -284,32 +294,50 @@ class PlotTestCase(test.TestCase):
"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 error"
data[52]["error"] = "bar error"
data[42]["error"] = ["foo", "bar", "spam"]
data[52]["error"] = ["spam", "bar", "foo"]
values_atomic_a1 = [i + 0.1 for i in range(rows_range)]
values_atomic_a2 = [i + 0.8 for i in range(rows_range)]
values_duration = [i * 3.1 for i in range(rows_range)]
values_idle = [i * 0.2 for i in range(rows_range)]
num_errors = 2
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_idle = [i * 0.2 for i in range(rows_num)]
prepared_data = plot._prepare_data({"result": data},
reduce_rows=limit)
self.assertEqual(num_errors, prepared_data["num_errors"])
prepared_data = plot._prepare_data({"result": data,
"duration": total_duration,
"sla": sla,
"key": "foo_key"})
self.assertEqual(2, len(prepared_data["errors"]))
calls = [mock.call(values_atomic_a1, limit=limit),
mock.call(values_atomic_a2, limit=limit),
mock.call(values_duration, limit=limit),
mock.call(values_idle, limit=limit)]
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},
"num_errors": num_errors
"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,
"duration": total_duration,
"sla": sla,
}, prepared_data)

View File

@ -132,7 +132,7 @@ class ScenarioHelpersTestCase(test.TestCase):
}
self.assertEqual(expected_result, result)
self.assertEqual(expected_error[:2],
[str(Exception), "Something went wrong"])
["Exception", "Something went wrong"])
class ScenarioRunnerResultTestCase(test.TestCase):

View File

@ -134,28 +134,81 @@ class TaskCommandsTestCase(test.TestCase):
self.task.detailed(test_uuid)
mock_db.task_get_detailed.assert_called_once_with(test_uuid)
@mock.patch('rally.cmd.commands.task.db')
@mock.patch('json.dumps')
def test_results(self, mock_json, mock_db):
test_uuid = 'aa808c14-69cc-4faf-a906-97e05f5aebbd'
value = [
{'key': 'key', 'data': {'raw': 'raw', 'sla': []}}
@mock.patch("json.dumps")
@mock.patch("rally.cmd.commands.task.task.Task.get")
def test_results(self, mock_get, mock_json):
task_id = "foo_task_id"
data = [
{"key": "foo_key", "data": {"raw": "foo_raw", "sla": []}}
]
result = map(lambda x: {"key": x["key"],
"result": x["data"]["raw"],
"sla": x["data"]["sla"]}, value)
mock_db.task_result_get_all_by_uuid.return_value = value
self.task.results(test_uuid)
mock_json.assert_called_once_with(result, sort_keys=True, indent=4)
mock_db.task_result_get_all_by_uuid.assert_called_once_with(test_uuid)
"sla": x["data"]["sla"]}, data)
mock_results = mock.Mock(return_value=data)
mock_get.return_value = mock.Mock(get_results=mock_results)
@mock.patch('rally.cmd.commands.task.db')
def test_invalid_results(self, mock_db):
test_uuid = 'd1f58069-d221-4577-b6ba-5c635027765a'
mock_db.task_result_get_all_by_uuid.return_value = []
return_value = self.task.results(test_uuid)
mock_db.task_result_get_all_by_uuid.assert_called_once_with(test_uuid)
self.assertEqual(1, return_value)
self.task.results(task_id)
mock_json.assert_called_once_with(result, sort_keys=True, indent=4)
mock_get.assert_called_once_with(task_id)
@mock.patch("rally.cmd.commands.task.task.Task.get")
def test_invalid_results(self, mock_get):
task_id = "foo_task_id"
data = []
mock_results = mock.Mock(return_value=data)
mock_get.return_value = mock.Mock(get_results=mock_results)
result = self.task.results(task_id)
mock_get.assert_called_once_with(task_id)
self.assertEqual(1, result)
@mock.patch("rally.cmd.commands.task.os.path.realpath",
side_effect=lambda p: "realpath_%s" % p)
@mock.patch("rally.cmd.commands.task.open", create=True)
@mock.patch("rally.cmd.commands.task.plot")
@mock.patch("rally.cmd.commands.task.webbrowser")
@mock.patch("rally.cmd.commands.task.task.Task.get")
def test_report(self, mock_get, mock_web, mock_plot, mock_open, mock_os):
task_id = "foo_task_id"
data = [
{"key": "foo_key", "data": {"raw": "foo_raw", "sla": "foo_sla",
"scenario_duration": "foo_duration"}},
{"key": "bar_key", "data": {"raw": "bar_raw", "sla": "bar_sla",
"scenario_duration": "bar_duration"}},
]
results = map(lambda x: {"key": x["key"],
"result": x["data"]["raw"],
"sla": x["data"]["sla"],
"duration": x["data"]["scenario_duration"]},
data)
mock_results = mock.Mock(return_value=data)
mock_get.return_value = mock.Mock(get_results=mock_results)
mock_plot.plot.return_value = "html_report"
mock_write = mock.Mock()
mock_open.return_value.__enter__.return_value = (
mock.Mock(write=mock_write))
def reset_mocks():
for m in mock_get, mock_web, mock_plot, mock_open:
m.reset_mock()
self.task.report(task_id)
mock_open.assert_called_once_with(task_id + ".html", "w+")
mock_plot.plot.assert_called_once_with(results)
mock_write.assert_called_once_with("html_report")
mock_get.assert_called_once_with(task_id)
reset_mocks()
self.task.report(task_id, out="bar.html")
mock_open.assert_called_once_with("bar.html", "w+")
mock_plot.plot.assert_called_once_with(results)
mock_write.assert_called_once_with("html_report")
mock_get.assert_called_once_with(task_id)
reset_mocks()
self.task.report(task_id, out="spam.html", open_it=True)
mock_web.open_new_tab.assert_called_once_with(
"file://realpath_spam.html")
@mock.patch('rally.cmd.commands.task.common_cliutils.print_list')
@mock.patch('rally.cmd.commands.task.envutils.get_global')
@ -206,7 +259,7 @@ class TaskCommandsTestCase(test.TestCase):
@mock.patch('rally.cmd.commands.task.common_cliutils.print_list')
@mock.patch("rally.cmd.commands.task.db")
def test_sla_check(self, mock_db, mock_print_list):
def _test_sla_check(self, mock_db, mock_print_list):
value = [{
"key": {
"name": "fake_name",

View File

@ -114,6 +114,14 @@ class TaskTestCase(test.TestCase):
{'verification_log': json.dumps({"a": "fake"})}
)
@mock.patch("rally.objects.task.db.task_result_get_all_by_uuid",
return_value="foo_results")
def test_get_results(self, mock_get):
task = objects.Task(task=self.task)
results = task.get_results()
mock_get.assert_called_once_with(self.task["uuid"])
self.assertEqual(results, "foo_results")
@mock.patch('rally.objects.task.db.task_result_create')
def test_append_results(self, mock_append_results):
task = objects.Task(task=self.task)

View File

View File

@ -0,0 +1,38 @@
# 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 mock
from rally.ui import utils
from tests.unit import test
class PlotTestCase(test.TestCase):
def test_lookup(self):
self.assertIsInstance(utils.lookup, utils.mako.lookup.TemplateLookup)
self.assertIsInstance(utils.lookup.get_template("/base.mako"),
utils.mako.lookup.Template)
self.assertRaises(
utils.mako.lookup.exceptions.TopLevelLookupException,
utils.lookup.get_template, "absent_template")
@mock.patch("rally.ui.utils.lookup")
def test_get_template(self, mock_lookup):
mock_lookup.get_template.return_value = "foo_template"
template = utils.get_template("foo_path")
self.assertEqual(template, "foo_template")
mock_lookup.get_template.assert_called_once_with("foo_path")