Make CtsTest resumable.

Change-Id: I5552ba3a5633a193a4cdc1b54918de985e548912
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java
index 9b14dd3..a1b29b9 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java
@@ -19,12 +19,14 @@
 import com.android.cts.tradefed.device.DeviceInfoCollector;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.IResumableTest;
 import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
 
 import java.io.BufferedInputStream;
@@ -34,7 +36,9 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Set;
 
 import junit.framework.Test;
@@ -44,7 +48,7 @@
  * <p/>
  * Supports running all the tests contained in a CTS plan, or individual test packages.
  */
-public class CtsTest implements IDeviceTest, IRemoteTest {
+public class CtsTest implements IDeviceTest, IResumableTest {
 
     private static final String LOG_TAG = "PlanTest";
 
@@ -85,6 +89,33 @@
         "flag to control whether to collect info from device. Default true")
     private boolean mCollectDeviceInfo = true;
 
+    @Option(name = "resume", description =
+        "flag to attempt to automatically resume aborted test run on another connected device. " +
+        "Default false.")
+    private boolean mResume = false;
+
+    /** data structure for a {@link IRemoteTest} and its known tests */
+    private class KnownTests {
+        private final IRemoteTest mTestForPackage;
+        private final Collection<TestIdentifier> mKnownTests;
+
+        KnownTests(IRemoteTest testForPackage, Collection<TestIdentifier> knownTests) {
+            mTestForPackage = testForPackage;
+            mKnownTests = knownTests;
+        }
+
+        IRemoteTest getTestForPackage() {
+            return mTestForPackage;
+        }
+
+        Collection<TestIdentifier> getKnownTests() {
+            return mKnownTests;
+        }
+    }
+
+    /** list of remaining tests to execute */
+    private List<KnownTests> mRemainingTests = null;
+
     /**
      * {@inheritDoc}
      */
@@ -174,49 +205,107 @@
     /**
      * {@inheritDoc}
      */
+    @Override
+    public boolean isResumable() {
+        return mResume;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
         checkFields();
 
-        Log.i(LOG_TAG, String.format("Executing CTS test plan %s", mPlanName));
+        if (mRemainingTests == null) {
+            mRemainingTests = buildTestsToRun();
+        }
+        // always collect the device info, even for resumed runs, since test will likely be running
+        // on a different device
+        collectDeviceInfo(getDevice(), mTestCaseDir, listener);
 
+        while (!mRemainingTests.isEmpty()) {
+            KnownTests testPair = mRemainingTests.get(0);
+            IRemoteTest test = testPair.getTestForPackage();
+            if (test instanceof IDeviceTest) {
+                ((IDeviceTest)test).setDevice(getDevice());
+            }
+            ResultFilter filter = new ResultFilter(listener, testPair.getKnownTests());
+            test.run(filter);
+            mRemainingTests.remove(0);
+        }
+    }
+
+    /**
+     * Build the list of test packages to run
+     *
+     * @return
+     */
+    private List<KnownTests> buildTestsToRun() {
+        List<KnownTests> testList = new LinkedList<KnownTests>();
         try {
             ITestCaseRepo testRepo = createTestCaseRepo();
-            Collection<String> testUris = getTestsToRun(testRepo);
-            collectDeviceInfo(getDevice(), mTestCaseDir, listener);
+            Collection<String> testUris = getTestPackageUrisToRun(testRepo);
+
             for (String testUri : testUris) {
                 ITestPackageDef testPackage = testRepo.getTestPackage(testUri);
-                if (testPackage != null) {
-                    runTest(listener, testPackage);
-                } else {
-                    Log.e(LOG_TAG, String.format("Could not find test package uri %s", testUri));
-                }
+                addTestPackage(testList, testUri, testPackage);
+            }
+            if (testList.isEmpty()) {
+                Log.logAndDisplay(LogLevel.WARN, LOG_TAG, "No tests to run");
             }
         } catch (FileNotFoundException e) {
             throw new IllegalArgumentException("failed to find CTS plan file", e);
         } catch (ParseException e) {
             throw new IllegalArgumentException("failed to parse CTS plan file", e);
         }
+        return testList;
     }
 
     /**
-     * Return the list of test uris to run
+     * Adds a test package to the list of packages to test
      *
-     * @return the list of test uris to run
+     * @param testList
+     * @param testUri
+     * @param testPackage
+     */
+    private void addTestPackage(List<KnownTests> testList, String testUri,
+            ITestPackageDef testPackage) {
+        if (testPackage != null) {
+            IRemoteTest testForPackage = testPackage.createTest(mTestCaseDir, mClassName,
+                    mMethodName);
+            if (testForPackage != null) {
+                Collection<TestIdentifier> knownTests = testPackage.getTests();
+                testList.add(new KnownTests(testForPackage, knownTests));
+            }
+        } else {
+            Log.e(LOG_TAG, String.format("Could not find test package uri %s", testUri));
+        }
+    }
+
+    /**
+     * Return the list of test package uris to run
+     *
+     * @return the list of test package uris to run
      * @throws ParseException
      * @throws FileNotFoundException
      */
-    private Collection<String> getTestsToRun(ITestCaseRepo testRepo) throws ParseException,
-            FileNotFoundException {
-        Set<String> testUris = new HashSet<String>();
+    private Collection<String> getTestPackageUrisToRun(ITestCaseRepo testRepo)
+            throws ParseException, FileNotFoundException {
+        // use LinkedHashSet to have predictable iteration order
+        Set<String> testUris = new LinkedHashSet<String>();
         if (mPlanName != null) {
+            Log.i(LOG_TAG, String.format("Executing CTS test plan %s", mPlanName));
             String ctsPlanRelativePath = String.format("%s.xml", mPlanName);
             File ctsPlanFile = new File(mTestPlanDir, ctsPlanRelativePath);
             IPlanXmlParser parser = createXmlParser();
             parser.parse(createXmlStream(ctsPlanFile));
             testUris.addAll(parser.getTestUris());
         } else if (mPackageNames.size() > 0){
+            Log.i(LOG_TAG, String.format("Executing CTS test packages %s", mPackageNames));
             testUris.addAll(mPackageNames);
         } else if (mClassName != null) {
+            Log.i(LOG_TAG, String.format("Executing CTS test class %s", mClassName));
             // try to find package to run from class name
             String packageUri = testRepo.findPackageForTest(mClassName);
             if (packageUri != null) {
@@ -233,6 +322,49 @@
         return testUris;
     }
 
+    /**
+     * Runs the device info collector instrumentation on device, and forwards it to test listeners
+     * as run metrics.
+     * <p/>
+     * Exposed so unit tests can mock.
+     *
+     * @param listeners
+     * @throws DeviceNotAvailableException
+     */
+    void collectDeviceInfo(ITestDevice device, File testApkDir, ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+        if (mCollectDeviceInfo) {
+            DeviceInfoCollector.collectDeviceInfo(device, testApkDir, listener);
+        }
+    }
+
+    /**
+     * Factory method for creating a {@link ITestCaseRepo}.
+     * <p/>
+     * Exposed for unit testing
+     */
+    ITestCaseRepo createTestCaseRepo() {
+        return new TestCaseRepo(mTestCaseDir);
+    }
+
+    /**
+     * Factory method for creating a {@link PlanXmlParser}.
+     * <p/>
+     * Exposed for unit testing
+     */
+    IPlanXmlParser createXmlParser() {
+        return new PlanXmlParser();
+    }
+
+    /**
+     * Factory method for creating a {@link InputStream} from a plan xml file.
+     * <p/>
+     * Exposed for unit testing
+     */
+    InputStream createXmlStream(File xmlFile) throws FileNotFoundException {
+        return new BufferedInputStream(new FileInputStream(xmlFile));
+    }
+
     private void checkFields() {
         // for simplicity of command line usage, make --plan, --package, and --class mutually
         // exclusive
@@ -278,66 +410,4 @@
         }
         return currentVal;
     }
-
-    /**
-     * Runs the test.
-     *
-     * @param listeners
-     * @param testPackage
-     * @throws DeviceNotAvailableException
-     */
-    private void runTest(ITestInvocationListener listener, ITestPackageDef testPackage)
-            throws DeviceNotAvailableException {
-        IRemoteTest test = testPackage.createTest(mTestCaseDir, mClassName, mMethodName);
-        if (test != null) {
-            if (test instanceof IDeviceTest) {
-                ((IDeviceTest)test).setDevice(getDevice());
-            }
-            ResultFilter filter = new ResultFilter(listener, testPackage);
-            test.run(filter);
-        }
-    }
-
-    /**
-     * Runs the device info collector instrumentation on device, and forwards it to test listeners
-     * as run metrics.
-     * <p/>
-     * Exposed so unit tests can mock.
-     *
-     * @param listeners
-     * @throws DeviceNotAvailableException
-     */
-    void collectDeviceInfo(ITestDevice device, File testApkDir, ITestInvocationListener listener)
-            throws DeviceNotAvailableException {
-        if (mCollectDeviceInfo) {
-            DeviceInfoCollector.collectDeviceInfo(device, testApkDir, listener);
-        }
-    }
-
-    /**
-     * Factory method for creating a {@link ITestCaseRepo}.
-     * <p/>
-     * Exposed for unit testing
-     */
-    ITestCaseRepo createTestCaseRepo() {
-        return new TestCaseRepo(mTestCaseDir);
-    }
-
-    /**
-     * Factory method for creating a {@link PlanXmlParser}.
-     * <p/>
-     * Exposed for unit testing
-     */
-    IPlanXmlParser createXmlParser() {
-        return new PlanXmlParser();
-    }
-
-    /**
-     * Factory method for creating a {@link InputStream} from a plan xml file.
-     * <p/>
-     * Exposed for unit testing
-     */
-    InputStream createXmlStream(File xmlFile) throws FileNotFoundException {
-        return new BufferedInputStream(new FileInputStream(xmlFile));
-    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java
index 057e803..e4f13b5 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.testtype.IRemoteTest;
 
 import java.io.File;
+import java.util.Collection;
 
 /**
  * Container for CTS test info.
@@ -63,4 +64,9 @@
      */
     public boolean isKnownTestClass(String testClassName);
 
+    /**
+     * Get the collection of tests in this test package.
+     */
+    public Collection<TestIdentifier> getTests();
+
 }
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 ed26c5e..02595c0 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
@@ -20,6 +20,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ResultForwarder;
 
+import java.util.Collection;
 import java.util.Map;
 
 /**
@@ -28,17 +29,17 @@
  */
 class ResultFilter extends ResultForwarder {
 
-    private final ITestPackageDef mTestPackage;
+    private final Collection<TestIdentifier> mKnownTests;
 
     /**
      * Create a {@link ResultFilter}.
      *
      * @param listener the real {@link ITestInvocationListener} to forward results to
-     * @param testPackage the {@link ITestPackageDef} that defines the expected tests
+     * @param expectedTests the full collection of known tests to expect
      */
-    ResultFilter(ITestInvocationListener listener, ITestPackageDef testPackage) {
+    ResultFilter(ITestInvocationListener listener, Collection<TestIdentifier> knownTests) {
         super(listener);
-        mTestPackage = testPackage;
+        mKnownTests = knownTests;
     }
 
     /**
@@ -88,6 +89,6 @@
      * @return
      */
     private boolean isKnownTest(TestIdentifier test) {
-        return mTestPackage.isKnownTest(test);
+        return mKnownTests.contains(test);
     }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java
index 3b89926..fd896f2 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java
@@ -252,10 +252,9 @@
 
     /**
      * Get the collection of tests in this test package.
-     * <p/>
-     * Exposed for unit testing.
      */
-    Collection<TestIdentifier> getTests() {
+    @Override
+    public Collection<TestIdentifier> getTests() {
         return mTests;
     }
 }