1169 lines
48 KiB
Python
1169 lines
48 KiB
Python
"""
|
||
|
||
这是一个用于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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
</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"> </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
|