/**
* Copyright (c) 2013 Puppet Labs, Inc. and other contributors, as listed below.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Puppet Labs
*/
package com.puppetlabs.geppetto.junitresult.util;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import com.puppetlabs.geppetto.junitresult.AbstractAggregatedTest;
import com.puppetlabs.geppetto.junitresult.Error;
import com.puppetlabs.geppetto.junitresult.Failure;
import com.puppetlabs.geppetto.junitresult.JunitResult;
import com.puppetlabs.geppetto.junitresult.JunitresultFactory;
import com.puppetlabs.geppetto.junitresult.NegativeResult;
import com.puppetlabs.geppetto.junitresult.Property;
import com.puppetlabs.geppetto.junitresult.Skipped;
import com.puppetlabs.geppetto.junitresult.Testcase;
import com.puppetlabs.geppetto.junitresult.Testrun;
import com.puppetlabs.geppetto.junitresult.Testsuite;
import com.puppetlabs.geppetto.junitresult.Testsuites;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotSupportedException;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
public class JunitresultLoader {
/**
* Loader of so called JUnit result format, as first defined by the ANT junit task. Three main formats
* exists, but these are not formalized so parsing is based on empirical studies. A result document
* may have:
* <ul>
* <li>single <code><testsuite></code> element with optional nested elements of the same type</li>
* <li>a <code><testrun></code> element with multiple child <code><testsuite></code> element, which may also be nested.</li>
* <li>a <code><testsuites></code> element with multiple child <code><testsuite></code> element, which may be nested. When this format
* is used, the testsuite elements have an extended attribute set ('id' and 'package') which are not present in the other formats.</li>
* </ul>
*
* @param f
* @return a {@link JunitResult} which is one of {@link Testsuite}, {@link Testrun} or {@link Testsuites}
* @throws IOException
* @throws RuntimeException
* with nested detail exception if there is an internal error.
*/
public static JunitResult loadFromXML(File f) throws IOException {
try {
return new JunitresultLoader().loadFromXMLFile(f);
}
catch(ParserConfigurationException e) {
throw new RuntimeException(e);
}
catch(SAXException e) {
throw new RuntimeException(e);
}
}
private int getIntAttributeWith0Default(Element element, String attribute) {
try {
return Integer.valueOf(element.getAttribute(attribute));
}
catch(NumberFormatException e) {
// ignore, will return 0
}
return 0;
}
/**
* Returns the node value of a child node of the given element tagged with the given tag.
* If no such child element exists <code>null</code> is returned.
*
* @param element
* @param tag
* @return
*/
private String getTagValue(Element element, String tag) {
NodeList children = element.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if(n.getNodeType() == Node.ELEMENT_NODE && tag.equalsIgnoreCase(n.getNodeName())) {
NodeList content = n.getChildNodes();
Node node = content.item(0);
return node == null
? null
: node.getNodeValue();
}
}
return null;
}
private List<String> getTagValues(Element element, String tag) {
List<String> result = Lists.newArrayList();
NodeList children = element.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if(n.getNodeType() == Node.ELEMENT_NODE && tag.equalsIgnoreCase(n.getNodeName())) {
NodeList content = n.getChildNodes();
Node node = content.item(0);
if(node != null)
result.add(node.getNodeValue());
}
}
return result;
}
private double getTime(Element element, String attribute) {
String t = element.getAttribute(attribute);
if(Strings.isNullOrEmpty(t))
return 0.0;
try {
return Math.abs(Double.parseDouble(t));
}
catch(NumberFormatException e) {
return 0.0; // always return a time
}
}
private Date getTimestamp(Element element, String attribute) {
try {
// jaxb parser has a useful method for parsing a timestamp in ISO8601 format
Calendar calendar = javax.xml.bind.DatatypeConverter.parseDateTime(element.getAttribute(attribute));
return calendar.getTime();
}
catch(IllegalArgumentException e) {
// ouch - a date is always expected in ISO8601 form (gregorian)
// be kind and do not fail - the spec is not formal.
// return new Date(); // pretend it was "now"
return null; // return null if no timestamp is available - it will be fixed later (based on the source XML file's timestamp)
}
}
private void loadAbstractAggregatedPart(AbstractAggregatedTest o, Element element) {
o.setName(element.getAttribute("name"));
o.setTests(getIntAttributeWith0Default(element, "tests"));
o.setFailures(getIntAttributeWith0Default(element, "failures"));
o.setErrors(getIntAttributeWith0Default(element, "errors"));
}
public JunitResult loadFromXMLFile(File f) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
// set coalescing to true to make text and CDATA in nodes using concatenated into a single
// text string.
dbFactory.setCoalescing(true);
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(f);
Element docElement = doc.getDocumentElement();
// There are three types of document elements possible:
final String rootName = docElement.getNodeName();
if("testrun".equalsIgnoreCase(rootName)) {
return loadTestrun(docElement);
}
else if("testsuites".equalsIgnoreCase(rootName)) {
return loadTestSuites(docElement);
}
else if("testsuite".equalsIgnoreCase(rootName)) {
return loadTestSuite(docElement);
}
else {
throw new SAXNotSupportedException("Can only load 'testrun', 'testsuites' or 'testsuite', but got: " +
rootName);
}
}
private void loadNegativeResult(NegativeResult o, NodeList negativeList) {
Node negativeNode = negativeList.item(0);
if(negativeNode != null) {
Element errorElement = (Element) negativeNode;
o.setMessage(errorElement.getAttribute("message"));
o.setType(errorElement.getAttribute("type"));
// value (if any) is in a child node
NodeList children = errorElement.getChildNodes();
Node n = children.item(0);
if(n != null)
o.setValue(n.getNodeValue());
}
}
private Testcase loadTestCase(Element element) {
if(!"testcase".equalsIgnoreCase(element.getNodeName()))
throw new IllegalArgumentException("Non 'testcase' element passed to #loadTestCase");
Testcase o = JunitresultFactory.eINSTANCE.createTestcase();
o.setClassname(element.getAttribute("classname"));
o.setName(element.getAttribute("name"));
o.setTime(getTime(element, "time"));
// Only one of these are allowed - error, failure, skipped
// let the last win
// Error element
NodeList errors = element.getElementsByTagName("error");
for(int i = 0; i < errors.getLength(); i++) {
Error error = JunitresultFactory.eINSTANCE.createError();
loadNegativeResult(error, errors);
o.getErrors().add(error);
}
// Failure element
NodeList failures = element.getElementsByTagName("failure");
for(int i = 0; i < failures.getLength(); i++) {
Failure failure = JunitresultFactory.eINSTANCE.createFailure();
loadNegativeResult(failure, failures);
o.getFailures().add(failure);
}
// Skipped element
NodeList skipps = element.getElementsByTagName("skipped");
if(skipps.getLength() > 0) {
Skipped skipped = JunitresultFactory.eINSTANCE.createSkipped();
loadNegativeResult(skipped, skipps);
o.setSkipped(skipped);
}
o.getSystem_err().addAll(getTagValues(element, "system-err"));
o.getSystem_out().addAll(getTagValues(element, "system-out"));
return o;
}
/**
* Loads a <testrun> element which is the format used by Eclipse JUnit result export.
*
* @param element
* @return
*/
private Testrun loadTestrun(Element element) {
if(!"testrun".equalsIgnoreCase(element.getNodeName())) {
throw new IllegalArgumentException("Non 'testrun' element passed to #loadTestrun");
}
Testrun o = JunitresultFactory.eINSTANCE.createTestrun();
loadAbstractAggregatedPart(o, element);
o.setProject(element.getAttribute("project"));
o.setStarted(getIntAttributeWith0Default(element, "started"));
o.setIgnored(getIntAttributeWith0Default(element, "ignored"));
// nested test suite(s)
NodeList children = element.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if(n.getNodeType() == Node.ELEMENT_NODE && "testsuite".equalsIgnoreCase(n.getNodeName()))
o.getTestsuites().add(loadTestSuite((Element) n, false));
}
return o;
}
/**
* Loads a <testsuite> element in the form used by Eclipse testrun format, and when a testsuite is
* the document root. (This implies that the extended attributes found in testsuite elements embedded in the
* junitreport format are not parsed).
*
* @param element
* @return
*/
private Testsuite loadTestSuite(Element element) {
return loadTestSuite(element, false);
}
/**
* Loads a <testsuite> element in one of two alternate forms as directed by the parameter <code>extendedForm</code>.
* The extended form should be used when the element is part of a <testsuites> element as
* generated by junitreport.
*
* @param element
* @param extendedForm
* @return
*/
private Testsuite loadTestSuite(Element element, boolean extendedForm) {
Testsuite o = JunitresultFactory.eINSTANCE.createTestsuite();
// super class part
loadAbstractAggregatedPart(o, element);
// attributes
o.setSystem_err(getTagValue(element, "system-err"));
o.setSystem_out(getTagValue(element, "system-out"));
o.setHostname(element.getAttribute("hostname"));
o.setTime(getTime(element, "time"));
o.setTimestamp(getTimestamp(element, "timestamp"));
// JUnit 4 - (?)
o.setDisabled(getIntAttributeWith0Default(element, "disabled"));
o.setSkipped(getIntAttributeWith0Default(element, "skipped"));
// when embedded in a junitreport result where <testsuites> is the document root these two
// attributes are present in each nested testsuite.
//
if(extendedForm) {
o.setId(getIntAttributeWith0Default(element, "id"));
o.setPackage(element.getAttribute("package"));
}
// child test suites (nested) & test cases
NodeList children = element.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if(n.getNodeType() == Node.ELEMENT_NODE)
if("testsuite".equalsIgnoreCase(n.getNodeName()))
o.getTestsuites().add(loadTestSuite((Element) n, extendedForm));
else if("testcase".equalsIgnoreCase(n.getNodeName()))
o.getTestcases().add(loadTestCase((Element) n));
else if("properties".equalsIgnoreCase(n.getNodeName())) {
NodeList properties = ((Element) n).getElementsByTagName("property");
for(int j = 0; j < properties.getLength(); j++) {
Node pn = properties.item(j);
if(pn.getNodeType() == Node.ELEMENT_NODE) {
Element propertyElement = (Element) pn;
Property p = JunitresultFactory.eINSTANCE.createProperty();
p.setName(propertyElement.getAttribute("name"));
p.setValue(propertyElement.getAttribute("value"));
o.getProperties().add(p);
}
}
}
}
return o;
}
/**
* Loads a <testsuites> element as found in the result from a junitreport. All nested
* <testsuite> elements have extended attributes.
*
* @param element
* @return
*/
private Testsuites loadTestSuites(Element element) {
Testsuites o = JunitresultFactory.eINSTANCE.createTestsuites();
loadAbstractAggregatedPart(o, element);
o.setTime(getTime(element, "time"));
o.setDisabled(getIntAttributeWith0Default(element, "disabled"));
NodeList children = element.getChildNodes();
for(int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if(n.getNodeType() == Node.ELEMENT_NODE && "testsuite".equalsIgnoreCase(n.getNodeName()))
o.getTestsuites().add(loadTestSuite((Element) n, true));
}
return o;
}
}