aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrett Chabot <brettchabot@android.com>2012-11-28 16:41:19 -0800
committerGerrit Code Review <noreply-gerritcodereview@google.com>2012-11-28 16:41:19 -0800
commite63c524d0c40b72ca10528398a2648b927f7e63a (patch)
tree677367e74563c4e7472a7fb013b7a7090c906441
parente8e5175fe823e79b6a22d4fd28272d144654520d (diff)
parent24fd7703acd668cc5193d701ced5b420f3a2429d (diff)
downloadsdk-e63c524d0c40b72ca10528398a2648b927f7e63a.zip
sdk-e63c524d0c40b72ca10528398a2648b927f7e63a.tar.gz
sdk-e63c524d0c40b72ca10528398a2648b927f7e63a.tar.bz2
Merge "Add a listener that can save test results as an ant XML file."
-rw-r--r--ddms/libs/ddmlib/.classpath1
-rw-r--r--ddms/libs/ddmlib/Android.mk2
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestResult.java147
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestRunResult.java324
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/XmlTestRunListener.java240
-rw-r--r--ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java159
6 files changed, 873 insertions, 0 deletions
diff --git a/ddms/libs/ddmlib/.classpath b/ddms/libs/ddmlib/.classpath
index 9762afc..22d0241 100644
--- a/ddms/libs/ddmlib/.classpath
+++ b/ddms/libs/ddmlib/.classpath
@@ -2,5 +2,6 @@
<classpath>
<classpathentry excluding="Android.mk" kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/misc/common/kxml2/kxml2-2.3.0.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/ddms/libs/ddmlib/Android.mk b/ddms/libs/ddmlib/Android.mk
index 978ac19..929522e 100644
--- a/ddms/libs/ddmlib/Android.mk
+++ b/ddms/libs/ddmlib/Android.mk
@@ -20,6 +20,8 @@ include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_RESOURCE_DIRS := src
+LOCAL_JAVA_LIBRARIES := kxml2-2.3.0
+
LOCAL_MODULE := ddmlib
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestResult.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestResult.java
new file mode 100644
index 0000000..57e91f7
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestResult.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2010 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 java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Container for a result of a single test.
+ */
+public class TestResult {
+
+ public enum TestStatus {
+ /** Test error */
+ ERROR,
+ /** Test failed. */
+ FAILURE,
+ /** Test passed */
+ PASSED,
+ /** Test started but not ended */
+ INCOMPLETE
+ }
+
+ private TestStatus mStatus;
+ private String mStackTrace;
+ private Map<String, String> mMetrics;
+ // the start and end time of the test, measured via {@link System#currentTimeMillis()}
+ private long mStartTime = 0;
+ private long mEndTime = 0;
+
+ public TestResult() {
+ mStatus = TestStatus.INCOMPLETE;
+ mStartTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Get the {@link TestStatus} result of the test.
+ */
+ public TestStatus getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * Get the associated {@link String} stack trace. Should be <code>null</code> if
+ * {@link #getStatus()} is {@link TestStatus.PASSED}.
+ */
+ public String getStackTrace() {
+ return mStackTrace;
+ }
+
+ /**
+ * Get the associated test metrics.
+ */
+ public Map<String, String> getMetrics() {
+ return mMetrics;
+ }
+
+ /**
+ * Set the test metrics, overriding any previous values.
+ */
+ public void setMetrics(Map<String, String> metrics) {
+ mMetrics = metrics;
+ }
+
+ /**
+ * Return the {@link System#currentTimeMillis()} time that the
+ * {@link ITestInvocationListener#testStarted(TestIdentifier)} event was received.
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * Return the {@link System#currentTimeMillis()} time that the
+ * {@link ITestInvocationListener#testEnded(TestIdentifier)} event was received.
+ */
+ public long getEndTime() {
+ return mEndTime;
+ }
+
+ /**
+ * Set the {@link TestStatus}.
+ */
+ public TestResult setStatus(TestStatus status) {
+ mStatus = status;
+ return this;
+ }
+
+ /**
+ * Set the stack trace.
+ */
+ public void setStackTrace(String trace) {
+ mStackTrace = trace;
+ }
+
+ /**
+ * Sets the end time
+ */
+ public void setEndTime(long currentTimeMillis) {
+ mEndTime = currentTimeMillis;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[] {mMetrics, mStackTrace, mStatus});
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ TestResult other = (TestResult) obj;
+ return equal(mMetrics, other.mMetrics) &&
+ equal(mStackTrace, other.mStackTrace) &&
+ equal(mStatus, other.mStatus);
+ }
+
+ private static boolean equal(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestRunResult.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestRunResult.java
new file mode 100644
index 0000000..14bb477
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestRunResult.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2010 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.ddmlib.Log;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Holds results from a single test run.
+ * <p/>
+ * Maintains an accurate count of tests during execution, and tracks incomplete tests.
+ */
+public class TestRunResult {
+ private static final String LOG_TAG = TestRunResult.class.getSimpleName();
+ private final String mTestRunName;
+ // Uses a synchronized map to make thread safe.
+ // Uses a LinkedHashmap to have predictable iteration order
+ private Map<TestIdentifier, TestResult> mTestResults =
+ Collections.synchronizedMap(new LinkedHashMap<TestIdentifier, TestResult>());
+ private Map<String, String> mRunMetrics = new HashMap<String, String>();
+ private boolean mIsRunComplete = false;
+ private long mElapsedTime = 0;
+ private int mNumFailedTests = 0;
+ private int mNumErrorTests = 0;
+ private int mNumPassedTests = 0;
+ private int mNumInCompleteTests = 0;
+ private String mRunFailureError = null;
+
+ /**
+ * Create a {@link TestRunResult}.
+ *
+ * @param runName
+ */
+ public TestRunResult(String runName) {
+ mTestRunName = runName;
+ }
+
+ /**
+ * Create an empty{@link TestRunResult}.
+ */
+ public TestRunResult() {
+ this("not started");
+ }
+
+ /**
+ * @return the test run name
+ */
+ public String getName() {
+ return mTestRunName;
+ }
+
+ /**
+ * Gets a map of the test results.
+ * @return
+ */
+ public Map<TestIdentifier, TestResult> getTestResults() {
+ return mTestResults;
+ }
+
+ /**
+ * Adds test run metrics.
+ * <p/>
+ * @param runMetrics the run metrics
+ * @param aggregateMetrics if <code>true</code>, attempt to add given metrics values to any
+ * currently stored values. If <code>false</code>, replace any currently stored metrics with
+ * the same key.
+ */
+ public void addMetrics(Map<String, String> runMetrics, boolean aggregateMetrics) {
+ if (aggregateMetrics) {
+ for (Map.Entry<String, String> entry : runMetrics.entrySet()) {
+ String existingValue = mRunMetrics.get(entry.getKey());
+ String combinedValue = combineValues(existingValue, entry.getValue());
+ mRunMetrics.put(entry.getKey(), combinedValue);
+ }
+ } else {
+ mRunMetrics.putAll(runMetrics);
+ }
+ }
+
+ /**
+ * Combine old and new metrics value
+ *
+ * @param existingValue
+ * @param value
+ * @return
+ */
+ private String combineValues(String existingValue, String newValue) {
+ if (existingValue != null) {
+ try {
+ Long existingLong = Long.parseLong(existingValue);
+ Long newLong = Long.parseLong(newValue);
+ return Long.toString(existingLong + newLong);
+ } catch (NumberFormatException e) {
+ // not a long, skip to next
+ }
+ try {
+ Double existingDouble = Double.parseDouble(existingValue);
+ Double newDouble = Double.parseDouble(newValue);
+ return Double.toString(existingDouble + newDouble);
+ } catch (NumberFormatException e) {
+ // not a double either, fall through
+ }
+ }
+ // default to overriding existingValue
+ return newValue;
+ }
+
+ /**
+ * @return a {@link Map} of the test test run metrics.
+ */
+ public Map<String, String> getRunMetrics() {
+ return mRunMetrics;
+ }
+
+ /**
+ * Gets the set of completed tests.
+ */
+ public Set<TestIdentifier> getCompletedTests() {
+ Set<TestIdentifier> completedTests = new LinkedHashSet<TestIdentifier>();
+ for (Map.Entry<TestIdentifier, TestResult> testEntry : getTestResults().entrySet()) {
+ if (!testEntry.getValue().getStatus().equals(TestStatus.INCOMPLETE)) {
+ completedTests.add(testEntry.getKey());
+ }
+ }
+ return completedTests;
+ }
+
+ /**
+ * @return <code>true</code> if test run failed.
+ */
+ public boolean isRunFailure() {
+ return mRunFailureError != null;
+ }
+
+ /**
+ * @return <code>true</code> if test run finished.
+ */
+ public boolean isRunComplete() {
+ return mIsRunComplete;
+ }
+
+ void setRunComplete(boolean runComplete) {
+ mIsRunComplete = runComplete;
+ }
+
+ void addElapsedTime(long elapsedTime) {
+ mElapsedTime+= elapsedTime;
+ }
+
+ void setRunFailureError(String errorMessage) {
+ mRunFailureError = errorMessage;
+ }
+
+ /**
+ * Gets the number of passed tests for this run.
+ */
+ public int getNumPassedTests() {
+ return mNumPassedTests;
+ }
+
+ /**
+ * Gets the number of tests in this run.
+ */
+ public int getNumTests() {
+ return mTestResults.size();
+ }
+
+ /**
+ * Gets the number of complete tests in this run ie with status != incomplete.
+ */
+ public int getNumCompleteTests() {
+ return getNumTests() - getNumIncompleteTests();
+ }
+
+ /**
+ * Gets the number of failed tests in this run.
+ */
+ public int getNumFailedTests() {
+ return mNumFailedTests;
+ }
+
+ /**
+ * Gets the number of error tests in this run.
+ */
+ public int getNumErrorTests() {
+ return mNumErrorTests;
+ }
+
+ /**
+ * Gets the number of incomplete tests in this run.
+ */
+ public int getNumIncompleteTests() {
+ return mNumInCompleteTests;
+ }
+
+ /**
+ * @return <code>true</code> if test run had any failed or error tests.
+ */
+ public boolean hasFailedTests() {
+ return getNumErrorTests() > 0 || getNumFailedTests() > 0;
+ }
+
+ /**
+ * @return
+ */
+ public long getElapsedTime() {
+ return mElapsedTime;
+ }
+
+ /**
+ * Return the run failure error message, <code>null</code> if run did not fail.
+ */
+ public String getRunFailureMessage() {
+ return mRunFailureError;
+ }
+
+ /**
+ * Report the start of a test.
+ * @param test
+ */
+ void reportTestStarted(TestIdentifier test) {
+ TestResult result = mTestResults.get(test);
+
+ if (result != null) {
+ Log.d(LOG_TAG, String.format("Replacing result for %s", test));
+ switch (result.getStatus()) {
+ case ERROR:
+ mNumErrorTests--;
+ break;
+ case FAILURE:
+ mNumFailedTests--;
+ break;
+ case PASSED:
+ mNumPassedTests--;
+ break;
+ case INCOMPLETE:
+ // ignore
+ break;
+ }
+ } else {
+ mNumInCompleteTests++;
+ }
+ mTestResults.put(test, new TestResult());
+ }
+
+ /**
+ * Report a test failure.
+ *
+ * @param test
+ * @param status
+ * @param trace
+ */
+ void reportTestFailure(TestIdentifier test, TestStatus status, String trace) {
+ TestResult result = mTestResults.get(test);
+ if (result == null) {
+ Log.d(LOG_TAG, String.format("Received test failure for %s without testStarted", test));
+ result = new TestResult();
+ mTestResults.put(test, result);
+ } else if (result.getStatus().equals(TestStatus.PASSED)) {
+ // this should never happen...
+ Log.d(LOG_TAG, String.format("Replacing passed result for %s", test));
+ mNumPassedTests--;
+ }
+
+ result.setStackTrace(trace);
+ switch (status) {
+ case ERROR:
+ mNumErrorTests++;
+ result.setStatus(TestStatus.ERROR);
+ break;
+ case FAILURE:
+ result.setStatus(TestStatus.FAILURE);
+ mNumFailedTests++;
+ break;
+ }
+ }
+
+ /**
+ * Report the end of the test
+ *
+ * @param test
+ * @param testMetrics
+ * @return <code>true</code> if test was recorded as passed, false otherwise
+ */
+ boolean reportTestEnded(TestIdentifier test, Map<String, String> testMetrics) {
+ TestResult result = mTestResults.get(test);
+ if (result == null) {
+ Log.d(LOG_TAG, String.format("Received test ended for %s without testStarted", test));
+ result = new TestResult();
+ mTestResults.put(test, result);
+ } else {
+ mNumInCompleteTests--;
+ }
+
+ result.setEndTime(System.currentTimeMillis());
+ result.setMetrics(testMetrics);
+ if (result.getStatus().equals(TestStatus.INCOMPLETE)) {
+ result.setStatus(TestStatus.PASSED);
+ mNumPassedTests++;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/XmlTestRunListener.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/XmlTestRunListener.java
new file mode 100644
index 0000000..2e48afe
--- /dev/null
+++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/XmlTestRunListener.java
@@ -0,0 +1,240 @@
+/*
+ * 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.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+
+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.Map;
+import java.util.TimeZone;
+
+/**
+ * Writes JUnit results to an XML files in a format consistent with
+ * Ant's XMLJUnitResultFormatter.
+ * <p/>
+ * Unlike Ant's formatter, this class does not report the execution time of
+ * tests.
+ * <p/>
+ * Creates a separate XML file per test run.
+ * <p/>
+ */
+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 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_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 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 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;
+ }
+
+ @Override
+ public void testRunStarted(String runName, int numTests) {
+ mRunResult = new TestRunResult(runName);
+ }
+
+ @Override
+ public void testStarted(TestIdentifier test) {
+ mRunResult.reportTestStarted(test);
+ }
+
+ @Override
+ public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ if (status.equals(TestFailure.ERROR)) {
+ mRunResult.reportTestFailure(test, TestStatus.ERROR, trace);
+ } else {
+ mRunResult.reportTestFailure(test, TestStatus.FAILURE, trace);
+ }
+ Log.d(LOG_TAG, String.format("%s %s: %s", test, status, trace));
+ }
+
+ @Override
+ public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+ mRunResult.reportTestEnded(test, testMetrics);
+ }
+
+ @Override
+ public void testRunFailed(String errorMessage) {
+ mRunResult.setRunFailureError(errorMessage);
+ }
+
+ @Override
+ public void testRunStopped(long arg0) {
+ // ignore
+ }
+
+ @Override
+ public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+ mRunResult.setRunComplete(true);
+ 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, "UTF-8");
+ serializer.startDocument("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. Total tests %d, " +
+ "Failed %d, Error %d", getAbsoluteReportPath(), mRunResult.getNumTests(),
+ mRunResult.getNumFailedTests(), mRunResult.getNumErrorTests());
+ 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");
+ TimeZone gmt = TimeZone.getTimeZone("UTC");
+ dateFormat.setTimeZone(gmt);
+ dateFormat.setLenient(true);
+ String timestamp = dateFormat.format(new Date());
+ return timestamp;
+ }
+
+ /**
+ * Creates the output stream to use for test results. Exposed for mocking.
+ */
+ OutputStream createOutputResultStream(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()));
+ mReportPath = reportFile.getAbsolutePath();
+ return new BufferedOutputStream(new FileOutputStream(reportFile));
+ }
+
+ void printTestResults(KXmlSerializer serializer, String timestamp, long elapsedTime)
+ throws IOException {
+ serializer.startTag(ns, TESTSUITE);
+ serializer.attribute(ns, ATTR_NAME, mRunResult.getName());
+ serializer.attribute(ns, ATTR_TESTS, Integer.toString(mRunResult.getNumTests()));
+ serializer.attribute(ns, ATTR_FAILURES, Integer.toString(mRunResult.getNumFailedTests()));
+ serializer.attribute(ns, ATTR_ERRORS, Integer.toString(mRunResult.getNumErrorTests()));
+ serializer.attribute(ns, ATTR_TIME, Long.toString(elapsedTime));
+ serializer.attribute(ns, TIMESTAMP, timestamp);
+ serializer.attribute(ns, HOSTNAME, "localhost");
+ serializer.startTag(ns, PROPERTIES);
+ 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);
+ }
+
+ void print(KXmlSerializer serializer, TestIdentifier testId, TestResult testResult)
+ throws IOException {
+
+ serializer.startTag(ns, TESTCASE);
+ serializer.attribute(ns, ATTR_NAME, testId.getTestName());
+ serializer.attribute(ns, ATTR_CLASSNAME, testId.getClassName());
+ serializer.attribute(ns, ATTR_TIME, "0");
+
+ if (!TestStatus.PASSED.equals(testResult.getStatus())) {
+ String result = testResult.getStatus().equals(TestStatus.FAILURE) ? FAILURE : ERROR;
+ serializer.startTag(ns, result);
+ // 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());
+ String stackText = sanitize(testResult.getStackTrace());
+ serializer.text(stackText);
+ serializer.endTag(ns, result);
+ }
+
+ serializer.endTag(ns, TESTCASE);
+ }
+
+ /**
+ * 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>");
+ }
+}
diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java
new file mode 100644
index 0000000..f5672fa
--- /dev/null
+++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/XmlTestRunListenerTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2012 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.ddmlib.testrunner.ITestRunListener.TestFailure;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link XmlTestRunListener}.
+ */
+public class XmlTestRunListenerTest extends TestCase {
+
+ private XmlTestRunListener mResultReporter;
+ private ByteArrayOutputStream mOutputStream;
+ private File mReportDir;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mOutputStream = new ByteArrayOutputStream();
+ mResultReporter = new XmlTestRunListener() {
+ @Override
+ OutputStream createOutputResultStream(File reportDir) throws IOException {
+ return mOutputStream;
+ }
+
+ @Override
+ String getTimestamp() {
+ return "ignore";
+ }
+ };
+ // TODO: use mock file dir instead
+ mReportDir = createTmpDir();
+ mResultReporter.setReportDir(mReportDir);
+ }
+
+ private File createTmpDir() throws IOException {
+ // create a temp file with unique name, then make it a directory
+ File tmpDir = File.createTempFile("foo", "dir");
+ tmpDir.delete();
+ if (!tmpDir.mkdirs()) {
+ throw new IOException("unable to create directory");
+ }
+ return tmpDir;
+ }
+
+ /**
+ * Recursively delete given file and all its contents
+ */
+ private static void recursiveDelete(File rootDir) {
+ if (rootDir.isDirectory()) {
+ File[] childFiles = rootDir.listFiles();
+ if (childFiles != null) {
+ for (File child : childFiles) {
+ recursiveDelete(child);
+ }
+ }
+ }
+ rootDir.delete();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mReportDir != null) {
+ recursiveDelete(mReportDir);
+ }
+ super.tearDown();
+ }
+
+ /**
+ * A simple test to ensure expected output is generated for test run with no tests.
+ */
+ public void testEmptyGeneration() {
+ final String expectedOutput = "<?xml version='1.0' encoding='UTF-8' ?>" +
+ "<testsuite name=\"test\" tests=\"0\" failures=\"0\" errors=\"0\" time=\"1\" " +
+ "timestamp=\"ignore\" hostname=\"localhost\"> " +
+ "<properties />" +
+ "</testsuite>";
+ mResultReporter.testRunStarted("test", 1);
+ mResultReporter.testRunEnded(1, Collections.<String, String> emptyMap());
+ assertEquals(expectedOutput, getOutput());
+ }
+
+ /**
+ * A simple test to ensure expected output is generated for test run with a single passed test.
+ */
+ public void testSinglePass() {
+ Map<String, String> emptyMap = Collections.emptyMap();
+ final TestIdentifier testId = new TestIdentifier("FooTest", "testFoo");
+ mResultReporter.testRunStarted("run", 1);
+ mResultReporter.testStarted(testId);
+ mResultReporter.testEnded(testId, emptyMap);
+ mResultReporter.testRunEnded(3, emptyMap);
+ String output = getOutput();
+ // TODO: consider doing xml based compare
+ assertTrue(output.contains("tests=\"1\" failures=\"0\" errors=\"0\""));
+ final String testCaseTag = String.format("<testcase name=\"%s\" classname=\"%s\"",
+ testId.getTestName(), testId.getClassName());
+ assertTrue(output.contains(testCaseTag));
+ }
+
+ /**
+ * A simple test to ensure expected output is generated for test run with a single failed test.
+ */
+ public void testSingleFail() {
+ Map<String, String> emptyMap = Collections.emptyMap();
+ final TestIdentifier testId = new TestIdentifier("FooTest", "testFoo");
+ final String trace = "this is a trace";
+ mResultReporter.testRunStarted("run", 1);
+ mResultReporter.testStarted(testId);
+ mResultReporter.testFailed(TestFailure.FAILURE, testId, trace);
+ mResultReporter.testEnded(testId, emptyMap);
+ mResultReporter.testRunEnded(3, emptyMap);
+ String output = getOutput();
+ // TODO: consider doing xml based compare
+ assertTrue(output.contains("tests=\"1\" failures=\"1\" errors=\"0\""));
+ final String testCaseTag = String.format("<testcase name=\"%s\" classname=\"%s\"",
+ testId.getTestName(), testId.getClassName());
+ assertTrue(output.contains(testCaseTag));
+ final String failureTag = String.format("<failure>%s</failure>", trace);
+ assertTrue(output.contains(failureTag));
+ }
+
+ /**
+ * Gets the output produced, stripping it of extraneous whitespace characters.
+ */
+ private String getOutput() {
+ String output = mOutputStream.toString();
+ // ignore newlines and tabs whitespace
+ output = output.replaceAll("[\\r\\n\\t]", "");
+ // replace two ws chars with one
+ return output.replaceAll(" ", " ");
+ }
+}