blob: d8fcccc150e19ad22bdc00b96209d9951522a267 [file] [log] [blame]
/*
* Copyright (C) 2009 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.ddmlib.testrunner;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.google.common.collect.ImmutableMap;
import org.kxml2.io.KXmlSerializer;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/**
* Writes JUnit results to an XML files in a format consistent with
* Ant's XMLJUnitResultFormatter.
* <p>
* Creates a separate XML file per test run.
*
* @see <a href="https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd">https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd</a> */
public class XmlTestRunListener implements ITestRunListener {
private static final String LOG_TAG = "XmlResultReporter";
private static final String TEST_RESULT_FILE_SUFFIX = ".xml";
private static final String TEST_RESULT_FILE_PREFIX = "test_result_";
private static final String TESTSUITE = "testsuite";
private static final String TESTCASE = "testcase";
private static final String ERROR = "error";
private static final String FAILURE = "failure";
private static final String SKIPPED_TAG = "skipped";
private static final String ATTR_NAME = "name";
private static final String ATTR_TIME = "time";
private static final String ATTR_ERRORS = "errors";
private static final String ATTR_FAILURES = "failures";
private static final String ATTR_SKIPPED = "skipped";
private static final String ATTR_ASSERTIOMS = "assertions";
private static final String ATTR_TESTS = "tests";
//private static final String ATTR_TYPE = "type";
//private static final String ATTR_MESSAGE = "message";
private static final String PROPERTIES = "properties";
private static final String PROPERTY = "property";
private static final String ATTR_CLASSNAME = "classname";
private static final String TIMESTAMP = "timestamp";
private static final String HOSTNAME = "hostname";
/** the XML namespace */
private static final String ns = null;
private String mHostName = "localhost";
private File mReportDir = new File(System.getProperty("java.io.tmpdir"));
private String mReportPath = "";
private TestRunResult mRunResult = new TestRunResult();
/**
* Sets the report file to use.
*/
public void setReportDir(File file) {
mReportDir = file;
}
public void setHostName(String hostName) {
mHostName = hostName;
}
/**
* Returns the {@link TestRunResult}
* @return the test run results.
*/
public TestRunResult getRunResult() {
return mRunResult;
}
@Override
public void testRunStarted(String runName, int numTests) {
mRunResult = new TestRunResult();
mRunResult.testRunStarted(runName, numTests);
}
@Override
public void testStarted(TestIdentifier test) {
mRunResult.testStarted(test);
}
@Override
public void testFailed(TestIdentifier test, String trace) {
mRunResult.testFailed(test, trace);
}
@Override
public void testAssumptionFailure(TestIdentifier test, String trace) {
mRunResult.testAssumptionFailure(test, trace);
}
@Override
public void testIgnored(TestIdentifier test) {
mRunResult.testIgnored(test);
}
@Override
public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
mRunResult.testEnded(test, testMetrics);
}
@Override
public void testRunFailed(String errorMessage) {
mRunResult.testRunFailed(errorMessage);
}
@Override
public void testRunStopped(long elapsedTime) {
mRunResult.testRunStopped(elapsedTime);
}
@Override
public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
mRunResult.testRunEnded(elapsedTime, runMetrics);
generateDocument(mReportDir, elapsedTime);
}
/**
* Creates a report file and populates it with the report data from the completed tests.
*/
private void generateDocument(File reportDir, long elapsedTime) {
String timestamp = getTimestamp();
OutputStream stream = null;
try {
stream = createOutputResultStream(reportDir);
KXmlSerializer serializer = new KXmlSerializer();
serializer.setOutput(stream, SdkConstants.UTF_8);
serializer.startDocument(SdkConstants.UTF_8, null);
serializer.setFeature(
"http://xmlpull.org/v1/doc/features.html#indent-output", true);
// TODO: insert build info
printTestResults(serializer, timestamp, elapsedTime);
serializer.endDocument();
String msg = String.format("XML test result file generated at %s. %s" ,
getAbsoluteReportPath(), mRunResult.getTextSummary());
Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg);
} catch (IOException e) {
Log.e(LOG_TAG, "Failed to generate report data");
// TODO: consider throwing exception
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException ignored) {
}
}
}
}
private String getAbsoluteReportPath() {
return mReportPath ;
}
/**
* Return the current timestamp as a {@link String}.
*/
String getTimestamp() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
Locale.getDefault());
TimeZone gmt = TimeZone.getTimeZone("UTC");
dateFormat.setTimeZone(gmt);
dateFormat.setLenient(true);
String timestamp = dateFormat.format(new Date());
return timestamp;
}
/**
* Creates a {@link File} where the report will be created.
* @param reportDir the root directory of the report.
* @return a file
* @throws IOException
*/
protected File getResultFile(File reportDir) throws IOException {
File reportFile = File.createTempFile(TEST_RESULT_FILE_PREFIX, TEST_RESULT_FILE_SUFFIX,
reportDir);
Log.i(LOG_TAG, String.format("Created xml report file at %s",
reportFile.getAbsolutePath()));
return reportFile;
}
/**
* Creates the output stream to use for test results. Exposed for mocking.
*/
OutputStream createOutputResultStream(File reportDir) throws IOException {
File reportFile = getResultFile(reportDir);
mReportPath = reportFile.getAbsolutePath();
return new BufferedOutputStream(new FileOutputStream(reportFile));
}
protected String getTestSuiteName() {
return mRunResult.getName();
}
void printTestResults(KXmlSerializer serializer, String timestamp, long elapsedTime)
throws IOException {
serializer.startTag(ns, TESTSUITE);
String name = getTestSuiteName();
if (name != null) {
serializer.attribute(ns, ATTR_NAME, name);
}
serializer.attribute(ns, ATTR_TESTS, Integer.toString(mRunResult.getNumTests()));
serializer.attribute(ns, ATTR_FAILURES, Integer.toString(
mRunResult.getNumAllFailedTests()));
// legacy - there are no errors in JUnit4
serializer.attribute(ns, ATTR_ERRORS, "0");
serializer.attribute(ns, ATTR_SKIPPED, Integer.toString(mRunResult.getNumTestsInState(
TestStatus.IGNORED)));
serializer.attribute(ns, ATTR_TIME, Double.toString((double) elapsedTime / 1000.f));
serializer.attribute(ns, TIMESTAMP, timestamp);
serializer.attribute(ns, HOSTNAME, mHostName);
serializer.startTag(ns, PROPERTIES);
for (Map.Entry<String,String> entry: getPropertiesAttributes().entrySet()) {
serializer.startTag(ns, PROPERTY);
serializer.attribute(ns, "name", entry.getKey());
serializer.attribute(ns, "value", entry.getValue());
serializer.endTag(ns, PROPERTY);
}
serializer.endTag(ns, PROPERTIES);
Map<TestIdentifier, TestResult> testResults = mRunResult.getTestResults();
for (Map.Entry<TestIdentifier, TestResult> testEntry : testResults.entrySet()) {
print(serializer, testEntry.getKey(), testEntry.getValue());
}
serializer.endTag(ns, TESTSUITE);
}
/**
* Get the properties attributes as key value pairs to be included in the test report.
*/
@NonNull
protected Map<String, String> getPropertiesAttributes() {
return ImmutableMap.of();
}
protected String getTestName(TestIdentifier testId) {
return testId.getTestName();
}
void print(KXmlSerializer serializer, TestIdentifier testId, TestResult testResult)
throws IOException {
serializer.startTag(ns, TESTCASE);
serializer.attribute(ns, ATTR_NAME, getTestName(testId));
serializer.attribute(ns, ATTR_CLASSNAME, testId.getClassName());
long elapsedTimeMs = testResult.getEndTime() - testResult.getStartTime();
serializer.attribute(ns, ATTR_TIME, Double.toString((double)elapsedTimeMs / 1000.f));
switch (testResult.getStatus()) {
case FAILURE:
printFailedTest(serializer, FAILURE, testResult.getStackTrace());
break;
case ASSUMPTION_FAILURE:
printFailedTest(serializer, SKIPPED_TAG, testResult.getStackTrace());
break;
case IGNORED:
serializer.startTag(ns, SKIPPED_TAG);
serializer.endTag(ns, SKIPPED_TAG);
break;
}
serializer.endTag(ns, TESTCASE);
}
private void printFailedTest(KXmlSerializer serializer, String tag, String stack)
throws IOException {
serializer.startTag(ns, tag);
// TODO: get message of stack trace ?
// String msg = testResult.getStackTrace();
// if (msg != null && msg.length() > 0) {
// serializer.attribute(ns, ATTR_MESSAGE, msg);
// }
// TODO: get class name of stackTrace exception
// serializer.attribute(ns, ATTR_TYPE, testId.getClassName());
serializer.text(sanitize(stack));
serializer.endTag(ns, tag);
}
/**
* Returns the text in a format that is safe for use in an XML document.
*/
private String sanitize(String text) {
return text.replace("\0", "<\\0>");
}
}