Display test results.

Create a new webpage to display test results.
Show all testcases ran, their status (PASS/FAIL) and elapsed time.
Also, restructure tempest_subunit_test_result.py so that I can
derive my own class to create a dictionary tailored for the webpage.

Change-Id: I0a30f5584ab2c2b1ae36e0834afb3d26a284f1f0
Partially-implements: blueprint display-test-results
This commit is contained in:
Raymond Wong 2014-04-02 17:05:06 -07:00
parent f46f831704
commit dc5d92132e
6 changed files with 278 additions and 53 deletions

View File

@ -4,12 +4,15 @@ body {
margin: 30px;
padding: 0;
}
.container{
width: !important;
}
.grid{
width: 1224px !important;
}
.inner_container{
z-index: 100;
position: relative;
@ -47,9 +50,10 @@ p.error {
z-index:10000;
float: right;
margin-top: 15px;
}
.ref_nav li {
.ref_nav li {
display: inline-block;
padding-left: 5px;
@ -68,7 +72,7 @@ input {
}
h1, h2, h3, h4 {
font-family: helvetica, san-serif;
margin:0px;
padding: 0px;
@ -87,10 +91,12 @@ h2{
font-size: 30px;
font-weight: bold;
}
a.logo:hover{
text-decoration: none;
color: #274F7D;
}
.logo span{
color: #329000;
}
@ -109,9 +115,11 @@ input[name="openid"] {
.index_left{
background-color: #5FD4B1;
}
.index_center{
background-color: #689AD3;
}
.index_right{
background-color: #99EE6B;
}
@ -123,6 +131,7 @@ input[name="openid"] {
height: 110px;
font-size: 20px;
}
.option p{
display: block;
width: 215px;
@ -132,12 +141,12 @@ input[name="openid"] {
.panel-heading{
font-weight: bold;
}
.panel-heading span{
color: #274F7D;
margin-right: 6px;
}
.red{
color: red;
}
@ -145,12 +154,56 @@ input[name="openid"] {
.green{
color: #329000;
}
.vendors{
text-align: center;
color: #274F7D;
}
.vendors li{
display: inline-block;
padding: 0px 5px;
}
.report_text_table,
.report_data_table{
width: auto;
}
.report_text_table tbody > tr > td,
.report_text_table tbody > tr > th{
border-top: 0px;
vertical-align: middle;
white-space: nowrap;
}
.report_text_table tbody > tr > th{
text-align: right;
}
.report_data_table{
border: 1px solid rgb(200, 200, 200);
border-collapse: initial;
border-radius: 5px;
}
.report_data_table tbody > tr > td,
.report_data_table tbody > tr > th{
padding-right: 15px;
padding-left: 15px;
text-align: center;
white-space: nowrap;
}
.report_data_table tbody > tr > th{
background-color: rgb(245, 245, 245);
}
.report_data_path{
background-color: rgb(250, 250, 250);
font-style: italic;
}
.blue{
color: blue;
}

View File

@ -72,6 +72,14 @@
onclick="window.location='/show-report/{{ test.id }}'">
<span class="glyphicon glyphicon-file"></span>
</button>
<button type="button"
class="btn btn-default btn-xs"
rel="tooltip"
title="Download Result"
data-placement="top"
onclick="window.location='/download-result/{{ test.id }}'">
<span class="glyphicon glyphicon-cloud-download"></span>
</button>
{% else %}
<button type="button"
class="btn btn-default btn-xs"

View File

@ -2,7 +2,64 @@
{% block title %}Test Report{% endblock %}
{% block body %}
<h2>Test Report</h2>
Test ID: {{ test.id }}
<br>
<table class="table report_text_table">
{% if test.cloud %}
<tr>
<th>Cloud:</th>
<td class="blue"> {{ test.cloud.label }}</td>
</tr>
{% endif %}
<tr>
<th>Test ID:</th>
<td class="blue"> {{ test.id}}</td>
</tr>
</table>
<table class="table report_data_table">
<tr>
<th>Pass</th>
<th>Fail</th>
<th>Fail Setup</th>
<th>Error</th>
<th>Skip</th>
<th>Total</th>
</tr>
<tr>
<td class="green">{{ test_result['summary']['PASS'] }}</td>
<td class="red">{{ test_result['summary']['FAIL'] }}</td>
<td class="red">{{ test_result['summary']['FAIL_SETUP'] }}</td>
<td class="red">{{ test_result['summary']['ERROR'] }}</td>
<td>{{ test_result['summary']['SKIP'] }}</td>
<td class="blue"><b>{{ test_result['summary']['Total'] }}</b></td>
</tr>
</table>
<table class="table report_data_table">
<tr>
<th class="left">Testcase</th>
<th>Status</th>
<th>Elapsed Time</th>
</tr>
{% for classname, testcases in test_result['data'].items() %}
<tr>
<td class="report_data_path" colspan=3 style="text-align:left"><b>Test class:</b> {{ classname }}</td>
</tr>
{% for testcase in testcases %}
<tr>
<td style="text-align:left">{{ testcase[0] }}</td>
{% if testcase[1] == "PASS" %}
<td class="green">{{ testcase[1] }}</td>
{% elif testcase[1] == "SKIP" %}
<td>{{ testcase[1] }}</td>
{% else %}
<td class="red">{{ testcase[1] }}</td>
{% endif %}
<td>{{ testcase[2] }}s</td>
</tr>
{% endfor %}
{% endfor %}
</table>
<input type='button' value="Back" onclick="location.href='{{ next_url }}'">
{% endblock %}

View File

@ -20,80 +20,149 @@ import testtools
import unittest
class TempestSubunitTestResult(testtools.TestResult):
class TempestSubunitTestResultBase(testtools.TestResult):
"""Class to process subunit stream.
This class is derived from testtools.TestResult.
This class overrides all the inherited addXXX methods
to call the new _process_result() method to process the data.
This class is designed to be a base class.
The _process_result() method should be overriden by the
derived class to customize the processing.
"""
result_type = ["SUCCESS", "FAILURE", "ERROR", "SKIP"]
def __init__(self, stream, descriptions, verbosity):
"""Initialize with super class signature."""
super(TempestSubunitTestResultBase, self).__init__()
def _process_result(self, result_type, testcase, *arg):
"""Process the data.
The value of parameter "result_type" can be SUCCESS, FAILURE,
ERROR, or SKIP.
It can be used to determine from which add method this is called.
"""
pass
def addSuccess(self, testcase):
"""Overwrite super class method for additional data processing."""
super(TempestSubunitTestResultBase, self).addSuccess(testcase)
self._process_result(self.result_type[0], testcase)
def addFailure(self, testcase, err):
"""Overwrite super class method for additional data processing."""
if testcase.id() == 'process-returncode':
return
super(TempestSubunitTestResultBase, self).addFailure(testcase, err)
self._process_result(self.result_type[1], testcase, err)
def addError(self, testcase, err):
"""Overwrite super class method for additional data processing."""
super(TempestSubunitTestResultBase, self).addFailure(testcase, err)
self._process_result(self.result_type[2], testcase, err)
def addSkip(self, testcase, reason=None, details=None):
"""Overwrite super class method for additional data processing."""
super(TempestSubunitTestResultBase,
self).addSkip(testcase, reason, details)
self._process_result(self.result_type[3], testcase, reason, details)
def startTest(self, testcase):
"""Overwrite super class method for additional data processing."""
self.start_time = self._now()
super(TempestSubunitTestResultBase, self).startTest(testcase)
class TempestSubunitTestResult(TempestSubunitTestResultBase):
"""Process subunit stream and save data into two dictionary objects.
1) The result dictionary object:
results={testcase_id: [status, elapsed],
testcase_id: [status, elapsed],
...: ..}
...}
testcase_id: the id fetched from subunit data.
For Tempest test: testcase_id = test_class_name + test_name
status: status of the testcase (OK, FAIL, ERROR, SKIP, FAIL_CLASS_SETUP)
status: status of the testcase (PASS, FAIL, FAIL_SETUP, ERROR, SKIP)
elapsed: testcase elapsed time
2) The summary dictionary object:
summary={"OK": count, "FAIL": count, "ERROR": count,
"SKIP": count, "FAIL_CLASS_SETUP: count", "Total": count}
summary={"PASS": count, "FAIL": count, "FAIL_SETUP: count",
"ERROR": count, "SKIP": count, "Total": count}
count: the number of occurrence
"""
def __init__(self, stream, descriptions, verbosity):
"""Initialize with supper class signature."""
super(TempestSubunitTestResult, self).__init__()
super(TempestSubunitTestResult, self).__init__(stream, descriptions,
verbosity)
self.start_time = None
self.status = ["OK", "FAIL", "ERROR", "SKIP",
"FAIL_CLASS_SETUP"]
self.status = ["PASS", "FAIL", "FAIL_SETUP", "ERROR", "SKIP"]
self.results = {}
self.summary = {self.status[0]: 0, self.status[1]: 0,
self.status[2]: 0, self.status[3]: 0,
self.status[4]: 0, "Total": 0}
def _process_result(self, status, testcase, *arg):
"""Process and append data to dictionary objects.
User can overload this method to customize the format of the output.
"""
def _process_result(self, result_type, testcase, *arg):
"""Process and append data to dictionary objects."""
testcase_id = testcase.id()
elapsed = (self._now() - self.start_time).total_seconds()
# Convert "SUCCESS" to "PASS"
# Separate "FAILURE" into "FAIL" and "FAIL_SETUP"
status = result_type
if status == self.result_type[0]:
status = self.status[0]
if (status == self.status[1]) and ("setUpClass" in testcase_id):
status = self.status[4]
status = self.status[3]
self.results.setdefault(testcase_id, [])
self.results[testcase_id] = [status, elapsed]
self.summary[status] += 1
self.summary["Total"] += 1
def addSuccess(self, testcase):
"""Overwrite super class method for additional data processing."""
super(TempestSubunitTestResult, self).addSuccess(testcase)
self._process_result(self.status[0], testcase)
def addFailure(self, testcase, err):
"""Overwrite super class method for additional data processing."""
if testcase.id() == 'process-returncode':
return
super(TempestSubunitTestResult, self).addFailure(testcase, err)
self._process_result(self.status[1], testcase, err)
class TempestSubunitTestResultTuples(TempestSubunitTestResult):
"""Process subunit stream and save data into two dictionary objects.
def addError(self, testcase, err):
"""Overwrite super class method for additional data processing."""
super(TempestSubunitTestResult, self).addFailure(testcase, err)
self._process_result(self.status[2], testcase, err)
1) The result dictionary object:
results={test_classname: [(test_name, status, elapsed),
(test_name, status, elapsed),...],
test_classname: [(test_name, status, elapsed),
(test_name, status, elapsed),...],
...}
def addSkip(self, testcase, reason=None, details=None):
"""Overwrite super class method for additional data processing."""
super(TempestSubunitTestResult,
self).addSkip(testcase, reason, details)
self._process_result(self.status[3], testcase, reason, details)
status: status of the testcase (PASS, FAIL, FAIL_SETUP, ERROR, SKIP)
elapsed: testcase elapsed time
def startTest(self, testcase):
"""Overwrite super class method for additional data processing."""
self.start_time = self._now()
super(TempestSubunitTestResult, self).startTest(testcase)
2) The summary dictionary object:
summary={"PASS": count, "FAIL": count, "FAIL_SETUP: count",
"ERROR": count, "SKIP": count, "Total": count}
count: the number of occurrence
"""
def _process_result(self, result_type, testcase, *arg):
"""Process and append data to dictionary objects."""
testcase_id = testcase.id()
elapsed = round((self._now() - self.start_time).total_seconds(), 2)
# Convert "SUCCESS" to "PASS"
# Separate "FAILURE" into "FAIL" and "FAIL_SETUP"
status = result_type
if status == self.result_type[0]:
status = self.status[0]
if (status == self.status[1]) and ("setUpClass" in testcase_id):
status = self.status[3]
classname, testname = testcase_id.rsplit('.', 1)
self.results.setdefault(classname, [])
self.results[classname].append((testname, status, elapsed))
self.summary[status] += 1
self.summary["Total"] += 1
class ProcessSubunitData():

View File

@ -14,11 +14,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import cStringIO
from docker_buildfile import DockerBuildFile
import json
import os
from refstack.models import Test
from refstack.refstack_config import RefStackConfig
from tempest_subunit_test_result import ProcessSubunitData
from tempest_subunit_test_result import TempestSubunitTestResultTuples
config_data = RefStackConfig()
@ -111,6 +114,23 @@ class TempestTester(object):
return json.dumps(testcases)
def get_result(self):
'''Return the test result objects.'''
if not self.test_obj.finished:
return None
try:
test_result = ProcessSubunitData(cStringIO.StringIO(
self.test_obj.subunit),
TempestSubunitTestResultTuples).get_result()
return {"summary": test_result.summary,
"data": test_result.results}
except Exception:
return None
def process_resultfile(self, filename):
'''Process the tempest result file.'''

View File

@ -413,21 +413,39 @@ def show_report(test_id):
test = Test.query.filter_by(id=test_id).first()
if not test:
flash(u'Not a valid Test ID!')
return redirect('/')
# Users can see report of all tests (including other people's tests)
test_result = TempestTester(test_id).get_result()
if not test_result:
flash(u"No test result available!")
return redirect('/')
return render_template('show_report.html', next_url='/', test=test,
test_result=test_result)
@app.route('/download-result/<int:test_id>', methods=['GET', 'POST'])
def download_result(test_id):
"""Handler for downloading test results."""
test = Test.query.filter_by(id=test_id).first()
if not test:
flash(u'Not a valid Test ID!')
return redirect('/')
elif not test.cloud.user_id == g.user.id:
# Users can only download result of their own tests
flash(u"This isn't your test!")
return redirect('/')
# This is a place holder for now
''' TODO: Generate the appropriate report page '''
''' ForNow: send back the subunit data stream for debugging '''
# Send back the subunit data stream
response = make_response(test.subunit)
response.headers["Content-Disposition"] = \
"attachment; filename=subunit.txt"
response.headers['Content-Disposition'] = \
'attachment; filename=subunit_%s.txt' % (test_id)
response.content_type = "text/plain"
return response
# return render_template('show_report.html', next_url='/', test=test)