[Reports] Significant improvements in verification report

* ability to show several verifications results (this
   deprecates command `rally verify compare')
 * command `rally verify compare' is removed - new report
   compares results in better way
 * new, unified, AngularJS/Jinja2-based template
 * new verifications results processing
 * new module rally.ui.report which cares about
   report generation (this is a place where code from
   rally.task.processing.plot should be also moved)

Examples:

  rally verify results\
    --uuid <uuid> --html > single_verification_result.html

  rally verify results\
    --uuid <uuid1> <uuid2> <uuid3> --html > compare_3.html

Co-Authored-By: Oleksandr Savatieiev <osavatieiev@mirantis.com>
Co-Authored-By: Alexander Maretskiy <amaretskiy@mirantis.com>
Change-Id: I942e0d9bf2094f3254dbeccbaa76dbbc3a3ca40e
This commit is contained in:
Oleksandr Savatieiev 2016-07-15 18:53:59 +03:00 committed by Alexander Maretskiy
parent d599de9a26
commit 70901577ab
16 changed files with 704 additions and 966 deletions

View File

@ -46,7 +46,7 @@ _rally()
OPTS["task_trends"]="--out --open --tasks"
OPTS["task_use"]="--uuid"
OPTS["task_validate"]="--deployment --task --task-args --task-args-file"
OPTS["verify_compare"]="--uuid-1 --uuid-2 --csv --html --json --output-file --threshold"
OPTS["verify_compare"]=""
OPTS["verify_detailed"]="--uuid --sort-by"
OPTS["verify_discover"]="--deployment --pattern --system-wide"
OPTS["verify_genconfig"]="--deployment --tempest-config --add-options --override"
@ -56,7 +56,7 @@ _rally()
OPTS["verify_list"]=""
OPTS["verify_listplugins"]="--deployment --system-wide"
OPTS["verify_reinstall"]="--deployment --source --version --system-wide"
OPTS["verify_results"]="--uuid --html --json --output-file"
OPTS["verify_results"]="--uuid --html --json --csv --output-file"
OPTS["verify_show"]="--uuid --sort-by --detailed"
OPTS["verify_showconfig"]="--deployment"
OPTS["verify_start"]="--deployment --set --regex --load-list --skip-list --tempest-config --xfail-list --no-use --system-wide --concurrency --failing"

View File

@ -15,8 +15,8 @@
"""Rally command: verify"""
import csv
import json
from __future__ import print_function
import os
import six
@ -31,9 +31,7 @@ from rally.common.i18n import _
from rally.common import utils
from rally import consts
from rally import exceptions
from rally.verification.tempest import diff
from rally.verification.tempest import json2html
from rally.ui import report
AVAILABLE_SETS = list(consts.TempestTestsSets) + list(consts.TempestTestsAPI)
@ -223,40 +221,88 @@ class VerifyCommands(object):
print(_("No verification was started yet. "
"To start verification use:\nrally verify start"))
@cliutils.args("--uuid", type=str, dest="verification",
help="UUID of a verification.")
@cliutils.args("--uuid", nargs="+", dest="uuids",
help="UUIDs of verifications.")
@cliutils.args("--html", action="store_true", dest="output_html",
help="Display results in HTML format.")
@cliutils.args("--json", action="store_true", dest="output_json",
help="Display results in JSON format.")
@cliutils.args("--csv", action="store_true", dest="output_csv",
help="Display results in CSV format")
@cliutils.args("--output-file", type=str, required=False,
dest="output_file", metavar="<path>",
help="Path to a file to save results to.")
@envutils.with_default_verification_id
@cliutils.suppress_warnings
def results(self, verification=None, output_file=None,
output_html=None, output_json=None):
def results(self, uuids=None, output_file=None,
output_html=False, output_json=False, output_csv=False):
"""Display results of a verification.
:param verification: UUID of a verification
:param output_file: Path to a file to save results
:param output_html: Display results in HTML format
:param output_json: Display results in JSON format (Default)
:param output_html: Display results in HTML format
:param output_csv: Display results in CSV format
"""
if not uuids:
uuid = envutils.get_global(envutils.ENV_VERIFICATION)
if not uuid:
raise exceptions.InvalidArgumentsException(
"Verification UUID is missing")
uuids = [uuid]
data = []
for uuid in uuids:
try:
results = api.Verification.get(verification).get_results()
verification = api.Verification.get(uuid)
except exceptions.NotFoundException as e:
print(six.text_type(e))
return 1
data.append(verification)
result = ""
if output_json + output_html > 1:
print(_("Please specify only one "
"output format: --json or --html."))
elif output_html:
result = json2html.generate_report(results)
if output_json + output_html + output_csv > 1:
print(_("Please specify only one format option from %s.")
% "--json, --html, --csv")
return 1
verifications = {}
for ver in data:
uuid = ver.db_object["uuid"]
res = ver.get_results() or {}
tests = {}
for test in list(res.get("test_cases", {}).values()):
name = test["name"]
if name in tests:
mesg = ("Duplicated test in verification "
"%(uuid)s: %(test)s" % {"uuid": uuid,
"test": name})
raise exceptions.RallyException(mesg)
tests[name] = {"tags": test["tags"],
"status": test["status"],
"duration": test["time"],
"details": (test.get("traceback", "").strip()
or test.get("reason"))}
verifications[uuid] = {
"tests": tests,
"duration": res.get("time", 0),
"total": res.get("tests", 0),
"skipped": res.get("skipped", 0),
"success": res.get("success", 0),
"expected_failures": res.get("expected_failures", 0),
"unexpected_success": res.get("unexpected_success", 0),
"failures": res.get("failures", 0),
"started_at": ver.db_object[
"created_at"].strftime("%Y-%d-%m %H:%M:%S"),
"finished_at": ver.db_object[
"updated_at"].strftime("%Y-%d-%m %H:%M:%S"),
"status": ver.db_object["status"],
"set_name": ver.db_object["set_name"]
}
if output_html:
result = report.VerificationReport(verifications).to_html()
elif output_csv:
result = report.VerificationReport(verifications).to_csv()
else:
result = json.dumps(results, sort_keys=True, indent=4)
result = report.VerificationReport(verifications).to_json()
if output_file:
output_file = os.path.expanduser(output_file)
@ -328,67 +374,13 @@ class VerifyCommands(object):
"""
self.show(verification, sort_by, True)
@cliutils.args("--uuid-1", type=str, required=True, dest="verification1",
help="UUID of the first verification")
@cliutils.args("--uuid-2", type=str, required=True, dest="verification2",
help="UUID of the second verification")
@cliutils.args("--csv", action="store_true", dest="output_csv",
help="Display results in CSV format")
@cliutils.args("--html", action="store_true", dest="output_html",
help="Display results in HTML format")
@cliutils.args("--json", action="store_true", dest="output_json",
help="Display results in JSON format")
@cliutils.args("--output-file", type=str, required=False,
dest="output_file", help="Path to a file to save results")
@cliutils.args("--threshold", type=int, required=False,
dest="threshold", default=0,
help="If specified, timing differences must exceed this "
"percentage threshold to be included in output")
def compare(self, verification1=None, verification2=None,
output_file=None, output_csv=None, output_html=None,
output_json=None, threshold=0):
"""Compare two verification results.
:param verification1: UUID of the first verification
:param verification2: UUID of the second verification
:param output_file: Path to a file to save results
:param output_csv: Display results in CSV format
:param output_html: Display results in HTML format
:param output_json: Display results in JSON format (Default)
:param threshold: Timing difference threshold percentage
"""
try:
res_1 = api.Verification.get(
verification1).get_results()["test_cases"]
res_2 = api.Verification.get(
verification2).get_results()["test_cases"]
_diff = diff.Diff(res_1, res_2, threshold)
except exceptions.NotFoundException as e:
print(six.text_type(e))
def compare(self, *args, **kwargs):
"""Deprecated."""
# NOTE(amaretskiy): this command is deprecated in favor of
# improved 'rally task results'
print("This command is deprecated. Use 'rally task results' instead.")
return 1
result = ""
if output_json + output_html + output_csv > 1:
print(_("Please specify only one output "
"format: --json, --html or --csv."))
return 1
elif output_html:
result = _diff.to_html()
elif output_csv:
result = _diff.to_csv()
else:
result = _diff.to_json()
if output_file:
with open(output_file, "wb") as f:
if output_csv:
writer = csv.writer(f, dialect="excel")
writer.writerows(result)
else:
f.write(result)
else:
print(result)
@cliutils.args("--uuid", type=str, dest="verification",
required=False, help="UUID of a verification")
def use(self, verification):

96
rally/ui/report.py Normal file
View File

@ -0,0 +1,96 @@
# Copyright 2016: Mirantis Inc.
# All Rights Reserved.
#
# Author: Oleksandr Savatieiev osavatieiev@mirantis.com
#
# 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 csv
import io
import json
import re
from jinja2 import utils as jinja_utils
from rally.ui import utils
class VerificationReport(object):
SKIP_RE = re.compile(
"Skipped until Bug: ?(?P<bug_number>\d+) is resolved.")
LP_BUG_LINK = "<a href='https://launchpad.net/bugs/{0}'>{0}</a>"
def __init__(self, verifications):
self._runs = verifications
self._uuids = list(verifications.keys())
# NOTE(amaretskiy): make aggregated list of all tests
tests = {}
for uuid, verification in self._runs.items():
for name, test in verification["tests"].items():
if name not in tests:
# NOTE(amaretskiy): it is suitable to see resource id
# at first place in the report
tags = sorted(test["tags"], reverse=True,
key=lambda tag: tag.startswith("id-"))
tests[name] = {"name": name,
"tags": tags,
"by_verification": {},
"has_details": False}
tests[name]["by_verification"][uuid] = {
"status": test["status"], "duration": test["duration"],
"details": test["details"]}
if test["details"]:
tests[name]["has_details"] = True
match = self.SKIP_RE.match(test["details"])
if match:
href = self.LP_BUG_LINK.format(
match.group("bug_number"))
test["details"] = re.sub(
match.group("bug_number"), href, test["details"])
test["details"] = jinja_utils.escape(test["details"])
self._tests = list(tests.values())
def to_html(self):
"""Make HTML report."""
template = utils.get_template("verification/report.html")
context = {"uuids": self._uuids, "verifications": self._runs,
"tests": self._tests}
return template.render(data=json.dumps(context), include_libs=False)
def to_json(self, indent=4):
"""Make JSON report."""
return json.dumps(self._tests, indent=indent)
def to_csv(self, **kwargs):
"""Make CSV report."""
header = ["test name", "tags", "has errors"]
for uuid in self._uuids:
header.extend(["%s status" % uuid, "%s duration" % uuid])
rows = [header]
for test in self._tests:
row = [test["name"], " ".join(test["tags"])]
for uuid in self._uuids:
if uuid not in test["by_verification"]:
row.extend([None, None])
continue
row.append(test["by_verification"][uuid]["status"])
row.append(test["by_verification"][uuid]["duration"])
rows.append(row)
with io.BytesIO() as stream:
csv.writer(stream, **kwargs).writerows(rows)
return stream.getvalue()

View File

@ -24,6 +24,7 @@
table.compact td { padding:4px 8px }
table.striped tr:nth-child(odd) td { background:#f9f9f9 }
table.linked tbody tr:hover { background:#f9f9f9; cursor:pointer }
.pointer { cursor:pointer }
.rich, .rich td { font-weight:bold }
.code { padding:10px; font-size:13px; color:#333; background:#f6f6f6; border:1px solid #e5e5e5; border-radius:4px }
@ -36,20 +37,20 @@
.status-fail, .status-fail td { color:red }
.capitalize { text-transform:capitalize }
{% block css %}{% endblock %}
.content-wrap { {% block css_content_wrap %}{% endblock %} margin:0 auto; padding:0 5px }
.content-wrap { margin:0 auto; padding:0 5px; {% block css_content_wrap %}{% endblock %} }
{% block media_queries %}{% endblock %}
</style>
</head>
<body{% block body_attr %}{% endblock %}>
<div class="header">
<div class="header" id="page-header">
<div class="content-wrap">
<a href="https://github.com/openstack/rally">Rally</a>&nbsp;
<span>{% block header_text %}{% endblock %}</span>
</div>
</div>
<div class="content-wrap">
<div class="content-wrap" id="page-content">
{% block content %}{% endblock %}
</div>

View File

@ -1,165 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!doctype html>
<html>
<head>
<title>${heading["title"]}</title>
<meta name="generator" content="${generator}">
<meta charset="utf-8">
<script type="text/javascript">
var DOWN = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAGCAYAAAAVMmT4AAAAJUlEQVQYlWNgYGD4TwJmYCBFIYYGFhYWvArx2YAXEK0QWQMGAADd8SPpeGzm9QAAAABJRU5ErkJggg==";
var NONE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAGCAYAAAAVMmT4AAAADUlEQVQYlWNgGAUIAAABDgAB6WzgmwAAAABJRU5ErkJggg==";
var UP = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAGCAYAAAAVMmT4AAAAK0lEQVQYlWNgwA7+4xDHqhCGiVaIVwNcAQsLC14N2EzEqoEYhf8ZGBj+AwCZbyPp8zIdEAAAAABJRU5ErkJggg==";
function sort_table(table_id, col, sort){
var table = document.getElementById(table_id);
var tbody = table.tBodies[0];
var header_row = table.tHead.rows[0];
render_header(col, sort, header_row);
sort_results(tbody, col, sort);
}
function render_header(col, sort, header_row){
var h_cells = header_row.cells;
for(i = 0; i < h_cells.length; i++){
var cell = h_cells[i];
var img = cell.firstElementChild;
if (i == col){
if (sort == 1){
img.src = UP;
}else{
img.src = DOWN;
}
}else{ //spacer image
img.src = NONE;
}
}
}
function sort_results(tbody, col, sort) {
var rows = tbody.rows, rlen = rows.length, arr = new Array(), i, j, cells, clen;
// fill the array with values from the table
for(i = 0; i < rlen; i++){
cells = rows[i].cells;
clen = cells.length;
arr[i] = new Array();
for(j = 0; j < clen; j++){
arr[i][j] = cells[j].innerHTML;
}
}
// sort the array by the specified column number (col) and order (sort)
arr.sort(function(a, b){
return (a[col] == b[col]) ? 0 : ((a[col] > b[col]) ? sort : -1*sort);
});
for(i = 0; i < rlen; i++){
arr[i] = "<td>"+arr[i].join("</td><td>")+"</td>";
}
tbody.innerHTML = "<tr>"+arr.join("</tr><tr>")+"</tr>";
}
</script>
<style type="text/css" media="screen">
body {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 80%;
}
table {
font-size: 100%; width: 100%;
}
h1 {
font-size: 16pt;
color: gray;
}
.heading {
margin-top: 0ex;
margin-bottom: 1ex;
}
.heading .attribute {
margin-top: 1ex;
margin-bottom: 0;
}
.heading .description {
margin-top: 4ex;
margin-bottom: 6ex;
}
#results_table {
width: 100%;
border-collapse: collapse;
border: 1px solid #777;
}
#header_row {
font-weight: bold;
color: white;
background-color: #777;
}
#results_table td {
border: 1px solid #777;
padding: 2px;
}
.testcase { margin-left: 2em;}
img.updown{
padding-left: 3px;
padding-bottom: 2px;
}
th:hover{
cursor:pointer;
}
.nowrap {white-space: nowrap;}
</style>
</head>
<body>
<div class="heading">
<h1>${heading["title"]}</h1>
% for name, value in heading["parameters"]:
<p class="attribute"><strong>${name}:</strong> ${value}</p>
% endfor
<p class="description">${heading["description"]}</p>
</div>
<table id="results_table">
<colgroup>
<col align="left" />
<col align="left" />
<col align="left" />
<col align="left" />
<col align="left" />
</colgroup>
<thead>
<tr id="header_row">
<th class="nowrap" onclick="sort_table('results_table', 0, col1_sort); col1_sort *= -1; col2_sort = 1; col3_sort = 1; col4_sort = 1; col5_sort = 1;">Type<img class="updown" src=NONE /></th>
<th class="nowrap" onclick="sort_table('results_table', 1, col2_sort); col2_sort *= -1; col1_sort = 1; col3_sort = 1; col4_sort = 1; col5_sort = 1;">Field<img class="updown" src=NONE /></th>
<th class="nowrap" onclick="sort_table('results_table', 2, col3_sort); col3_sort *= -1; col1_sort = 1; col2_sort = 1; col4_sort = 1; col5_sort = 1;">Value 1<img class="updown" src=NONE /></th>
<th class="nowrap" onclick="sort_table('results_table', 3, col4_sort); col4_sort *= -1; col1_sort = 1; col2_sort = 1; col3_sort = 1; col5_sort = 1;">Value 2<img class="updown" src=NONE /></th>
<th onclick="sort_table('results_table', 4, col5_sort); col5_sort *= -1; col1_sort = 1; col2_sort = 1; col3_sort = 1; col4_sort = 1;">Test Name<img class="updown" src=NONE /></th>
</tr>
</thead>
<tbody id="results">
% for diff in results:
<tr class="">
<td class="type">${diff.get("type")}</td>
<td class="field">${diff.get("field", "")}</td>
<td class="val">${diff.get("val1", "")}</td>
<td class="val">${diff.get("val2", "")}</td>
<td class="testname">${diff.get("test_name")}</td>
</tr>
% endfor
</table>
<script type="text/javascript">
var col1_sort = 1, col2_sort = 1, col3_sort = 1; col4_sort = 1; col5_sort = 1;
sort_table("results_table", 4, col5_sort);
col5_sort *= -1;
</script>
</body>
</html>

View File

@ -0,0 +1,321 @@
{% extends "/base.html" %}
{% block html_attr %} ng-app="App" ng-controller="Controller" id="page-html"{% endblock %}
{% block title_text %}{% raw %}{{title}}{% endraw %}{% endblock %}
{% block libs %}
{% if include_libs %}
<script type="text/javascript">
{{ include_raw_file("/libs/angular.1.3.3.min.js") }}
</script>
{% else %}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.min.js"></script>
{% endif %}
{% endblock %}
{% block js_before %}
"use strict";
{{ include_raw_file("/task/directive_widget.js") }}
var controllerFunction = function($scope, $location) {
$scope.data = {{ data }};
/* Calculate columns width in percent */
var td_ctr_width = 4;
var td_result_width = Math.round(1 / ($scope.data.uuids.length+3) * 100);
$scope.td_width_ = {
counter: td_ctr_width,
test_name: (100 - td_ctr_width - (td_result_width * $scope.data.uuids.length)),
test_result: td_result_width
}
$scope.td_width = (function(vers_num) {
var uuid_w = Math.round(1 / (vers_num+3) * 100);
return {test: 100 - (uuid_w * vers_num),
uuid: uuid_w}
})($scope.data.uuids.length)
var bitmask = {"success": 1,
"x-fail": 2,
"skip": 4,
"ux-ok": 8,
"fail": 16};
for (var i in $scope.data.tests) {
var t = $scope.data.tests[i];
var bits = 0;
for (var uuid in t.by_verification) {
var status = t.by_verification[uuid].status;
if (status in bitmask) {
bits |= bitmask[status]
}
$scope.data.tests[i].by_verification[uuid].show_duration = (
t.by_verification[uuid].duration > 0.0001)
}
$scope.data.tests[i].filter = bits;
}
$scope.set_filter = function(status) {
if (status in $scope.state) {
$scope.state[status] = !$scope.state[status];
$scope.filter_bits ^= bitmask[status]
}
}
$scope.state = {"success": true,
"x-fail": true,
"skip": true,
"ux-ok": true,
"fail": true};
$scope.filter_by_status = function(test, index, arr) {
return test.filter & $scope.filter_bits
}
$scope.filter_bits = (function(filter){
var bits = 0;
for (var status in $scope.state){
if ($scope.state[status]) { bits ^= bitmask[status] }
}
return bits
})();
$scope.toggle_filters_flag = true;
$scope.toggle_filters = function() {
if ($scope.toggle_filters_flag) {
$scope.toggle_filters_flag = false;
$scope.state = {"success": false,
"x-fail": false,
"skip": false,
"ux-ok": false,
"fail": false};
$scope.filter_bits = 0
} else {
$scope.toggle_filters_flag = true
$scope.state = {"success": true,
"x-fail": true,
"skip": true,
"ux-ok": true,
"fail": true};
$scope.filter_bits = 31
}
}
var title = "verification result";
if ($scope.data.uuids.length > 1) {
title = "verifications results"
}
$scope.title = title;
$scope.srt_dir = false;
$scope.get_tests_count = function() {
var ctr = 0;
for (var i in $scope.data.tests) {
if ($scope.data.tests[i].filter & $scope.filter_bits) {
ctr++
}
}
return ctr
}
var title = angular.element(document.getElementById("page-header"));
var header = angular.element(document.getElementById("content-header"));
var tests = angular.element(document.getElementById("tests"));
var sync_positions = function() {
var title_h = title[0].offsetHeight;
var header_h = header[0].offsetHeight;
header.css({top:title_h+"px"})
tests.css({"margin-top": (title_h+header_h)+"px"});
}
/* Make page head sticky */
window.onload = function() {
title.css({position:"fixed", top:0, width:"100%"});
header.css({position:"fixed", width:"100%", background:"#fff"});
sync_positions();
window.onresize = sync_positions;
var gotop = document.getElementById("button-gotop");
gotop.onclick = function () { scrollTo(0, 0) };
window.onscroll = function() {
if (window.scrollY > 50) {
gotop.style.display = "block";
} else {
gotop.style.display = "none";
}
}
}
$scope.toggle_header = (function(e) {
return function() {
e.style.display = (e.style.display === "none") ? "table" : "none";
sync_positions()
}
})(document.getElementById("verifications"))
};
if (typeof angular === "object") {
angular.module("App", [])
.controller("Controller", ["$scope", "$location", controllerFunction])
.directive("widget", widgetDirective)
}
{% endblock %}
{% block css %}
div.header {margin:0 !important}
div.header .content-wrap { padding-left:10px }
.status.status-success { background: #cfc; color: #333 }
.status.status-ux-ok { background: #ffd7af; color: #333 }
.status.status-fail { background: #fbb; color: #333 }
.status.status-x-fail { background: #ccf5ff; color: #333 }
.status.status-skip { background: #ffb; color: #333 }
.status.checkbox { font-size:18px; text-align:center; cursor:pointer; padding:0 }
.column { display:block; float:left; padding:4px 0 4px 8px; box-sizing:border-box;
background:#fff; font-size:12px; font-weight:bold;
border:#ccc solid; border-width:0 0 1px }
.button { margin:0 5px; padding:0 8px 1px; background:#47a; color:#fff; cursor:pointer;
border:1px #036 solid; border-radius:11px; font-size:12px; font-weight:normal;
line-height:12px; opacity:.8}
.button:hover { opacity:1 }
#button-gotop { padding:3px 10px 5px; text-align:center; cursor:pointer;
background:#fff; color:#036; line-height:14px; font-size:14px;
position:fixed; bottom:0; right:10px;
border:#ccc solid; border-width:1px 1px 0; border-radius:15px 15px 0 0}
{% endblock %}
{% block css_content_wrap %}width:100%; padding:0{% endblock %}
{% block body_attr %} id="page-body" style="position:relative"{% endblock %}
{% block header_text %}{% raw %}{{title}}{% endraw %}{% endblock %}
{% block content %}
{% raw %}
<h3 ng-hide="true" style="padding-left:10px">processing ...</h3>
<div id="content-header" ng-cloak>
<table class="compact" id="verifications"
style="border:#fff solid; border-width:2px 0 15px; margin:0">
<thead>
<tr>
<th>UUID
<th>Set name
<th>Status
<th>Started at
<th>Duration, s
<th>Total tests
<th style="width:9%">success
<th style="width:9%">expected failures
<th style="width:9%">skipped
<th style="width:9%">unexpected success
<th style="width:9%">failures
</tr>
</thead>
<tbody>
<tr ng-repeat="uuid in data.uuids">
<td>{{uuid}}
<td>{{data.verifications[uuid].set_name}}
<td>{{data.verifications[uuid].status}}
<td>{{data.verifications[uuid].started_at}}
<td>{{data.verifications[uuid].duration}}
<td>{{data.verifications[uuid].total}}
<td class="status status-success">{{data.verifications[uuid].success}}
<td class="status status-x-fail">{{data.verifications[uuid].expected_failures}}
<td class="status status-skip">{{data.verifications[uuid].skipped}}
<td class="status status-ux-ok">{{data.verifications[uuid].unexpected_success}}
<td class="status status-fail">{{data.verifications[uuid].failures}}
</tr>
<tr>
<td colspan="6" style="text-align:right; font-weight:bold">
<span ng-click="toggle_filters()" class="button" style="margin-right:10px">
toggle all filters
</span>
Filter tests by status:
<td class="checkbox status status-success" ng-click="set_filter('success')">
<span ng-hide="state.success">&#x2610;</span>
<span ng-show="state.success">&#x2611;</span>
<td class="checkbox status status-x-fail" ng-click="set_filter('x-fail')">
<span ng-hide="state['x-fail']">&#x2610;</span>
<span ng-show="state['x-fail']">&#x2611;</span>
<td class="checkbox status status-skip" ng-click="set_filter('skip')">
<span ng-hide="state.skip">&#x2610;</span>
<span ng-show="state.skip">&#x2611;</span>
<td class="checkbox status status-ux-ok" ng-click="set_filter('ux-ok')">
<span ng-hide="state['ux-ok']">&#x2610;</span>
<span ng-show="state['ux-ok']">&#x2611;</span>
<td class="checkbox status status-fail" ng-click="set_filter('fail')">
<span ng-hide="state.fail">&#x2610;</span>
<span ng-show="state.fail">&#x2611;</span>
</tr>
</tbody>
</table>
<div style="text-align:left; padding:6px 3px; background:#fff">
<span class="button" ng-click="show_tags=!show_tags">
toggle tags
</span>
<span class="button" ng-click="toggle_header()">
toggle header
</span>
</div>
<div style="clear:both"></div>
<div style="width:{{td_width.test}}%" class="column">
<span ng-click="srt_dir=!srt_dir" class="pointer">
Test name
<span style="color:#777">(shown {{get_tests_count()}})</span>
<span style="color:orange">
<span ng-hide="srt_dir">&#x25be;</span>
<span ng-show="srt_dir">&#x25b4;</span>
</span>
</span>
</div>
<div ng-repeat="uuid in data.uuids"
class="column"
style="width:{{td_width.uuid}}%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis">
{{uuid}}
</div>
<div style="clear:both"></div>
</div>
<table class="compact" id="tests" style="margin:0; width:100%" ng-cloak>
<tbody ng-repeat="t in data.tests | orderBy:'name':srt_dir track by $index" ng-show="filter_by_status(t)">
<tr ng-click="t.expanded=!t.expanded" ng-class="{pointer:t.has_details}">
<td style="width:{{td_width.test}}%; word-break:break-all">
{{t.name}}
<div ng-show="show_tags" style="font-size:12px; color:#999; word-break:normal">
<span ng-repeat="tag in t.tags"> {{tag}}</span>
</div>
<td ng-repeat="uuid in data.uuids"
class="status status-{{t.by_verification[uuid].status}}"
style="width:{{td_width.uuid}}%">
<div ng-if="t.by_verification[uuid]">
{{t.by_verification[uuid].status}}
<span ng-if="t.by_verification[uuid].show_duration">{{t.by_verification[uuid].duration}}</span>
</div>
<div ng-if="!t.by_verification[uuid]" style="color:#999">
&ndash;
</div>
</tr>
<tr ng-if="t.has_details" ng-show="t.expanded" style="width:100%">
<td colspan="{{3+data.uuids.length}}" style="padding:0">
<div ng-repeat="uuid in data.uuids" ng-if="t.by_verification[uuid].details"
class="status status-{{t.by_verification[uuid].status}}"
style="padding:5px">
<div style="font-weight:bold; color:#333">{{uuid}}</div>
<pre style="text-overflow:hidden">{{t.by_verification[uuid].details}}</pre>
</div>
</tr>
</tbody>
</table>
<span id="button-gotop" style="display:none">go top</span>
{% endraw %}
{% endblock %}

View File

@ -1,129 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/base.mako"/>
<%block name="title_text">Tempest report</%block>
<%block name="libs">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</%block>
<%block name="css">
.test-details-row { display:none }
.test-details { font-family:monospace; white-space:pre; overflow:auto }
.test-expandable { cursor:pointer }
.test-expandable:hover { background:#f3f3f3 }
.nav { margin: 15px 0 }
.nav span { padding:1px 15px; margin:0 2px 0 0; cursor:pointer; background:#f3f3f3;
color: black; font-size:12px; border:2px #ddd solid; border-radius:10px }
.nav span.active { background:#cfe3ff; border-color:#ace; color:#369 }
table td { padding:4px 8px; word-wrap:break-word; word-break:break-all }
table.stat { width:auto; margin:0 0 15px }
td.not_break_column {word-break:keep-all}
.status-success, .status-success td { color:green }
.status-uxsuccess, .status-uxsuccess td { color:orange }
.status-xfail, .status-xfail td { color:#CCCC00}
</%block>
<%block name="css_content_wrap">
margin:0 auto; padding:0 5px
</%block>
<%block name="media_queries">
@media only screen and (min-width: 300px) { .content-wrap { width:370px } .test-details { width:360px } }
@media only screen and (min-width: 500px) { .content-wrap { width:470px } .test-details { width:460px } }
@media only screen and (min-width: 600px) { .content-wrap { width:570px } .test-details { width:560px } }
@media only screen and (min-width: 700px) { .content-wrap { width:670px } .test-details { width:660px } }
@media only screen and (min-width: 800px) { .content-wrap { width:770px } .test-details { width:760px } }
@media only screen and (min-width: 900px) { .content-wrap { width:870px } .test-details { width:860px } }
@media only screen and (min-width: 1000px) { .content-wrap { width:970px } .test-details { width:960px } }
@media only screen and (min-width: 1200px) { .content-wrap { width:auto } .test-details { width:94% } }
</%block>
<%block name="header_text">Tempest Report</%block>
<%block name="content">
<p id="page-error" class="notify-error" style="display:none">Failed to load jQuery</p>
<table class="stat">
<thead>
<tr>
<th>Total
<th>Total Time
<th>Success
<th>Fails
<th>Unexpected Success
<th>Expected Fails
<th>Skipped
</tr>
</thead>
<tbody>
<tr>
<td>${report['total']}
<td>${report['time']}
<td>${report['success']}
<td>${report['failures']}
<td>${report['unexpected_success']}
<td>${report['expected_failures']}
<td>${report['skipped']}
</tr>
</tbody>
</table>
<div class="nav">
<span data-navselector=".test-row">all</span>
<span data-navselector=".status-success">success</span>
<span data-navselector=".status-fail">failed</span>
<span data-navselector=".status-uxsuccess">uxsuccess</span>
<span data-navselector=".status-xfail">xfailed</span>
<span data-navselector=".status-skip">skipped</span>
</div>
<table id="tests">
<thead>
<tr>
<th>Status
<th>Time
<th colspan="5">Test Case
<tr>
</thead>
<tbody>
% for test in report['tests']:
<tr id="${test['id']}" class="test-row status-${test['status']}">
<td class="not_break_column">${test['status']}
<td class="not_break_column">${test['time']}
<td colspan="5">${test['name']}
</tr>
% if 'output' in test:
<tr class="test-details-row">
<td colspan="6"><div class="test-details">${test['output'] | n}</div>
</tr>
% endif
% endfor
</tbody>
</table>
</%block>
<%block name="js_after">
if (typeof $ === "undefined") {
/* If jQuery loading has failed */
document.getElementById("page-error").style.display = "block"
} else {
$(function(){
$(".test-details-row")
.prev()
.addClass("test-expandable")
.click( function(){ $(this).next().toggle() });
(function($navs) {
$navs.click(function(){
var $this = $(this);
$navs.removeClass("active").filter($this).addClass("active");
$("#tests tbody tr").hide().filter($this.attr("data-navselector")).show();
}).first().click()
}($(".nav [data-navselector]")));
})
}
</%block>

View File

@ -1,35 +0,0 @@
# 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.
"""Output verification comparison results in html."""
from rally.ui import utils as ui_utils
__description__ = "List differences between two verification runs"
__title__ = "Verification Comparison"
__version__ = "0.1"
def create_report(results):
template_kw = {
"heading": {
"title": __title__,
"description": __description__,
"parameters": [("Difference Count", len(results))]
},
"generator": "compare2html %s" % __version__,
"results": results
}
template = ui_utils.get_template("verification/compare.mako")
output = template.render(**template_kw)
return output.encode("utf8")

View File

@ -1,105 +0,0 @@
# Copyright 2014 Dell Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from rally.verification.tempest import compare2html
class Diff(object):
def __init__(self, test_cases1, test_cases2, threshold):
"""Compare two verification results.
Compares two verification results and emits
desired output, csv, html, json or pprint.
:param test_cases1: older verification json
:param test_cases2: newer verification json
:param threshold: test time difference percentage threshold
"""
self.threshold = threshold
self.diffs = self._compare(test_cases1, test_cases2)
def _compare(self, tc1, tc2):
"""Compare two verification results.
:param tc1: first verification test cases json
:param tc2: second verification test cases json
Typical test case json schema:
"test_case_key": {
"traceback": "", # exists only for "fail" status
"reason": "", # exists only for "skip" status
"name": "",
"status": "",
"time": 0.0
}
"""
diffs = []
names1 = set(tc1.keys())
names2 = set(tc2.keys())
common_tests = list(names1.intersection(names2))
removed_tests = list(names1.difference(common_tests))
new_tests = list(names2.difference(common_tests))
for name in removed_tests:
diffs.append({"type": "removed_test", "test_name": name})
for name in new_tests:
diffs.append({"type": "new_test", "test_name": name})
for name in common_tests:
diffs.extend(self._diff_values(name, tc1[name], tc2[name]))
return diffs
def _diff_values(self, name, result1, result2):
fields = ["status", "time", "traceback", "reason"]
diffs = []
for field in fields:
val1 = result1.get(field, 0)
val2 = result2.get(field, 0)
if val1 != val2:
if field == "time":
max_ = max(float(val1), float(val2))
min_ = min(float(val1), float(val2))
time_threshold = ((max_ - min_) / (min_ or 1)) * 100
if time_threshold < self.threshold:
continue
diffs.append({
"field": field,
"type": "value_changed",
"test_name": name,
"val1": val1,
"val2": val2
})
return diffs
def to_csv(self):
rows = (("Type", "Field", "Value 1", "Value 2", "Test Name"),)
for res in self.diffs:
row = (res.get("type"), res.get("field", ""),
res.get("val1", ""), res.get("val2", ""),
res.get("test_name"))
rows = rows + (row,)
return rows
def to_json(self):
return json.dumps(self.diffs, sort_keys=True, indent=4)
def to_html(self):
return compare2html.create_report(self.diffs)

View File

@ -1,61 +0,0 @@
# 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 datetime as dt
import re
from jinja2 import utils
from rally.ui import utils as ui_utils
SKIP_RE = re.compile("Skipped until Bug: ?(?P<bug_number>\d+) is resolved.")
LAUNCHPAD_BUG_LINK = "<a href='https://launchpad.net/bugs/{0}'>{0}</a>"
def generate_report(results):
"""Generates HTML report from test results in JSON format."""
tests = []
for i, name in enumerate(sorted(results["test_cases"])):
test = results["test_cases"][name]
output = ""
if "reason" in test:
output += "Reason:\n "
matcher = SKIP_RE.match(test["reason"])
if matcher:
href = LAUNCHPAD_BUG_LINK.format(matcher.group("bug_number"))
output += re.sub(matcher.group("bug_number"), href,
test["reason"])
else:
output += utils.escape(test["reason"])
if "traceback" in test:
if output:
output += "\n\n"
output += utils.escape(test["traceback"])
tests.append({"id": i,
"time": test["time"],
"name": name,
"output": output,
"status": test["status"]})
template = ui_utils.get_template("verification/report.mako")
return template.render(report={
"tests": tests,
"total": results["tests"],
"time": "{0} ({1} s)".format(
dt.timedelta(seconds=round(
float(results["time"]))), results["time"]),
"success": results["success"],
"failures": results["failures"],
"skipped": results["skipped"],
"expected_failures": results["expected_failures"],
"unexpected_success": results["unexpected_success"]})

View File

@ -140,7 +140,7 @@ def do_compare(uuid_1, uuid_2):
"""Compare and save results in different formats."""
results = {}
for output_format in ("csv", "html", "json"):
cmd = "verify compare --uuid-1 %(uuid-1)s --uuid-2 %(uuid-2)s" % {
cmd = "verify results --uuid %(uuid-1)s %(uuid-2)s" % {
"uuid-1": uuid_1,
"uuid-2": uuid_2
}

View File

@ -17,6 +17,7 @@ import datetime as dt
import os.path
import tempfile
import ddt
import mock
from rally.cli.commands import verify
@ -25,6 +26,7 @@ from rally import exceptions
from tests.unit import test
@ddt.ddt
class VerifyCommandsTestCase(test.TestCase):
def setUp(self):
super(VerifyCommandsTestCase, self).setUp()
@ -266,16 +268,42 @@ class VerifyCommandsTestCase(test.TestCase):
mock_print_list.call_args_list)
mock_verification.get.assert_called_once_with(verification_id)
@ddt.data({"uuids": ["foo_uuid"], "expected_uuid": "foo_uuid"},
{"uuids": None, "expected_uuid": "DFLT_UUID"})
@ddt.unpack
@mock.patch("rally.cli.commands.verify.envutils")
@mock.patch("rally.api.Verification")
@mock.patch("json.dumps")
def test_results(self, mock_json_dumps, mock_verification):
def test_results(self, mock_json_dumps, mock_verification, mock_envutils,
uuids, expected_uuid):
mock_envutils.get_global.return_value = "DFLT_UUID"
mock_verification.get.return_value.get_results.return_value = {}
verification_uuid = "a0231bdf-6a4e-4daf-8ab1-ae076f75f070"
self.verify.results(verification_uuid, output_html=False,
self.verify.results(uuids, output_html=False,
output_json=True)
mock_verification.get.assert_called_once_with(verification_uuid)
mock_json_dumps.assert_called_once_with({}, sort_keys=True, indent=4)
mock_verification.get.assert_called_once_with(expected_uuid)
mock_json_dumps.assert_called_once_with([], indent=4)
if uuids:
self.assertFalse(mock_envutils.get_global.called)
else:
mock_envutils.get_global.assert_called_once_with(
mock_envutils.ENV_VERIFICATION)
@mock.patch("rally.api.Verification")
def test_results_many_formats_given(self, mock_verification):
mock_verification.get.return_value.get_results.return_value = {}
self.assertEqual(1, self.verify.results(
["foo_uuid"], output_html=True, output_json=True))
mock_verification.get.assert_called_once_with("foo_uuid")
@mock.patch("rally.api.Verification")
def test_results_duplicated_test(self, mock_verification):
ver_res = {"test_cases": {
"a": {"name": "foo", "tags": [], "status": "success", "time": 4},
"b": {"name": "foo", "tags": [], "status": "success", "time": 2}}}
mock_verification.get.return_value.get_results.return_value = ver_res
self.assertRaises(exceptions.RallyException, self.verify.results,
["uuid"], output_html=False, output_json=True)
@mock.patch("rally.api.Verification.get")
def test_results_verification_not_found(
@ -284,8 +312,7 @@ class VerifyCommandsTestCase(test.TestCase):
mock_verification_get.side_effect = (
exceptions.NotFoundException()
)
self.assertEqual(self.verify.results(verification_uuid,
output_html=False,
self.assertEqual(self.verify.results([verification_uuid],
output_json=True), 1)
mock_verification_get.assert_called_once_with(verification_uuid)
@ -298,128 +325,88 @@ class VerifyCommandsTestCase(test.TestCase):
mock_verification.get.return_value.get_results.return_value = {}
mock_open.side_effect = mock.mock_open()
verification_uuid = "94615cd4-ff45-4123-86bd-4b0741541d09"
self.verify.results(verification_uuid, output_file="results",
self.verify.results([verification_uuid], output_file="results",
output_html=False, output_json=True)
mock_verification.get.assert_called_once_with(verification_uuid)
mock_open.assert_called_once_with("results", "wb")
mock_open.side_effect().write.assert_called_once_with("{}")
mock_open.side_effect().write.assert_called_once_with("[]")
@mock.patch("rally.cli.commands.verify.report.VerificationReport")
@mock.patch("rally.cli.commands.verify.open",
side_effect=mock.mock_open(), create=True)
@mock.patch("rally.api.Verification")
@mock.patch("rally.verification.tempest.json2html.generate_report")
@mock.patch("rally.cli.commands.verify.api.Verification")
def test_results_with_output_html_and_output_file(
self, mock_generate_report, mock_verification, mock_open):
self, mock_verification, mock_open, mock_verification_report):
verification_uuid = "7140dd59-3a7b-41fd-a3ef-5e3e615d7dfa"
self.verify.results(verification_uuid, output_html=True,
output_json=False, output_file="results")
mock_vr = mock.Mock()
mock_verification_report.return_value = mock_vr
mock_strftime_created = mock.Mock(return_value="ts_created")
mock_strftime_updated = mock.Mock(return_value="ts_updated")
mock_test_cases = mock.Mock()
mock_test_cases.values.return_value = [
{"name": "foo_name", "status": "success",
"time": 10.0, "tags": ["foo-tag", "id-foo"]},
{"name": "bar_name", "status": "success",
"time": 30.1, "tags": ["bar-tag", "id-bar"]},
{"name": "spam_name", "status": "skip",
"time": 0, "tags": ["id-spam", "spam-tag"]},
{"name": "quiz_name", "status": "fail", "time": 0,
"tags": ["quiz-tag", "id-quiz"], "traceback": " Quiz error "}]
results = {
"test_cases": mock_test_cases,
"time": 42.1,
"tests": 4,
"skipped": 1,
"success": 2,
"expected_failures": 0,
"unexpected_success": 0,
"failures": 1}
mock_verification.get.return_value.db_object = {
"uuid": verification_uuid,
"created_at": mock.Mock(strftime=mock_strftime_created),
"updated_at": mock.Mock(strftime=mock_strftime_updated),
"status": "foo_status",
"set_name": "foo_set"}
mock_verification.get.return_value.get_results.return_value = results
expected = {
"status": "foo_status",
"tests": {
"foo_name": {"status": "success", "duration": 10.0,
"details": None, "tags": ["foo-tag", "id-foo"]},
"spam_name": {"status": "skip", "duration": 0, "details": None,
"tags": ["id-spam", "spam-tag"]},
"quiz_name": {"status": "fail", "duration": 0,
"details": "Quiz error",
"tags": ["quiz-tag", "id-quiz"]},
"bar_name": {"status": "success", "duration": 30.1,
"details": None, "tags": ["bar-tag", "id-bar"]}},
"skipped": 1, "finished_at": "ts_updated", "duration": 42.1,
"started_at": "ts_created", "set_name": "foo_set", "total": 4,
"success": 2, "expected_failures": 0, "failures": 1,
"unexpected_success": 0}
self.verify.results([verification_uuid], output_html=True,
output_file="results")
mock_verification.get.assert_called_once_with(verification_uuid)
mock_generate_report.assert_called_once_with(
mock_verification.get.return_value.get_results.return_value)
mock_verification_report.assert_called_once_with(
{verification_uuid: expected})
mock_open.assert_called_once_with("results", "wb")
mock_open.side_effect().write.assert_called_once_with(
mock_generate_report.return_value)
mock_vr.to_html.return_value)
@mock.patch("rally.api.Verification")
@mock.patch("json.dumps")
def test_compare(self, mock_json_dumps, mock_verification):
mock_verification.get.return_value.get_results.return_value = {
"test_cases": {}}
uuid1 = "8eda1b10-c8a4-4316-9603-8468ff1d1560"
uuid2 = "f6ef0a98-1b18-452f-a6a7-922555c2e326"
self.verify.compare(uuid1, uuid2, output_csv=False, output_html=False,
output_json=True)
@mock.patch("rally.cli.commands.verify.envutils")
def test_results_no_uuid_given(self, mock_envutils):
mock_envutils.get_global.return_value = None
self.assertRaises(exceptions.InvalidArgumentsException,
self.verify.results, None,
output_html=True, output_file="results")
mock_envutils.get_global.assert_called_once_with(
mock_envutils.ENV_VERIFICATION)
fake_data = []
calls = [mock.call(uuid1),
mock.call(uuid2)]
mock_verification.get.assert_has_calls(calls, True)
mock_json_dumps.assert_called_once_with(fake_data, sort_keys=True,
indent=4)
@mock.patch("rally.api.Verification.get",
side_effect=exceptions.NotFoundException())
def test_compare_verification_not_found(self, mock_verification_get):
uuid1 = "f7dc82da-31a6-4d40-bbf8-6d366d58960f"
uuid2 = "2f8a05f3-d310-4f02-aabf-e1165aaa5f9c"
self.assertEqual(self.verify.compare(uuid1, uuid2, output_csv=False,
output_html=False,
output_json=True), 1)
mock_verification_get.assert_called_once_with(uuid1)
@mock.patch("rally.cli.commands.verify.open",
side_effect=mock.mock_open(), create=True)
@mock.patch("rally.api.Verification")
def test_compare_with_output_csv_and_output_file(
self, mock_verification, mock_open):
mock_verification.get.return_value.get_results.return_value = {
"test_cases": {}}
fake_string = "Type,Field,Value 1,Value 2,Test Name\r\n"
uuid1 = "5e744557-4c3a-414f-9afb-7d3d8708028f"
uuid2 = "efe1c74d-a632-476e-bb6a-55a9aa9cf76b"
self.verify.compare(uuid1, uuid2, output_file="results",
output_csv=True, output_html=False,
output_json=False)
calls = [mock.call(uuid1),
mock.call(uuid2)]
mock_verification.get.assert_has_calls(calls, True)
mock_open.assert_called_once_with("results", "wb")
mock_open.side_effect().write.assert_called_once_with(fake_string)
@mock.patch("rally.cli.commands.verify.open",
side_effect=mock.mock_open(), create=True)
@mock.patch("rally.api.Verification")
def test_compare_with_output_json_and_output_file(
self, mock_verification, mock_open):
mock_verification.get.return_value.get_results.return_value = {
"test_cases": {}}
fake_json_string = "[]"
uuid1 = "0505e33a-738d-4474-a611-9db21547d863"
uuid2 = "b1908417-934e-481c-8d23-bc0badad39ed"
self.verify.compare(uuid1, uuid2, output_file="results",
output_csv=False, output_html=False,
output_json=True)
calls = [mock.call(uuid1),
mock.call(uuid2)]
mock_verification.get.assert_has_calls(calls, True)
mock_open.assert_called_once_with("results", "wb")
mock_open.side_effect().write.assert_called_once_with(fake_json_string)
@mock.patch("rally.cli.commands.verify.open",
side_effect=mock.mock_open(), create=True)
@mock.patch("rally.api.Verification")
@mock.patch("rally.verification.tempest.compare2html.create_report",
return_value="")
def test_compare_with_output_html_and_output_file(
self, mock_compare2html_create_report,
mock_verification, mock_open):
mock_verification.get.return_value.get_results.return_value = {
"test_cases": {}}
uuid1 = "cdf64228-77e9-414d-9d4b-f65e9d62c61f"
uuid2 = "39393eec-1b45-4103-8ec1-631edac4b8f0"
fake_data = []
self.verify.compare(uuid1, uuid2,
output_file="results",
output_csv=False, output_html=True,
output_json=False)
calls = [mock.call(uuid1),
mock.call(uuid2)]
mock_verification.get.assert_has_calls(calls, True)
mock_compare2html_create_report.assert_called_once_with(fake_data)
mock_open.assert_called_once_with("results", "wb")
mock_open.side_effect().write.assert_called_once_with("")
def test_compare(self):
self.assertEqual(1, self.verify.compare())
@mock.patch("rally.common.fileutils._rewrite_env_file")
@mock.patch("rally.api.Verification.get")

View File

@ -0,0 +1,105 @@
# 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 collections
import mock
from rally.ui import report
from tests.unit import test
class VerificationReportTestCase(test.TestCase):
def gen_instance(self, runs=None, uuids=None, tests=None):
ins = report.VerificationReport({})
ins._runs = runs or {}
ins._uuids = uuids or list(ins._runs.keys())
ins._tests = tests or []
return ins
def test___init__(self):
verifications = collections.OrderedDict([
("a_uuid", {
"tests": {"spam": {"status": "fail",
"duration": 4.2,
"details": "Some error",
"tags": ["a-tag", "id-tag", "z-tag"]}}}),
("b_uuid", {
"tests": {"foo": {"status": "success", "duration": 0,
"details": None,
"tags": ["a-tag", "id-tag", "z-tag"]},
"bar": {"status": "skip", "duration": 4.2,
"details": None,
"tags": ["a-tag", "id-tag", "z-tag"]}}})])
ins = report.VerificationReport(verifications)
self.assertEqual(verifications, ins._runs)
self.assertEqual(["a_uuid", "b_uuid"], ins._uuids)
tests = [
{"has_details": False, "by_verification": {
"b_uuid": {"duration": 4.2, "status": "skip",
"details": None}},
"name": "bar", "tags": ["id-tag", "a-tag", "z-tag"]},
{"has_details": False, "by_verification": {
"b_uuid": {"duration": 0, "status": "success",
"details": None}},
"name": "foo", "tags": ["id-tag", "a-tag", "z-tag"]},
{"has_details": True, "by_verification": {
"a_uuid": {"duration": 4.2, "status": "fail",
"details": "Some error"}},
"name": "spam", "tags": ["id-tag", "a-tag", "z-tag"]}]
self.assertEqual(tests, sorted(ins._tests, key=lambda i: i["name"]))
@mock.patch("rally.ui.report.utils")
@mock.patch("rally.ui.report.json.dumps", return_value="json!")
def test_to_html(self, mock_dumps, mock_utils):
mock_render = mock.Mock(return_value="HTML")
mock_utils.get_template.return_value.render = mock_render
ins = self.gen_instance(runs="runs!", uuids="uuids!", tests="tests!")
self.assertEqual("HTML", ins.to_html())
mock_utils.get_template.assert_called_once_with(
"verification/report.html")
mock_dumps.assert_called_once_with(
{"tests": "tests!", "uuids": "uuids!", "verifications": "runs!"})
mock_render.assert_called_once_with(data="json!", include_libs=False)
@mock.patch("rally.ui.report.json.dumps", return_value="json!")
def test_to_json(self, mock_dumps):
ins = self.gen_instance(tests="tests!")
self.assertEqual("json!", ins.to_json())
mock_dumps.assert_called_once_with("tests!", indent=4)
@mock.patch("rally.ui.report.csv")
@mock.patch("rally.ui.report.io.BytesIO")
def test_to_csv(self, mock_bytes_io, mock_csv):
ins = self.gen_instance(
uuids=["foo", "bar"],
tests=[{"name": "test-1", "tags": ["tag1", "tag2"],
"has_details": False,
"by_verification": {
"foo": {"status": "success", "duration": 1.2}}},
{"name": "test-2", "tags": ["tag3", "tag4"],
"has_details": False,
"by_verification": {
"bar": {"status": "success", "duration": 3.4}}}])
mock_stream = mock.Mock()
mock_stream.getvalue.return_value = "CSV!"
mock_bytes_io.return_value.__enter__.return_value = mock_stream
self.assertEqual("CSV!", ins.to_csv())
mock_csv.writer.assert_called_once_with(mock_stream)
# Custom kwargs
mock_csv.writer.reset_mock()
self.assertEqual("CSV!", ins.to_csv(foo="bar"))
mock_csv.writer.assert_called_once_with(mock_stream, foo="bar")

View File

@ -1,48 +0,0 @@
# 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.verification.tempest import compare2html
from tests.unit import test
class Compare2HtmlTestCase(test.TestCase):
@mock.patch("rally.ui.utils.get_template")
def test_main(self, mock_get_template):
results = [{"val2": 0.0111, "field": u"time", "val1": 0.0222,
"type": "CHANGED", "test_name": u"test.one"},
{"val2": 0.111, "field": u"time", "val1": 0.222,
"type": "CHANGED", "test_name": u"test.two"},
{"val2": 1.11, "field": u"time", "val1": 2.22,
"type": "CHANGED", "test_name": u"test.three"}]
fake_template_kw = {
"heading": {
"title": compare2html.__title__,
"description": compare2html.__description__,
"parameters": [("Difference Count", len(results))]
},
"generator": "compare2html %s" % compare2html.__version__,
"results": results
}
template_mock = mock.MagicMock()
mock_get_template.return_value = template_mock
output_mock = mock.MagicMock()
template_mock.render.return_value = output_mock
compare2html.create_report(results)
mock_get_template.assert_called_once_with("verification/compare.mako")
template_mock.render.assert_called_once_with(**fake_template_kw)
output_mock.encode.assert_called_once_with("utf8")

View File

@ -1,107 +0,0 @@
# 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 rally.verification.tempest import diff
from tests.unit import test
class DiffTestCase(test.TestCase):
def test_main(self):
results1 = {"test.NONE": {"name": "test.NONE",
"output": "test.NONE",
"status": "SKIPPED",
"time": 0.000},
"test.zerofive": {"name": "test.zerofive",
"output": "test.zerofive",
"status": "FAILED",
"time": 0.05},
"test.one": {"name": "test.one",
"output": "test.one",
"status": "OK",
"time": 0.111},
"test.two": {"name": "test.two",
"output": "test.two",
"status": "OK",
"time": 0.222},
"test.three": {"name": "test.three",
"output": "test.three",
"status": "FAILED",
"time": 0.333},
"test.four": {"name": "test.four",
"output": "test.four",
"status": "OK",
"time": 0.444},
"test.five": {"name": "test.five",
"output": "test.five",
"status": "OK",
"time": 0.555}
}
results2 = {"test.one": {"name": "test.one",
"output": "test.one",
"status": "FAIL",
"time": 0.1111},
"test.two": {"name": "test.two",
"output": "test.two",
"status": "OK",
"time": 0.222},
"test.three": {"name": "test.three",
"output": "test.three",
"status": "OK",
"time": 0.3333},
"test.four": {"name": "test.four",
"output": "test.four",
"status": "FAIL",
"time": 0.4444},
"test.five": {"name": "test.five",
"output": "test.five",
"status": "OK",
"time": 0.555},
"test.six": {"name": "test.six",
"output": "test.six",
"status": "OK",
"time": 0.666},
"test.seven": {"name": "test.seven",
"output": "test.seven",
"status": "OK",
"time": 0.777}
}
diff_ = diff.Diff(results1, results2, 0)
assert len(diff_.diffs) == 10
assert len([test for test in diff_.diffs
if test["type"] == "removed_test"]) == 2
assert len([test for test in diff_.diffs
if test["type"] == "new_test"]) == 2
assert len([test for test in diff_.diffs
if test["type"] == "value_changed"]) == 6
assert diff_.to_csv() != ""
assert diff_.to_html() != ""
assert diff_.to_json() != ""
def test_zero_values(self):
results1 = {"test.one": {"name": "test.one",
"output": "test.one",
"status": "OK",
"time": 1}}
results2 = {"test.one": {"name": "test.one",
"output": "test.one",
"status": "FAIL",
"time": 0}}
# This must NOT raise ZeroDivisionError
diff_ = diff.Diff(results1, results2, 0)
self.assertEqual(2, len(diff_.diffs))
diff_ = diff.Diff(results2, results1, 0)
self.assertEqual(2, len(diff_.diffs))

View File

@ -1,114 +0,0 @@
# 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 datetime as dt
import mock
from rally.verification.tempest import json2html
from tests.unit import test
BASE = "rally.verification.tempest"
class HtmlOutputTestCase(test.TestCase):
@mock.patch(BASE + ".json2html.ui_utils.get_template")
def test_generate_report(self, mock_get_template):
results = {
"time": 22.75,
"tests": 4,
"success": 1,
"skipped": 1,
"failures": 1,
"expected_failures": 0,
"unexpected_success": 0,
"test_cases": {
"tp": {"name": "tp",
"status": "success",
"time": 2},
"ts": {"name": "ts",
"status": "skip",
"reason": "ts_skip",
"time": 4},
"tf": {"name": "tf",
"status": "fail",
"time": 6,
"traceback": "fail_log"}}}
expected_report = {
"failures": 1,
"success": 1,
"skipped": 1,
"expected_failures": 0,
"unexpected_success": 0,
"total": 4,
"time": "{0} ({1} s)".format(
dt.timedelta(seconds=23), 22.75),
"tests": [{"name": "tf",
"id": 0,
"output": "fail_log",
"status": "fail",
"time": 6},
{"name": "tp",
"id": 1,
"output": "",
"status": "success",
"time": 2},
{"name": "ts",
"id": 2,
"output": "Reason:\n ts_skip",
"status": "skip",
"time": 4}]}
json2html.generate_report(results)
mock_get_template.assert_called_once_with("verification/report.mako")
mock_get_template.return_value.render.assert_called_once_with(
report=expected_report)
@mock.patch(BASE + ".json2html.ui_utils.get_template")
def test_convert_bug_id_in_reason_into_bug_link(self, mock_get_template):
results = {
"failures": 0,
"success": 0,
"skipped": 1,
"expected_failures": 0,
"unexpected_success": 0,
"tests": 1,
"time": 0,
"test_cases": {"one_test": {
"status": "skip",
"name": "one_test",
"reason": "Skipped until Bug: 666666 is resolved.",
"time": "time"}}}
expected_report = {
"failures": 0,
"success": 0,
"skipped": 1,
"expected_failures": 0,
"unexpected_success": 0,
"total": 1,
"time": "{0} ({1} s)".format(dt.timedelta(seconds=0), 0),
"tests": [{
"id": 0,
"status": "skip",
"name": "one_test",
"output": "Reason:\n Skipped until Bug: <a href='https://"
"launchpad.net/bugs/666666'>666666</a> is resolved.",
"time": "time"}]}
json2html.generate_report(results)
mock_get_template.assert_called_once_with("verification/report.mako")
mock_get_template.return_value.render.assert_called_once_with(
report=expected_report)