package hudson.plugins.testng.parser;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;
import hudson.FilePath;
import hudson.plugins.testng.results.ClassResult;
import hudson.plugins.testng.results.MethodResult;
import hudson.plugins.testng.results.MethodResultException;
import hudson.plugins.testng.results.PackageResult;
import hudson.plugins.testng.results.TestNGResult;
import hudson.plugins.testng.results.TestNGTestResult;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
/**
* Parses TestNG result XMLs generated using org.testng.reporters.XmlReporter
* into objects that are then used to display results in Jenkins.
*
* (For those trying to modify this class, pay attention to logging. We are using
* two different loggers. If build's {@link PrintStream} is not available, we log
* using {@link Logger}. Also, logging is done only using the {@link #log(String)}
* and {@link #log(Exception)} methods.)
*
* Note that instances of this class are not thread-safe to use!
*
* @author nullin
*/
public class ResultsParser {
/** Prints the logs to the web server's console / log files */
private static final Logger log = Logger.getLogger(ResultsParser.class.getName());
public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
public static final XmlPullParserFactory PARSER_FACTORY = createParserFactory();
private final DateFormat dateFormat;
/** Build's logger to print logs as part of build's console output */
private PrintStream logger;
/*
* We maintain only a single TestResult for all <test>s with the same name
*/
private Map<String, TestNGTestResult> testResultMap = new HashMap<String, TestNGTestResult>();
/*
* We maintain only a single ClassResult for all <class>s with the same fqdn
*/
private Map<String, ClassResult> classResultMap = new HashMap<String, ClassResult>();
private StringBuilder reporterOutputBuilder;
private Map<String, List<String>> methodGroupMap = new HashMap<String, List<String>>();
private TestNGResult finalResults;
private List<TestNGTestResult> testList;
private List<ClassResult> currentClassList;
private List<MethodResult> currentMethodList;
private List<String> currentMethodParamsList;
private TestNGTestResult currentTest;
private ClassResult currentClass;
private String currentTestRunId;
private MethodResult currentMethod;
private XmlPullParser xmlPullParser;
private TAGS currentCDATAParent = TAGS.UNKNOWN;
private String currentMessage;
private String currentShortStackTrace;
private String currentFullStackTrace;
private String currentGroupName;
private String currentSuite;
private String currentLine;
private String exceptionName;
private enum TAGS {
TESTNG_RESULTS, SUITE, TEST, CLASS, TEST_METHOD,
PARAMS, PARAM, VALUE, EXCEPTION, UNKNOWN, MESSAGE,
SHORT_STACKTRACE, FULL_STACKTRACE, GROUPS, GROUP, METHOD,
REPORTER_OUTPUT, LINE;
public static TAGS fromString(String val) {
if (val == null) {
return UNKNOWN;
}
val = val.toUpperCase().replace('-', '_');
try {
return TAGS.valueOf(val);
} catch (IllegalArgumentException e) {
return UNKNOWN;
}
}
}
private static XmlPullParserFactory createParserFactory() {
try {
XmlPullParserFactory f = XmlPullParserFactory.newInstance();
f.setNamespaceAware(true);
f.setValidating(false);
return f;
} catch (XmlPullParserException e) {
log.severe(e.toString());
return null;
}
}
public ResultsParser() {
this.dateFormat = new SimpleDateFormat(DATE_FORMAT);
}
public ResultsParser(PrintStream logger) {
this();
this.logger = logger;
}
/**
* Parses the XML for relevant information
*
* @param paths a file hopefully containing test related data in correct format
* @return a collection of test results
*/
public TestNGResult parse(FilePath[] paths) {
if (null == paths) {
log("File paths not specified. paths var is null. Returning empty test results.");
return new TestNGResult();
}
finalResults = new TestNGResult();
for (FilePath path : paths) {
File file = new File(path.getRemote());
if (!file.isFile()) {
log("'" + file.getAbsolutePath() + "' points to an invalid test report");
continue; //move to next file
} else {
log("Processing '" + file.getAbsolutePath() + "'");
}
BufferedInputStream bufferedInputStream = null;
try {
bufferedInputStream = new BufferedInputStream(new FileInputStream(file));
xmlPullParser = createXmlPullParser(bufferedInputStream);
//some initial setup
testList = new ArrayList<TestNGTestResult>();
while (XmlPullParser.END_DOCUMENT != xmlPullParser.nextToken()) {
TAGS tag = TAGS.fromString(xmlPullParser.getName());
int eventType = xmlPullParser.getEventType();
switch (eventType) {
//all opening tags
case XmlPullParser.START_TAG:
switch (tag) {
case SUITE:
startSuite(get("name"));
break;
case GROUPS:
startGroups();
break;
case GROUP:
startGroup(get("name"));
break;
case METHOD:
startGroupMethod(get("class"), get("name"));
break;
case TEST:
startTest(get("name"));
break;
case CLASS:
startClass(get("name"));
break;
case TEST_METHOD:
startTestMethod(get("name"), get("test-instance-name"),
get("status"), get("description"),
get("duration-ms"), get("started-at"),
get("is-config"));
break;
case REPORTER_OUTPUT:
startReporterOutput();
break;
case LINE:
startLine();
currentCDATAParent = TAGS.LINE;
break;
case PARAMS:
startMethodParameters();
currentCDATAParent = TAGS.PARAMS;
break;
case EXCEPTION:
startException(get("class"));
break;
case MESSAGE:
currentCDATAParent = TAGS.MESSAGE;
break;
case SHORT_STACKTRACE:
currentCDATAParent = TAGS.SHORT_STACKTRACE;
break;
case FULL_STACKTRACE:
currentCDATAParent = TAGS.FULL_STACKTRACE;
break;
default:
//TODO: log ignored tags
}
break;
// all closing tags
case XmlPullParser.END_TAG:
switch (tag) {
case SUITE:
finishSuite();
break;
case GROUP:
finishGroup();
break;
case METHOD:
finishGroupMethod();
break;
case TEST:
finishTest();
break;
case CLASS:
finishClass();
break;
case TEST_METHOD:
finishTestMethod();
break;
case REPORTER_OUTPUT:
endReporterOutput();
break;
case LINE:
endLine();
currentCDATAParent = TAGS.UNKNOWN;
break;
case PARAMS:
finishMethodParameters();
currentCDATAParent = TAGS.UNKNOWN;
break;
case EXCEPTION:
finishException();
break;
case MESSAGE:
case SHORT_STACKTRACE:
case FULL_STACKTRACE:
currentCDATAParent = TAGS.UNKNOWN;
break;
default:
//TODO: log ignored tags
}
break;
// all cdata reading
case XmlPullParser.CDSECT:
handleCDATA();
break;
default:
// ignore others
}
}
finalResults.addUniqueTests(testList);
} catch (XmlPullParserException e) {
log("Failed to parse XML: " + e.getMessage());
log(e);
} catch (FileNotFoundException e) {
log("Failed to find XML file");
log(e);
} catch (IOException e) {
log(e);
} finally {
try {
if (bufferedInputStream != null) {
bufferedInputStream.close();
}
} catch (IOException e) {
log(e);
}
}
}
//tally up the results properly before returning
finalResults.tally();
return finalResults;
}
private void startLine()
{
if (currentMethod != null && reporterOutputBuilder == null)
{
reporterOutputBuilder = new StringBuilder("");
}
}
private void endLine()
{
if (currentMethod != null)
{
reporterOutputBuilder.append(currentLine).append("<br/>");
}
}
private void startReporterOutput()
{
// do nothing (here for symmetry)
// might be used in future if we start capturing suite level reporter logs as well
}
private void endReporterOutput()
{
// some test method might have reporter output lines
if (currentMethod != null && reporterOutputBuilder != null) {
currentMethod.setReporterOutput(reporterOutputBuilder.toString());
}
reporterOutputBuilder = null;
}
private void startGroupMethod(String className, String methodName)
{
String key = className + "|" + methodName;
List<String> groups = methodGroupMap.get(key);
if (groups == null) {
groups = new ArrayList<String>(3);
groups.add(currentGroupName);
methodGroupMap.put(key, groups);
} else {
groups.add(currentGroupName);
}
}
private void finishGroupMethod()
{
// nothing to do
}
private void startGroup(String groupName)
{
currentGroupName = groupName;
}
private void finishGroup()
{
currentGroupName = null;
}
private void startGroups()
{
methodGroupMap = new HashMap<String, List<String>>();
}
private void startSuite(String name)
{
currentSuite = name;
}
private void finishSuite()
{
methodGroupMap.clear();
currentSuite = null;
}
private void startException(String exceptionName)
{
this.exceptionName = exceptionName;
}
private void finishException()
{
MethodResultException mrEx = new MethodResultException(exceptionName, currentMessage,
currentShortStackTrace, currentFullStackTrace);
currentMethod.setException(mrEx);
mrEx = null;
currentMessage = null;
currentShortStackTrace = null;
currentFullStackTrace = null;
exceptionName = null;
}
private void startMethodParameters()
{
currentMethodParamsList = new ArrayList<String>();
}
private void finishMethodParameters()
{
currentMethod.setParameters(currentMethodParamsList);
currentMethodParamsList = null;
}
private void handleCDATA()
{
switch (currentCDATAParent) {
case PARAMS:
currentMethodParamsList.add(xmlPullParser.getText());
break;
case MESSAGE:
currentMessage = xmlPullParser.getText();
break;
case FULL_STACKTRACE:
currentFullStackTrace = xmlPullParser.getText();
break;
case SHORT_STACKTRACE:
currentShortStackTrace = xmlPullParser.getText();
break;
case LINE:
currentLine = xmlPullParser.getText();
break;
case UNKNOWN:
//do nothing
}
}
private void startTestMethod(String name,
String testInstanceName,
String status,
String description,
String duration,
String startedAt,
String isConfig)
{
Date startedAtDate = null;
try {
startedAtDate = this.dateFormat.parse(startedAt);
} catch (ParseException e) {
log("Unable to parse started-at value: " + startedAt);
}
currentMethod = new MethodResult(name, status, description, duration,
startedAtDate == null ? -1 : startedAtDate.getTime(),
isConfig, currentTestRunId, currentTest.getName(),
currentSuite, testInstanceName);
List<String> groups = methodGroupMap.get(currentClass.getCanonicalName() + "|" + name);
if (groups != null) {
currentMethod.setGroups(groups);
}
}
private void finishTestMethod()
{
updateTestMethodLists(currentMethod);
// add to test methods list for each class
currentMethodList.add(currentMethod);
currentMethod = null;
}
private void startClass(String name)
{
int idx = name.lastIndexOf('.');
String simpleName = idx == -1 ?
name : name.substring(idx + 1, name.length());
String pkgName = idx == -1 ?
PackageResult.NO_PKG_NAME : name.substring(0, idx);
if (classResultMap.containsKey(name)) {
currentClass = classResultMap.get(name);
} else {
currentClass = new ClassResult(pkgName, simpleName);
classResultMap.put(name, currentClass);
}
currentMethodList = new ArrayList<MethodResult>();
//reset for each class
currentTestRunId = UUID.randomUUID().toString();
}
private void finishClass()
{
currentClass.addTestMethods(currentMethodList);
currentClassList.add(currentClass);
currentMethodList = null;
currentClass = null;
currentTestRunId = null;
}
private void startTest(String name)
{
if (testResultMap.containsKey(name)) {
currentTest = testResultMap.get(name);
} else {
currentTest = new TestNGTestResult(name);
testResultMap.put(name, currentTest);
}
currentClassList = new ArrayList<ClassResult>();
}
private void finishTest()
{
currentTest.addClassList(currentClassList);
testList.add(currentTest);
currentClassList = null;
currentTest = null;
}
private void updateTestMethodLists(MethodResult testMethod) {
if (testMethod.isConfig()) {
if ("FAIL".equals(testMethod.getStatus())) {
finalResults.getFailedConfigs().add(testMethod);
} else if ("SKIP".equals(testMethod.getStatus())) {
finalResults.getSkippedConfigs().add(testMethod);
}
} else {
if ("FAIL".equals(testMethod.getStatus())) {
finalResults.getFailedTests().add(testMethod);
} else if ("SKIP".equals(testMethod.getStatus())) {
finalResults.getSkippedTests().add(testMethod);
} else if ("PASS".equals(testMethod.getStatus())) {
finalResults.getPassedTests().add(testMethod);
}
}
}
private String get(String attr)
{
return xmlPullParser.getAttributeValue(null, attr);
}
private XmlPullParser createXmlPullParser(BufferedInputStream
bufferedInputStream) throws XmlPullParserException {
if (PARSER_FACTORY == null) {
throw new XmlPullParserException("XML Parser Factory has not been initialized properly");
}
XmlPullParser parser = PARSER_FACTORY.newPullParser();
parser.setInput(bufferedInputStream, null);
return parser;
}
private void log(String str) {
if (logger != null) {
logger.println(str);
} else {
log.fine(str);
}
}
private void log(Exception ex) {
if (logger != null) {
ex.printStackTrace(logger);
} else {
log.severe(ex.toString());
}
}
}