Groovy解析Junit report 填充email信息
2020-04-12 本文已影响0人
jaymz明
我们一直想做一件事情,就是在email中能够加入一些automation report的信息。曾经我们做过几个尝试:
- 将automation report用pod起在那边,作为一个后端service,和当前的环境处于同一个生命周期。
- 开发一个长活的service,无论哪个平台的automation 数据,都往这个平台发送数据并存储。用户想troubleshooting的时候,只需要访问这个平台就行。
针对第一种情况,我们还做了很酷炫的report template,饼图啥的都有。但是发现迁移性很差,而且兼容性不是很给力。只适用于一类环境的report结果。而我们知道部署环境对于跑case的要求也是多种多样。举个例子(nightly build的,可能需要所有case,PP build的,需要gatekeeper的case等等。)
第二种情况设计和兼容性都没问题,但是存在运维cost,要维护数据储存,平台的可用性。往往都会迎来老板们的challenge。
那么我们只能综合经济/技术等方面的因素,自己简单开发一个解析junit的脚本了。
先看效果图:
image.png image.pngimport java.text.DecimalFormat
class PackageInfo{
double duration
int fail
int skip
int pass
int total
String packageName
String getPackageName() {
return packageName
}
void setPackageName(String packageName) {
this.packageName = packageName
}
double getDuration() {
return duration
}
void setDuration(double duration) {
this.duration = duration
}
int getFail() {
return fail
}
void setFail(int fail) {
this.fail = fail
}
int getSkip() {
return skip
}
void setSkip(int skip) {
this.skip = skip
}
int getPass() {
return pass
}
void setPass(int pass) {
this.pass = pass
}
int getTotal() {
return total
}
void setTotal(int total) {
this.total = total
}
}
class TestCaseInfo{
int age
double duration
String className
String status
String name
String errorDetail
String errorStackTrace
boolean skipped
String getErrorStackTrace() {
return errorStackTrace
}
void setErrorStackTrace(String errorStackTrace) {
this.errorStackTrace = errorStackTrace
}
boolean getSkipped() {
return skipped
}
void setSkipped(boolean skipped) {
this.skipped = skipped
}
String getErrorDetail() {
return errorDetail
}
void setErrorDetail(String errorDetail) {
this.errorDetail = errorDetail
}
int getAge() {
return age
}
void setAge(int age) {
this.age = age
}
double getDuration() {
return duration
}
void setDuration(double duration) {
this.duration = duration
}
String getClassName() {
return className
}
void setClassName(String className) {
this.className = className
}
String getStatus() {
return status
}
void setStatus(String status) {
this.status = status
}
String getName() {
return name
}
void setName(String name) {
this.name = name
}
}
class TestResult{
int failCount
int passCount
int skipCount
List<TestCaseInfo> allCaseInfos = new ArrayList<>()
List<TestCaseInfo> caseFailInfos = new ArrayList<>()
TestResult(failCount, passCount, skipCount, allCaseInfos, caseFailInfos){
this.failCount = failCount
this.passCount = passCount
this.skipCount = skipCount
this.allCaseInfos = allCaseInfos
this.caseFailInfos = caseFailInfos
}
int getFailCount() {
return failCount
}
void setFailCount(int failCount) {
this.failCount = failCount
}
int getPassCount() {
return passCount
}
void setPassCount(int passCount) {
this.passCount = passCount
}
int getSkipCount() {
return skipCount
}
void setSkipCount(int skipCount) {
this.skipCount = skipCount
}
List<TestCaseInfo> getAllCaseInfos() {
return allCaseInfos
}
void setAllCaseInfos(List<TestCaseInfo> casePassInfos) {
this.allCaseInfos = casePassInfos
}
List<TestCaseInfo> getCaseFailInfos() {
return caseFailInfos
}
void setCaseFailInfos(List<TestCaseInfo> caseFailInfos) {
this.caseFailInfos = caseFailInfos
}
}
def getJunitResult(String logUrl){
def result = httpRequest url:logUrl+'testReport/api/json?pretty=true', authentication: 'github-ci'
def states = readJSON text:result.content
String[] suites = states.suites
List<TestCaseInfo> allCaseInfos = new ArrayList<>()
List<TestCaseInfo> caseFailInfos = new ArrayList<>()
if(suites != null) {
for (int i = 0; i < suites.size(); i++) {
String[] caseSize = states.suites[i].cases
for(int m=0;m<caseSize.size();m++) {
def tmp = states.suites[i].cases[m]
String errorDetail = tmp.errorDetails
String errorStackTrace = tmp.errorStackTrace
TestCaseInfo testCaseInfo = new TestCaseInfo()
testCaseInfo.setAge(tmp.age)
testCaseInfo.setDuration(tmp.duration)
testCaseInfo.setClassName(tmp.className+tmp.name)
testCaseInfo.setStatus(tmp.status)
testCaseInfo.setName(tmp.name)
if(errorDetail != null) {
testCaseInfo.setErrorDetail(errorDetail)
}
testCaseInfo.setSkipped(tmp.skipped)
if(errorStackTrace != null) {
testCaseInfo.setErrorStackTrace(errorStackTrace)
}
if (errorDetail != null || errorStackTrace != null) {
if(!testCaseInfo.getSkipped()) {
caseFailInfos.add(testCaseInfo)
}
}
allCaseInfos.add(testCaseInfo)
}
}
}
TestResult testResult = new TestResult(states.failCount,
states.passCount,
states.skipCount,allCaseInfos,caseFailInfos)
return testResult
}
def handlePackageName(Map<String,PackageInfo> map,PackageInfo packageInfo){
String packageName = packageInfo.getPackageName()
String key = packageName.substring(0,packageName.lastIndexOf('.'))
if(map == null || !map.containsKey(key)){
map.put(key,packageInfo)
}else{
PackageInfo tmp = map.get(key)
DecimalFormat df = new DecimalFormat("#.0")
double dura = df.format(tmp.getDuration()+packageInfo.getDuration())
tmp.setDuration(dura)
tmp.setFail(tmp.getFail()+packageInfo.getFail())
tmp.setPass(tmp.getPass()+packageInfo.getPass())
tmp.setSkip(tmp.getSkip()+packageInfo.getSkip())
tmp.setTotal(tmp.getTotal()+packageInfo.getTotal())
map.put(key,tmp)
}
return map
}
def drawJunitHtml(template,testInfo,archive_reports,test_result){
if(testInfo == null){
return ''
}
TestResult testResult = (TestResult)testInfo
String failCaseHtml = ''
List<TestCaseInfo> caseFailInfos = testResult.getCaseFailInfos()
if(caseFailInfos != null){
for(int i=0;i<caseFailInfos.size();i++){
String detailFailHtml = """
<tr>
<td class="pane no-wrap"> <font color="red">${caseFailInfos.get(i).getClassName()}</font></td>
<td data="0.01" style="text-align:right;" class="pane no-wrap">${caseFailInfos.get(i).getDuration()*1000} ms</td>
<td class="pane" style="text-align:right;">${caseFailInfos.get(i).getAge()}</td>
</tr>
"""
failCaseHtml+=detailFailHtml
}
}
if(failCaseHtml == ""){
failCaseHtml = """
<tr>
<td class="pane no-wrap"> NULL</td>
<td data="0.01" style="text-align:right;" class="pane no-wrap">NULL</td>
<td class="pane" style="text-align:right;">NULL</td>
</tr>
"""
}
List<TestCaseInfo> allCaseInfos = testResult.getAllCaseInfos()
Map<String,PackageInfo> map = new HashMap<>()
if(allCaseInfos != null) {
for (int j = 0; j < allCaseInfos.size(); j++) {
PackageInfo packageInfo = new PackageInfo()
TestCaseInfo testCaseInfo = allCaseInfos.get(j)
if (testCaseInfo.getStatus() == 'PASSED' || testCaseInfo.getStatus() == 'FIXED') {
packageInfo.setPass(1)
}
packageInfo.setDuration(testCaseInfo.getDuration())
packageInfo.setPackageName(testCaseInfo.getClassName())
if (testCaseInfo.getErrorDetail() != null || testCaseInfo.getErrorStackTrace() != null) {
if(!testCaseInfo.getSkipped()) {
packageInfo.setFail(1)
}
}
if (testCaseInfo.getSkipped()) {
packageInfo.setSkip(1)
}
packageInfo.setTotal(packageInfo.getSkip() + packageInfo.getPass() + packageInfo.getFail())
map = handlePackageName(map, packageInfo)
}
}
String allCaseHtml = ''
if(map != null){
String[] keys = map.keySet().toArray()
for(int k=0;k<keys.length;k++){
String caseDetailHtml = """
<tr>
<td class="pane">
<span style="">${keys[k]}</span></a>
</td>
<td data="0.020000001" style="text-align:right" class="pane no-wrap">${map.get(keys[k]).getDuration()} s</td>
<td class="pane" style="text-align:right"><font color="red">${map.get(keys[k]).getFail()}</font></td>
<td class="pane" style="text-align:right">${map.get(keys[k]).getSkip()}</td>
<td class="pane" style="text-align:right">${map.get(keys[k]).getPass()}</td>
<td class="pane" style="text-align:right">${map.get(keys[k]).getTotal()}</td>
</tr>
"""
allCaseHtml += caseDetailHtml
}
}
int total = testResult.getFailCount()+testResult.getPassCount()+testResult.getSkipCount()
html = String.format(template,testResult.getFailCount(),testResult.getPassCount(),testResult.getSkipCount(),total,test_result,test_result,archive_reports,archive_reports,failCaseHtml,allCaseHtml)
Map resultMap = new HashMap<>()
resultMap.put("html",html)
if(testResult.getFailCount()>0){
resultMap.put("status",'Unstable')
}else{
resultMap.put("status","SUCCESS")
}
return resultMap
}
return this
外部传入report 模版,并将数据junit的report路径传递给该函数即可。由于返回的也是html,所以我们可以把这部分信息加到任何需要传入case信息的email中去。
testInfos = parseReportUtil.getJunitResult(buildUrl)
Map resultMap = parseReportUtil.drawJunitHtml(templateReport,testInfos,archive_reports,test_result)
report模版也很简单:
<html>
<head resurl="/jenkins/static/ef2a5808" data-rooturl="/jenkins" data-resurl="/jenkins/static/ef2a5808">
<title>Test Results [Jenkins]</title>
</head>
<body data-model-type="hudson.tasks.junit.TestResult" id="jenkins" class="yui-skin-sam two-column jenkins-2.138.4" data-version="2.138.4">
<div id="main-panel">
<h4>Test Result</h4>
<div>
<h5><font color="red"><span style="margin-left: 5px">failed %s</span></font> <font color="green"><span style="margin-left: 5px">passed %s </span></font> <span style="margin-left: 5px">skipped %s</span> <span style="margin-left: 5px">total %s </span></h5>
</div>
<h4>Test Report</h4>
<table class="pane sortable bigtable stripped" id="testresult">
<tbody>
<tr>
<td>test report path:</td>
<td><a href=%s>%s</a></td>
</tr>
</tbody>
</table>
<h4>Log Link</h4>
<table class="pane sortable bigtable stripped" id="testresult">
<tbody>
<tr>
<td>test logs path:</td>
<td><a href=%s>%s</a></td>
</tr>
</tbody>
</table>
</div>
<table style="margin-top: 1em; margin-left:0em;"></table>
<h4>All Failed Tests</h4>
<table class="pane sortable bigtable stripped">
<tbody>
<tr>
<td class="pane-header">Test Name</td>
<td class="pane-header" style="width:4em"><span class="sortarrow">Duration</span></td>
<td class="pane-header" style="width:2em"><span class="sortarrow">Age</span></td>
</tr>
%s
</tbody>
</table>
<h4>All Tests</h4>
<table class="pane sortable bigtable stripped" id="testresult">
<tbody>
<tr>
<td class="pane-header">
package<span class="sortarrow"></span>
</td>
<td class="pane-header" style="width:5em; text-align:right">
duration<span class="sortarrow"></span>
</td>
<td class="pane-header" style="width:5em; text-align:right">
failed<span class="sortarrow"></span>
</td>
<td class="pane-header" style="width:5em; text-align:right">
skipped<span class="sortarrow"></span>
</td>
<td class="pane-header" style="width:5em; text-align:right">
passed<span class="sortarrow"></span>
</td>
<td class="pane-header" style="width:5em; text-align:right">
total<span class="sortarrow"></span>
</td>
</tr>
</tbody>
<tbody>
%s
</tbody>
</table>
</body></html>
emailext to: to,
subject: subject,
body: html+caseHtml,
mimeType: "text/html"
说点好处就是任何report,只要是junit生成的,都可以用它来解析,而且代码量也不是很多,跨平台,可反复运行。