web-automation-demo/Library/HTMLTestRunner.py

1169 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
这是一个用于unittest框架的TestRunner用它可以生成HTML测试报告包含用例执行情况以
及图表;
使用它最简单方法是调用main方法例如
------------------------------------------------------------------------
# import unittest
# import HTMLTestRunner
# 在这里,
# 定义你的测试代码。
# if __name__ == '__main__':
# HTMLTestRunner.main()
------------------------------------------------------------------------
"""
import os, re, sys, io, time, datetime, platform, json, unittest, logging, shutil
from xml.sax import saxutils
class OutputRedirector(object):
"""
重定向stdout和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)
class ReportTemplate:
"""
报告模板
"""
STATUS = {
0: '通过',
1: '失败',
2: '错误',
}
DEFAULT_TITLE = '自动化测试报告'
DEFAULT_DESCRIPTION = '暂无描述'
# ------------------------------------------------------------------------
# 网页模板
# 变量列表 title, generator, styles, header, report, footer
HTML_TMPL = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="zh-CN" 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 type="text/javascript">
$(function(){
// 按钮颜色
$("button").each(function(){
let text = $(this).text();
switch($(this).text()){
case '通过':
$(this).addClass("btn-success");
break;
case '失败':
$(this).addClass("btn-danger");
break;
case '错误':
$(this).addClass("btn-warning");
break;
default:
$(this).addClass("btn-danger");
}
});
// 异常用例合集查看按钮
$(".showDetail").click(function(){
let expand = "点击查看";
let pickup = "点击收起";
if($(this).html() != pickup){$(this).html(pickup)}else{$(this).html(expand)}
});
// 异常用例合集样式设置
var p_attribute = $("p.attribute");
p_attribute.eq(4).addClass("failedCollection");
p_attribute.eq(5).addClass("errorsCollection");
// 打开截图点击任意位置可以关闭图片
$(".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(){
let browserWidth = $(window).width();
let 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 - 16) + "px");
}
};
resize_action();
// 改变窗口大小时自动调整图表和图片边距
$(window).resize(function(){
let browserHeight = $(window).height();
let pic_boxHeight = $(".pic_box").height();
let top = (browserHeight - pic_boxHeight)/2;
$('.pic_box').css("margin-top", top + "px");
resize_action();
});
// 超过页面高度时显示回到顶部按钮
$(window).scroll(function(){if($(window).scrollTop() >= $(window).height()){$("#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: !1
},
navigation: {
buttonOptions: {
enabled: !1
}
},
title: {
text: "用例集合情况"
},
xAxis: {
categories: %(casesets)s
},
yAxis: {
min: 0,
title: {
text: "用例数量"
},
reversedStacks: !1
},
legend: {
reversed: !1
},
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: !1,
spacing: [0, 0, 0, 0]
},
credits: {
enabled: !1
},
navigation: {
buttonOptions: {
enabled: !1
}
},
title: {
floating: !0,
text: "测试结果占比"
},
tooltip: {
pointFormat: '<text style="font-size:10px">{series.name}: {point.percentage:.1f}%%</text>'
},
plotOptions: {
pie: {
allowPointSelect: !0,
cursor: "pointer",
colors: ["#64bb64", "#f16d7e", "#fdc68c"],
dataLabels: {
enabled: !0,
format: "<b>{point.name}</b>: {point.percentage:.1f} %%",
style: {
color: (Highcharts.theme && Highcharts.theme.contrastTextColor) || "black"
}
},
point: {
events: {
mouseOver: function(a) {
chart.setTitle({
text: a.target.name + "\t" + a.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;
});
});
function showCase(level){
/*
0: //All hiddenRow.
1:Failed //Show Failed Only.
2:Passed //Show Passed Only.
3:Errors //Show Errors Only.
4:All //All Show.
*/
let trs = document.getElementsByTagName("tr");
for(let i = 0; i < trs.length; i++){
let tr = trs[i];
let id = tr.id;
if(id.substr(0, 2) == 'ft'){
if(level == 2 || level == 0 || level == 3){
tr.className = 'hiddenRow';
}
else{
tr.className = '';
$("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 = '';
$("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 = '';
$("div[id^='div_ft']").attr("class", "collapse");
$("div[id^='div_et']").attr("class", "collapse");
$("div[id^='div_pt']").attr("class", "collapse");
}
}
}
let detail_class = document.getElementsByClassName('detail');
if(level == 4){
for(let i = 0; i < detail_class.length; i++){
detail_class[i].innerHTML = "收起";
}
}
else{
for(let i = 0; i < detail_class.length; i++){
detail_class[i].innerHTML = "详细";
}
}
}
function showClassDetail(cid, count){
let id_list = Array(count);
let toHide = 1;
for(let i = 0; i < count; i++){
let tid_suffix = 't' + cid.substr(1) + '_' + (i+1);
let tid = '';
let tel;
let p = ['p', 'f', 'e'];
for(let j = 0; j < p.length; j++){
let cur = p[j];
tid = cur + tid_suffix;
tel = document.getElementById(tid);
if(tel){break}
}
id_list[i] = tid;
if(tel.className){toHide=0}
}
for(let i = 0; i < count; i++){
let tid = id_list[i];
if(toHide){
document.getElementById(tid).className = 'hiddenRow';
document.getElementById(cid).innerText = "详细";
}
else{
document.getElementById(tid).className = '';
document.getElementById(cid).innerText = "收起";
}
}
}
function html_escape(s){
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
</script>
%(header)s
%(report)s
%(footer)s
</body>
</html>
"""
# ------------------------------------------------------------------------
# 网页样式
STYLES_TMPL = """
<style type="text/css" media="screen">
body{font-family:Microsoft YaHei;padding:15px 20px 20px 20px;font-size:100%}
table{font-size:100%}
.table tbody tr td{vertical-align:middle}
.attribute,.header .description{clear:both}
.errorsCollection,.failedCollection{width:auto;float:left}
#failedCaseOl li{color:red}
#errorsCaseOl li{color:orange}
.data-img{cursor:pointer}
.pic_looper{width:100%;height:100%;position:fixed;left:0;top:0;opacity:.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:0 0 20px 0 #000;-webkit-box-shadow:0 0 20px 0 #000;box-shadow:0 0 20px 0 #000}
#container{max-width:100%;width:450px;height:350px;float:left}
#container_extend{max-width:100%;width:550px;height:400px;float:left}
#total_row{font-weight:700}
.passedCase{color:#3fb83f;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:14px;font-weight:700}
.failedCase{color:#d9433f;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:14px;font-weight:700}
.errorsCase{color:#f0a02f;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:14px;font-weight:700}
.hiddenRow{display:none}
.testcase{margin-left:1em;word-break:break-all;white-space:pre-wrap}
.screenshot:link{text-decoration:none;color:#ff1493}
.screenshot:visited{text-decoration:none;color:#ff1493}
.screenshot:hover{text-decoration:none;color:#008b8b}
.screenshot:active{text-decoration:none;color:#ff1493}
</style>
"""
# ------------------------------------------------------------------------
# 报告标题、测试信息、报告描述、截图显示容器、图表容器
# 变量列表 title, parameters, description, logo, sign
HEADER_TMPL = """
<div class="pic_looper"></div>
<div class="pic_show"><div class="pic_box"><img src=""/></div></div>
<div style="width:auto;height:auto;border:1px solid #e3e3e3;text-align:center;color:#505050;display:%(rail_hidden)s;justify-content:center">
<img src="%(logo)s" style="width:auto;height:auto;max-height:40px;padding:1px 4px 1px 4px;"/>
<div style="display:flex;flex-direction:row;align-items:center"><b style="font-size:22px;color:#353535;text-align:center">%(sign)s</b></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-family:monospace;font-size:12px;color:%(pre_color)s">%(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-family:monospace;font-size:12px;color:%(pre_color)s">%(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">&nbsp;</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='', data=''):
return re.findall('\\[' + mark + '](.*?)\\[/' + mark + ']' + '*?', data)
def _makeMark(mark='', cont=''):
return '[' + mark + ']' + cont + '[/' + mark + ']'
def _Color(fc=0, bc=0, bo=0, text=''):
b = 'PYCHARM_HOSTED' in os.environ.keys()
return "\033[" + str(bo) + ['', ';' + str(fc)][fc > 0] + ['', ';' + str(bc)][bc > 0] + "m" + text + "\033[0m" if b else text
class _TestResult(unittest.TestResult):
def __init__(self, verbosity=1, log='', success_log_in_report=False):
super().__init__(verbosity=verbosity)
self.verbosity = verbosity
self.log = log
self.success_log_in_report = success_log_in_report
self.fh = None
self.lh = None
self.ch = None
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
self.passrate = float(0)
# 分类统计数量耗时
self.casesort = {}
# 增加失败用例合集
self.failedCase = ''
self.failedCaseList = []
# 增加错误用例合集
self.errorsCase = ''
self.errorsCaseList = []
self.logger = logging.getLogger('test')
# result is a list of result in 4 tuple:
# 1. result code (0: passed; 1: failed; 2: errors),
# 2. testcase object,
# 3. test output (byte string),
# 4. stack trace.
self.result = []
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 >= 2:
sys.stderr.write(_Color(fc=39, bc=4, bo=1, text='%-79s' % ('%04d @ Testing...' % (len(self.result) + 1))) + "\n")
# 开始测试
super().startTest(test)
# 终端日志
if self.verbosity >= 2:
self.ch = logging.StreamHandler(sys.stderr)
self.ch.setLevel(logging.DEBUG)
self.ch.setFormatter(logging.Formatter(fmt='{asctime} - {levelname[0]}: {message}', style='{'))
self.logger.addHandler(self.ch)
# 报告日志
if True:
self.loggerStream = io.StringIO()
self.lh = logging.StreamHandler(self.loggerStream)
self.lh.setLevel(logging.DEBUG)
self.lh.setFormatter(logging.Formatter(fmt='{asctime} - {levelname[0]}: {message}', style='{'))
self.logger.addHandler(self.lh)
# 文件日志
if self.log:
self.fh = logging.FileHandler(filename=self.log, mode='a+', encoding='utf-8')
self.fh.setLevel(logging.DEBUG)
self.fh.setFormatter(logging.Formatter(fmt='{asctime} - {levelname[0]}: {module}.{funcName}\t{message}', style='{'))
self.logger.addHandler(self.fh)
# 用例执行过程中的输出
# 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)
def completeOutput(self):
"""
单条用例执行结束后,添加结果前的动作;
添加结果需要调用的方法;
断开输出重定向;
返回日志和输出;
"""
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)
# 移除日志Handler
for handler in [self.fh, self.lh, self.ch]:
if handler:
self.logger.removeHandler(handler)
def singleEndConsolePrint(self, test, term_mark, term_head, term_clor, duration):
"""
将用例执行结果打印到终端显示
"""
self.verbosity >= 1 and sys.stderr.write('%s %s %s %s.%s.%-18s\t%s %s\n' % (
_Color(fc=term_clor, bo=1, text='%04d' % (len(self.result)) + ' ' + term_mark + ' ' + term_head + ':'),
_Color(fc=37, bo=0, text=datetime.datetime.utcfromtimestamp(duration).strftime('%H:%M:%S.%f')[0:12]),
_Color(fc=37, bo=0, text='<='),
_Color(fc=37, bo=0, text=str(test.__module__).strip('_')),
_Color(fc=37, bo=0, text=str(test.__class__.__qualname__)),
_Color(fc=37, bo=0, text=str(test.__dict__['_testMethodName'])),
_Color(fc=37, bo=0, text='<='),
_Color(fc=37, bo=0, text=str(test.__dict__['_testMethodDoc'] and test.__dict__['_testMethodDoc'].strip().splitlines()[0].strip() or ''))
))
def addSuccess(self, test):
"""
单条用例执行结束后添加成功结果动作
"""
term_mark, term_head, term_clor = '=', 'Passed', 32
self.passed_count += 1
super().addSuccess(test)
output = self.completeOutput()
duration = round(self.etime - self.stime, 3)
self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=duration)
# 添加测试结果
self.result.append((0, test, ['', output[0]][bool(self.success_log_in_report)] + output[1], '', duration))
# 单条用例执行结束后在终端打印结果
self.singleEndConsolePrint(test, term_mark, term_head, term_clor, duration)
def addError(self, test, err):
"""
单条用例执行结束后添加错误结果动作
"""
term_mark, term_head, term_clor = '?', 'Errors', 33
self.errors_count += 1
super().addError(test, err)
_, _exc_str = self.errors[-1]
output = self.completeOutput()
duration = round(self.etime - self.stime, 3)
self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=duration)
# 添加测试结果
self.result.append((2, test, output[0] + output[1], _exc_str, duration))
# 用例执行结束后在终端打印结果
self.singleEndConsolePrint(test, term_mark, term_head, term_clor, duration)
# 收集错误测试用例名称以在测试报告中显示
testcase_name = '%s.%s.%s' % (str(test.__module__).strip('_'), test.__class__.__qualname__, test.__dict__['_testMethodName'])
self.errorsCase += '<li>%s</li>' % testcase_name
self.errorsCaseList.append(testcase_name)
def addFailure(self, test, err):
"""
单条用例执行结束后添加失败结果动作
"""
term_mark, term_head, term_clor = '!', 'Failed', 31
self.failed_count += 1
super().addFailure(test, err)
_, _exc_str = self.failures[-1]
output = self.completeOutput()
duration = round(self.etime - self.stime, 3)
self.sortCount(cls=test.__class__.__qualname__, res=term_head, dur=duration)
# 添加测试结果
self.result.append((1, test, output[0] + output[1], _exc_str, duration))
# 用例执行结束后在终端打印结果
self.singleEndConsolePrint(test, term_mark, term_head, term_clor, duration)
# 收集失败测试用例名称以在测试报告中显示
testcase_name = '%s.%s.%s' % (str(test.__module__).strip('_'), test.__class__.__qualname__, test.__dict__['_testMethodName'])
self.failedCase += '<li>%s</li>' % testcase_name
self.failedCaseList.append(testcase_name)
class HTMLTestRunner(ReportTemplate):
def __init__(
self,
stream=None,
verbosity: int = 1,
title: str = None,
description: str = None,
success_log_in_report=False,
report_home: str = None,
report_home_latest_name: str = None,
report: str = None,
log: str = None,
logo='',
sign=''
):
"""
This is HTMLTestRunner.
:param stream: HTML report write file stream.
:param verbosity:
0: Terminal only shows the test results summary.
1: Terminal shows the results summary and results of each case test.
2: Terminal shows the results summary and results of each case test, and print log.
:param title: Title of report.
:param description: Description of report.
:param success_log_in_report: The testcase passed is also displayed in the HTML report.
:param report_home:
Set home directory of the report for this test,
all the results related files will be storage inside(including HTML report, log, images),
at this time,
other related parameters will fail.
:param report_home_latest_name: Create <latest> directory on the path where the report directory is located.
:param report: HTML report file location(useless when stream parameter inset).
:param log: Log file.
:param logo: Logo of report, pass an image URL.
:param sign: Sign of report, Pass in one piece short text.
"""
if report_home:
try:
os.mkdir(report_home)
except Exception:
raise Exception('Report directory already exists.')
stream = report = log = None
report, log = '%s/index.html' % report_home, '%s/HTMLTestRunner.log' % report_home
self.report_home = report_home
self.result_associate_files = []
self.verbosity = verbosity
self.success_log_in_report = success_log_in_report
self.report_home_latest_name = report_home_latest_name
self.stream = stream or (report and open(os.path.abspath(report), 'wb'))
self.report = None
if self.stream:
stream_name = os.path.abspath(self.stream.name)
self.result_associate_files.append(stream_name)
os.environ['HTML_REPORT_ROOT'] = os.path.dirname(stream_name)
self.report = stream_name
else:
os.environ['HTML_REPORT_ROOT'] = ''
self.log = log
self.log_location = log and os.path.abspath(log)
self.log_location and self.result_associate_files.append(self.log_location)
self.log_location and open(self.log_location, 'wb').close()
self.title = self.DEFAULT_TITLE if title is None else title
self.description = self.DEFAULT_DESCRIPTION if description is None else description
self.logo = logo or ''
self.sign = sign or ''
self.passrate = None
self.errormsg = None
self.runstime = None
self.runetime = None
def run(self, test):
sys.stderr.write(_Color(fc=38, bo=1, text='* * * * * * * * * * * * * * * * * * 开始测试 * * * * * * * * * * * * * * * * * *') + '\n')
self.verbosity >= 2 and sys.stderr.write("\n")
self.runstime = round(time.time(), 3)
# 获取测试结果
result = _TestResult(verbosity=self.verbosity, log=self.log_location, success_log_in_report=self.success_log_in_report)
test(result)
self.runetime = round(time.time(), 3)
self.verbosity >= 2 and sys.stderr.write("\n")
sys.stderr.write(_Color(fc=38, bo=1, text='* * * * * * * * * * * * * * * * * * 结束测试 * * * * * * * * * * * * * * * * * *') + '\n')
# 生成测试报告
self.generateReport(test, result)
if len(result.result) == 0:
raise Exception('No testcases found.')
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=36, 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='%-22s' % ('用例集合') + "\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='%-22s' % (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" +
'%-8s' % ('%.3f' % (round(value["d"], 3)) + '') + "\t") + "\n")
return result
@staticmethod
def sortResult(result_list):
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):
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 = ''
uname = platform.uname()
return [
['开始时间', runstime],
['合计耗时', duration],
['主机名称', '%s (%s %s) %s' % (uname.node, uname.system, uname.release, uname.machine.lower())],
['测试结果', '%s,通过率%s' % (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 and self.stream.write(output.encode('utf-8'))
self.stream and self.stream.close()
if self.report_home and self.report_home_latest_name:
latest = '%s/%s' % (os.path.dirname(self.report_home), self.report_home_latest_name)
os.path.exists(latest) and shutil.rmtree(latest)
shutil.copytree(self.report_home, latest)
if self.report_home:
open('%s/latest.ini' % os.path.dirname(self.report_home), mode='w').write(os.path.basename(self.report_home))
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(str(name)), value=saxutils.escape(str(value)))
line_list.append(line)
return self.HEADER_TMPL % dict(
rail_hidden='none' if not self.logo and not self.sign else 'flex',
logo=self.logo,
sign=self.sign,
title=saxutils.escape(self.title),
parameters=''.join(line_list),
description=saxutils.escape(self.description)
)
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 'errorsClass' or nf > 0 and 'failedClass' or 'passedClass',
name=cls.__qualname__,
docs=cls.__doc__ and cls.__doc__.strip().splitlines()[0].strip() 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):
hasout = bool(o or e)
match n:
case 0:
tid_flag = 'p'
pre_clor = '#119611'
case 1:
tid_flag = 'f'
pre_clor = '#e52000'
case 2:
tid_flag = 'e'
pre_clor = '#e54f00'
case _:
tid_flag = 'u'
pre_clor = '#808080'
# ID修改点为下划线
# 支持Bootstrap折叠展开特效
# 例如:'pt1_1', 'ft1_1', 'et1_1'
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):
# 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):
# 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)))
# 截图名称通过抛出异常在
# 标准错误当中,
# 判断是否包含截图信息
# 实际检测输出当中是否包含report-screenshot关键字
output = uo + ue
self.errormsg = output.find('report-screenshot')
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],
pre_color=pre_clor
)
else:
# 包含截图信息
template = hasout and self.REPORT_TEST_WITH_OUTPUT_TMPL_1 or self.REPORT_TEST_NO_OUTPUT_TMPL
screenshot_list = _findMark(mark='report-screenshot', data=output)
screenshot = ''
for image in screenshot_list:
bn = os.path.basename(image)
abs_image = os.path.abspath(image)
self.result_associate_files.append(abs_image)
# 移动图片位置到报告所在目录
try:
self.report and shutil.move(abs_image, '%s/%s' % (os.path.dirname(self.report), bn))
except Exception:
pass
screenshot += '<a style="font-family:Consolas,monospace;color:#e52000;text-decoration:underline" ' \
'class="screenshot" href="javascript:void(0)" img="' + './%s' % bn + '">' + 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],
pre_color=pre_clor,
screenshot=screenshot
)
return row
def _generate_footer(self):
return self.FOOTER_TMPL