Complete 'continue-session' support

Change the CTS xml generator to read in results from a previous XML file.
The XML generator has also been changed to store results in its own
data structure, rather than reusing CollectingTestListener.

Bug 5171576

Change-Id: Ie4d2aaca47bfb01a7203e9227d4506e2f9a2abd2
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
index 3ae1f06..62827d5 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.tradefed.result;
 
+import android.tests.getinfo.DeviceInfoConstants;
+
 import com.android.cts.tradefed.build.CtsBuildHelper;
 import com.android.cts.tradefed.build.CtsBuildProvider;
 import com.android.cts.tradefed.device.DeviceInfoCollector;
@@ -27,18 +29,15 @@
 import com.android.tradefed.build.IFolderBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.TestSummary;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.StreamUtil;
 
 import org.kxml2.io.KXmlSerializer;
 
-import android.tests.getinfo.DeviceInfoConstants;
-
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -57,7 +56,7 @@
  * <p/>
  * Outputs xml in format governed by the cts_result.xsd
  */
-public class CtsXmlResultReporter extends CollectingTestListener {
+public class CtsXmlResultReporter implements ITestInvocationListener {
 
     private static final String LOG_TAG = "CtsXmlResultReporter";
 
@@ -88,14 +87,18 @@
     @Option(name = CtsTest.PLAN_OPTION, description = "the test plan to run.")
     private String mPlanName = "NA";
 
+    // listen in on the continue-session option provided to CtsTest
+    @Option(name = CtsTest.CONTINUE_OPTION, description = "the test result session to continue.")
+    private Integer mContinueSessionId = null;
+
     @Option(name = "quiet-output", description = "Mute display of test results.")
     private boolean mQuietOutput = false;
 
     protected IBuildInfo mBuildInfo;
-
     private String mStartTime;
-
     private String mDeviceSerial;
+    private TestResults mResults = new TestResults();
+    private TestPackageResult mCurrentPkgResult = null;
 
     public void setReportDir(File reportDir) {
         mReportDir = reportDir;
@@ -106,29 +109,51 @@
      */
     @Override
     public void invocationStarted(IBuildInfo buildInfo) {
-        super.invocationStarted(buildInfo);
-        if (mReportDir == null) {
-            if (!(buildInfo instanceof IFolderBuildInfo)) {
-                throw new IllegalArgumentException("build info is not a IFolderBuildInfo");
-            }
-
-            IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo;
-            try {
-                CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir());
-                buildHelper.validateStructure();
-                mReportDir = buildHelper.getResultsDir();
-            } catch (FileNotFoundException e) {
-                throw new IllegalArgumentException("Invalid CTS build", e);
-            }
+        mBuildInfo = buildInfo;
+        if (!(buildInfo instanceof IFolderBuildInfo)) {
+            throw new IllegalArgumentException("build info is not a IFolderBuildInfo");
         }
-        // create a unique directory for saving results, using old cts host convention
-        // TODO: in future, consider using LogFileSaver to create build-specific directories
-        mReportDir = new File(mReportDir, TimeUtil.getResultTimestamp());
-        mReportDir.mkdirs();
-        mStartTime = getTimestamp();
+        IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo;
         mDeviceSerial = buildInfo.getDeviceSerial() == null ? "unknown_device" :
-                buildInfo.getDeviceSerial();
-        logResult("Created result dir %s", mReportDir.getName());
+            buildInfo.getDeviceSerial();
+        if (mContinueSessionId != null) {
+            CLog.d("Continuing session %d", mContinueSessionId);
+            // reuse existing directory
+            TestResultRepo resultRepo = new TestResultRepo(getBuildHelper(ctsBuild).getResultsDir());
+            mResults = resultRepo.getResult(mContinueSessionId);
+            if (mResults == null) {
+                throw new IllegalArgumentException(String.format("Could not find session %d",
+                        mContinueSessionId));
+            }
+            mPlanName = resultRepo.getSummaries().get(mContinueSessionId).getTestPlan();
+            mStartTime = resultRepo.getSummaries().get(mContinueSessionId).getTimestamp();
+            mReportDir = resultRepo.getReportDir(mContinueSessionId);
+        } else {
+            if (mReportDir == null) {
+                mReportDir = getBuildHelper(ctsBuild).getResultsDir();
+            }
+            // create a unique directory for saving results, using old cts host convention
+            // TODO: in future, consider using LogFileSaver to create build-specific directories
+            mReportDir = new File(mReportDir, TimeUtil.getResultTimestamp());
+            mReportDir.mkdirs();
+            mStartTime = getTimestamp();
+            logResult("Created result dir %s", mReportDir.getName());
+        }
+    }
+
+    /**
+     * Helper method to retrieve the {@link CtsBuildHelper}.
+     * @param ctsBuild
+     * @return
+     */
+    CtsBuildHelper getBuildHelper(IFolderBuildInfo ctsBuild) {
+        CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir());
+        try {
+            buildHelper.validateStructure();
+        } catch (FileNotFoundException e) {
+            throw new IllegalArgumentException("Invalid CTS build", e);
+        }
+        return buildHelper;
     }
 
     /**
@@ -149,30 +174,51 @@
         }
     }
 
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public void testRunStarted(String name, int numTests) {
+        if (mCurrentPkgResult != null && !name.equals(mCurrentPkgResult.getAppPackageName())) {
+            // display results from previous run
+            logCompleteRun(mCurrentPkgResult);
+        }
         if (name.equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
             logResult("Collecting device info");
-        } else if (!name.equals(getCurrentRunResults().getName())) {
-            // this is a new run
-            if (getCurrentRunResults().isRunComplete()) {
-                // display results from previous run
-                logCompleteRun(getCurrentRunResults());
-            }
+        } else  if (mCurrentPkgResult == null || !name.equals(
+                mCurrentPkgResult.getAppPackageName())) {
             logResult("-----------------------------------------");
             logResult("Test package %s started", name);
             logResult("-----------------------------------------");
         }
-        super.testRunStarted(name, numTests);
+        mCurrentPkgResult = mResults.getOrCreatePackage(name);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testStarted(TestIdentifier test) {
+        mCurrentPkgResult.insertTest(test);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+        mCurrentPkgResult.reportTestFailure(test, CtsTestStatus.FAIL, trace);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
-        super.testEnded(test, testMetrics);
-        TestRunResult results = getCurrentRunResults();
-        TestResult result = results.getTestResults().get(test);
+        mCurrentPkgResult.reportTestEnded(test);
+        Test result = mCurrentPkgResult.findTest(test);
         String stack = result.getStackTrace() == null ? "" : "\n" + result.getStackTrace();
-        logResult("%s#%s %s %s", test.getClassName(), test.getTestName(), result.getStatus(),
+        logResult("%s#%s %s %s", test.getClassName(), test.getTestName(), result.getResult(),
                 stack);
     }
 
@@ -180,12 +226,19 @@
      * {@inheritDoc}
      */
     @Override
+    public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+        mCurrentPkgResult.populateMetrics(runMetrics);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void invocationEnded(long elapsedTime) {
         // display the results of the last completed run
-        if (getCurrentRunResults().isRunComplete()) {
-            logCompleteRun(getCurrentRunResults());
+        if (mCurrentPkgResult != null) {
+            logCompleteRun(mCurrentPkgResult);
         }
-        super.invocationEnded(elapsedTime);
         createXmlResult(mReportDir, mStartTime, elapsedTime);
         copyFormattingFiles(mReportDir);
         zipResults(mReportDir);
@@ -199,16 +252,15 @@
         }
     }
 
-    private void logCompleteRun(TestRunResult runResults) {
-        if (runResults.getName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
+    private void logCompleteRun(TestPackageResult pkgResult) {
+        if (pkgResult.getAppPackageName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
             logResult("Device info collection complete");
             return;
         }
         logResult("%s package complete: Passed %d, Failed %d, Not Executed %d",
-                runResults.getName(), runResults.getNumPassedTests(),
-                runResults.getNumFailedTests() +
-                runResults.getNumErrorTests(),
-                runResults.getNumIncompleteTests());
+                pkgResult.getAppPackageName(), pkgResult.countTests(CtsTestStatus.PASS),
+                pkgResult.countTests(CtsTestStatus.FAIL),
+                pkgResult.countTests(CtsTestStatus.NOT_EXECUTED));
     }
 
     /**
@@ -230,8 +282,10 @@
             serializeResultsDoc(serializer, startTimestamp, endTime);
             serializer.endDocument();
             String msg = String.format("XML test result file generated at %s. Passed %d, " +
-                    "Failed %d, Not Executed %d", mReportDir.getName(), getNumPassedTests(),
-                    getNumFailedTests() + getNumErrorTests(), getNumIncompleteTests());
+                    "Failed %d, Not Executed %d", mReportDir.getName(),
+                    mResults.countTests(CtsTestStatus.PASS),
+                    mResults.countTests(CtsTestStatus.FAIL),
+                    mResults.countTests(CtsTestStatus.NOT_EXECUTED));
             logResult(msg);
             logResult("Time: %s", TimeUtil.formatElapsedTime(elapsedTime));
         } catch (IOException e) {
@@ -260,7 +314,7 @@
         serializeDeviceInfo(serializer);
         serializeHostInfo(serializer);
         serializeTestSummary(serializer);
-        serializeTestResults(serializer);
+        mResults.serialize(serializer);
         // TODO: not sure why, but the serializer doesn't like this statement
         //serializer.endTag(ns, RESULT_TAG);
     }
@@ -273,15 +327,16 @@
     private void serializeDeviceInfo(KXmlSerializer serializer) throws IOException {
         serializer.startTag(ns, "DeviceInfo");
 
-        TestRunResult deviceInfoResult = findRunResult(DeviceInfoCollector.APP_PACKAGE_NAME);
-        if (deviceInfoResult == null) {
-            Log.w(LOG_TAG, String.format("Could not find device info run %s",
-                    DeviceInfoCollector.APP_PACKAGE_NAME));
+        Map<String, String> deviceInfoMetrics = mResults.getDeviceInfoMetrics();
+        if (deviceInfoMetrics == null || deviceInfoMetrics.isEmpty()) {
+            // this might be expected, if device info collection was turned off
+            CLog.d("Could not find device info");
             return;
         }
+
         // Extract metrics that need extra handling, and then dump the remainder into BuildInfo
         Map<String, String> metricsCopy = new HashMap<String, String>(
-                deviceInfoResult.getRunMetrics());
+                deviceInfoMetrics);
         serializer.startTag(ns, "Screen");
         String screenWidth = metricsCopy.remove(DeviceInfoConstants.SCREEN_WIDTH);
         String screenHeight = metricsCopy.remove(DeviceInfoConstants.SCREEN_HEIGHT);
@@ -390,21 +445,6 @@
     }
 
     /**
-     * Finds the {@link TestRunResult} with the given name.
-     *
-     * @param runName
-     * @return the {@link TestRunResult}
-     */
-    private TestRunResult findRunResult(String runName) {
-        for (TestRunResult runResult : getRunResults()) {
-            if (runResult.getName().equals(runName)) {
-                return runResult;
-            }
-        }
-        return null;
-    }
-
-    /**
      * Output the host info XML.
      *
      * @param serializer
@@ -451,69 +491,18 @@
      */
     private void serializeTestSummary(KXmlSerializer serializer) throws IOException {
         serializer.startTag(ns, SUMMARY_TAG);
-        serializer.attribute(ns, FAILED_ATTR, Integer.toString(getNumErrorTests() +
-                getNumFailedTests()));
-        serializer.attribute(ns, NOT_EXECUTED_ATTR,  Integer.toString(getNumIncompleteTests()));
+        serializer.attribute(ns, FAILED_ATTR, Integer.toString(mResults.countTests(
+                CtsTestStatus.FAIL)));
+        serializer.attribute(ns, NOT_EXECUTED_ATTR,  Integer.toString(mResults.countTests(
+                CtsTestStatus.NOT_EXECUTED)));
         // ignore timeouts - these are reported as errors
         serializer.attribute(ns, TIMEOUT_ATTR, "0");
-        serializer.attribute(ns, PASS_ATTR, Integer.toString(getNumPassedTests()));
+        serializer.attribute(ns, PASS_ATTR, Integer.toString(mResults.countTests(
+                CtsTestStatus.PASS)));
         serializer.endTag(ns, SUMMARY_TAG);
     }
 
     /**
-     * Output the detailed test results XML.
-     *
-     * @param serializer
-     * @throws IOException
-     */
-    private void serializeTestResults(KXmlSerializer serializer) throws IOException {
-        for (TestRunResult runResult : getRunResults()) {
-            serializeTestRunResult(serializer, runResult);
-        }
-    }
-
-    /**
-     * Output the XML for one test run aka test package.
-     *
-     * @param serializer
-     * @param runResult the {@link TestRunResult}
-     * @throws IOException
-     */
-    private void serializeTestRunResult(KXmlSerializer serializer, TestRunResult runResult)
-            throws IOException {
-        if (runResult.getName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
-            // ignore run results for the info collecting packages
-            return;
-        }
-        TestPackageResult packageResult = new TestPackageResult();
-        packageResult.setName(getMetric(runResult, CtsTest.PACKAGE_NAME_METRIC));
-        packageResult.setAppPackageName(runResult.getName());
-        packageResult.setDigest(getMetric(runResult, CtsTest.PACKAGE_DIGEST_METRIC));
-        // organize the tests into data structures that mirror the expected xml output.
-        for (Map.Entry<TestIdentifier, TestResult> testEntry : runResult.getTestResults()
-                .entrySet()) {
-            packageResult.insertTest(testEntry.getKey(), testEntry.getValue());
-        }
-        // dump the results
-        packageResult.serialize(serializer);
-    }
-
-    /**
-     * Helper method to retrieve the metric value with given name, or blank if not found
-     *
-     * @param runResult
-     * @param string
-     * @return
-     */
-    private String getMetric(TestRunResult runResult, String keyName) {
-        String value = runResult.getRunMetrics().get(keyName);
-        if (value == null) {
-            return "";
-        }
-        return value;
-    }
-
-    /**
      * Creates the output stream to use for test results. Exposed for mocking.
      * @param mReportPath
      */
@@ -570,4 +559,36 @@
     String getTimestamp() {
         return TimeUtil.getTimestamp();
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testRunFailed(String errorMessage) {
+        // ignore
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testRunStopped(long elapsedTime) {
+        // ignore
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void invocationFailed(Throwable cause) {
+        // ignore
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public TestSummary getSummary() {
+        return null;
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/ITestResultRepo.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/ITestResultRepo.java
index bed2719..d672f41 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/ITestResultRepo.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/ITestResultRepo.java
@@ -15,6 +15,7 @@
  */
 package com.android.cts.tradefed.result;
 
+import java.io.File;
 import java.util.List;
 
 /**
@@ -37,4 +38,11 @@
      */
     public TestResults getResult(int sessionId);
 
+    /**
+     * Get the report directory for given result
+     * @param sessionId
+     * @return
+     */
+    public File getReportDir(int sessionId);
+
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java
index 184b8ab..917ccbe 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java
@@ -15,9 +15,7 @@
  */
 package com.android.cts.tradefed.result;
 
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
 
 import org.kxml2.io.KXmlSerializer;
 import org.xmlpull.v1.XmlPullParser;
@@ -40,7 +38,7 @@
     private static final String STACK_TAG = "StackTrace";
 
     private String mName;
-    private String mResult;
+    private CtsTestStatus mResult;
     private String mStartTime;
     private String mEndTime;
     private String mMessage;
@@ -56,18 +54,12 @@
      * Create a {@link Test} from a {@link TestResult}.
      *
      * @param name
-     * @param result
      */
-    public Test(String name, TestResult result) {
+    public Test(String name) {
         mName = name;
-        mResult = convertStatus(result.getStatus());
-        mStartTime = TimeUtil.getTimestamp(result.getStartTime());
-        mEndTime = TimeUtil.getTimestamp(result.getEndTime());
-        if (result.getStackTrace() != null) {
-            String sanitizedStack = sanitizeStackTrace(result.getStackTrace());
-            mMessage = getFailureMessageFromStackTrace(sanitizedStack);
-            mStackTrace = sanitizedStack;
-        }
+        mResult = CtsTestStatus.NOT_EXECUTED;
+        mStartTime = TimeUtil.getTimestamp();
+        updateEndTime();
     }
 
     /**
@@ -84,7 +76,7 @@
         return mName;
     }
 
-    public String getResult() {
+    public CtsTestStatus getResult() {
         return mResult;
     }
 
@@ -104,6 +96,20 @@
         return mStackTrace;
     }
 
+    public void setStackTrace(String stackTrace) {
+
+        mStackTrace = sanitizeStackTrace(stackTrace);
+        mMessage = getFailureMessageFromStackTrace(mStackTrace);
+    }
+
+    public void updateEndTime() {
+        mEndTime = TimeUtil.getTimestamp();
+    }
+
+    public void setResultStatus(CtsTestStatus status) {
+        mResult = status;
+    }
+
     /**
      * Serialize this object and all its contents to XML.
      *
@@ -114,7 +120,7 @@
             throws IOException {
         serializer.startTag(CtsXmlResultReporter.ns, TAG);
         serializer.attribute(CtsXmlResultReporter.ns, NAME_ATTR, getName());
-        serializer.attribute(CtsXmlResultReporter.ns, RESULT_ATTR, mResult);
+        serializer.attribute(CtsXmlResultReporter.ns, RESULT_ATTR, mResult.getValue());
         serializer.attribute(CtsXmlResultReporter.ns, STARTTIME_ATTR, mStartTime);
         serializer.attribute(CtsXmlResultReporter.ns, ENDTIME_ATTR, mEndTime);
 
@@ -132,27 +138,6 @@
     }
 
     /**
-     * Convert a {@link TestStatus} to the result text to output in XML
-     *
-     * @param status the {@link TestStatus}
-     * @return
-     */
-    private String convertStatus(TestStatus status) {
-        switch (status) {
-            case ERROR:
-                return CtsTestStatus.FAIL.getValue();
-            case FAILURE:
-                return CtsTestStatus.FAIL.getValue();
-            case PASSED:
-                return CtsTestStatus.PASS.getValue();
-            case INCOMPLETE:
-                return CtsTestStatus.NOT_EXECUTED.getValue();
-        }
-        CLog.w("Unrecognized status %s", status);
-        return CtsTestStatus.FAIL.getValue();
-    }
-
-    /**
      * Strip out any invalid XML characters that might cause the report to be unviewable.
      * http://www.w3.org/TR/REC-xml/#dt-character
      */
@@ -187,7 +172,7 @@
                     "invalid XML: Expected %s tag but received %s", TAG, parser.getName()));
         }
         setName(getAttribute(parser, NAME_ATTR));
-        mResult = getAttribute(parser, RESULT_ATTR);
+        mResult = CtsTestStatus.getStatus(getAttribute(parser, RESULT_ATTR));
         mStartTime = getAttribute(parser, STARTTIME_ATTR);
         mEndTime = getAttribute(parser, ENDTIME_ATTR);
 
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java
index 138fe7c..fdb8b3b 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java
@@ -16,7 +16,6 @@
 package com.android.cts.tradefed.result;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.result.TestResult;
 import com.android.tradefed.util.ArrayUtil;
 
 import org.kxml2.io.KXmlSerializer;
@@ -67,24 +66,17 @@
     }
 
     /**
-     * Inserts given test result
-     *
      * @param testName
-     * @param testResult
+     * @param insertIfMissing
+     * @return
      */
-    public void insertTest(String testName, TestResult testResult) {
-        Test t = new Test(testName, testResult);
-        insertTest(t);
-    }
-
-    /**
-     * Inserts given test result
-     *
-     * @param testName
-     * @param testResult
-     */
-    private void insertTest(Test test) {
-        mChildTestMap.put(test.getName(), test);
+    public Test findTest(String testName, boolean insertIfMissing) {
+        Test t = mChildTestMap.get(testName);
+        if (t == null && insertIfMissing) {
+            t = new Test(testName);
+            mChildTestMap.put(t.getName(), t);
+        }
+        return t;
     }
 
     /**
@@ -122,7 +114,7 @@
             if (eventType == XmlPullParser.START_TAG && parser.getName().equals(Test.TAG)) {
                 Test test = new Test();
                 test.parse(parser);
-                insertTest(test);
+                mChildTestMap.put(test.getName(), test);
             } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG)) {
                 return;
             }
@@ -145,7 +137,7 @@
         }
         String fullClassName = ArrayUtil.join(".", parentSuiteNames);
         for (Test test : mChildTestMap.values()) {
-            if (resultFilter.getValue().equals(test.getResult())) {
+            if (resultFilter.equals(test.getResult())) {
                 tests.add(new TestIdentifier(fullClassName, test.getName()));
             }
         }
@@ -153,4 +145,20 @@
             parentSuiteNames.removeLast();
         }
     }
+
+    /**
+     * Count the number of tests in this {@link TestCase} with given status.
+     *
+     * @param status
+     * @return the test count
+     */
+    public int countTests(CtsTestStatus status) {
+        int total = 0;
+        for (Test test : mChildTestMap.values()) {
+            if (test.getResult().equals(status)) {
+                total++;
+            }
+        }
+        return total;
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java
index 8480803..7d5db75 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java
@@ -15,9 +15,9 @@
  */
 package com.android.cts.tradefed.result;
 
+import com.android.cts.tradefed.testtype.CtsTest;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.TestResult;
 
 import org.kxml2.io.KXmlSerializer;
 import org.xmlpull.v1.XmlPullParser;
@@ -27,8 +27,10 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Data structure for a CTS test package result.
@@ -48,6 +50,8 @@
     private String mName;
     private String mDigest;
 
+    private Map<String, String> mMetrics = new HashMap<String, String>();
+
     private TestSuite mSuiteRoot = new TestSuite(null);
 
     public void setAppPackageName(String appPackageName) {
@@ -87,17 +91,31 @@
      * @param testId
      * @param testResult
      */
-    public void insertTest(TestIdentifier testId, TestResult testResult) {
+    public Test insertTest(TestIdentifier testId) {
+        return findTest(testId, true);
+    }
+
+    private Test findTest(TestIdentifier testId, boolean insertIfMissing) {
         List<String> classNameSegments = new LinkedList<String>();
         Collections.addAll(classNameSegments, testId.getClassName().split("\\."));
         if (classNameSegments.size() <= 0) {
             CLog.e("Unrecognized package name format for test class '%s'",
                     testId.getClassName());
-        } else {
-            String testCaseName = classNameSegments.remove(classNameSegments.size()-1);
-            mSuiteRoot.insertTest(classNameSegments, testCaseName, testId.getTestName(),
-                    testResult);
+            // should never happen
+            classNameSegments.add("UnknownTestClass");
         }
+            String testCaseName = classNameSegments.remove(classNameSegments.size()-1);
+            return mSuiteRoot.findTest(classNameSegments, testCaseName, testId.getTestName(), insertIfMissing);
+    }
+
+
+    /**
+     * Find the test result for given {@link TestIdentifier}.
+     * @param testId
+     * @return the {@link Test} or <code>null</code>
+     */
+    public Test findTest(TestIdentifier testId) {
+        return findTest(testId, false);
     }
 
     /**
@@ -108,10 +126,10 @@
      */
     public void serialize(KXmlSerializer serializer) throws IOException {
         serializer.startTag(ns, TAG);
-        serializer.attribute(ns, NAME_ATTR, mName);
-        serializer.attribute(ns, APP_PACKAGE_NAME_ATTR, mAppPackageName);
-        serializer.attribute(ns, DIGEST_ATTR, getDigest());
-        if (mName.equals(SIGNATURE_TEST_PKG)) {
+        serializeAttribute(serializer, NAME_ATTR, mName);
+        serializeAttribute(serializer, APP_PACKAGE_NAME_ATTR, mAppPackageName);
+        serializeAttribute(serializer, DIGEST_ATTR, getDigest());
+        if (SIGNATURE_TEST_PKG.equals(mName)) {
             serializer.attribute(ns, "signatureCheck", "true");
         }
         mSuiteRoot.serialize(serializer);
@@ -119,6 +137,21 @@
     }
 
     /**
+     * Helper method to serialize attributes.
+     * Can handle null values. Useful for cases where test package has not been fully populated
+     * such as when unit testing.
+     *
+     * @param attrName
+     * @param attrValue
+     * @throws IOException
+     */
+    private void serializeAttribute(KXmlSerializer serializer, String attrName, String attrValue)
+            throws IOException {
+        attrValue = attrValue == null ? "" : attrValue;
+        serializer.attribute(ns, attrName, attrValue);
+    }
+
+    /**
      * Populates this class with package result data parsed from XML.
      *
      * @param parser the {@link XmlPullParser}. Expected to be pointing at start
@@ -159,4 +192,63 @@
         mSuiteRoot.addTestsWithStatus(tests, suiteNames, resultFilter);
         return tests;
     }
+
+    /**
+     * Populate values in this package result from run metrics
+     * @param runResult
+     */
+    public void populateMetrics(Map<String, String> metrics) {
+        String name = metrics.get(CtsTest.PACKAGE_NAME_METRIC);
+        if (name != null) {
+            setName(name);
+        }
+        String digest = metrics.get(CtsTest.PACKAGE_DIGEST_METRIC);
+        if (digest != null) {
+            setDigest(digest);
+        }
+        mMetrics.putAll(metrics);
+    }
+
+    /**
+     * Report the given test as a failure.
+     *
+     * @param test
+     * @param status
+     * @param trace
+     */
+    public void reportTestFailure(TestIdentifier test, CtsTestStatus status, String trace) {
+        Test result = findTest(test);
+        result.setResultStatus(status);
+        result.setStackTrace(trace);
+    }
+
+    /**
+     * Report that the given test has completed.
+     *
+     * @param test
+     */
+    public void reportTestEnded(TestIdentifier test) {
+        Test result = findTest(test);
+        if (!result.getResult().equals(CtsTestStatus.FAIL)) {
+            result.setResultStatus(CtsTestStatus.PASS);
+        }
+        result.updateEndTime();
+    }
+
+    /**
+     * Return the number of tests with given status
+     *
+     * @param status
+     * @return the total number of tests with given status
+     */
+    public int countTests(CtsTestStatus status) {
+        return mSuiteRoot.countTests(status);
+    }
+
+    /**
+     * @return
+     */
+    public Map<String, String> getMetrics() {
+        return mMetrics;
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResultRepo.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResultRepo.java
index 9178e91..fd42892 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResultRepo.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResultRepo.java
@@ -54,12 +54,17 @@
                 File resultFile = new File(resultList.get(i),
                         CtsXmlResultReporter.TEST_RESULT_FILE_NAME);
                 if (resultFile.exists()) {
-                    mResultDirs.add(i, resultList.get(i));
+                    mResultDirs.add(resultList.get(i));
                 }
             }
         }
     }
 
+    @Override
+    public File getReportDir(int sessionId) {
+        return mResultDirs.get(sessionId);
+    }
+
     private ITestSummary parseSummary(int id, File resultDir) {
         TestSummaryXml result = new TestSummaryXml(id, resultDir.getName());
         try {
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
index 6900e58..a874227 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
@@ -15,12 +15,21 @@
  */
 package com.android.cts.tradefed.result;
 
+import com.android.cts.tradefed.device.DeviceInfoCollector;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import org.kxml2.io.KXmlSerializer;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Data structure for the detailed CTS test results.
@@ -29,7 +38,8 @@
  */
 class TestResults extends AbstractXmlPullParser {
 
-    private List<TestPackageResult> mPackages = new ArrayList<TestPackageResult>();
+    private Map<String, TestPackageResult> mPackageMap = new LinkedHashMap<String, TestPackageResult>();
+    private TestPackageResult mDeviceInfoPkg = new TestPackageResult();
 
     /**
      * {@inheritDoc}
@@ -42,16 +52,91 @@
                     TestPackageResult.TAG)) {
                 TestPackageResult pkg = new TestPackageResult();
                 pkg.parse(parser);
-                mPackages.add(pkg);
+                if (pkg.getAppPackageName() != null) {
+                    mPackageMap.put(pkg.getAppPackageName(), pkg);
+                } else {
+                    CLog.w("Found package with no app package name");
+                }
             }
             eventType = parser.next();
         }
     }
 
     /**
-     * @return the list of parsed {@link TestPackageResult}.
+     * @return the list of {@link TestPackageResult}.
      */
-    public List<TestPackageResult> getPackages() {
-        return mPackages;
+    public Collection<TestPackageResult> getPackages() {
+        return mPackageMap.values();
+    }
+
+    /**
+     * Count the number of tests with given status
+     * @param pass
+     * @return
+     */
+    public int countTests(CtsTestStatus status) {
+        int total = 0;
+        for (TestPackageResult result : mPackageMap.values()) {
+            total += result.countTests(status);
+        }
+        return total;
+    }
+
+    /**
+     * @return
+     */
+    public Map<String, String> getDeviceInfoMetrics() {
+        return mDeviceInfoPkg.getMetrics();
+    }
+
+    /**
+     * @param mCurrentPkgResult
+     */
+    public void addPackageResult(TestPackageResult pkgResult) {
+        mPackageMap.put(pkgResult.getName(), pkgResult);
+    }
+
+    /**
+     * @param serializer
+     * @throws IOException
+     */
+    public void serialize(KXmlSerializer serializer) throws IOException {
+        // sort before serializing
+        List<TestPackageResult> pkgs = new ArrayList<TestPackageResult>(mPackageMap.values());
+        Collections.sort(pkgs, new PkgComparator());
+        for (TestPackageResult r : pkgs) {
+            r.serialize(serializer);
+        }
+    }
+
+    private static class PkgComparator implements Comparator<TestPackageResult> {
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int compare(TestPackageResult o1, TestPackageResult o2) {
+            return o1.getAppPackageName().compareTo(o2.getAppPackageName());
+        }
+
+    }
+
+    /**
+     * Return existing package with given app package name. If not found, create a new one.
+     * @param name
+     * @return
+     */
+    public TestPackageResult getOrCreatePackage(String appPackageName) {
+        if (appPackageName.equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
+            mDeviceInfoPkg.setAppPackageName(DeviceInfoCollector.APP_PACKAGE_NAME);
+            return mDeviceInfoPkg ;
+        }
+        TestPackageResult pkgResult = mPackageMap.get(appPackageName);
+        if (pkgResult == null) {
+            pkgResult = new TestPackageResult();
+            pkgResult.setAppPackageName(appPackageName);
+            mPackageMap.put(appPackageName, pkgResult);
+        }
+        return pkgResult;
     }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java
index 426ff2d..df1dceb 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java
@@ -74,16 +74,16 @@
      * @param testName the test method name
      * @param testResult the {@link TestResult}
      */
-    public void insertTest(List<String> suiteNames, String testClassName, String testName,
-            TestResult testResult) {
+    public Test findTest(List<String> suiteNames, String testClassName, String testName,
+            boolean insertIfMissing) {
         if (suiteNames.size() <= 0) {
             // no more package segments
             TestCase testCase = getTestCase(testClassName);
-            testCase.insertTest(testName, testResult);
+            return testCase.findTest(testName, insertIfMissing);
         } else {
             String rootName = suiteNames.remove(0);
             TestSuite suite = getTestSuite(rootName);
-            suite.insertTest(suiteNames, testClassName, testName, testResult);
+            return suite.findTest(suiteNames, testClassName, testName, insertIfMissing);
         }
     }
 
@@ -221,4 +221,21 @@
             parentSuiteNames.removeLast();
         }
     }
+
+    /**
+     * Count the number of tests in this {@link TestSuite} with given status.
+     *
+     * @param status
+     * @return the test count
+     */
+    public int countTests(CtsTestStatus status) {
+        int total = 0;
+        for (TestSuite suite : mChildSuiteMap.values()) {
+            total += suite.countTests(status);
+        }
+        for (TestCase testCase : mChildTestCaseMap.values()) {
+            total += testCase.countTests(status);
+        }
+        return total;
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ResultFilter.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ResultFilter.java
index 68bb62e..e8fdad81 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ResultFilter.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ResultFilter.java
@@ -130,14 +130,16 @@
      */
     public void reportUnexecutedTests() {
         for (Map.Entry<String, Collection<TestIdentifier>> entry : mRemainingTestsMap.entrySet()) {
-            super.testRunStarted(entry.getKey(), entry.getValue().size());
-            for (TestIdentifier test : entry.getValue()) {
-                // an unexecuted test is currently reported as a 'testStarted' event without a
-                // 'testEnded'. TODO: consider adding an explict API for reporting an unexecuted
-                // test
-                super.testStarted(test);
+            if (!entry.getValue().isEmpty()) {
+                super.testRunStarted(entry.getKey(), entry.getValue().size());
+                for (TestIdentifier test : entry.getValue()) {
+                    // an unexecuted test is currently reported as a 'testStarted' event without a
+                    // 'testEnded'. TODO: consider adding an explict API for reporting an unexecuted
+                    // test
+                    super.testStarted(test);
+                }
+                super.testRunEnded(0, new HashMap<String,String>());
             }
-            super.testRunEnded(0, new HashMap<String,String>());
         }
     }
 }
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
index 4e6b914..7566fce 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
@@ -15,13 +15,17 @@
  */
 package com.android.cts.tradefed.result;
 
-import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
-import com.android.tradefed.build.BuildInfo;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.build.IFolderBuildInfo;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.XmlResultReporter;
 import com.android.tradefed.util.FileUtil;
 
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
@@ -29,8 +33,6 @@
 import java.util.Collections;
 import java.util.Map;
 
-import junit.framework.TestCase;
-
 /**
  * Unit tests for {@link XmlResultReporter}.
  */
@@ -39,6 +41,7 @@
     private CtsXmlResultReporter mResultReporter;
     private ByteArrayOutputStream mOutputStream;
     private File mReportDir;
+    private IFolderBuildInfo mMockBuild;
 
     /**
      * {@inheritDoc}
@@ -62,6 +65,7 @@
         // TODO: use mock file dir instead
         mReportDir = FileUtil.createTempDir("foo");
         mResultReporter.setReportDir(mReportDir);
+        mMockBuild = EasyMock.createNiceMock(IFolderBuildInfo.class);
     }
 
     @Override
@@ -83,7 +87,7 @@
         final String expectedSummaryOutput =
             "<Summary failed=\"0\" notExecuted=\"0\" timeout=\"0\" pass=\"0\" />";
         final String expectedEndTag = "</TestResult>";
-        mResultReporter.invocationStarted(new BuildInfo("1", "test", "test"));
+        mResultReporter.invocationStarted(mMockBuild);
         mResultReporter.invocationEnded(1);
         String actualOutput = getOutput();
         assertTrue(actualOutput.startsWith(expectedHeaderOutput));
@@ -101,7 +105,7 @@
     public void testSinglePass() {
         Map<String, String> emptyMap = Collections.emptyMap();
         final TestIdentifier testId = new TestIdentifier("com.foo.FooTest", "testFoo");
-        mResultReporter.invocationStarted(new BuildInfo());
+        mResultReporter.invocationStarted(mMockBuild);
         mResultReporter.testRunStarted("run", 1);
         mResultReporter.testStarted(testId);
         mResultReporter.testEnded(testId, emptyMap);
@@ -128,7 +132,7 @@
         Map<String, String> emptyMap = Collections.emptyMap();
         final TestIdentifier testId = new TestIdentifier("FooTest", "testFoo");
         final String trace = "this is a trace\nmore trace";
-        mResultReporter.invocationStarted(new BuildInfo());
+        mResultReporter.invocationStarted(mMockBuild);
         mResultReporter.testRunStarted("run", 1);
         mResultReporter.testStarted(testId);
         mResultReporter.testFailed(TestFailure.FAILURE, testId, trace);
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestPackageResultTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestPackageResultTest.java
index b3c903b..df80dbb 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestPackageResultTest.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestPackageResultTest.java
@@ -16,8 +16,6 @@
 package com.android.cts.tradefed.result;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
 
 import junit.framework.TestCase;
 
@@ -34,14 +32,11 @@
     public void testGetTestsWithStatus() {
         TestPackageResult pkgResult = new TestPackageResult();
         TestIdentifier excludedTest = new TestIdentifier("com.example.ExampleTest", "testPass");
-        TestResult passed = new TestResult();
-        passed.setStatus(TestStatus.PASSED);
-        pkgResult.insertTest(excludedTest, passed);
+        pkgResult.insertTest(excludedTest);
+        pkgResult.reportTestEnded(excludedTest);
         TestIdentifier includedTest = new TestIdentifier("com.example.ExampleTest",
                 "testNotExecuted");
-        TestResult notExecuted = new TestResult();
-        notExecuted.setStatus(TestStatus.INCOMPLETE);
-        pkgResult.insertTest(includedTest, notExecuted);
+        pkgResult.insertTest(includedTest);
         Collection<TestIdentifier> tests =  pkgResult.getTestsWithStatus(
                 CtsTestStatus.NOT_EXECUTED);
         assertEquals(1, tests.size());
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestResultsTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestResultsTest.java
index a0360cd..75f545e 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestResultsTest.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestResultsTest.java
@@ -64,7 +64,7 @@
         TestResults parser = new TestResults();
         parser.parse(new StringReader(TEST_PACKAGE_FULL));
         assertEquals(1, parser.getPackages().size());
-        TestPackageResult pkg = parser.getPackages().get(0);
+        TestPackageResult pkg = parser.getPackages().iterator().next();
         assertEquals("pkgName", pkg.getName());
         assertEquals("appPkgName", pkg.getAppPackageName());
         assertEquals("digValue", pkg.getDigest());
@@ -91,7 +91,7 @@
         TestResults parser = new TestResults();
         parser.parse(new StringReader(TEST_FULL));
         assertEquals(1, parser.getPackages().size());
-        TestPackageResult pkg = parser.getPackages().get(0);
+        TestPackageResult pkg = parser.getPackages().iterator().next();
         TestSuite comSuite = pkg.getTestSuites().iterator().next();
         assertEquals("com", comSuite.getName());
         TestSuite exampleSuite = comSuite.getTestSuites().iterator().next();