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:
parent
c9e9ca124a
commit
4901d45bbf
File diff suppressed because one or more lines are too long
@ -15,45 +15,82 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
|
|
||||||
import mako.template
|
|
||||||
|
|
||||||
from rally.benchmark.processing.charts import histogram as histo
|
from rally.benchmark.processing.charts import histogram as histo
|
||||||
from rally.benchmark.processing import utils
|
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 = []
|
durations = []
|
||||||
idle_durations = []
|
idle_durations = []
|
||||||
atomic_durations = {}
|
atomic_durations = {}
|
||||||
num_errors = 0
|
output = {}
|
||||||
|
output_errors = []
|
||||||
|
output_stacked = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
for i in data["result"]:
|
# NOTE(maretskiy): We need this extra iteration
|
||||||
# TODO(maretskiy): store error value and scenario output
|
# 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"]:
|
for idx, r in enumerate(data["result"]):
|
||||||
num_errors += 1
|
# 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"])
|
if len(r["scenario_output"]["data"]) < len(output_names):
|
||||||
idle_durations.append(i["idle_duration"])
|
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:
|
try:
|
||||||
atomic_durations[met].append(duration)
|
atomic_durations[met].append(duration)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
atomic_durations[met] = [duration]
|
atomic_durations[met] = [duration]
|
||||||
|
|
||||||
for k, v in atomic_durations.items():
|
for k, v in output.iteritems():
|
||||||
atomic_durations[k] = utils.compress(v, limit=reduce_rows)
|
output_stacked.append({"key": k, "values": utils.compress(v)})
|
||||||
|
|
||||||
|
for k, v in atomic_durations.iteritems():
|
||||||
|
atomic_durations[k] = utils.compress(v)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_durations": {
|
"total_durations": {
|
||||||
"duration": utils.compress(durations, limit=reduce_rows),
|
"duration": utils.compress(durations),
|
||||||
"idle_duration": utils.compress(idle_durations,
|
"idle_duration": utils.compress(idle_durations)},
|
||||||
limit=reduce_rows)},
|
|
||||||
"atomic_durations": atomic_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 {
|
return {
|
||||||
"pie": [
|
"pie": [
|
||||||
{"key": "success", "value": len(histogram_data)},
|
{"key": "success", "value": len(histogram_data)},
|
||||||
{"key": "errors", "value": data["num_errors"]},
|
{"key": "errors", "value": len(data["errors"])},
|
||||||
],
|
],
|
||||||
"iter": stacked_area,
|
"iter": stacked_area,
|
||||||
"histogram": [
|
"histogram": [
|
||||||
@ -214,14 +251,14 @@ def _get_atomic_action_durations(result):
|
|||||||
def _process_results(results):
|
def _process_results(results):
|
||||||
output = []
|
output = []
|
||||||
for result in results:
|
for result in results:
|
||||||
table_cols = ["action",
|
table_cols = ["Action",
|
||||||
"min (sec)",
|
"Min (sec)",
|
||||||
"avg (sec)",
|
"Avg (sec)",
|
||||||
"max (sec)",
|
"Max (sec)",
|
||||||
"90 percentile",
|
"90 percentile",
|
||||||
"95 percentile",
|
"95 percentile",
|
||||||
"success",
|
"Success",
|
||||||
"count"]
|
"Count"]
|
||||||
table_rows = _get_atomic_action_durations(result)
|
table_rows = _get_atomic_action_durations(result)
|
||||||
name, kw, pos = (result["key"]["name"],
|
name, kw, pos = (result["key"]["name"],
|
||||||
result["key"]["kw"], result["key"]["pos"])
|
result["key"]["kw"], result["key"]["pos"])
|
||||||
@ -239,15 +276,16 @@ def _process_results(results):
|
|||||||
"atomic": _process_atomic(result, data),
|
"atomic": _process_atomic(result, data),
|
||||||
"table_cols": table_cols,
|
"table_cols": table_cols,
|
||||||
"table_rows": table_rows,
|
"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"]))
|
return sorted(output, key=lambda r: "%s%s" % (r["cls"], r["name"]))
|
||||||
|
|
||||||
|
|
||||||
def plot(results):
|
def plot(results):
|
||||||
data = _process_results(results)
|
data = _process_results(results)
|
||||||
|
template = ui_utils.get_template("task/report.mako")
|
||||||
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))
|
return template.render(data=json.dumps(data))
|
||||||
|
@ -134,7 +134,7 @@ def wait_for_delete(resource, update_resource=None, timeout=60,
|
|||||||
|
|
||||||
|
|
||||||
def format_exc(exc):
|
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):
|
def infinite_run_args_generator(args_func):
|
||||||
|
@ -32,6 +32,7 @@ from rally.cmd import envutils
|
|||||||
from rally import db
|
from rally import db
|
||||||
from rally import exceptions
|
from rally import exceptions
|
||||||
from rally.i18n import _
|
from rally.i18n import _
|
||||||
|
from rally.objects import task
|
||||||
from rally.openstack.common import cliutils as common_cliutils
|
from rally.openstack.common import cliutils as common_cliutils
|
||||||
from rally.orchestrator import api
|
from rally.orchestrator import api
|
||||||
from rally import utils as rutils
|
from rally import utils as rutils
|
||||||
@ -313,9 +314,9 @@ class TaskCommands(object):
|
|||||||
:param task_id: Task uuid
|
: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"]},
|
"sla": x["data"]["sla"]},
|
||||||
db.task_result_get_all_by_uuid(task_id))
|
task.Task.get(task_id).get_results())
|
||||||
|
|
||||||
if results:
|
if results:
|
||||||
print(json.dumps(results, sort_keys=True, indent=4))
|
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
|
:param open_it: bool, whether to open output file in web browser
|
||||||
"""
|
"""
|
||||||
results = map(lambda x: {"key": x["key"],
|
results = map(lambda x: {"key": x["key"],
|
||||||
"result": x["data"]["raw"]},
|
"sla": x["data"]["sla"],
|
||||||
db.task_result_get_all_by_uuid(task_id))
|
"result": x["data"]["raw"],
|
||||||
|
"duration": x["data"]["scenario_duration"]},
|
||||||
|
task.Task.get(task_id).get_results())
|
||||||
if out:
|
if out:
|
||||||
out = os.path.expanduser(out)
|
out = os.path.expanduser(out)
|
||||||
output_file = out or ("%s.html" % task_id)
|
output_file = out or ("%s.html" % task_id)
|
||||||
@ -403,20 +406,19 @@ class TaskCommands(object):
|
|||||||
:param task_id: Task uuid.
|
:param task_id: Task uuid.
|
||||||
:returns: Number of failed criteria.
|
: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
|
failed_criteria = 0
|
||||||
results = []
|
data = []
|
||||||
for result in task:
|
for result in results:
|
||||||
key = result["key"]
|
key = result["key"]
|
||||||
for sla in result["data"]["sla"]:
|
for sla in result["data"]["sla"]:
|
||||||
sla["benchmark"] = key["name"]
|
sla["benchmark"] = key["name"]
|
||||||
sla["pos"] = key["pos"]
|
sla["pos"] = key["pos"]
|
||||||
failed_criteria += 0 if sla['success'] else 1
|
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:
|
if tojson:
|
||||||
print(json.dumps(results))
|
print(json.dumps(data))
|
||||||
else:
|
else:
|
||||||
common_cliutils.print_list(results, ('benchmark', 'pos',
|
common_cliutils.print_list(data, ("benchmark", "pos", "criterion",
|
||||||
'criterion', 'success',
|
"success", "detail"))
|
||||||
'detail'))
|
|
||||||
return failed_criteria
|
return failed_criteria
|
||||||
|
@ -53,6 +53,9 @@ class Task(object):
|
|||||||
'status': consts.TaskStatus.FAILED,
|
'status': consts.TaskStatus.FAILED,
|
||||||
'verification_log': json.dumps(log)})
|
'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):
|
def append_results(self, key, value):
|
||||||
db.task_result_create(self.task['uuid'], key, value)
|
db.task_result_create(self.task['uuid'], key, value)
|
||||||
|
|
||||||
|
0
rally/ui/__init__.py
Normal file
0
rally/ui/__init__.py
Normal file
49
rally/ui/templates/base.mako
Normal file
49
rally/ui/templates/base.mako
Normal 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>
|
||||||
|
<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>
|
@ -1,15 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
## -*- coding: utf-8 -*-
|
||||||
<html ng-app="BenchmarkApp">
|
<%inherit file="/base.mako"/>
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<%block name="html_attr"> ng-app="BenchmarkApp"</%block>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Rally | Benchmark Task Report</title>
|
<%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">
|
<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://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" src="http://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.15-beta/nv.d3.min.js"></script>
|
||||||
<script type="text/javascript">
|
</%block>
|
||||||
app = angular.module("BenchmarkApp", []);
|
|
||||||
|
<%block name="js_before">
|
||||||
|
|
||||||
|
var app = angular.module("BenchmarkApp", [])
|
||||||
|
|
||||||
app.controller("BenchmarkController", ["$scope", "$location", function($scope, $location) {
|
app.controller("BenchmarkController", ["$scope", "$location", function($scope, $location) {
|
||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
@ -29,6 +35,14 @@
|
|||||||
id: "details",
|
id: "details",
|
||||||
name: "Details",
|
name: "Details",
|
||||||
visible: function(){ return !! $scope.scenario.atomic.pie.length }
|
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",
|
id: "config",
|
||||||
name: "Config",
|
name: "Config",
|
||||||
@ -109,6 +123,7 @@
|
|||||||
var chart = nv.models.multiBarChart()
|
var chart = nv.models.multiBarChart()
|
||||||
.reduceXTicks(true)
|
.reduceXTicks(true)
|
||||||
.showControls(false)
|
.showControls(false)
|
||||||
|
.transitionDuration(0)
|
||||||
.groupSpacing(0.05);
|
.groupSpacing(0.05);
|
||||||
chart.legend
|
chart.legend
|
||||||
.radioButtonMode(true)
|
.radioButtonMode(true)
|
||||||
@ -152,6 +167,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.renderOutput = function() {
|
||||||
|
if ($scope.scenario) {
|
||||||
|
Charts.stack("#output-stack", $scope.scenario.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Scenario */
|
/* Scenario */
|
||||||
|
|
||||||
$scope.showScenario = function(nav_idx, scenario_idx) {
|
$scope.showScenario = function(nav_idx, scenario_idx) {
|
||||||
@ -165,7 +186,6 @@
|
|||||||
|
|
||||||
angular.element(document).ready(function () {
|
angular.element(document).ready(function () {
|
||||||
$scope.scenarios = ${data};
|
$scope.scenarios = ${data};
|
||||||
|
|
||||||
$scope.histogramOptions = [];
|
$scope.histogramOptions = [];
|
||||||
$scope.totalHistogramModel = {label:'', value:0};
|
$scope.totalHistogramModel = {label:'', value:0};
|
||||||
$scope.atomicHistogramModel = {label:'', value:0};
|
$scope.atomicHistogramModel = {label:'', value:0};
|
||||||
@ -231,79 +251,63 @@
|
|||||||
$scope.$digest()
|
$scope.$digest()
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
</script>
|
</%block>
|
||||||
<style>
|
|
||||||
body { margin:0 0 50px; padding:0; font-size:14px; font-family:Helvetica,Arial,sans-serif }
|
<%block name="css">
|
||||||
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 }
|
|
||||||
pre { padding:10px; font-size:13px; color:#333; background:#f5f5f5; border:1px solid #ccc; border-radius:4px }
|
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 { margin:0 20px 0 0; display:block; width:255px; float:left }
|
||||||
.aside div:first-child { border-radius:4px 4px 0 0 }
|
.aside div:first-child { border-radius:4px 4px 0 0 }
|
||||||
.aside div:last-child { border-radius:0 0 4px 4px }
|
.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 { 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-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: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 { list-style:outside none none; margin-bottom:0; padding-left:0; border-bottom:1px solid #ddd }
|
||||||
.tabs:after { clear:both }
|
.tabs:after { clear:both }
|
||||||
.tabs li { float:left; margin-bottom:-1px; display:block; position:relative }
|
.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 { 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 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 { height:300px }
|
||||||
.chart .chart-dropdown { float:right; margin:0 35px 0 }
|
.chart .chart-dropdown { float:right; margin:0 35px 0 }
|
||||||
.chart.lesser { padding:0; margin:0; float:left; width:40% }
|
.chart.lesser { padding:0; margin:0; float:left; width:40% }
|
||||||
.chart.larger { padding:0; margin:0; float:left; width:59% }
|
.chart.larger { padding:0; margin:0; float:left; width:59% }
|
||||||
|
|
||||||
.content-wrap { margin:0 auto; padding:0 5px }
|
.expandable { cursor:pointer }
|
||||||
.content-main { margin:0 5px; display:block; float:left }
|
|
||||||
|
|
||||||
.clearfix { clear:both }
|
.clearfix { clear:both }
|
||||||
.text-error { color:red }
|
|
||||||
.top-margin { margin-top:40px !important }
|
.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: 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: 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: 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: 1100px) { .content-wrap { width:1080px } .content-main { width:790px } }
|
||||||
@media only screen and (min-width: 1200px) { .content-wrap { width:1180px } .content-main { width:890px } }
|
@media only screen and (min-width: 1200px) { .content-wrap { width:1180px } .content-main { width:890px } }
|
||||||
</style>
|
</%block>
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="header">
|
<%block name="body_attr"> ng-controller="BenchmarkController"</%block>
|
||||||
<div class="content-wrap">
|
|
||||||
<a href="https://github.com/stackforge/rally">Rally</a>
|
|
||||||
<span>benchmark results</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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}}"
|
<div class="nav-group" title="{{n.cls}}"
|
||||||
ng-repeat-start="n in nav track by $index"
|
ng-repeat-start="n in nav track by $index"
|
||||||
ng-click="showNav(n.idx)"
|
ng-click="showNav(n.idx)"
|
||||||
ng-class="{active:n.idx == nav_idx}">
|
ng-class="{active:n.idx == nav_idx}">
|
||||||
<span ng-hide="n.idx == nav_idx">►</span>
|
<span ng-hide="n.idx == nav_idx">►</span>
|
||||||
<span ng-show="n.idx == nav_idx">▼</span>
|
<span ng-show="n.idx == nav_idx">▼</span>
|
||||||
{{n.cls}}
|
{{n.cls}}</div>
|
||||||
</div>
|
|
||||||
<div class="nav-item" title="{{m.name}}"
|
<div class="nav-item" title="{{m.name}}"
|
||||||
ng-show="n.idx == nav_idx"
|
ng-show="n.idx == nav_idx"
|
||||||
ng-class="{active:m.idx == scenario_idx}"
|
ng-class="{active:m.idx == scenario_idx}"
|
||||||
@ -312,9 +316,9 @@
|
|||||||
ng-repeat-end>{{m.name}}</div>
|
ng-repeat-end>{{m.name}}</div>
|
||||||
</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">
|
<ul class="tabs">
|
||||||
<li ng-repeat="tab in tabs"
|
<li ng-repeat="tab in tabs"
|
||||||
ng-class="{active:tab.id == tabId}"
|
ng-class="{active:tab.id == tabId}"
|
||||||
@ -329,21 +333,43 @@
|
|||||||
<script type="text/ng-template" id="overview">
|
<script type="text/ng-template" id="overview">
|
||||||
{{renderTotal()}}
|
{{renderTotal()}}
|
||||||
|
|
||||||
<h2>Table for task results</h2>
|
<div ng-show="scenario.sla.length">
|
||||||
<table class="striped lastrow">
|
<h2>Service-level agreement</h2>
|
||||||
|
<table class="striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th ng-repeat="i in scenario.table_cols track by $index">{{i}}</th>
|
<th>Criterion
|
||||||
|
<th>Detail
|
||||||
|
<th>Success
|
||||||
<tr>
|
<tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-class="{highlight:$last}" ng-repeat="row in scenario.table_rows track by $index">
|
<tr class="rich"
|
||||||
<td ng-repeat="i in row track by $index">{{i}}</td>
|
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}}
|
||||||
|
<tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<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>
|
<tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>Charts for the Total Duration</h2>
|
<h2>Charts for the Total durations</h2>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<svg id="total-stack"></svg>
|
<svg id="total-stack"></svg>
|
||||||
</div>
|
</div>
|
||||||
@ -364,7 +390,7 @@
|
|||||||
<script type="text/ng-template" id="details">
|
<script type="text/ng-template" id="details">
|
||||||
{{renderDetails()}}
|
{{renderDetails()}}
|
||||||
|
|
||||||
<h2>Charts for every Atomic Action</h2>
|
<h2>Charts for each Atomic Action</h2>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<svg id="atomic-stack"></svg>
|
<svg id="atomic-stack"></svg>
|
||||||
</div>
|
</div>
|
||||||
@ -382,6 +408,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</script>
|
</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">►</span>
|
||||||
|
<span ng-show="i.expanded">▼</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">
|
<script type="text/ng-template" id="config">
|
||||||
<h2>Scenario Configuration</h2>
|
<h2>Scenario Configuration</h2>
|
||||||
<pre>{{scenario.config}}</pre>
|
<pre>{{scenario.config}}</pre>
|
||||||
@ -389,8 +456,13 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
</%block>
|
||||||
|
|
||||||
</div>
|
<%block name="js_after">
|
||||||
|
if (! window.angular) {
|
||||||
</body>
|
document.getElementById("page-error").style.display = "block";
|
||||||
</html>
|
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
48
rally/ui/utils.py
Normal 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)
|
@ -45,7 +45,8 @@ rally show keypairs
|
|||||||
rally -v task start --task $SCENARIO
|
rally -v task start --task $SCENARIO
|
||||||
|
|
||||||
mkdir -p rally-plot/extra
|
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
|
cp $SCENARIO rally-plot/task.txt
|
||||||
tar -czf rally-plot/plugins.tar.gz -C $RALLY_PLUGINS_DIR .
|
tar -czf rally-plot/plugins.tar.gz -C $RALLY_PLUGINS_DIR .
|
||||||
rally task report --out rally-plot/results.html
|
rally task report --out rally-plot/results.html
|
||||||
|
@ -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>
|
|
61
tests/ci/rally-gate/index.mako
Normal file
61
tests/ci/rally-gate/index.mako
Normal 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>
|
@ -69,7 +69,7 @@ class TaskTestCase(unittest.TestCase):
|
|||||||
rally("task start --task %s" % config.filename)
|
rally("task start --task %s" % config.filename)
|
||||||
if os.path.exists(html_file):
|
if os.path.exists(html_file):
|
||||||
os.remove(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))
|
self.assertTrue(os.path.exists(html_file))
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
|
@ -22,31 +22,22 @@ from tests.unit import test
|
|||||||
|
|
||||||
|
|
||||||
class PlotTestCase(test.TestCase):
|
class PlotTestCase(test.TestCase):
|
||||||
@mock.patch("rally.benchmark.processing.plot.open", create=True)
|
@mock.patch("rally.benchmark.processing.plot.ui_utils")
|
||||||
@mock.patch("rally.benchmark.processing.plot.mako.template.Template")
|
|
||||||
@mock.patch("rally.benchmark.processing.plot.os.path.dirname")
|
|
||||||
@mock.patch("rally.benchmark.processing.plot._process_results")
|
@mock.patch("rally.benchmark.processing.plot._process_results")
|
||||||
def test_plot(self, mock_proc_results, mock_dirname, mock_template,
|
def test_plot(self, mock_proc_results, mock_utils):
|
||||||
mock_open):
|
mock_render = mock.Mock(return_value="plot_html")
|
||||||
mock_dirname.return_value = "abspath"
|
mock_utils.get_template = mock.Mock(
|
||||||
mock_open.return_value = mock_open
|
return_value=mock.Mock(render=mock_render))
|
||||||
mock_open.__enter__.return_value = mock_open
|
|
||||||
mock_open.read.return_value = "some_template"
|
|
||||||
|
|
||||||
templ = mock.MagicMock()
|
|
||||||
templ.render.return_value = "output"
|
|
||||||
mock_template.return_value = templ
|
|
||||||
mock_proc_results.return_value = [{"name": "a"}, {"name": "b"}]
|
mock_proc_results.return_value = [{"name": "a"}, {"name": "b"}]
|
||||||
|
|
||||||
result = plot.plot(["abc"])
|
result = plot.plot(["abc"])
|
||||||
|
|
||||||
self.assertEqual(result, templ.render.return_value)
|
self.assertEqual(result, "plot_html")
|
||||||
templ.render.assert_called_once_with(
|
mock_render.assert_called_once_with(
|
||||||
data=json.dumps(mock_proc_results.return_value)
|
data=json.dumps(mock_proc_results.return_value)
|
||||||
)
|
)
|
||||||
mock_template.assert_called_once_with(mock_open.read.return_value)
|
mock_utils.get_template.assert_called_once_with("task/report.mako")
|
||||||
mock_open.assert_called_once_with("%s/src/index.mako"
|
|
||||||
% mock_dirname.return_value)
|
|
||||||
|
|
||||||
@mock.patch("rally.benchmark.processing.plot._prepare_data")
|
@mock.patch("rally.benchmark.processing.plot._prepare_data")
|
||||||
@mock.patch("rally.benchmark.processing.plot._process_atomic")
|
@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_foo", "pos": 1, "kw": "config2"}},
|
||||||
{"key": {"name": "Klass.method_bar", "pos": 0, "kw": "config3"}}
|
{"key": {"name": "Klass.method_bar", "pos": 0, "kw": "config3"}}
|
||||||
]
|
]
|
||||||
table_cols = ["action",
|
table_cols = ["Action",
|
||||||
"min (sec)",
|
"Min (sec)",
|
||||||
"avg (sec)",
|
"Avg (sec)",
|
||||||
"max (sec)",
|
"Max (sec)",
|
||||||
"90 percentile",
|
"90 percentile",
|
||||||
"95 percentile",
|
"95 percentile",
|
||||||
"success",
|
"Success",
|
||||||
"count"]
|
"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_main_duration.return_value = "main_duration"
|
||||||
mock_atomic.return_value = "main_atomic"
|
mock_atomic.return_value = "main_atomic"
|
||||||
|
|
||||||
@ -90,7 +86,12 @@ class PlotTestCase(test.TestCase):
|
|||||||
"duration": mock_main_duration.return_value,
|
"duration": mock_main_duration.return_value,
|
||||||
"atomic": mock_atomic.return_value,
|
"atomic": mock_atomic.return_value,
|
||||||
"table_cols": table_cols,
|
"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):
|
def test__process_main_time(self):
|
||||||
@ -100,21 +101,26 @@ class PlotTestCase(test.TestCase):
|
|||||||
"error": [],
|
"error": [],
|
||||||
"duration": 1,
|
"duration": 1,
|
||||||
"idle_duration": 2,
|
"idle_duration": 2,
|
||||||
"atomic_actions": {}
|
"atomic_actions": {},
|
||||||
|
"scenario_output": {"errors": [], "data": {}}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"error": True,
|
"error": ["some", "error", "occurred"],
|
||||||
"duration": 1,
|
"duration": 1,
|
||||||
"idle_duration": 1,
|
"idle_duration": 1,
|
||||||
"atomic_actions": {}
|
"atomic_actions": {},
|
||||||
|
"scenario_output": {"errors": [], "data": {}}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"error": [],
|
"error": [],
|
||||||
"duration": 2,
|
"duration": 2,
|
||||||
"idle_duration": 3,
|
"idle_duration": 3,
|
||||||
"atomic_actions": {}
|
"atomic_actions": {},
|
||||||
|
"scenario_output": {"errors": [], "data": {}}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"sla": "foo_sla",
|
||||||
|
"duration": 12345.67
|
||||||
}
|
}
|
||||||
|
|
||||||
output = plot._process_main_duration(result,
|
output = plot._process_main_duration(result,
|
||||||
@ -168,26 +174,30 @@ class PlotTestCase(test.TestCase):
|
|||||||
"atomic_actions": {
|
"atomic_actions": {
|
||||||
"action1": 1,
|
"action1": 1,
|
||||||
"action2": 2
|
"action2": 2
|
||||||
}
|
},
|
||||||
|
"scenario_output": {"errors": [], "data": {}}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"error": ["some", "error", "occurred"],
|
"error": ["some", "error", "occurred"],
|
||||||
"atomic_actions": {
|
"atomic_actions": {
|
||||||
"action1": 1,
|
"action1": 1,
|
||||||
"action2": 2
|
"action2": 2
|
||||||
}
|
},
|
||||||
|
"scenario_output": {"errors": [], "data": {}}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"error": [],
|
"error": [],
|
||||||
"atomic_actions": {
|
"atomic_actions": {
|
||||||
"action1": 3,
|
"action1": 3,
|
||||||
"action2": 4
|
"action2": 4
|
||||||
}
|
},
|
||||||
|
"scenario_output": {"errors": [], "data": {}}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
data = {"atomic_durations": {
|
data = {
|
||||||
|
"atomic_durations": {
|
||||||
"action1": [(1, 1.0), (2, 0.0), (3, 3.0)],
|
"action1": [(1, 1.0), (2, 0.0), (3, 3.0)],
|
||||||
"action2": [(1, 2.0), (2, 0.0), (3, 4.0)]}}
|
"action2": [(1, 2.0), (2, 0.0), (3, 4.0)]}}
|
||||||
|
|
||||||
@ -270,11 +280,11 @@ class PlotTestCase(test.TestCase):
|
|||||||
def test__prepare_data(self, mock_compress):
|
def test__prepare_data(self, mock_compress):
|
||||||
|
|
||||||
mock_compress.side_effect = lambda i, **kv: i
|
mock_compress.side_effect = lambda i, **kv: i
|
||||||
rows_range = 100
|
rows_num = 100
|
||||||
limit = 10
|
total_duration = 12345.67
|
||||||
|
sla = [{"foo": "bar"}]
|
||||||
data = []
|
data = []
|
||||||
for i in range(rows_range):
|
for i in range(rows_num):
|
||||||
atomic_actions = {
|
atomic_actions = {
|
||||||
"a1": i + 0.1,
|
"a1": i + 0.1,
|
||||||
"a2": i + 0.8,
|
"a2": i + 0.8,
|
||||||
@ -284,32 +294,50 @@ class PlotTestCase(test.TestCase):
|
|||||||
"idle_duration": i * 0.2,
|
"idle_duration": i * 0.2,
|
||||||
"error": [],
|
"error": [],
|
||||||
"atomic_actions": atomic_actions,
|
"atomic_actions": atomic_actions,
|
||||||
|
"scenario_output": {"errors": ["err"],
|
||||||
|
"data": {"out_key": "out_value"}}
|
||||||
}
|
}
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
data[42]["error"] = "foo error"
|
data[42]["error"] = ["foo", "bar", "spam"]
|
||||||
data[52]["error"] = "bar error"
|
data[52]["error"] = ["spam", "bar", "foo"]
|
||||||
|
|
||||||
values_atomic_a1 = [i + 0.1 for i in range(rows_range)]
|
values_atomic_a1 = [i + 0.1 for i in range(rows_num)]
|
||||||
values_atomic_a2 = [i + 0.8 for i in range(rows_range)]
|
values_atomic_a2 = [i + 0.8 for i in range(rows_num)]
|
||||||
values_duration = [i * 3.1 for i in range(rows_range)]
|
values_duration = [i * 3.1 for i in range(rows_num)]
|
||||||
values_idle = [i * 0.2 for i in range(rows_range)]
|
values_idle = [i * 0.2 for i in range(rows_num)]
|
||||||
num_errors = 2
|
|
||||||
|
|
||||||
prepared_data = plot._prepare_data({"result": data},
|
prepared_data = plot._prepare_data({"result": data,
|
||||||
reduce_rows=limit)
|
"duration": total_duration,
|
||||||
self.assertEqual(num_errors, prepared_data["num_errors"])
|
"sla": sla,
|
||||||
|
"key": "foo_key"})
|
||||||
|
self.assertEqual(2, len(prepared_data["errors"]))
|
||||||
|
|
||||||
calls = [mock.call(values_atomic_a1, limit=limit),
|
calls = [mock.call(values_atomic_a1),
|
||||||
mock.call(values_atomic_a2, limit=limit),
|
mock.call(values_atomic_a2),
|
||||||
mock.call(values_duration, limit=limit),
|
mock.call(values_duration),
|
||||||
mock.call(values_idle, limit=limit)]
|
mock.call(values_idle)]
|
||||||
mock_compress.assert_has_calls(calls)
|
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({
|
self.assertEqual({
|
||||||
"total_durations": {"duration": values_duration,
|
"total_durations": {"duration": values_duration,
|
||||||
"idle_duration": values_idle},
|
"idle_duration": values_idle},
|
||||||
"atomic_durations": {"a1": values_atomic_a1,
|
"atomic_durations": {"a1": values_atomic_a1,
|
||||||
"a2": values_atomic_a2},
|
"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)
|
}, prepared_data)
|
||||||
|
@ -132,7 +132,7 @@ class ScenarioHelpersTestCase(test.TestCase):
|
|||||||
}
|
}
|
||||||
self.assertEqual(expected_result, result)
|
self.assertEqual(expected_result, result)
|
||||||
self.assertEqual(expected_error[:2],
|
self.assertEqual(expected_error[:2],
|
||||||
[str(Exception), "Something went wrong"])
|
["Exception", "Something went wrong"])
|
||||||
|
|
||||||
|
|
||||||
class ScenarioRunnerResultTestCase(test.TestCase):
|
class ScenarioRunnerResultTestCase(test.TestCase):
|
||||||
|
@ -134,28 +134,81 @@ class TaskCommandsTestCase(test.TestCase):
|
|||||||
self.task.detailed(test_uuid)
|
self.task.detailed(test_uuid)
|
||||||
mock_db.task_get_detailed.assert_called_once_with(test_uuid)
|
mock_db.task_get_detailed.assert_called_once_with(test_uuid)
|
||||||
|
|
||||||
@mock.patch('rally.cmd.commands.task.db')
|
@mock.patch("json.dumps")
|
||||||
@mock.patch('json.dumps')
|
@mock.patch("rally.cmd.commands.task.task.Task.get")
|
||||||
def test_results(self, mock_json, mock_db):
|
def test_results(self, mock_get, mock_json):
|
||||||
test_uuid = 'aa808c14-69cc-4faf-a906-97e05f5aebbd'
|
task_id = "foo_task_id"
|
||||||
value = [
|
data = [
|
||||||
{'key': 'key', 'data': {'raw': 'raw', 'sla': []}}
|
{"key": "foo_key", "data": {"raw": "foo_raw", "sla": []}}
|
||||||
]
|
]
|
||||||
result = map(lambda x: {"key": x["key"],
|
result = map(lambda x: {"key": x["key"],
|
||||||
"result": x["data"]["raw"],
|
"result": x["data"]["raw"],
|
||||||
"sla": x["data"]["sla"]}, value)
|
"sla": x["data"]["sla"]}, data)
|
||||||
mock_db.task_result_get_all_by_uuid.return_value = value
|
mock_results = mock.Mock(return_value=data)
|
||||||
self.task.results(test_uuid)
|
mock_get.return_value = mock.Mock(get_results=mock_results)
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('rally.cmd.commands.task.db')
|
self.task.results(task_id)
|
||||||
def test_invalid_results(self, mock_db):
|
mock_json.assert_called_once_with(result, sort_keys=True, indent=4)
|
||||||
test_uuid = 'd1f58069-d221-4577-b6ba-5c635027765a'
|
mock_get.assert_called_once_with(task_id)
|
||||||
mock_db.task_result_get_all_by_uuid.return_value = []
|
|
||||||
return_value = self.task.results(test_uuid)
|
@mock.patch("rally.cmd.commands.task.task.Task.get")
|
||||||
mock_db.task_result_get_all_by_uuid.assert_called_once_with(test_uuid)
|
def test_invalid_results(self, mock_get):
|
||||||
self.assertEqual(1, return_value)
|
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.common_cliutils.print_list')
|
||||||
@mock.patch('rally.cmd.commands.task.envutils.get_global')
|
@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.common_cliutils.print_list')
|
||||||
@mock.patch("rally.cmd.commands.task.db")
|
@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 = [{
|
value = [{
|
||||||
"key": {
|
"key": {
|
||||||
"name": "fake_name",
|
"name": "fake_name",
|
||||||
|
@ -114,6 +114,14 @@ class TaskTestCase(test.TestCase):
|
|||||||
{'verification_log': json.dumps({"a": "fake"})}
|
{'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')
|
@mock.patch('rally.objects.task.db.task_result_create')
|
||||||
def test_append_results(self, mock_append_results):
|
def test_append_results(self, mock_append_results):
|
||||||
task = objects.Task(task=self.task)
|
task = objects.Task(task=self.task)
|
||||||
|
0
tests/unit/ui/__init__.py
Normal file
0
tests/unit/ui/__init__.py
Normal file
38
tests/unit/ui/test_utils.py
Normal file
38
tests/unit/ui/test_utils.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user