blob: 533f73e46a9f87ccfaa17661ea6d5c701dedac17 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed 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.
*/
package com.android.tradefed.util;
import com.android.ddmlib.testrunner.XmlTestRunListener;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.util.xml.AbstractXmlParser;
import com.google.common.base.Strings;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.text.NumberFormat;
import java.util.HashMap;
/**
* Parser that extracts test result data from JUnit results stored in ant's XMLJUnitResultFormatter
* and forwards it to a ITestInvocationListener.
* <p/>
* @see XmlTestRunListener
*/
public class JUnitXmlParser extends AbstractXmlParser {
private final String mRunName;
private final ITestInvocationListener mTestListener;
/**
* Parses the xml format. Expected tags/attributes are
*
* testsuite name="runname" tests="X"
* testcase classname="FooTest" name="testMethodName"
* failure type="blah" message="exception message"
* stack
*/
private class JUnitXmlHandler extends DefaultHandler {
/**
* The total number of tests in the suite that errored. An errored test is one that had an
* unanticipated problem, for example an unchecked throwable; or a problem with the
* implementation of the test. optional.
*/
private static final String ERROR_TAG = "error";
/**
* The total number of tests in the suite that failed. A failure is a test which the code
* has explicitly failed by using the mechanisms for that purpose. e.g., via an
* assertEquals. optional.
*/
private static final String FAILURE_TAG = "failure";
private static final String SKIPPED_TAG = "skipped";
private static final String TESTSUITE_TAG = "testsuite";
private static final String TESTCASE_TAG = "testcase";
private TestDescription mCurrentTest = null;
private StringBuffer mFailureContent = null;
private String mCurrentMessage = null;
private long mRunTimeMillis = 0L;
/**
* {@inheritDoc}
*/
@Override
public void startElement(String uri, String localName, String name, Attributes attributes)
throws SAXException {
mFailureContent = null;
mCurrentMessage = null;
if (TESTSUITE_TAG.equalsIgnoreCase(name)) {
// top level tag - maps to a test run in TF terminology
String testSuiteName = getMandatoryAttribute(name, "name", attributes);
String testCountString = getMandatoryAttribute(name, "tests", attributes);
mRunTimeMillis = getTimeMillis(name, attributes);
int testCount = Integer.parseInt(testCountString);
String runName = (mRunName != null) ? mRunName : testSuiteName;
mTestListener.testRunStarted(runName, testCount);
}
if (TESTCASE_TAG.equalsIgnoreCase(name)) {
// start of description of an individual test method - extract out test name and
// store it
String testClassName = Strings.nullToEmpty(attributes.getValue("classname"));
testClassName = "".equals(testClassName) // TODO(b/120500865): remove this kludge
? JUnitXmlParser.class.getSimpleName()
: testClassName;
String methodName = getMandatoryAttribute(name, "name", attributes);
mCurrentTest = new TestDescription(testClassName, methodName);
mTestListener.testStarted(mCurrentTest);
}
if (SKIPPED_TAG.equalsIgnoreCase(name)) {
if (mCurrentTest != null) {
mTestListener.testIgnored(mCurrentTest);
}
}
if (FAILURE_TAG.equalsIgnoreCase(name) || ERROR_TAG.equalsIgnoreCase(name)) {
// current testcase has a failure - will be extracted in characters() callback
mFailureContent = new StringBuffer();
String value = attributes.getValue("message");
if (value != null) {
mCurrentMessage = value + ".";
}
}
}
/**
* Called when parsing CDATA content of a tag
*/
@Override
public void characters(char[] data, int offset, int len) {
// if currently parsing a failure, add stack data to content
if (mFailureContent != null) {
mFailureContent.append(data, offset, len);
}
}
/**
* {@inheritDoc}
*/
@Override
public void endElement(String uri, String localName, String name) {
if (mFailureContent != null && mFailureContent.length() == 0) {
// If there was no error content, use the error message if any.
if (mCurrentMessage != null) {
mFailureContent.append(mCurrentMessage);
}
}
if (TESTSUITE_TAG.equalsIgnoreCase(name)) {
mTestListener.testRunEnded(mRunTimeMillis, new HashMap<String, Metric>());
}
if (TESTCASE_TAG.equalsIgnoreCase(name)) {
mTestListener.testEnded(mCurrentTest, new HashMap<String, Metric>());
}
if (FAILURE_TAG.equalsIgnoreCase(name) || ERROR_TAG.equalsIgnoreCase(name)) {
mTestListener.testFailed(mCurrentTest,
mFailureContent.toString());
}
mFailureContent = null;
mCurrentMessage = null;
}
/**
* Retrieves an attributes value.
*
* @throws SAXException if attribute is not present
*/
String getMandatoryAttribute(String tagName, String attrName, Attributes attributes)
throws SAXException {
String value = attributes.getValue(attrName);
if (value == null) {
throw new SAXException(String.format(
"Malformed XML, could not find '%s' attribute in '%s'", attrName, tagName));
}
return value;
}
/**
* Parse the time attributes from the xml, and put it in milliseconds instead of seconds.
*/
long getTimeMillis(String tagName, Attributes attributes) throws SAXException {
String value = attributes.getValue("time");
if (value == null) {
return 0L;
}
NumberFormat f = NumberFormat.getInstance();
Number n;
try {
n = f.parse(value);
} catch (java.text.ParseException e) {
throw new SAXException(
String.format("%s.time value '%s' is not a number", tagName, value), e);
}
return (long) (n.floatValue() * 1000L);
}
}
/**
* Create a JUnitXmlParser
*
* @param listener the {@link ITestInvocationListener} to forward results to
*/
public JUnitXmlParser(ITestInvocationListener listener) {
mRunName = null;
mTestListener = listener;
}
/**
* Creates a {@code JUnitXmlParser} that notifies {@code listener}, passing {@code runName}
* to {@link ITestInvocationListener#testRunStarted}.
*/
public JUnitXmlParser(String runName, ITestInvocationListener listener) {
mRunName = runName;
mTestListener = listener;
}
/**
* {@inheritDoc}
*/
@Override
protected DefaultHandler createXmlHandler() {
return new JUnitXmlHandler();
}
}