一.背景
小组内自动化二期,需要前端展示结果;前端(php)根据执行mvn test -Dtest=测试类,surefire生成的stdout, 正则匹配输出结果,进行展示。由于标准的surefire插件输出的标准格式如下:
由三部分组成( 忽略失败,上述故意失败用例,方便调试),1.failed的用例,2.error的用例,3.数量统计。
缺少success 和 skipped用例详情。
二.解决方案
通过查看surefire项目(maven工程,https://github.com/apache/maven-surefire),找到surefire stdout的java文件
package org.apache.maven.plugin.surefire.report;
DefaultReporterFactory.java;
使用工厂模式,监听执行结果,判断测试方法执行结果,放到各自map中,再输出,可以看注释分析。
package org.apache.maven.plugin.surefire.report;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.maven.plugin.surefire.StartupReportConfiguration;
import org.apache.maven.plugin.surefire.runorder.StatisticsReporter;
import org.apache.maven.surefire.report.DefaultDirectConsoleReporter;
import org.apache.maven.surefire.report.ReporterFactory;
import org.apache.maven.surefire.report.RunListener;
import org.apache.maven.surefire.report.RunStatistics;
import org.apache.maven.surefire.report.StackTraceWriter;
import org.apache.maven.surefire.suite.RunResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Provides reporting modules on the plugin side.
* <p/>
* Keeps a centralized count of test run results.
*
* @author Kristian Rosenvold
*
* modify by hugang
* stdout success and skipped
*/
public class DefaultReporterFactory
implements ReporterFactory
{
private RunStatistics globalStats = new RunStatistics();
private final StartupReportConfiguration reportConfiguration;
private final StatisticsReporter statisticsReporter;
private final List<TestSetRunListener> listeners =
Collections.synchronizedList( new ArrayList<TestSetRunListener>() );
// from "<testclass>.<testmethod>" -> statistics about all the runs for flaky tests
private Map<String, List<TestMethodStats>> flakyTests;
// from "<testclass>.<testmethod>" -> statistics about all the runs for failed tests
private Map<String, List<TestMethodStats>> failedTests;
// from "<testclass>.<testmethod>" -> statistics about all the runs for error tests
private Map<String, List<TestMethodStats>> errorTests;
// added by hugang, from "<testclass>.<testmethod>" -> statistics about all the runs for success tests
private Map<String, List<TestMethodStats>> successTests;
// added by hugang, from "<testclass>.<testmethod>" -> statistics about all the runs for skipped tests
private Map<String, List<TestMethodStats>> skippedTests;
public DefaultReporterFactory( StartupReportConfiguration reportConfiguration )
{
this.reportConfiguration = reportConfiguration;
this.statisticsReporter = reportConfiguration.instantiateStatisticsReporter();
}
public RunListener createReporter()
{
return createTestSetRunListener();
}
public void mergeFromOtherFactories( Collection<DefaultReporterFactory> factories )
{
for ( DefaultReporterFactory factory : factories )
{
for ( TestSetRunListener listener : factory.listeners )
{
listeners.add( listener );
}
}
}
public RunListener createTestSetRunListener()
{
TestSetRunListener testSetRunListener =
new TestSetRunListener( reportConfiguration.instantiateConsoleReporter(),
reportConfiguration.instantiateFileReporter(),
reportConfiguration.instantiateStatelessXmlReporter(),
reportConfiguration.instantiateConsoleOutputFileReporter(), statisticsReporter,
reportConfiguration.isTrimStackTrace(),
ConsoleReporter.PLAIN.equals( reportConfiguration.getReportFormat() ),
reportConfiguration.isBriefOrPlainFormat() );
listeners.add( testSetRunListener );
return testSetRunListener;
}
public void addListener( TestSetRunListener listener )
{
listeners.add( listener );
}
public RunResult close()
{
mergeTestHistoryResult();
runCompleted();
for ( TestSetRunListener listener : listeners )
{
listener.close();
}
return globalStats.getRunResult();
}
private DefaultDirectConsoleReporter createConsoleLogger()
{
return new DefaultDirectConsoleReporter( reportConfiguration.getOriginalSystemOut() );
}
// 测试开始
public void runStarting()
{
final DefaultDirectConsoleReporter consoleReporter = createConsoleLogger();
consoleReporter.info( "" );
consoleReporter.info( "-------------------------------------------------------" );
consoleReporter.info( " T E S T S" );
consoleReporter.info( "-------------------------------------------------------" );
}
// 测试结束
private void runCompleted()
{
final DefaultDirectConsoleReporter logger = createConsoleLogger();
if ( reportConfiguration.isPrintSummary() )
{
logger.info( "" );
logger.info( "Results :" );
logger.info( "" );
}
// 输出不同类型用例信息
boolean printedFailures = printTestFailures( logger, TestResultType.failure );
printedFailures |= printTestFailures( logger, TestResultType.error );
printedFailures |= printTestFailures( logger, TestResultType.flake );
// added by hugang, 添加success and skipped用例详细
printedFailure |= printTestFailures( logger, TestResultType.success);
printedFailure |= printTestFailures( logger, TestResultType.skipped);
if ( printedFailures )
{
logger.info( "" );
}
// globalStats.getSummary() 打印数量
logger.info( globalStats.getSummary() );
logger.info( "" );
}
public RunStatistics getGlobalRunStatistics()
{
mergeTestHistoryResult();
return globalStats;
}
public static DefaultReporterFactory defaultNoXml()
{
return new DefaultReporterFactory( StartupReportConfiguration.defaultNoXml() );
}
/**
* Get the result of a test based on all its runs. If it has success and failures/errors, then it is a flake;
* if it only has errors or failures, then count its result based on its first run
*
* @param reportEntryList the list of test run report type for a given test
* @param rerunFailingTestsCount configured rerun count for failing tests
* @return the type of test result
*/
// Use default visibility for testing
static TestResultType getTestResultType( List<ReportEntryType> reportEntryList, int rerunFailingTestsCount )
{
if ( reportEntryList == null || reportEntryList.isEmpty() )
{
return TestResultType.unknown;
}
boolean seenSuccess = false, seenFailure = false, seenError = false;
for ( ReportEntryType resultType : reportEntryList )
{
if ( resultType == ReportEntryType.SUCCESS )
{
seenSuccess = true;
}
else if ( resultType == ReportEntryType.FAILURE )
{
seenFailure = true;
}
else if ( resultType == ReportEntryType.ERROR )
{
seenError = true;
}
}
if ( seenFailure || seenError )
{
if ( seenSuccess && rerunFailingTestsCount > 0 )
{
return TestResultType.flake;
}
else
{
if ( seenError )
{
return TestResultType.error;
}
else
{
return TestResultType.failure;
}
}
}
else if ( seenSuccess )
{
return TestResultType.success;
}
else
{
return TestResultType.skipped;
}
}
/**
* Merge all the TestMethodStats in each TestRunListeners and put results into flakyTests, failedTests and
* errorTests, indexed by test class and method name. Update globalStatistics based on the result of the merge.
*/
void mergeTestHistoryResult()
{
globalStats = new RunStatistics();
flakyTests = new TreeMap<String, List<TestMethodStats>>();
failedTests = new TreeMap<String, List<TestMethodStats>>();
errorTests = new TreeMap<String, List<TestMethodStats>>();
// added by hugang, 存success 和 skpped 用例信息
successTests = new TreeMap<String, List<TestMethodStats>>();
skippedTests = new TreeMap<String, List<TestMethodStats>>();
Map<String, List<TestMethodStats>> mergedTestHistoryResult = new HashMap<String, List<TestMethodStats>>();
// Merge all the stats for tests from listeners
for ( TestSetRunListener listener : listeners )
{
List<TestMethodStats> testMethodStats = listener.getTestMethodStats();
for ( TestMethodStats methodStats : testMethodStats )
{
List<TestMethodStats> currentMethodStats =
mergedTestHistoryResult.get( methodStats.getTestClassMethodName() );
if ( currentMethodStats == null )
{
currentMethodStats = new ArrayList<TestMethodStats>();
currentMethodStats.add( methodStats );
mergedTestHistoryResult.put( methodStats.getTestClassMethodName(), currentMethodStats );
}
else
{
currentMethodStats.add( methodStats );
}
}
}
// Update globalStatistics by iterating through mergedTestHistoryResult
int completedCount = 0, skipped = 0;
// 遍历所有的类,判断每一个类中的方法执行结果,TestMethodStats用于记录每个测试方法的结果
for ( Map.Entry<String, List<TestMethodStats>> entry : mergedTestHistoryResult.entrySet() )
{
//
List<TestMethodStats> testMethodStats = entry.getValue();
String testClassMethodName = entry.getKey();
completedCount++;
List<ReportEntryType> resultTypeList = new ArrayList<ReportEntryType>();
for ( TestMethodStats methodStats : testMethodStats )
{
resultTypeList.add( methodStats.getResultType() );
}
TestResultType resultType = getTestResultType( resultTypeList,
reportConfiguration.getRerunFailingTestsCount() );
// 根据一个类的不同方法执行结果,放到对应的map中
switch ( resultType )
{
case success:
// If there are multiple successful runs of the same test, count all of them
int successCount = 0;
for ( ReportEntryType type : resultTypeList )
{
if ( type == ReportEntryType.SUCCESS )
{
successCount++;
}
}
completedCount += successCount - 1;
// added by hugang, 把success 用例信息,put 到map中
successTests.put( testClassMethodName, testMethodStats );
break;
case skipped:
// added by hugang, 把skipped 用例信息,put 到map中
skippedTests.put( testClassMethodName, testMethodStats );
skipped++;
break;
case flake:
flakyTests.put( testClassMethodName, testMethodStats );
break;
case failure:
failedTests.put( testClassMethodName, testMethodStats );
break;
case error:
errorTests.put( testClassMethodName, testMethodStats );
break;
default:
throw new IllegalStateException( "Get unknown test result type" );
}
}
globalStats.set( completedCount, errorTests.size(), failedTests.size(), skipped, flakyTests.size() );
}
/**
* Print failed tests and flaked tests. A test is considered as a failed test if it failed/got an error with
* all the runs. If a test passes in ever of the reruns, it will be count as a flaked test
*
* @param logger the logger used to log information
* @param type the type of results to be printed, could be error, failure or flake
* @return {@code true} if printed some lines
*/
// Use default visibility for testing
boolean printTestFailures( DefaultDirectConsoleReporter logger, TestResultType type )
{
boolean printed = false;
final Map<String, List<TestMethodStats>> testStats;
switch ( type )
{
case failure:
testStats = failedTests;
break;
case error:
testStats = errorTests;
break;
case flake:
testStats = flakyTests;
break;
// added by hugang;支持success and skipped
case success:
testStats = successTests;
break;
case skipped:
testStats = skippedTests;
break;
default:
return printed;
}
if ( !testStats.isEmpty() )
{
// 被注释,添加到每行用例信息前,便于正则匹配
// logger.info( type.getLogPrefix() );
printed = true;
}
for ( Map.Entry<String, List<TestMethodStats>> entry : testStats.entrySet() )
{
printed = true;
List<TestMethodStats> testMethodStats = entry.getValue();
if ( testMethodStats.size() == 1 )
{
// 被注释
// No rerun, follow the original output format
// logger.info( " " + testMethodStats.get( 0 ).getStackTraceWriter().smartTrimmedStackTrace() );
// added by hugang , 每行用例信息前,便于正则匹配
logger.info(type.getLogPrefix() + "---" + testMethodStats.get( 0 ).getStackTraceWriter().smartTrimmedStackTrace() );
}
else
{
logger.info( entry.getKey() );
for ( int i = 0; i < testMethodStats.size(); i++ )
{
StackTraceWriter failureStackTrace = testMethodStats.get( i ).getStackTraceWriter();
if ( failureStackTrace == null )
{
logger.info( " Run " + ( i + 1 ) + ": PASS" );
}
else
{
logger.info( " Run " + ( i + 1 ) + ": " + failureStackTrace.smartTrimmedStackTrace() );
}
}
logger.info( "" );
}
}
return printed;
}
// Describe the result of a given test
static enum TestResultType
{
error( "Tests in error: " ),
failure( "Failed tests: " ),
flake( "Flaked tests: " ),
success( "Success: " ),
skipped( "Skipped: " ),
unknown( "Unknown: " );
private final String logPrefix;
private TestResultType( String logPrefix )
{
this.logPrefix = logPrefix;
}
public String getLogPrefix()
{
return logPrefix;
}
}
}
预期输出:
Failed tests: ---用例信息
Tests in error: ---用例信息
Success: ---用例信息
Skipped: ---用例信息