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:
parent
f46f831704
commit
dc5d92132e
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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 %}
|
||||
|
@ -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():
|
||||
|
@ -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.'''
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user