1301 lines
48 KiB
Python
1301 lines
48 KiB
Python
"""
|
||
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 HTMLTestRunner
|
||
|
||
... define your tests ...
|
||
|
||
if __name__ == '__main__':
|
||
HTMLTestRunner.main()
|
||
|
||
|
||
For more customization options, instantiates a HTMLTestRunner object.
|
||
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
|
||
|
||
# output to a file
|
||
fp = file('my_report.html', 'wb')
|
||
runner = HTMLTestRunner.HTMLTestRunner(
|
||
stream=fp,
|
||
title='My unit test',
|
||
description='This demonstrates the report output by HTMLTestRunner.'
|
||
)
|
||
|
||
# Use an external stylesheet.
|
||
# See the Template_mixin class for more customizable options
|
||
runner.STYLES_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
|
||
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.
|
||
"""
|
||
|
||
import os, re, sys, io, time, datetime, json, unittest, logging
|
||
from xml.sax import saxutils
|
||
|
||
_global_dict = {}
|
||
|
||
|
||
class GlobalMsg(object):
|
||
def __init__(self):
|
||
global _global_dict
|
||
_global_dict = {}
|
||
|
||
@staticmethod
|
||
def set_value(name, value):
|
||
_global_dict[name] = value
|
||
|
||
@staticmethod
|
||
def get_value(name):
|
||
try:
|
||
return _global_dict[name]
|
||
except KeyError:
|
||
return None
|
||
|
||
|
||
# ------------------------------------------------------------------------
|
||
# 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 HTMLTestRunner 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=HTMLTestRunner.stdout_redirector)
|
||
# >>>
|
||
|
||
|
||
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):
|
||
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> |
|
||
| |
|
||
| STYLES |
|
||
| +----------------+ |
|
||
| | | |
|
||
| +----------------+ |
|
||
| |
|
||
| </head> |
|
||
| |
|
||
| <body> |
|
||
| |
|
||
| HEADER |
|
||
| +----------------+ |
|
||
| | | |
|
||
| +----------------+ |
|
||
| |
|
||
| REPORT |
|
||
| +----------------+ |
|
||
| | | |
|
||
| +----------------+ |
|
||
| |
|
||
| FOOTER |
|
||
| +----------------+ |
|
||
| | | |
|
||
| +----------------+ |
|
||
| |
|
||
| </body> |
|
||
|</html> |
|
||
+------------------------+
|
||
"""
|
||
|
||
STATUS = {
|
||
0: '通过',
|
||
1: '失败',
|
||
2: '错误',
|
||
}
|
||
|
||
DEFAULT_TITLE = '测试报告'
|
||
DEFAULT_DESCRIPTION = ''
|
||
DEFAULT_TESTER = 'Tester'
|
||
|
||
# ------------------------------------------------------------------------
|
||
# 网页模板开始
|
||
# 网页模板,变量列表 title, generator, styles, header, report, footer
|
||
HTML_TMPL = r"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<title>%(title)s</title>
|
||
<meta name="generator" content="%(generator)s"/>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||
<link rel="stylesheet" href="https://www.fanscloud.net/res/bootstrap/3.0.3/css/bootstrap.min.css">
|
||
<script src="https://www.fanscloud.net/res/jquery/2.0.0/jquery.min.js"></script>
|
||
<script src="https://www.fanscloud.net/res/bootstrap/3.0.3/js/bootstrap.min.js"></script>
|
||
<script src="https://img.hcharts.cn/highcharts/highcharts.js"></script>
|
||
<script src="https://img.hcharts.cn/highcharts/modules/exporting.js"></script>
|
||
%(styles)s
|
||
</head>
|
||
<body >
|
||
<script language="javascript" type="text/javascript">
|
||
|
||
$(function(){
|
||
// 修改 失败 和 错误 用例里对应按钮的颜色ClassName为动态加载 -- Gelomen
|
||
$("button").each(function () {
|
||
var text = $(this).text();
|
||
if(text == "失败"){
|
||
$(this).addClass("btn-danger")
|
||
}else if(text == "错误") {
|
||
$(this).addClass("btn-warning")
|
||
}else if(text == "通过") {
|
||
$(this).addClass("btn-success")
|
||
}
|
||
});
|
||
|
||
// 给失败和错误合集加样式 -- Gelomen
|
||
var p_attribute = $("p.attribute");
|
||
p_attribute.eq(4).addClass("failCollection");
|
||
p_attribute.eq(5).addClass("errorCollection");
|
||
|
||
// 打开截图,放大,点击任何位置可以关闭图片
|
||
$(".screenshot").click(function(){
|
||
var img = $(this).attr("img");
|
||
$('.pic_show img').attr('src', img);
|
||
$('.pic_looper').fadeIn(200);
|
||
$('.pic_show').fadeIn(200);
|
||
|
||
var browserHeight = $(window).height();
|
||
var pic_boxHeight = $(".pic_box").height();
|
||
var top = (browserHeight - pic_boxHeight)/2;
|
||
$('.pic_box').css("margin-top", top + "px")
|
||
|
||
});
|
||
$('.pic_looper, .pic_show').click(function(){
|
||
$('.pic_looper').fadeOut(200);
|
||
$('.pic_show').fadeOut(200)
|
||
});
|
||
|
||
var resize_action = function(){
|
||
// 改变窗口大小时,自动改变图表边距
|
||
var browserWidth = $(window).width();
|
||
var margin_left = browserWidth - 360 - 450 - 550 - 40;
|
||
if(margin_left <= 0){
|
||
$("#container").css("width", "100%%");
|
||
$("#container_extend").css("width", "100%%");
|
||
$("#testinfo").css("width", "100%%");
|
||
$("#container").css("margin-left", "0px");
|
||
}else {
|
||
$("#container").css("width", "450px");
|
||
$("#container_extend").css("width", "550px");
|
||
$("#testinfo").css("width", "25%%");
|
||
$("#container").css("margin-left", (margin_left - 1) + "px");
|
||
}
|
||
}
|
||
resize_action();
|
||
|
||
$(window).resize(function(){
|
||
// 改变窗口大小时,自动改变图片与顶部的距离
|
||
var browserHeight = $(window).height();
|
||
var pic_boxHeight = $(".pic_box").height();
|
||
var top = (browserHeight - pic_boxHeight)/2;
|
||
$('.pic_box').css("margin-top", top + "px");
|
||
resize_action();
|
||
});
|
||
// 超过浏览器高度时,回到顶部按钮出现
|
||
$(window).scroll(function(){
|
||
var browserHeight = $(window).height();
|
||
var top = $(window).scrollTop();
|
||
if(top >= browserHeight){
|
||
$("#toTop").css("display", "block")
|
||
}else {
|
||
$("#toTop").css("display", "none")
|
||
}
|
||
})
|
||
// 增加回到顶部过程动画
|
||
$("#toTop").click(function() {
|
||
$("html,body").animate({"scrollTop":0}, 500)
|
||
})
|
||
// 增加条形图
|
||
$('#container_extend').highcharts({
|
||
chart: {
|
||
type: 'bar'
|
||
},
|
||
credits: {
|
||
enabled: false
|
||
},
|
||
navigation: {
|
||
buttonOptions: {
|
||
enabled: false
|
||
}
|
||
},
|
||
title: {
|
||
text: '用例集合情况'
|
||
},
|
||
xAxis: {
|
||
categories: %(casesets)s
|
||
},
|
||
yAxis: {
|
||
min: 0,
|
||
title: {
|
||
text: '用例数量'
|
||
},
|
||
reversedStacks: false
|
||
},
|
||
legend: {
|
||
reversed: false
|
||
},
|
||
plotOptions: {
|
||
series: {
|
||
stacking: 'normal'
|
||
}
|
||
},
|
||
series: [{
|
||
name: '通过',
|
||
color: '#64BB64',
|
||
data: %(casesets_passed)s
|
||
}, {
|
||
name: '失败',
|
||
color: '#F16D7E',
|
||
data: %(casesets_failed)s
|
||
}, {
|
||
name: '错误',
|
||
color: '#FDC68C',
|
||
data: %(casesets_errors)s
|
||
}]
|
||
})
|
||
// 增加饼状图
|
||
$('#container').highcharts({
|
||
chart: {
|
||
plotBackgroundColor: null,
|
||
plotBorderWidth: null,
|
||
plotShadow: false,
|
||
spacing : [0, 0, 0, 0]
|
||
},
|
||
credits: {
|
||
enabled: false
|
||
},
|
||
navigation: {
|
||
buttonOptions: {
|
||
enabled: false
|
||
}
|
||
},
|
||
title: {
|
||
floating: true,
|
||
text: '测试结果占比'
|
||
},
|
||
tooltip: {
|
||
pointFormat: '<text style="font-size:10px">{series.name}: {point.percentage:.1f}%%</text>'
|
||
},
|
||
plotOptions: {
|
||
pie: {
|
||
allowPointSelect: true,
|
||
cursor: 'pointer',
|
||
colors: ['#81ca9d', '#f16d7e', '#fdc68c'],
|
||
dataLabels: {
|
||
enabled: true,
|
||
format: '<b>{point.name}</b>: {point.percentage:.1f} %%',
|
||
style: {
|
||
color: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black'
|
||
}
|
||
},
|
||
point: {
|
||
events: {
|
||
mouseOver: function(e) { // 鼠标滑过时动态更新标题
|
||
chart.setTitle({
|
||
text: e.target.name+ '\t'+ e.target.y + ' 个'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
type: 'pie',
|
||
innerSize: '80%%',
|
||
name: '比例',
|
||
data: [
|
||
['通过', %(passed)s], ['失败', %(failed)s], ['错误', %(errors)s]
|
||
]
|
||
}]
|
||
}, function(c) {
|
||
// 环形圆心
|
||
var centerY = c.series[0].center[1],
|
||
titleHeight = parseInt(c.title.styles.fontSize);
|
||
c.setTitle({
|
||
x:0,
|
||
y:centerY + titleHeight/2
|
||
});
|
||
chart = c;
|
||
});
|
||
|
||
// 查看 失败 和 错误 合集链接文字切换 -- Gelomen
|
||
$(".showDetail").click(function () {
|
||
if($(this).html() == "点击查看"){
|
||
$(this).html("点击收起")
|
||
}else {
|
||
$(this).html("点击查看")
|
||
}
|
||
})
|
||
});
|
||
|
||
|
||
output_list = Array();
|
||
|
||
/*level 调整增加只显示通过用例的分类 --Findyou / 修复筛选显示bug --Gelomen
|
||
0:Summary //all hiddenRow
|
||
1:Failed //pt&et hiddenRow, ft none
|
||
2:Passed //pt none, ft&et hiddenRow
|
||
3:Errors //pt&ft hiddenRow, et none
|
||
4:All //all none
|
||
*/
|
||
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 == 2 || level == 0 || level == 3) {
|
||
tr.className = 'hiddenRow';
|
||
}
|
||
else {
|
||
tr.className = '';
|
||
// 切换筛选时只显示预览 -- Gelomen
|
||
// 失败
|
||
$("div[id^='div_ft']").attr("class", "collapse");
|
||
$("div[id^='div_et']").attr("class", "collapse");
|
||
$("div[id^='div_pt']").attr("class", "collapse");
|
||
}
|
||
}
|
||
if (id.substr(0,2) == 'pt') {
|
||
if (level == 1 || level == 0 || level == 3) {
|
||
tr.className = 'hiddenRow';
|
||
}
|
||
else {
|
||
tr.className = '';
|
||
// 切换筛选时只显示预览 -- Gelomen
|
||
// 通过
|
||
$("div[id^='div_ft']").attr("class", "collapse");
|
||
$("div[id^='div_et']").attr("class", "collapse");
|
||
$("div[id^='div_pt']").attr("class", "collapse");
|
||
}
|
||
}
|
||
if (id.substr(0,2) == 'et') {
|
||
if (level == 1 || level == 0 || level == 2) {
|
||
tr.className = 'hiddenRow';
|
||
}
|
||
else {
|
||
tr.className = '';
|
||
// 切换筛选时只显示预览 -- Gelomen
|
||
// 错误
|
||
$("div[id^='div_ft']").attr("class", "collapse");
|
||
$("div[id^='div_et']").attr("class", "collapse");
|
||
$("div[id^='div_pt']").attr("class", "collapse");
|
||
}
|
||
}
|
||
}
|
||
|
||
//加入详细切换文字变化
|
||
detail_class=document.getElementsByClassName('detail');
|
||
if (level == 4) {
|
||
for (var i = 0; i < detail_class.length; i++){
|
||
detail_class[i].innerHTML="收起"
|
||
}
|
||
}
|
||
else{
|
||
for (var i = 0; i < detail_class.length; i++){
|
||
detail_class[i].innerHTML="详细"
|
||
}
|
||
}
|
||
}
|
||
|
||
function showClassDetail(cid, count) {
|
||
var id_list = Array(count);
|
||
var toHide = 1;
|
||
for (var i = 0; i < count; i++) {
|
||
//ID修改.为_
|
||
tid0 = 't' + cid.substr(1) + '_' + (i+1);
|
||
tid = 'f' + tid0;
|
||
tr = document.getElementById(tid);
|
||
if (!tr) {
|
||
tid = 'p' + tid0;
|
||
tr = document.getElementById(tid);
|
||
if (!tr) {
|
||
tid = 'e' + 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];
|
||
//修改点击无法收起的BUG,加入【详细】切换文字变化 --Findyou
|
||
if (toHide) {
|
||
document.getElementById(tid).className = 'hiddenRow';
|
||
document.getElementById(cid).innerText = "详细"
|
||
}
|
||
else {
|
||
document.getElementById(tid).className = '';
|
||
document.getElementById(cid).innerText = "收起"
|
||
}
|
||
}
|
||
}
|
||
|
||
function html_escape(s) {
|
||
s = s.replace(/&/g,'&');
|
||
s = s.replace(/</g,'<');
|
||
s = s.replace(/>/g,'>');
|
||
return s;
|
||
}
|
||
</script>
|
||
%(header)s
|
||
%(report)s
|
||
%(footer)s
|
||
<div style='width:auto;height:24px;line-height:24px;border:1px solid #e3e3e3;text-align:center'><a href='#' style='color:#505050'></a></div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
# 网页模板结束
|
||
# ------------------------------------------------------------------------
|
||
|
||
# ------------------------------------------------------------------------
|
||
# Stylesheet
|
||
#
|
||
# alternatively use a <link> for external style sheet, e.g.
|
||
# <link rel="stylesheet" href="$url" type="text/css">
|
||
|
||
STYLES_TMPL = """
|
||
<style type="text/css" media="screen">
|
||
body { font-family: Microsoft YaHei;padding: 20px; font-size: 100%; }
|
||
table { font-size: 100%; }
|
||
.table tbody tr td{
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* -- header ---------------------------------------------------------------------- */
|
||
.header .description, .attribute {
|
||
clear: both;
|
||
}
|
||
|
||
/* --- 失败和错误合集样式 -- Gelomen --- */
|
||
.failCollection, .errorCollection {
|
||
width: auto;
|
||
float: left;
|
||
}
|
||
#failedCaseOl li {
|
||
color: red
|
||
}
|
||
#errorsCaseOl li {
|
||
color: orange
|
||
}
|
||
|
||
/* --- 打开截图特效样式 -- Gelomen --- */
|
||
.data-img{
|
||
cursor:pointer
|
||
}
|
||
|
||
.pic_looper{
|
||
width:100%;
|
||
height:100%;
|
||
position: fixed;
|
||
left: 0;
|
||
top:0;
|
||
opacity: 0.6;
|
||
background: #000;
|
||
display: none;
|
||
z-index: 100;
|
||
}
|
||
|
||
.pic_show{
|
||
width:100%;
|
||
position:fixed;
|
||
left:0;
|
||
top:0;
|
||
right:0;
|
||
bottom:0;
|
||
margin:auto;
|
||
text-align: center;
|
||
display: none;
|
||
z-index: 100;
|
||
}
|
||
|
||
.pic_box{
|
||
padding:10px;
|
||
width:90%;
|
||
height:90%;
|
||
margin:40px auto;
|
||
text-align: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pic_box img{
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
width: auto;
|
||
height: auto;
|
||
-moz-box-shadow: 0px 0px 20px 0px #000;
|
||
-webkit-box-shadow: 0px 0px 20px 0px #000;
|
||
box-shadow: 0px 0px 20px 0px #000;
|
||
}
|
||
|
||
/* --- 饼状图样式 */
|
||
#container {
|
||
max-width: 100%;
|
||
width: 450px;
|
||
height: 350px;
|
||
float: left;
|
||
}
|
||
#container_extend {
|
||
max-width: 100%;
|
||
width: 550px;
|
||
height: 400px;
|
||
float: left;
|
||
}
|
||
|
||
|
||
/* -- report ------------------------------------------------------------------------ */
|
||
#total_row { font-weight: bold; }
|
||
.passedCase { color: #3FB83F; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; font-size: 14px; font-weight: bold; }
|
||
.failedCase { color: #D9433F; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; font-size: 14px; font-weight: bold; }
|
||
.errorsCase { color: #F0A02F; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; font-size: 14px; font-weight: bold; }
|
||
.hiddenRow { display: none; }
|
||
.testcase { margin-left: 1em; word-break: break-all; white-space: pre-wrap; }
|
||
.screenshot:link { text-decoration: none;color: deeppink; }
|
||
.screenshot:visited { text-decoration: none;color: deeppink; }
|
||
.screenshot:hover { text-decoration: none;color: darkcyan; }
|
||
.screenshot:active { text-decoration: none;color: deeppink; }
|
||
</style>
|
||
"""
|
||
|
||
# ------------------------------------------------------------------------
|
||
# 头部信息开始
|
||
# 添加显示截图和统计图div,变量列表 title, parameters, description
|
||
HEADER_TMPL = """
|
||
<div class='pic_looper'></div>
|
||
<div class='pic_show'>
|
||
<div class='pic_box'>
|
||
<img src=''/>
|
||
</div>
|
||
</div>
|
||
<div class='header'>
|
||
<div id="testinfo" style="max-width:360px ;width: auto; float: left;">
|
||
<h1 style="margin: 5px 0px 10px 0px; font-family: Microsoft YaHei;">%(title)s</h1>
|
||
%(parameters)s
|
||
<p class='description'>%(description)s</p>
|
||
</div>
|
||
<div id="container"></div>
|
||
<div id="container_extend"></div>
|
||
</div>
|
||
"""
|
||
|
||
# 测试信息模板,变量列表 name, value
|
||
HEADER_ATTRIBUTE_TMPL = """
|
||
<p class='attribute'><strong>%(name)s : </strong> %(value)s</p>
|
||
"""
|
||
# 头部信息结束
|
||
# ------------------------------------------------------------------------
|
||
|
||
# ------------------------------------------------------------------------
|
||
# 报告模板开始
|
||
# 变量列表 test_list, counts, passed, failed, errors ,passrate
|
||
REPORT_TMPL = """
|
||
<div style="width: auto; clear: both;">
|
||
<p id='show_detail_line'>
|
||
<a class="btn btn-primary" href='javascript:showCase(0)'>概要 %(passrate)s</a>
|
||
<a class="btn btn-success" href='javascript:showCase(2)'>通过 %(passed)s</a>
|
||
<a class="btn btn-danger" href='javascript:showCase(1)'>失败 %(failed)s</a>
|
||
<a class="btn btn-warning" href='javascript:showCase(3)'>错误 %(errors)s</a>
|
||
<a class="btn btn-info" href='javascript:showCase(4)'>全部 %(counts)s</a>
|
||
</p>
|
||
</div>
|
||
<table id='result_table' class="table table-condensed table-bordered table-hover">
|
||
<colgroup>
|
||
<col align='left' style="width: 300px;"/>
|
||
<col align='right' style="width: 285px;"/>
|
||
<col align='right' />
|
||
<col align='right' />
|
||
<col align='right' />
|
||
<col align='right' />
|
||
<col align='right' />
|
||
<col align='right' style="width: 120px;"/>
|
||
</colgroup>
|
||
<tr id='header_row' class="text-center success" style="font-weight: bold;font-size: 14px;">
|
||
<td>测试用例</td>
|
||
<td>说明</td>
|
||
<td>总计</td>
|
||
<td>通过</td>
|
||
<td>失败</td>
|
||
<td>错误</td>
|
||
<td>耗时</td>
|
||
<td>详细</td>
|
||
</tr>
|
||
%(test_list)s
|
||
<tr id='total_row' class="text-center active">
|
||
<td colspan='2'>总计</td>
|
||
<td>%(counts)s</td>
|
||
<td>%(passed)s</td>
|
||
<td>%(failed)s</td>
|
||
<td>%(errors)s</td>
|
||
<td>%(time_usage)s</td>
|
||
<td>通过:%(passrate)s</td>
|
||
</tr>
|
||
</table>
|
||
"""
|
||
|
||
# 变量列表 style, desc, counts, passed, failed, errors, cid
|
||
REPORT_CLASS_TMPL = """
|
||
<tr class='%(style)s warning'>
|
||
<td>%(name)s</td>
|
||
<td>%(docs)s</td>
|
||
<td class="text-center">%(counts)s</td>
|
||
<td class="text-center">%(passed)s</td>
|
||
<td class="text-center">%(failed)s</td>
|
||
<td class="text-center">%(errors)s</td>
|
||
<td class="text-center">%(time_usage)s</td>
|
||
<td class="text-center"><a href="javascript:showClassDetail('%(cid)s',%(counts)s)" class="detail" id='%(cid)s'>查看全部</a></td>
|
||
</tr>
|
||
"""
|
||
|
||
# 失败样式(有截图列),变量列表 tid, Class, style, desc, status
|
||
REPORT_TEST_WITH_OUTPUT_TMPL_1 = """
|
||
<tr id='%(tid)s' class='%(Class)s'>
|
||
<td class='%(style)s' style="vertical-align: middle"><div class='testcase'>%(name)s</div></td>
|
||
<td style="vertical-align: middle">%(docs)s</td>
|
||
<td colspan='5' align='center'>
|
||
<button id='btn_%(tid)s' type="button" class="btn btn-xs" data-toggle="collapse" data-target='#div_%(tid)s,#div_%(tid)s_screenshot'>%(status)s</button>
|
||
<div id='div_%(tid)s' class="collapse in">
|
||
<pre style="text-align:left;font-size:12px;color:#e52000">%(script)s</pre>
|
||
</div>
|
||
</td>
|
||
<td class="text-center" style="vertical-align: middle">
|
||
<div style='width:auto;height:auto;border:1px solid #b5b5b5;color:#202020font-weight:bold;text-align:center'>截图信息</div>
|
||
<div id='div_%(tid)s_screenshot' style="text-align: left" class="collapse in">
|
||
%(screenshot)s
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
"""
|
||
|
||
# 失败样式(无截图列),变量列表 tid, Class, style, desc, status
|
||
REPORT_TEST_WITH_OUTPUT_TMPL_0 = """
|
||
<tr id='%(tid)s' class='%(Class)s'>
|
||
<td class='%(style)s' style="vertical-align: middle"><div class='testcase'>%(name)s</div></td>
|
||
<td style="vertical-align: middle">%(docs)s</td>
|
||
<td colspan='5' align='center'>
|
||
<button id='btn_%(tid)s' type="button" class="btn btn-xs" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button>
|
||
<div id='div_%(tid)s' class="collapse in">
|
||
<pre style="text-align:left;font-size:12px;color:#e52000">%(script)s</pre>
|
||
</div>
|
||
</td>
|
||
<td class='%(style)s' style="vertical-align: middle"></td>
|
||
</tr>
|
||
"""
|
||
|
||
# 通过样式,变量列表 tid, Class, style, desc, status
|
||
REPORT_TEST_NO_OUTPUT_TMPL = """
|
||
<tr id='%(tid)s' class='%(Class)s'>
|
||
<td class='%(style)s' style="vertical-align: middle"><div class='testcase'>%(name)s</div></td>
|
||
<td style="vertical-align: left">%(docs)s</td>
|
||
<td colspan='5' align='center'><button type="button" class="btn btn-xs">%(status)s</button></td>
|
||
<td class='%(style)s' style="vertical-align: middle"></td>
|
||
</tr>
|
||
"""
|
||
|
||
# 测试输出内容
|
||
REPORT_TEST_OUTPUT_TMPL = '%(id)s:' + "\n" + '%(output)s'
|
||
|
||
# 返回顶部按钮
|
||
FOOTER_TMPL = """
|
||
<div id='footer'> </div>
|
||
<div id="toTop" style="position:fixed;right:50px; bottom:30px; width:20px; height:20px;cursor:pointer; display: none">
|
||
<a>
|
||
<span class="glyphicon glyphicon-eject" style = "font-size:28px;color:#b0b0b0" aria-hidden="true">
|
||
</span>
|
||
</a>
|
||
</div>
|
||
"""
|
||
# 报告模板结束
|
||
# ------------------------------------------------------------------------
|
||
|
||
|
||
def _findMark(mark='', text=''):
|
||
return re.findall('\[' + mark + '](.*?)\[/' + mark + ']' + '*?', text)
|
||
|
||
|
||
def _makeMark(mark='', cont=''):
|
||
return '[' + mark + ']' + cont +'[/' + mark + ']'
|
||
|
||
|
||
def _Color(fc=0, bc=0, bo=0, text=''):
|
||
if "PYCHARM" not in os.environ.keys():
|
||
return text
|
||
return "\033[" + str(bo) + ['', ';' + str(fc)][fc > 0] + ['', ';' + str(bc)][bc > 0] + "m" + text + "\033[0m"
|
||
|
||
|
||
class _TestResult(unittest.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):
|
||
super().__init__(verbosity=verbosity)
|
||
self.verbosity = verbosity
|
||
self.loggerStream = None
|
||
self.outputBuffer = None
|
||
self.stdout0 = None
|
||
self.stderr0 = None
|
||
self.passed_count = 0
|
||
self.failed_count = 0
|
||
self.errors_count = 0
|
||
self.stime = None
|
||
self.etime = None
|
||
# 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 = []
|
||
self.passrate = float(0)
|
||
# 分类统计数量耗时
|
||
self.casesort = {}
|
||
# 增加失败用例合集
|
||
self.failedCase = ""
|
||
self.failedCaseList = []
|
||
# 增加错误用例合集
|
||
self.errorsCase = ""
|
||
self.errorsCaseList = []
|
||
|
||
self.logger = logging.getLogger('test')
|
||
|
||
def sortCount(self, cls, res, dur=float(0)):
|
||
s = self.casesort
|
||
if cls not in s.keys():
|
||
s[cls] = {"p": 0, "f": 0, "e": 0, "d": 0}
|
||
if str(res).upper().startswith('P'):
|
||
s[cls]['p'] += 1
|
||
if str(res).upper().startswith('F'):
|
||
s[cls]['f'] += 1
|
||
if str(res).upper().startswith('E'):
|
||
s[cls]['e'] += 1
|
||
s[cls]['d'] += dur
|
||
return True
|
||
|
||
def startTest(self, test):
|
||
# 单条用例执行开始前的动作
|
||
if self.verbosity > 1:
|
||
sys.stderr.write(_Color(fc=30, bc=47, bo=1, text='%04d' % (len(self.result) + 1)) + ' ')
|
||
super().startTest(test)
|
||
# Just one buffer for both stdout and stderr
|
||
self.outputBuffer = io.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
|
||
self.stime = round(time.time(), 3)
|
||
self.loggerStream = io.StringIO()
|
||
self.ch = logging.StreamHandler(self.loggerStream)
|
||
self.ch.setLevel(logging.DEBUG)
|
||
self.ch.setFormatter(
|
||
logging.Formatter('%(asctime)s - %(name)s -%(levelname)s -%(process)d -%(processName)s - %(message)s'))
|
||
self.logger.addHandler(self.ch)
|
||
|
||
def complete_output(self):
|
||
# 单条用例执行结束后,添加结果前的动作
|
||
# 添加结果需要调用的方法
|
||
"""
|
||
Disconnect output redirection and return buffer.
|
||
Safe to call multiple times.
|
||
"""
|
||
self.etime = round(time.time(), 3)
|
||
if self.stdout0:
|
||
sys.stdout = self.stdout0
|
||
sys.stderr = self.stderr0
|
||
self.stdout0 = None
|
||
self.stderr0 = None
|
||
return self.loggerStream.getvalue() + self.outputBuffer.getvalue()
|
||
|
||
def stopTest(self, test):
|
||
# 单条用例执行结束后,添加结果后的动作
|
||
super().stopTest(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()
|
||
# 移除日志Handler
|
||
self.logger.removeHandler(self.ch)
|
||
|
||
def addSuccess(self, test):
|
||
# 单条用例执行结束后,添加结果时的动作
|
||
term_mark = '='
|
||
term_head = 'Passed'
|
||
term_clor = 32
|
||
self.passed_count += 1
|
||
super().addSuccess(test)
|
||
output = self.complete_output()
|
||
utime = round(self.etime - self.stime, 3)
|
||
self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=utime)
|
||
self.result.append((0, test, output, '', utime))
|
||
# 单条用例执行结束后在终端打印结果
|
||
if self.verbosity > 1:
|
||
sys.stderr.write('%s %s %s %s.%s.%-18s\t%s %s\n' % (
|
||
_Color(fc=term_clor, bo=1, text=term_mark + ' ' + term_head + ':'),
|
||
datetime.datetime.utcfromtimestamp(utime).strftime('%H:%M:%S.%f')[0:12],
|
||
_Color(fc=term_clor, bo=1, text='<='),
|
||
str(test.__module__).strip('_'),
|
||
str(test.__class__.__qualname__),
|
||
str(test.__dict__["_testMethodName"]),
|
||
_Color(fc=term_clor, bo=1, text='<='),
|
||
str(test.__dict__["_testMethodDoc"] or "")
|
||
))
|
||
|
||
def addError(self, test, err):
|
||
# 单条用例执行结束后,添加结果时的动作
|
||
term_mark = '?'
|
||
term_head = 'Errors'
|
||
term_clor = 33
|
||
self.errors_count += 1
|
||
super().addError(test, err)
|
||
_, _exc_str = self.errors[-1]
|
||
output = self.complete_output()
|
||
utime = round(self.etime - self.stime, 3)
|
||
self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=utime)
|
||
self.result.append((2, test, output, _exc_str, utime))
|
||
# 单条用例执行结束后在终端打印结果
|
||
if self.verbosity > 1:
|
||
sys.stderr.write('%s %s %s %s.%s.%-18s\t%s %s\n' % (
|
||
_Color(fc=term_clor, bo=1, text=term_mark + ' ' + term_head + ':'),
|
||
datetime.datetime.utcfromtimestamp(utime).strftime('%H:%M:%S.%f')[0:12],
|
||
_Color(fc=term_clor, bo=1, text='<='),
|
||
str(test.__module__).strip('_'),
|
||
str(test.__class__.__qualname__),
|
||
str(test.__dict__["_testMethodName"]),
|
||
_Color(fc=term_clor, bo=1, text='<='),
|
||
str(test.__dict__["_testMethodDoc"] or "")
|
||
))
|
||
# 收集错误测试用例名称以在测试报告中显示
|
||
casename = str(test.__module__).strip('_') + '.' + str(test.__class__.__qualname__) + '.' + str(
|
||
test.__dict__["_testMethodName"])
|
||
self.errorsCase += "<li>" + casename + "</li>"
|
||
self.errorsCaseList.append(casename)
|
||
|
||
def addFailure(self, test, err):
|
||
# 单条用例执行结束后,添加结果时的动作
|
||
term_mark = '!'
|
||
term_head = 'Failed'
|
||
term_clor = 31
|
||
self.failed_count += 1
|
||
super().addFailure(test, err)
|
||
_, _exc_str = self.failures[-1]
|
||
output = self.complete_output()
|
||
utime = round(self.etime - self.stime, 3)
|
||
self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=utime)
|
||
self.result.append((1, test, output, _exc_str, utime))
|
||
# 单条用例执行结束后在终端打印结果
|
||
if self.verbosity > 1:
|
||
sys.stderr.write('%s %s %s %s.%s.%-18s\t%s %s\n' % (
|
||
_Color(fc=term_clor, bo=1, text=term_mark + ' ' + term_head + ':'),
|
||
datetime.datetime.utcfromtimestamp(utime).strftime('%H:%M:%S.%f')[0:12],
|
||
_Color(fc=term_clor, bo=1, text='<='),
|
||
str(test.__module__).strip('_'),
|
||
str(test.__class__.__qualname__),
|
||
str(test.__dict__["_testMethodName"]),
|
||
_Color(fc=term_clor, bo=1, text='<='),
|
||
str(test.__dict__["_testMethodDoc"] or "")
|
||
))
|
||
# 收集失败测试用例名称以在测试报告中显示
|
||
casename = str(test.__module__).strip('_') + '.' + str(test.__class__.__qualname__) + '.' + str(
|
||
test.__dict__["_testMethodName"])
|
||
self.failedCase += "<li>" + casename + "</li>"
|
||
self.failedCaseList.append(casename)
|
||
|
||
|
||
class HTMLTestRunner(Template_mixin):
|
||
# 新增 errormsg 参数,-1为无需截图,否则需要截图
|
||
def __init__(self, stream=None, verbosity=2, title=None, description=None, tester=None):
|
||
self.passrate = None
|
||
self.errormsg = None
|
||
self.stream = stream
|
||
self.verbosity = verbosity
|
||
self.runstime = None
|
||
self.runetime = None
|
||
|
||
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
|
||
if tester is None:
|
||
self.tester = self.DEFAULT_TESTER
|
||
else:
|
||
self.tester = tester
|
||
|
||
def run(self, test):
|
||
"""
|
||
Run the given test case or test suite.
|
||
"""
|
||
# The final result is output when the verbosity is 1,
|
||
# and the result of each use case is output when the verbosity is 2.
|
||
# Start testing.
|
||
sys.stderr.write('********************************* 开始测试 *********************************' + '\n')
|
||
sys.stderr.write("\n")
|
||
self.runstime = round(time.time(), 3)
|
||
result = _TestResult(self.verbosity)
|
||
test(result)
|
||
self.runetime = round(time.time(), 3)
|
||
sys.stderr.write("\n")
|
||
sys.stderr.write('********************************* 结束测试 *********************************' + '\n')
|
||
# Generate test report.
|
||
self.generateReport(test, result)
|
||
case_count = {
|
||
"t": len(result.result),
|
||
"p": result.passed_count,
|
||
"f": result.failed_count,
|
||
"e": result.errors_count
|
||
}
|
||
dura = time.strftime('%H:%M:%S', time.gmtime((self.runetime - self.runstime)))
|
||
rate_passed = str("%.2f%%" % (float(case_count["p"] / case_count["t"] * 100)))
|
||
rate_failed = str("%.2f%%" % (float(case_count["f"] / case_count["t"] * 100)))
|
||
rate_errors = str("%.2f%%" % (float(case_count["e"] / case_count["t"] * 100)))
|
||
list_failed = result.failedCaseList
|
||
list_errors = result.errorsCaseList
|
||
casesort = result.casesort
|
||
sys.stderr.write(_Color(fc=38, bo=1, text='结果概要') + "\n")
|
||
sys.stderr.write(_Color(fc=37, bo=0, text='总共') + "\x20" + str(case_count["t"]) + "\x20" +
|
||
_Color(fc=37, bo=0, text='通过') + "\x20" + str(case_count["p"]) + "\x20" +
|
||
_Color(fc=37, bo=0, text='失败') + "\x20" + str(case_count["f"]) + "\x20" +
|
||
_Color(fc=37, bo=0, text='错误') + "\x20" + str(case_count["e"]) + "\x20" +
|
||
_Color(fc=37, bo=0, text='耗时') + "\x20" + dura + "\x20" +
|
||
_Color(fc=37, bo=0, text='通过率') + "\x20" + rate_passed + "\x20" +
|
||
_Color(fc=37, bo=0, text='失败率') + "\x20" + rate_failed + "\x20" +
|
||
_Color(fc=37, bo=0, text='错误率') + "\x20" + rate_errors + "\x20" +
|
||
"\n"
|
||
)
|
||
for value in [['失败用例', list_failed, 31], ['错误用例', list_errors, 33]]:
|
||
if len(value[1]):
|
||
sys.stderr.write(_Color(fc=value[2], bo=1, text=value[0]) + "\n")
|
||
for i in range(len(value[1])):
|
||
sys.stderr.write(_Color(fc=37, bo=0, text=str(i + 1) + '. ' + value[1][i]) + "\n")
|
||
if len(casesort.keys()):
|
||
sys.stderr.write(_Color(fc=34, bo=1,
|
||
text='%-16s' % ('用例集合') + "\t" +
|
||
'%-4s' % ('总计') + "\t" +
|
||
'%-4s' % ('通过') + "\t" +
|
||
'%-4s' % ('失败') + "\t" +
|
||
'%-4s' % ('错误') + "\t" +
|
||
'%-8s' % ('耗时') + "\t") + "\n")
|
||
for key, value in casesort.items():
|
||
sys.stderr.write(_Color(fc=37, bo=0,
|
||
text='%-16s' % (str(key)) + "\t" +
|
||
'%-4s' % (str(value["p"] + value["f"] + value["e"])) + "\t" +
|
||
'%-4s' % (str(value["p"])) + "\t" +
|
||
'%-4s' % (str(value["f"])) + "\t" +
|
||
'%-4s' % (str(value["e"])) + "\t" +
|
||
'%.3f' % (round(value["d"], 3)) + '秒' + "\t"
|
||
) + "\n")
|
||
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, s in result_list:
|
||
cls = t.__class__
|
||
if cls not in rmap:
|
||
rmap[cls] = []
|
||
classes.append(cls)
|
||
rmap[cls].append((n, t, o, e, s))
|
||
return [(cls, rmap[cls]) for cls in classes]
|
||
|
||
def getReportAttributes(self, result):
|
||
"""
|
||
Return report attributes as a list of (name, value).
|
||
Override this to add custom attributes.
|
||
"""
|
||
runstime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.runstime))
|
||
duration = time.strftime('%H:%M:%S', time.gmtime((self.runetime - self.runstime)))
|
||
status = ','.join([
|
||
'总共 %s' % (result.passed_count + result.failed_count + result.errors_count),
|
||
'通过 %s' % (result.passed_count),
|
||
'失败 %s' % (result.failed_count),
|
||
'错误 %s' % (result.errors_count)
|
||
])
|
||
if (result.passed_count + result.failed_count + result.errors_count) > 0:
|
||
self.passrate = str("%.2f%%" % (float(result.passed_count) / float(
|
||
result.passed_count + result.failed_count + result.errors_count) * 100))
|
||
else:
|
||
self.passrate = '0.00%'
|
||
|
||
if len(result.failedCase) > 0:
|
||
failedCase = result.failedCase
|
||
else:
|
||
failedCase = '无'
|
||
|
||
if len(result.errorsCase) > 0:
|
||
errorsCase = result.errorsCase
|
||
else:
|
||
errorsCase = '无'
|
||
|
||
return [
|
||
('测试人员', self.tester),
|
||
('开始时间', runstime),
|
||
('合计耗时', duration),
|
||
('测试结果', status + ',通过率 ' + self.passrate),
|
||
('失败用例', failedCase),
|
||
('错误用例', errorsCase),
|
||
]
|
||
|
||
def generateReport(self, test, result):
|
||
report_attrs = self.getReportAttributes(result)
|
||
report_count = self._generate_report(result)
|
||
output = self.HTML_TMPL % dict(
|
||
title=saxutils.escape(self.title),
|
||
generator='HTMLTestRunner',
|
||
styles=self._generate_styles(),
|
||
passed=report_count["passed"],
|
||
failed=report_count["failed"],
|
||
errors=report_count["errors"],
|
||
casesets=report_count["casesets"],
|
||
casesets_passed=report_count["casesets_passed"],
|
||
casesets_failed=report_count["casesets_failed"],
|
||
casesets_errors=report_count["casesets_errors"],
|
||
header=self._generate_header(report_attrs),
|
||
report=report_count["report"],
|
||
footer=self._generate_footer(),
|
||
)
|
||
self.stream.write(output.encode('utf-8'))
|
||
|
||
def _generate_styles(self):
|
||
return self.STYLES_TMPL
|
||
|
||
def _generate_header(self, report_attrs):
|
||
line_list = []
|
||
for name, value in report_attrs:
|
||
match name:
|
||
case '失败用例':
|
||
if value == "无":
|
||
line = self.HEADER_ATTRIBUTE_TMPL % dict(name=name, value=value)
|
||
else:
|
||
line = self.HEADER_ATTRIBUTE_TMPL % dict(name=name, value="<a class='showDetail' data-toggle='collapse' href='#failedCaseOl' style='text-decoration: none;'>点击查看</a>"
|
||
"<ol id='failedCaseOl' class='collapse' style='float: left; font-family: Menlo,Monaco,Consolas,monospace;'>" + value + "</ol>"
|
||
)
|
||
case '错误用例':
|
||
if value == "无":
|
||
line = self.HEADER_ATTRIBUTE_TMPL % dict(name=name, value=value)
|
||
else:
|
||
line = self.HEADER_ATTRIBUTE_TMPL % dict(name=name, value="<a class='showDetail' data-toggle='collapse' href='#errorsCaseOl' style='text-decoration: none;'>点击查看</a>"
|
||
"<ol id='errorsCaseOl' class='collapse' style='float: left; font-family: Menlo,Monaco,Consolas,monospace;'>" + value + "</ol>"
|
||
)
|
||
case _:
|
||
line = self.HEADER_ATTRIBUTE_TMPL % dict(name=saxutils.escape(name), value=saxutils.escape(value))
|
||
line_list.append(line)
|
||
return self.HEADER_TMPL % dict(
|
||
title=saxutils.escape(self.title),
|
||
parameters=''.join(line_list),
|
||
description=saxutils.escape(self.description),
|
||
tester=saxutils.escape(self.tester),
|
||
)
|
||
|
||
# 生成报告
|
||
def _generate_report(self, result):
|
||
rows = []
|
||
sortedResult = self.sortResult(result.result)
|
||
dura_caseset = 0
|
||
for cid, (cls, cls_results) in enumerate(sortedResult):
|
||
np = nf = ne = ns = 0
|
||
for n, t, o, e, s in cls_results:
|
||
# 遍历每条用例
|
||
match n:
|
||
case 0:
|
||
np += 1
|
||
case 1:
|
||
nf += 1
|
||
case 2:
|
||
ne += 1
|
||
ns += s
|
||
# 单个用例集合耗时
|
||
ns = round(ns, 3)
|
||
dura_caseset += ns
|
||
row = self.REPORT_CLASS_TMPL % dict(
|
||
style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
|
||
name=cls.__name__,
|
||
docs=cls.__doc__ and cls.__doc__.split("\n")[0] or '',
|
||
counts=np + nf + ne,
|
||
passed=np,
|
||
failed=nf,
|
||
errors=ne,
|
||
cid='c%s' % (cid + 1),
|
||
time_usage='%.3f' % (ns) + "秒"
|
||
)
|
||
rows.append(row)
|
||
for tid, (n, t, o, e, s) in enumerate(cls_results):
|
||
rows.append(self._generate_report_test(rows, cid, tid, n, t, o, e))
|
||
# 全部用例集合耗时
|
||
dura_caseset = round(dura_caseset, 3)
|
||
report = self.REPORT_TMPL % dict(
|
||
test_list=''.join(rows),
|
||
counts=str(result.passed_count + result.failed_count + result.errors_count),
|
||
passed=str(result.passed_count),
|
||
failed=str(result.failed_count),
|
||
errors=str(result.errors_count),
|
||
time_usage='%.3f' % (dura_caseset) + "秒",
|
||
passrate=self.passrate
|
||
)
|
||
casesets = list(result.casesort.keys())
|
||
casesets_passed = []
|
||
for value in casesets:
|
||
casesets_passed.append(result.casesort[value]["p"])
|
||
casesets_failed = []
|
||
for value in casesets:
|
||
casesets_failed.append(result.casesort[value]["f"])
|
||
casesets_errors = []
|
||
for value in casesets:
|
||
casesets_errors.append(result.casesort[value]["e"])
|
||
return {
|
||
"report": report,
|
||
"passed": str(result.passed_count),
|
||
"failed": str(result.failed_count),
|
||
"errors": str(result.errors_count),
|
||
"casesets": json.dumps(casesets, ensure_ascii=False),
|
||
"casesets_passed": json.dumps(casesets_passed, ensure_ascii=False),
|
||
"casesets_failed": json.dumps(casesets_failed, ensure_ascii=False),
|
||
"casesets_errors": json.dumps(casesets_errors, ensure_ascii=False)
|
||
}
|
||
|
||
def _generate_report_test(self, rows, cid, tid, n, t, o, e):
|
||
# e.g. 'pt1_1', 'ft1_1', 'et1_1'
|
||
hasout = bool(o or e)
|
||
# ID修改点为下划线,支持Bootstrap折叠展开特效
|
||
match n:
|
||
case 0:
|
||
tid_flag = 'p'
|
||
case 1:
|
||
tid_flag = 'f'
|
||
case 2:
|
||
tid_flag = 'e'
|
||
case _:
|
||
tid_flag = 'u'
|
||
tid = tid_flag + 't%s_%s' % (cid + 1, tid + 1)
|
||
name = t.id().split('.')[-1]
|
||
docs = t.shortDescription() or ""
|
||
# 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.decode('latin-1')
|
||
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.decode('latin-1')
|
||
ue = e
|
||
else:
|
||
ue = e
|
||
script = self.REPORT_TEST_OUTPUT_TMPL % dict(id=tid, output=re.compile('\[[A-Za-z]+].*?\[/[A-Za-z]+][\r\n]').sub(
|
||
'', saxutils.escape(uo + ue)))
|
||
|
||
# 截图名称通过抛出异常在标准错误中
|
||
output = uo + ue
|
||
# 先判断是否需要截图
|
||
self.errormsg = output.find("TestError")
|
||
|
||
if self.errormsg == -1:
|
||
# 没有截图信息
|
||
template = hasout and self.REPORT_TEST_WITH_OUTPUT_TMPL_0 or self.REPORT_TEST_NO_OUTPUT_TMPL
|
||
row = template % dict(
|
||
tid=tid,
|
||
Class=n == 0 and 'hiddenRow' or 'none',
|
||
style=n == 2 and 'errorsCase' or (n == 1 and 'failedCase' or 'passedCase'),
|
||
name=name,
|
||
docs=docs,
|
||
script=script,
|
||
status=self.STATUS[n],
|
||
)
|
||
else:
|
||
# 包含截图信息
|
||
template = hasout and self.REPORT_TEST_WITH_OUTPUT_TMPL_1 or self.REPORT_TEST_NO_OUTPUT_TMPL
|
||
screenshot_list = _findMark(mark='TestErrorImg', text=output)
|
||
screenshot = ''
|
||
for value in screenshot_list:
|
||
try:
|
||
bn = os.path.basename(value)
|
||
except:
|
||
bn = '点击查看'
|
||
screenshot += '<a style="font-family:Consolas,monospace;color:#e52000;text-decoration:underline;" class="screenshot" href="javascript:void(0)" img="' + value + '">' + bn + '</a></br>'
|
||
row = template % dict(
|
||
tid=tid,
|
||
Class=n == 0 and 'hiddenRow' or 'none',
|
||
style=n == 2 and 'errorsCase' or (n == 1 and 'failedCase' or 'passedCase'),
|
||
name=name,
|
||
docs=docs,
|
||
script=script,
|
||
status=self.STATUS[n],
|
||
screenshot=screenshot
|
||
)
|
||
return row
|
||
|
||
def _generate_footer(self):
|
||
return self.FOOTER_TMPL
|