新版本初次提交
This commit is contained in:
806
Public/BSTestRunner.py
Normal file
806
Public/BSTestRunner.py
Normal file
@@ -0,0 +1,806 @@
|
||||
"""
|
||||
A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance.
|
||||
|
||||
The simplest way to use this is to invoke its main method. E.g.
|
||||
|
||||
import unittest
|
||||
import BSTestRunner
|
||||
|
||||
... define your tests ...
|
||||
|
||||
if __name__ == '__main__':
|
||||
BSTestRunner.main()
|
||||
|
||||
|
||||
For more customization options, instantiates a BSTestRunner object.
|
||||
BSTestRunner is a counterpart to unittest's TextTestRunner. E.g.
|
||||
|
||||
# output to a file
|
||||
fp = file('my_report.html', 'wb')
|
||||
runner = BSTestRunner.BSTestRunner(
|
||||
stream=fp,
|
||||
title='My unit test',
|
||||
description='This demonstrates the report output by BSTestRunner.'
|
||||
)
|
||||
|
||||
# Use an external stylesheet.
|
||||
# See the Template_mixin class for more customizable options
|
||||
runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
|
||||
|
||||
# run the test
|
||||
runner.run(my_test_suite)
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
Copyright (c) 2004-2007, Wai Yip Tung
|
||||
Copyright (c) 2016, Eason Han
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name Wai Yip Tung nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
|
||||
__author__ = "Wai Yip Tung && Eason Han"
|
||||
__version__ = "0.8.4"
|
||||
|
||||
|
||||
"""
|
||||
Change History
|
||||
|
||||
Version 0.8.3
|
||||
* Modify html style using bootstrap3.
|
||||
|
||||
Version 0.8.3
|
||||
* Prevent crash on class or module-level exceptions (Darren Wurf).
|
||||
|
||||
Version 0.8.2
|
||||
* Show output inline instead of popup window (Viorel Lupu).
|
||||
|
||||
Version in 0.8.1
|
||||
* Validated XHTML (Wolfgang Borgert).
|
||||
* Added description of test classes and test cases.
|
||||
|
||||
Version in 0.8.0
|
||||
* Define Template_mixin class for customization.
|
||||
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.
|
||||
|
||||
Version in 0.7.1
|
||||
* Back port to Python 2.3 (Frank Horowitz).
|
||||
* Fix missing scroll bars in detail log (Podi).
|
||||
"""
|
||||
|
||||
# TODO: color stderr
|
||||
# TODO: simplify javascript using ,ore than 1 class in the class attribute?
|
||||
|
||||
import datetime
|
||||
from io import StringIO as StringIO
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from xml.sax import saxutils
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# The redirectors below are used to capture output during testing. Output
|
||||
# sent to sys.stdout and sys.stderr are automatically captured. However
|
||||
# in some cases sys.stdout is already cached before BSTestRunner is
|
||||
# invoked (e.g. calling logging.basicConfig). In order to capture those
|
||||
# output, use the redirectors for the cached stream.
|
||||
#
|
||||
# e.g.
|
||||
# >>> logging.basicConfig(stream=BSTestRunner.stdout_redirector)
|
||||
# >>>
|
||||
|
||||
def to_unicode(s):
|
||||
return s
|
||||
# try:
|
||||
# return unicode(s)
|
||||
# except UnicodeDecodeError:
|
||||
# # s is non ascii byte string
|
||||
# return s.decode('unicode_escape')
|
||||
|
||||
class OutputRedirector(object):
|
||||
""" Wrapper to redirect stdout or stderr """
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
|
||||
def write(self, s):
|
||||
self.fp.write(s)
|
||||
|
||||
def writelines(self, lines):
|
||||
lines = map(to_unicode, lines)
|
||||
self.fp.writelines(lines)
|
||||
|
||||
def flush(self):
|
||||
self.fp.flush()
|
||||
|
||||
stdout_redirector = OutputRedirector(sys.stdout)
|
||||
stderr_redirector = OutputRedirector(sys.stderr)
|
||||
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Template
|
||||
|
||||
class Template_mixin(object):
|
||||
"""
|
||||
Define a HTML template for report customerization and generation.
|
||||
|
||||
Overall structure of an HTML report
|
||||
|
||||
HTML
|
||||
+------------------------+
|
||||
|<html> |
|
||||
| <head> |
|
||||
| |
|
||||
| STYLESHEET |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| </head> |
|
||||
| |
|
||||
| <body> |
|
||||
| |
|
||||
| HEADING |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| REPORT |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| ENDING |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| </body> |
|
||||
|</html> |
|
||||
+------------------------+
|
||||
"""
|
||||
|
||||
STATUS = {
|
||||
0: 'pass',
|
||||
1: 'fail',
|
||||
2: 'error',
|
||||
}
|
||||
|
||||
DEFAULT_TITLE = 'Unit Test Report'
|
||||
DEFAULT_DESCRIPTION = ''
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# HTML Template
|
||||
|
||||
HTML_TMPL = r"""<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<title>%(title)s</title>
|
||||
<meta name="generator" content="%(generator)s"/>
|
||||
<link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
|
||||
%(stylesheet)s
|
||||
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||
<script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<script language="javascript" type="text/javascript"><!--
|
||||
output_list = Array();
|
||||
|
||||
/* level - 0:Summary; 1:Failed; 2:All */
|
||||
function showCase(level) {
|
||||
trs = document.getElementsByTagName("tr");
|
||||
for (var i = 0; i < trs.length; i++) {
|
||||
tr = trs[i];
|
||||
id = tr.id;
|
||||
if (id.substr(0,2) == 'ft') {
|
||||
if (level < 1) {
|
||||
tr.className = 'hiddenRow';
|
||||
}
|
||||
else {
|
||||
tr.className = '';
|
||||
}
|
||||
}
|
||||
if (id.substr(0,2) == 'pt') {
|
||||
if (level > 1) {
|
||||
tr.className = '';
|
||||
}
|
||||
else {
|
||||
tr.className = 'hiddenRow';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showClassDetail(cid, count) {
|
||||
var id_list = Array(count);
|
||||
var toHide = 1;
|
||||
for (var i = 0; i < count; i++) {
|
||||
tid0 = 't' + cid.substr(1) + '.' + (i+1);
|
||||
tid = 'f' + tid0;
|
||||
tr = document.getElementById(tid);
|
||||
if (!tr) {
|
||||
tid = 'p' + tid0;
|
||||
tr = document.getElementById(tid);
|
||||
}
|
||||
id_list[i] = tid;
|
||||
if (tr.className) {
|
||||
toHide = 0;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < count; i++) {
|
||||
tid = id_list[i];
|
||||
if (toHide) {
|
||||
document.getElementById('div_'+tid).style.display = 'none'
|
||||
document.getElementById(tid).className = 'hiddenRow';
|
||||
}
|
||||
else {
|
||||
document.getElementById(tid).className = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showTestDetail(div_id){
|
||||
var details_div = document.getElementById(div_id)
|
||||
var displayState = details_div.style.display
|
||||
// alert(displayState)
|
||||
if (displayState != 'block' ) {
|
||||
displayState = 'block'
|
||||
details_div.style.display = 'block'
|
||||
}
|
||||
else {
|
||||
details_div.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function html_escape(s) {
|
||||
s = s.replace(/&/g,'&');
|
||||
s = s.replace(/</g,'<');
|
||||
s = s.replace(/>/g,'>');
|
||||
return s;
|
||||
}
|
||||
|
||||
/* obsoleted by detail in <div>
|
||||
function showOutput(id, name) {
|
||||
var w = window.open("", //url
|
||||
name,
|
||||
"resizable,scrollbars,status,width=800,height=450");
|
||||
d = w.document;
|
||||
d.write("<pre>");
|
||||
d.write(html_escape(output_list[id]));
|
||||
d.write("\n");
|
||||
d.write("<a href='javascript:window.close()'>close</a>\n");
|
||||
d.write("</pre>\n");
|
||||
d.close();
|
||||
}
|
||||
*/
|
||||
--></script>
|
||||
|
||||
<div class="container">
|
||||
%(heading)s
|
||||
%(report)s
|
||||
%(ending)s
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
# variables: (title, generator, stylesheet, heading, report, ending)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Stylesheet
|
||||
#
|
||||
# alternatively use a <link> for external style sheet, e.g.
|
||||
# <link rel="stylesheet" href="$url" type="text/css">
|
||||
|
||||
STYLESHEET_TMPL = """
|
||||
<style type="text/css" media="screen">
|
||||
|
||||
/* -- css div popup ------------------------------------------------------------------------ */
|
||||
.popup_window {
|
||||
display: none;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
/*border: solid #627173 1px; */
|
||||
padding: 10px;
|
||||
background-color: #99CCFF;
|
||||
font-family: "Lucida Console", "Courier New", Courier, monospace;
|
||||
text-align: left;
|
||||
font-size: 10pt;
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
/* -- report ------------------------------------------------------------------------ */
|
||||
|
||||
#show_detail_line .label {
|
||||
font-size: 85%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#show_detail_line {
|
||||
margin: 2em auto 1em auto;
|
||||
}
|
||||
|
||||
#total_row { font-weight: bold; }
|
||||
.hiddenRow { display: none; }
|
||||
.testcase { margin-left: 2em; }
|
||||
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Heading
|
||||
#
|
||||
|
||||
HEADING_TMPL = """<div class='heading'>
|
||||
<h1>%(title)s</h1>
|
||||
%(parameters)s
|
||||
<p class='description'>%(description)s</p>
|
||||
</div>
|
||||
|
||||
""" # variables: (title, parameters, description)
|
||||
|
||||
HEADING_ATTRIBUTE_TMPL = """<p><strong>%(name)s:</strong> %(value)s</p>
|
||||
""" # variables: (name, value)
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Report
|
||||
#
|
||||
|
||||
REPORT_TMPL = """
|
||||
<p id='show_detail_line'>
|
||||
<span class="label label-primary" onclick="showCase(0)">Summary</span>
|
||||
<span class="label label-danger" onclick="showCase(1)">Failed</span>
|
||||
<span class="label label-default" onclick="showCase(2)">All</span>
|
||||
</p>
|
||||
<table id='result_table' class="table">
|
||||
<thead>
|
||||
<tr id='header_row'>
|
||||
<th>Test Group/Test case</td>
|
||||
<th>Count</td>
|
||||
<th>Pass</td>
|
||||
<th>Fail</td>
|
||||
<th>Error</td>
|
||||
<th>View</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
%(test_list)s
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr id='total_row'>
|
||||
<td>Total</td>
|
||||
<td>%(count)s</td>
|
||||
<td class="text text-success">%(Pass)s</td>
|
||||
<td class="text text-danger">%(fail)s</td>
|
||||
<td class="text text-warning">%(error)s</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
""" # variables: (test_list, count, Pass, fail, error)
|
||||
|
||||
REPORT_CLASS_TMPL = r"""
|
||||
<tr class='%(style)s'>
|
||||
<td>%(desc)s</td>
|
||||
<td>%(count)s</td>
|
||||
<td>%(Pass)s</td>
|
||||
<td>%(fail)s</td>
|
||||
<td>%(error)s</td>
|
||||
<td><a class="btn btn-xs btn-primary"href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
|
||||
</tr>
|
||||
""" # variables: (style, desc, count, Pass, fail, error, cid)
|
||||
|
||||
|
||||
REPORT_TEST_WITH_OUTPUT_TMPL = r"""
|
||||
<tr id='%(tid)s' class='%(Class)s'>
|
||||
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
|
||||
<td colspan='5' align='center'>
|
||||
|
||||
<!--css div popup start-->
|
||||
<a class="popup_link btn btn-xs btn-default" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
|
||||
%(status)s</a>
|
||||
|
||||
<div id='div_%(tid)s' class="popup_window">
|
||||
<div style='text-align: right;cursor:pointer'>
|
||||
<a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
|
||||
[x]</a>
|
||||
</div>
|
||||
<pre>
|
||||
%(script)s
|
||||
</pre>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
""" # variables: (tid, Class, style, desc, status)
|
||||
|
||||
|
||||
REPORT_TEST_NO_OUTPUT_TMPL = r"""
|
||||
<tr id='%(tid)s' class='%(Class)s'>
|
||||
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
|
||||
<td colspan='5' align='center'>%(status)s</td>
|
||||
</tr>
|
||||
""" # variables: (tid, Class, style, desc, status)
|
||||
|
||||
|
||||
REPORT_TEST_OUTPUT_TMPL = r"""
|
||||
%(id)s: %(output)s
|
||||
""" # variables: (id, output)
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# ENDING
|
||||
#
|
||||
|
||||
ENDING_TMPL = """<div id='ending'> </div>"""
|
||||
|
||||
# -------------------- The end of the Template class -------------------
|
||||
|
||||
|
||||
TestResult = unittest.TestResult
|
||||
|
||||
class _TestResult(TestResult):
|
||||
# note: _TestResult is a pure representation of results.
|
||||
# It lacks the output and reporting ability compares to unittest._TextTestResult.
|
||||
|
||||
def __init__(self, verbosity=1):
|
||||
TestResult.__init__(self)
|
||||
self.outputBuffer = StringIO()
|
||||
self.stdout0 = None
|
||||
self.stderr0 = None
|
||||
self.success_count = 0
|
||||
self.failure_count = 0
|
||||
self.error_count = 0
|
||||
self.verbosity = verbosity
|
||||
|
||||
# result is a list of result in 4 tuple
|
||||
# (
|
||||
# result code (0: success; 1: fail; 2: error),
|
||||
# TestCase object,
|
||||
# Test output (byte string),
|
||||
# stack trace,
|
||||
# )
|
||||
self.result = []
|
||||
|
||||
|
||||
def startTest(self, test):
|
||||
TestResult.startTest(self, test)
|
||||
# just one buffer for both stdout and stderr 更改
|
||||
self.outputBuffer = StringIO()
|
||||
stdout_redirector.fp = self.outputBuffer
|
||||
stderr_redirector.fp = self.outputBuffer
|
||||
self.stdout0 = sys.stdout
|
||||
self.stderr0 = sys.stderr
|
||||
sys.stdout = stdout_redirector
|
||||
sys.stderr = stderr_redirector
|
||||
|
||||
|
||||
def complete_output(self):
|
||||
"""
|
||||
Disconnect output redirection and return buffer.
|
||||
Safe to call multiple times.
|
||||
"""
|
||||
if self.stdout0:
|
||||
sys.stdout = self.stdout0
|
||||
sys.stderr = self.stderr0
|
||||
self.stdout0 = None
|
||||
self.stderr0 = None
|
||||
return self.outputBuffer.getvalue()
|
||||
|
||||
|
||||
def stopTest(self, test):
|
||||
# Usually one of addSuccess, addError or addFailure would have been called.
|
||||
# But there are some path in unittest that would bypass this.
|
||||
# We must disconnect stdout in stopTest(), which is guaranteed to be called.
|
||||
self.complete_output()
|
||||
|
||||
|
||||
def addSuccess(self, test):
|
||||
self.success_count += 1
|
||||
TestResult.addSuccess(self, test)
|
||||
output = self.complete_output()
|
||||
self.result.append((0, test, output, ''))
|
||||
if self.verbosity > 1:
|
||||
sys.stderr.write('ok ')
|
||||
sys.stderr.write(str(test))
|
||||
sys.stderr.write('\n')
|
||||
else:
|
||||
sys.stderr.write('.')
|
||||
|
||||
def addError(self, test, err):
|
||||
self.error_count += 1
|
||||
TestResult.addError(self, test, err)
|
||||
_, _exc_str = self.errors[-1]
|
||||
output = self.complete_output()
|
||||
self.result.append((2, test, output, _exc_str))
|
||||
if self.verbosity > 1:
|
||||
sys.stderr.write('E ')
|
||||
sys.stderr.write(str(test))
|
||||
sys.stderr.write('\n')
|
||||
else:
|
||||
sys.stderr.write('E')
|
||||
|
||||
def addFailure(self, test, err):
|
||||
self.failure_count += 1
|
||||
TestResult.addFailure(self, test, err)
|
||||
_, _exc_str = self.failures[-1]
|
||||
output = self.complete_output()
|
||||
self.result.append((1, test, output, _exc_str))
|
||||
if self.verbosity > 1:
|
||||
sys.stderr.write('F ')
|
||||
sys.stderr.write(str(test))
|
||||
sys.stderr.write('\n')
|
||||
else:
|
||||
sys.stderr.write('F')
|
||||
|
||||
|
||||
class BSTestRunner(Template_mixin):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
|
||||
self.stream = stream
|
||||
self.verbosity = verbosity
|
||||
if title is None:
|
||||
self.title = self.DEFAULT_TITLE
|
||||
else:
|
||||
self.title = title
|
||||
if description is None:
|
||||
self.description = self.DEFAULT_DESCRIPTION
|
||||
else:
|
||||
self.description = description
|
||||
|
||||
self.startTime = datetime.datetime.now()
|
||||
|
||||
|
||||
def run(self, test):
|
||||
"Run the given test case or test suite."
|
||||
result = _TestResult(self.verbosity)
|
||||
try:
|
||||
test(result)
|
||||
except TypeError:
|
||||
pass
|
||||
self.stopTime = datetime.datetime.now()
|
||||
self.generateReport(test, result)
|
||||
print('\n测试耗时: %s' % (self.stopTime-self.startTime))
|
||||
return result
|
||||
|
||||
|
||||
def sortResult(self, result_list):
|
||||
# unittest does not seems to run in any particular order.
|
||||
# Here at least we want to group them together by class.
|
||||
rmap = {}
|
||||
classes = []
|
||||
for n,t,o,e in result_list:
|
||||
cls = t.__class__
|
||||
if not cls in rmap:
|
||||
rmap[cls] = []
|
||||
classes.append(cls)
|
||||
rmap[cls].append((n,t,o,e))
|
||||
r = [(cls, rmap[cls]) for cls in classes]
|
||||
return r
|
||||
|
||||
|
||||
def getReportAttributes(self, result):
|
||||
"""
|
||||
Return report attributes as a list of (name, value).
|
||||
Override this to add custom attributes.
|
||||
"""
|
||||
startTime = str(self.startTime)[:19]
|
||||
duration = str(self.stopTime - self.startTime)
|
||||
status = []
|
||||
if result.success_count: status.append('<span class="text text-success">Pass <strong>%s</strong></span>' % result.success_count)
|
||||
if result.failure_count: status.append('<span class="text text-danger">Failure <strong>%s</strong></span>' % result.failure_count)
|
||||
if result.error_count: status.append('<span class="text text-warning">Error <strong>%s</strong></span>' % result.error_count )
|
||||
if status:
|
||||
status = ' '.join(status)
|
||||
else:
|
||||
status = 'none'
|
||||
return [
|
||||
('Start Time', startTime),
|
||||
('Duration', duration),
|
||||
('Status', status),
|
||||
]
|
||||
|
||||
|
||||
def generateReport(self, test, result):
|
||||
report_attrs = self.getReportAttributes(result)
|
||||
generator = 'BSTestRunner %s' % __version__
|
||||
stylesheet = self._generate_stylesheet()
|
||||
heading = self._generate_heading(report_attrs)
|
||||
report = self._generate_report(result)
|
||||
ending = self._generate_ending()
|
||||
output = self.HTML_TMPL % dict(
|
||||
title = saxutils.escape(self.title),
|
||||
generator = generator,
|
||||
stylesheet = stylesheet,
|
||||
heading = heading,
|
||||
report = report,
|
||||
ending = ending,
|
||||
)
|
||||
self.stream.write(output.encode('utf-8'))
|
||||
|
||||
|
||||
def _generate_stylesheet(self):
|
||||
return self.STYLESHEET_TMPL
|
||||
|
||||
|
||||
def _generate_heading(self, report_attrs):
|
||||
a_lines = []
|
||||
for name, value in report_attrs:
|
||||
line = self.HEADING_ATTRIBUTE_TMPL % dict(
|
||||
name = saxutils.escape(name),####更改
|
||||
# value = saxutils.escape(value),
|
||||
|
||||
value = value,
|
||||
)
|
||||
a_lines.append(line)
|
||||
heading = self.HEADING_TMPL % dict(
|
||||
title = saxutils.escape(self.title),
|
||||
parameters = ''.join(a_lines),
|
||||
description = saxutils.escape(self.description),
|
||||
)
|
||||
return heading
|
||||
|
||||
|
||||
def _generate_report(self, result):
|
||||
rows = []
|
||||
sortedResult = self.sortResult(result.result)
|
||||
for cid, (cls, cls_results) in enumerate(sortedResult):
|
||||
# subtotal for a class
|
||||
np = nf = ne = 0
|
||||
for n,t,o,e in cls_results:
|
||||
if n == 0: np += 1
|
||||
elif n == 1: nf += 1
|
||||
else: ne += 1
|
||||
|
||||
# format class description
|
||||
if cls.__module__ == "__main__":
|
||||
name = cls.__name__
|
||||
else:
|
||||
name = "%s.%s" % (cls.__module__, cls.__name__)
|
||||
doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
|
||||
desc = doc and '%s: %s' % (name, doc) or name
|
||||
|
||||
row = self.REPORT_CLASS_TMPL % dict(
|
||||
style = ne > 0 and 'text text-warning' or nf > 0 and 'text text-danger' or 'text text-success',
|
||||
desc = desc,
|
||||
count = np+nf+ne,
|
||||
Pass = np,
|
||||
fail = nf,
|
||||
error = ne,
|
||||
cid = 'c%s' % (cid+1),
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
for tid, (n,t,o,e) in enumerate(cls_results):
|
||||
self._generate_report_test(rows, cid, tid, n, t, o, e)
|
||||
|
||||
report = self.REPORT_TMPL % dict(
|
||||
test_list = ''.join(rows),
|
||||
count = str(result.success_count+result.failure_count+result.error_count),
|
||||
Pass = str(result.success_count),
|
||||
fail = str(result.failure_count),
|
||||
error = str(result.error_count),
|
||||
)
|
||||
return report
|
||||
|
||||
|
||||
def _generate_report_test(self, rows, cid, tid, n, t, o, e):
|
||||
# e.g. 'pt1.1', 'ft1.1', etc
|
||||
has_output = bool(o or e)
|
||||
tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
|
||||
name = t.id().split('.')[-1]
|
||||
doc = t.shortDescription() or ""
|
||||
desc = doc and ('%s: %s' % (name, doc)) or name
|
||||
tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
|
||||
|
||||
# o and e should be byte string because they are collected from stdout and stderr?
|
||||
if isinstance(o,str):
|
||||
# TODO: some problem with 'string_escape': it escape \n and mess up formating
|
||||
# uo = unicode(o.encode('string_escape'))
|
||||
uo = o
|
||||
else:
|
||||
uo = o
|
||||
if isinstance(e,str):
|
||||
# TODO: some problem with 'string_escape': it escape \n and mess up formating
|
||||
# ue = unicode(e.encode('string_escape'))
|
||||
ue = e
|
||||
else:
|
||||
ue = e
|
||||
|
||||
script = self.REPORT_TEST_OUTPUT_TMPL % dict(
|
||||
id = tid,
|
||||
output = saxutils.escape(uo+ue),
|
||||
)
|
||||
|
||||
row = tmpl % dict(
|
||||
tid = tid,
|
||||
Class = (n == 0 and 'hiddenRow' or 'none'),
|
||||
# Class = (n == 0 and 'hiddenRow' or 'text text-success'),
|
||||
# style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
|
||||
style = n == 2 and 'text text-warning' or (n == 1 and 'text text-danger' or 'text text-success'),
|
||||
desc = desc,
|
||||
script = script,
|
||||
status = self.STATUS[n],
|
||||
)
|
||||
rows.append(row)
|
||||
if not has_output:
|
||||
return
|
||||
|
||||
def _generate_ending(self):
|
||||
return self.ENDING_TMPL
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Facilities for running tests from the command line
|
||||
##############################################################################
|
||||
|
||||
# Note: Reuse unittest.TestProgram to launch test. In the future we may
|
||||
# build our own launcher to support more specific command line
|
||||
# parameters like test title, CSS, etc.
|
||||
class TestProgram(unittest.TestProgram):
|
||||
"""
|
||||
A variation of the unittest.TestProgram. Please refer to the base
|
||||
class for command line parameters.
|
||||
"""
|
||||
def runTests(self):
|
||||
# Pick BSTestRunner as the default test runner.
|
||||
# base class's testRunner parameter is not useful because it means
|
||||
# we have to instantiate BSTestRunner before we know self.verbosity.
|
||||
if self.testRunner is None:
|
||||
self.testRunner = BSTestRunner(verbosity=self.verbosity)
|
||||
unittest.TestProgram.runTests(self)
|
||||
|
||||
main = TestProgram
|
||||
|
||||
##############################################################################
|
||||
# Executing this module from the command line
|
||||
##############################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(module=None)
|
||||
@@ -7,7 +7,7 @@ import xlrd,xlwt
|
||||
import unittest,sys
|
||||
from xlutils.copy import copy
|
||||
from Interface.test_requests import requ
|
||||
from .log import LOG,logger
|
||||
from Public.log import LOG,logger
|
||||
@logger('解析测试用例文件')
|
||||
def datacel():
|
||||
try:
|
||||
@@ -32,4 +32,15 @@ def datacel():
|
||||
listfangshi.append((me.cell(i,5).value))
|
||||
listqiwang.append((me.cell(i,6).value))
|
||||
return listid,listkey,listconeent,listurl,listfangshi,listqiwang,listname
|
||||
except:LOG.info('打开测试用例失败,原因是:%s'%Exception)
|
||||
except:LOG.info('打开测试用例失败,原因是:%s'%Exception)
|
||||
@logger('生成数据驱动所用数据')
|
||||
def makedata():
|
||||
listid, listkey, listconeent, listurl, listfangshi, listqiwang, listname=datacel()
|
||||
i=0
|
||||
make_data=[]
|
||||
for i in range(len(listid)):
|
||||
make_data.append({'url':listurl[i],'key':listkey[i],'coneent':listconeent[i],'fangshi':listfangshi[i],'qiwang':listqiwang[i]})
|
||||
i+=1
|
||||
return make_data
|
||||
|
||||
|
||||
|
||||
21
ceshi.py
Normal file
21
ceshi.py
Normal file
@@ -0,0 +1,21 @@
|
||||
list1=[1,2,3,4,5,6,7]
|
||||
list2=['a','b','c','d']
|
||||
list3=[]
|
||||
i=0
|
||||
for i in range(len(list2)):
|
||||
list3.append({'id':list1[i],'password':list2[i]})
|
||||
i+=1
|
||||
print(list3)
|
||||
# import ddt,unittest
|
||||
# data=[{1:1, 2:2},{1:2, 2:3},{1:3, 2:4}]
|
||||
# @ddt.ddt
|
||||
# class Testcase(unittest.TestCase):
|
||||
# def setUp(self):
|
||||
# pass
|
||||
# def tearDown(self):
|
||||
# pass
|
||||
# @ddt.data(*data)
|
||||
# def test(self,data):
|
||||
# self.assertEqual(data[1],data[2])
|
||||
# if __name__=='__main__':
|
||||
# unittest.main()
|
||||
BIN
img/xin_testcase.png
Normal file
BIN
img/xin_testcase.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
img/xinlog.png
Normal file
BIN
img/xinlog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
10
log/jiekou-2017-10-31-21.log
Normal file
10
log/jiekou-2017-10-31-21.log
Normal file
@@ -0,0 +1,10 @@
|
||||
[2017-10-31 21:17:01.191553] INFO: jiekou: 当前模块 requests封装
|
||||
[2017-10-31 21:17:01.273558] INFO: jiekou: 当前模块 生成数据驱动所用数据
|
||||
[2017-10-31 21:17:01.274558] INFO: jiekou: 当前模块 解析测试用例文件
|
||||
[2017-10-31 21:17:16.660438] INFO: jiekou: 当前模块 requests封装
|
||||
[2017-10-31 21:17:16.739442] INFO: jiekou: 当前模块 生成数据驱动所用数据
|
||||
[2017-10-31 21:17:16.740442] INFO: jiekou: 当前模块 解析测试用例文件
|
||||
[2017-10-31 21:17:16.754443] INFO: jiekou: 当前模块 执行测试用例
|
||||
[2017-10-31 21:18:18.624982] INFO: jiekou: 当前模块 requests封装
|
||||
[2017-10-31 21:18:18.704987] INFO: jiekou: 当前模块 生成数据驱动所用数据
|
||||
[2017-10-31 21:18:18.705987] INFO: jiekou: 当前模块 解析测试用例文件
|
||||
13
run_new.py
Normal file
13
run_new.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from testCase.test import ApiTest
|
||||
import unittest,time,os
|
||||
from Public import BSTestRunner
|
||||
if __name__=='__main__':
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ApiTest))
|
||||
now = time.strftime('%Y-%m%d', time.localtime(time.time()))
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
file_dir = os.path.join(basedir, 'test_Report')
|
||||
file = os.path.join(file_dir, (now + '.html'))
|
||||
re_open = open(file, 'wb')
|
||||
runner = BSTestRunner.BSTestRunner(stream=re_open, title='接口测试报告', description='测试结果')
|
||||
runner.run(suite)
|
||||
20
testCase/test.py
Normal file
20
testCase/test.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from Interface.testFengzhuang import TestApi
|
||||
from Public.get_excel import datacel,makedata
|
||||
from Public.log import LOG,logger
|
||||
import ddt,unittest
|
||||
data_test=makedata()
|
||||
@logger('执行测试用例')
|
||||
@ddt.ddt
|
||||
class ApiTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
def tearDown(self):
|
||||
pass
|
||||
@ddt.data(*data_test)
|
||||
def test_api(self,data_test):
|
||||
api = TestApi(url=data_test['url'], key=data_test['key'], connent=data_test['coneent'], fangshi=data_test['fangshi'])
|
||||
LOG.info('输入参数:url:%s,key:%s,参数:%s,请求方式:%s'%(data_test['url'],data_test['key'],data_test['coneent'], LOG.info('输入参数:url:%s,key:%s,参数:%s,请求方式:%s'%(data_test['url'],data_test['key'],data_test['coneent'],data_test['fangshi']))))
|
||||
apijson = api.getJson()
|
||||
LOG.info('返回结果:%s'%apijson)
|
||||
self.assertEqual(data_test['qiwang'],apijson,msg='预期和返回不一致')
|
||||
|
||||
197
test_Report/2017-1031.html
Normal file
197
test_Report/2017-1031.html
Normal file
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<title>接口测试报告</title>
|
||||
<meta name="generator" content="BSTestRunner 0.8.4"/>
|
||||
<link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">
|
||||
|
||||
<style type="text/css" media="screen">
|
||||
|
||||
/* -- css div popup ------------------------------------------------------------------------ */
|
||||
.popup_window {
|
||||
display: none;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
/*border: solid #627173 1px; */
|
||||
padding: 10px;
|
||||
background-color: #99CCFF;
|
||||
font-family: "Lucida Console", "Courier New", Courier, monospace;
|
||||
text-align: left;
|
||||
font-size: 10pt;
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
/* -- report ------------------------------------------------------------------------ */
|
||||
|
||||
#show_detail_line .label {
|
||||
font-size: 85%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#show_detail_line {
|
||||
margin: 2em auto 1em auto;
|
||||
}
|
||||
|
||||
#total_row { font-weight: bold; }
|
||||
.hiddenRow { display: none; }
|
||||
.testcase { margin-left: 2em; }
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||
<script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<script language="javascript" type="text/javascript"><!--
|
||||
output_list = Array();
|
||||
|
||||
/* level - 0:Summary; 1:Failed; 2:All */
|
||||
function showCase(level) {
|
||||
trs = document.getElementsByTagName("tr");
|
||||
for (var i = 0; i < trs.length; i++) {
|
||||
tr = trs[i];
|
||||
id = tr.id;
|
||||
if (id.substr(0,2) == 'ft') {
|
||||
if (level < 1) {
|
||||
tr.className = 'hiddenRow';
|
||||
}
|
||||
else {
|
||||
tr.className = '';
|
||||
}
|
||||
}
|
||||
if (id.substr(0,2) == 'pt') {
|
||||
if (level > 1) {
|
||||
tr.className = '';
|
||||
}
|
||||
else {
|
||||
tr.className = 'hiddenRow';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showClassDetail(cid, count) {
|
||||
var id_list = Array(count);
|
||||
var toHide = 1;
|
||||
for (var i = 0; i < count; i++) {
|
||||
tid0 = 't' + cid.substr(1) + '.' + (i+1);
|
||||
tid = 'f' + tid0;
|
||||
tr = document.getElementById(tid);
|
||||
if (!tr) {
|
||||
tid = 'p' + tid0;
|
||||
tr = document.getElementById(tid);
|
||||
}
|
||||
id_list[i] = tid;
|
||||
if (tr.className) {
|
||||
toHide = 0;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < count; i++) {
|
||||
tid = id_list[i];
|
||||
if (toHide) {
|
||||
document.getElementById('div_'+tid).style.display = 'none'
|
||||
document.getElementById(tid).className = 'hiddenRow';
|
||||
}
|
||||
else {
|
||||
document.getElementById(tid).className = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showTestDetail(div_id){
|
||||
var details_div = document.getElementById(div_id)
|
||||
var displayState = details_div.style.display
|
||||
// alert(displayState)
|
||||
if (displayState != 'block' ) {
|
||||
displayState = 'block'
|
||||
details_div.style.display = 'block'
|
||||
}
|
||||
else {
|
||||
details_div.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function html_escape(s) {
|
||||
s = s.replace(/&/g,'&');
|
||||
s = s.replace(/</g,'<');
|
||||
s = s.replace(/>/g,'>');
|
||||
return s;
|
||||
}
|
||||
|
||||
/* obsoleted by detail in <div>
|
||||
function showOutput(id, name) {
|
||||
var w = window.open("", //url
|
||||
name,
|
||||
"resizable,scrollbars,status,width=800,height=450");
|
||||
d = w.document;
|
||||
d.write("<pre>");
|
||||
d.write(html_escape(output_list[id]));
|
||||
d.write("\n");
|
||||
d.write("<a href='javascript:window.close()'>close</a>\n");
|
||||
d.write("</pre>\n");
|
||||
d.close();
|
||||
}
|
||||
*/
|
||||
--></script>
|
||||
|
||||
<div class="container">
|
||||
<div class='heading'>
|
||||
<h1>接口测试报告</h1>
|
||||
<p><strong>Start Time:</strong> 2017-10-31 21:15:03</p>
|
||||
<p><strong>Duration:</strong> 0:00:00</p>
|
||||
<p><strong>Status:</strong> none</p>
|
||||
|
||||
<p class='description'>测试结果</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p id='show_detail_line'>
|
||||
<span class="label label-primary" onclick="showCase(0)">Summary</span>
|
||||
<span class="label label-danger" onclick="showCase(1)">Failed</span>
|
||||
<span class="label label-default" onclick="showCase(2)">All</span>
|
||||
</p>
|
||||
<table id='result_table' class="table">
|
||||
<thead>
|
||||
<tr id='header_row'>
|
||||
<th>Test Group/Test case</td>
|
||||
<th>Count</td>
|
||||
<th>Pass</td>
|
||||
<th>Fail</td>
|
||||
<th>Error</td>
|
||||
<th>View</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr id='total_row'>
|
||||
<td>Total</td>
|
||||
<td>0</td>
|
||||
<td class="text text-success">0</td>
|
||||
<td class="text text-danger">0</td>
|
||||
<td class="text text-warning">0</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div id='ending'> </div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user