Merge "Add --class and --method options to cts tradefed." into honeycomb
diff --git a/tools/tradefed-host/res/config/cts.xml b/tools/tradefed-host/res/config/cts.xml
index d98c7fb..165ba96 100644
--- a/tools/tradefed-host/res/config/cts.xml
+++ b/tools/tradefed-host/res/config/cts.xml
@@ -19,7 +19,7 @@
     <build_provider class="com.android.cts.tradefed.targetsetup.CtsBuildProvider" />
     <device_recovery class="com.android.tradefed.device.WaitDeviceRecovery" />
     <target_preparer class="com.android.cts.tradefed.targetsetup.CtsSetup" />
-    <test class="com.android.cts.tradefed.testtype.PlanTest" />
+    <test class="com.android.cts.tradefed.testtype.CtsTest" />
     <logger class="com.android.tradefed.log.FileLogger" />
     <result_reporter class="com.android.cts.tradefed.result.CtsXmlResultReporter" />
 
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 c160aa5..702956a 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
@@ -18,6 +18,7 @@
 
 import com.android.cts.tradefed.device.DeviceInfoCollector;
 import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -53,6 +54,8 @@
     public static final String TEST_PLANS_DIR_OPTION = "test-plans-path";
     private static final String PLAN_OPTION = "plan";
     private static final String PACKAGE_OPTION = "package";
+    private static final String CLASS_OPTION = "class";
+    private static final String METHOD_OPTION = "method";
 
     private ITestDevice mDevice;
 
@@ -65,6 +68,13 @@
     @Option(name = "exclude-package", description = "the test packages(s) to exclude from the run")
     private Collection<String> mExcludedPackageNames = new ArrayList<String>();
 
+    @Option(name = CLASS_OPTION, shortName = 'c', description = "run a specific test class")
+    private String mClassName = null;
+
+    @Option(name = METHOD_OPTION, shortName = 'm',
+            description = "run a specific test method, from given --class")
+    private String mMethodName = null;
+
     @Option(name = TEST_CASES_DIR_OPTION, description =
         "file path to directory containing CTS test cases")
     private File mTestCaseDir = null;
@@ -146,6 +156,24 @@
     }
 
     /**
+     * Set the test class name to run.
+     * <p/>
+     * Exposed for unit testing
+     */
+    void setClassName(String className) {
+        mClassName = className;
+    }
+
+    /**
+     * Set the test method name to run.
+     * <p/>
+     * Exposed for unit testing
+     */
+    void setMethodName(String methodName) {
+        mMethodName = methodName;
+    }
+
+    /**
      * {@inheritDoc}
      */
     public void run(List<ITestInvocationListener> listeners) throws DeviceNotAvailableException {
@@ -154,8 +182,8 @@
         Log.i(LOG_TAG, String.format("Executing CTS test plan %s", mPlanName));
 
         try {
-            Collection<String> testUris = getTestsToRun();
             ITestCaseRepo testRepo = createTestCaseRepo();
+            Collection<String> testUris = getTestsToRun(testRepo);
             collectDeviceInfo(getDevice(), mTestCaseDir, listeners);
             for (String testUri : testUris) {
                 ITestPackageDef testPackage = testRepo.getTestPackage(testUri);
@@ -179,31 +207,49 @@
      * @throws ParseException
      * @throws FileNotFoundException
      */
-    private Collection<String> getTestsToRun() throws ParseException, FileNotFoundException {
+    private Collection<String> getTestsToRun(ITestCaseRepo testRepo) throws ParseException,
+            FileNotFoundException {
         Set<String> testUris = new HashSet<String>();
-        if (mPlanName == null) {
-            testUris.addAll(mPackageNames);
-        } else {
+        if (mPlanName != null) {
             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){
+            testUris.addAll(mPackageNames);
+        } else if (mClassName != null) {
+            // try to find package to run from class name
+            String packageUri = testRepo.findPackageForTest(mClassName);
+            if (packageUri != null) {
+                testUris.add(packageUri);
+            } else {
+                Log.logAndDisplay(LogLevel.WARN, LOG_TAG, String.format(
+                        "Could not find package for test class %s", mClassName));
+            }
+        } else {
+            // should never get here - was checkFields() not called?
+            throw new IllegalStateException("nothing to run?");
         }
         testUris.removeAll(mExcludedPackageNames);
         return testUris;
     }
 
     private void checkFields() {
-        if (mPlanName == null && mPackageNames.size() <= 0) {
+        // for simplicity of command line usage, make --plan, --package, and --class mutually
+        // exclusive
+        boolean mutualExclusiveArgs = xor(mPlanName != null, mPackageNames.size() > 0,
+                mClassName != null);
+
+        if (!mutualExclusiveArgs) {
             throw new IllegalArgumentException(String.format(
-                    "Missing the --%s or --%s(s) to run", PLAN_OPTION, PACKAGE_OPTION));
+                    "Ambiguous or missing arguments. " +
+                    "One and only of --%s --%s(s) or --%s to run can be specified",
+                    PLAN_OPTION, PACKAGE_OPTION, CLASS_OPTION));
         }
-        // for simplicity of command line usage, don't allow both --plan and --package
-        if (mPlanName != null && mPackageNames.size() > 0) {
+        if (mMethodName != null && mClassName == null) {
             throw new IllegalArgumentException(String.format(
-                    "Only one of a --%s or --%s(s) to run can be specified", PLAN_OPTION,
-                    PACKAGE_OPTION));
+                    "Must specify --%s when --%s is used", CLASS_OPTION, METHOD_OPTION));
         }
         if (getDevice() == null) {
             throw new IllegalArgumentException("missing device");
@@ -218,6 +264,24 @@
     }
 
     /**
+     * Helper method to perform exclusive or on list of boolean arguments
+     *
+     * @param args set of booleans on which to perform exclusive or
+     * @return <code>true</code> if one and only one of <var>args</code> is <code>true</code>.
+     *         Otherwise return <code>false</code>.
+     */
+    private boolean xor(boolean... args) {
+        boolean currentVal = args[0];
+        for (int i=1; i < args.length; i++) {
+            if (currentVal && args[i]) {
+                return false;
+            }
+            currentVal |= args[i];
+        }
+        return currentVal;
+    }
+
+    /**
      * Runs the test.
      *
      * @param listeners
@@ -226,7 +290,7 @@
      */
     private void runTest(List<ITestInvocationListener> listeners, ITestPackageDef testPackage)
             throws DeviceNotAvailableException {
-        IRemoteTest test = testPackage.createTest(mTestCaseDir);
+        IRemoteTest test = testPackage.createTest(mTestCaseDir, mClassName, mMethodName);
         if (test != null) {
             if (test instanceof IDeviceTest) {
                 ((IDeviceTest)test).setDevice(getDevice());
@@ -278,5 +342,4 @@
     InputStream createXmlStream(File xmlFile) throws FileNotFoundException {
         return new BufferedInputStream(new FileInputStream(xmlFile));
     }
-
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestCaseRepo.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestCaseRepo.java
index 96546d0..ffcde46 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestCaseRepo.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestCaseRepo.java
@@ -30,4 +30,12 @@
      */
     public ITestPackageDef getTestPackage(String testUri);
 
+    /**
+     * Attempt to find the package uri for a given test class name
+     *
+     * @param testClassName the test class name
+     * @return the package uri or <code>null</code> if the package cannot be found
+     */
+    public String findPackageForTest(String testClassName);
+
 }
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 f6febdb..057e803 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
@@ -38,10 +38,14 @@
      * Creates a runnable {@link IRemoteTest} from info stored in this definition.
      *
      * @param testCaseDir {@link File} representing directory of test case data
+     * @param className the test class to restrict this run to or <code>null</code> to run all tests
+     *            in package
+     * @param methodName the optional test method to restrict this run to, or <code>null</code> to
+     *            run all tests in class/package
      * @return a {@link IRemoteTest} with all necessary data populated to run the test or
      *         <code>null</code> if test could not be created
      */
-    public IRemoteTest createTest(File testCaseDir);
+    public IRemoteTest createTest(File testCaseDir, String className, String methodName);
 
     /**
      * Determine if given test is defined in this package.
@@ -51,4 +55,12 @@
      */
     public boolean isKnownTest(TestIdentifier testDef);
 
+    /**
+     * Determine if given test class is defined in this package.
+     *
+     * @param testClassName the fully qualified test class name
+     * @return <code>true</code> if test class is defined
+     */
+    public boolean isKnownTestClass(String testClassName);
+
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestCaseRepo.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestCaseRepo.java
index fc20314..404da4d 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestCaseRepo.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestCaseRepo.java
@@ -116,4 +116,17 @@
     public ITestPackageDef getTestPackage(String testUri) {
         return mTestMap.get(testUri);
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String findPackageForTest(String testClassName) {
+        for (Map.Entry<String, TestPackageDef> entry : mTestMap.entrySet()) {
+            if (entry.getValue().isKnownTestClass(testClassName)) {
+                return entry.getKey();
+            }
+        }
+        return null;
+    }
 }
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 cf67365..79723c1 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
@@ -23,6 +23,7 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.LinkedHashSet;
 
 /**
  * Container for CTS test info.
@@ -42,10 +43,10 @@
     private boolean mIsSignatureTest = false;
     private boolean mIsReferenceAppTest = false;
 
-    private Collection<TestIdentifier> mTests = new ArrayList<TestIdentifier>();
-
-    /** the cached {@link IRemoteTest} */
-    private IRemoteTest mRemoteTest;
+    // use a LinkedHashSet for predictable iteration insertion-order, and fast lookups
+    private Collection<TestIdentifier> mTests = new LinkedHashSet<TestIdentifier>();
+    // also maintain an index of known test classes
+    private Collection<String> mTestClasses = new LinkedHashSet<String>();
 
     void setUri(String uri) {
         mUri = uri;
@@ -118,25 +119,14 @@
     /**
      * {@inheritDoc}
      */
-    public IRemoteTest createTest(File testCaseDir) {
-        if (mRemoteTest == null) {
-            mRemoteTest = doCreateTest(testCaseDir);
-        }
-        return mRemoteTest;
-    }
-
-    /**
-     * @param testCaseDir
-     * @return
-     */
-    private IRemoteTest doCreateTest(File testCaseDir) {
+    public IRemoteTest createTest(File testCaseDir, String className, String methodName) {
         if (mIsHostSideTest) {
             Log.d(LOG_TAG, String.format("Creating host test for %s", mName));
             JarHostTest hostTest = new JarHostTest();
             hostTest.setRunName(mName);
             hostTest.setJarFile(new File(testCaseDir, mJarPath));
             hostTest.setTestAppPath(testCaseDir.getAbsolutePath());
-            hostTest.setTests(mTests);
+            hostTest.setTests(filterTests(mTests, className, methodName));
             return hostTest;
         } else if (mIsSignatureTest) {
             // TODO: implement this
@@ -153,6 +143,8 @@
             InstrumentationTest instrTest = new InstrumentationTest();
             instrTest.setPackageName(mAppNameSpace);
             instrTest.setRunnerName(mRunner);
+            instrTest.setClassName(className);
+            instrTest.setMethodName(methodName);
             // mName means 'apk file name' for instrumentation tests
             File apkFile = new File(testCaseDir, String.format("%s.apk", mName));
             if (!apkFile.exists()) {
@@ -166,6 +158,27 @@
     }
 
     /**
+     * Filter the tests to run based on class and method name
+     *
+     * @param tests the full set of tests in package
+     * @param className the test class name filter. <code>null</code> to run all test classes
+     * @param methodName the test method name. <code>null</code> to run all test methods
+     * @return the filtered collection of tests
+     */
+    private Collection<TestIdentifier> filterTests(Collection<TestIdentifier> tests,
+            String className, String methodName) {
+        Collection<TestIdentifier> filteredTests = new ArrayList<TestIdentifier>(tests.size());
+        for (TestIdentifier test : tests) {
+            if (className == null || test.getClassName().equals(className)) {
+                if (methodName == null || test.getTestName().equals(methodName)) {
+                    filteredTests.add(test);
+                }
+            }
+        }
+        return filteredTests;
+    }
+
+    /**
      * {@inheritDoc}
      */
     public boolean isKnownTest(TestIdentifier testDef) {
@@ -173,12 +186,20 @@
     }
 
     /**
+     * {@inheritDoc}
+     */
+    public boolean isKnownTestClass(String className) {
+        return mTestClasses.contains(className);
+    }
+
+    /**
      * Add a {@link TestDef} to the list of tests in this package.
      *
      * @param testdef
      */
     void addTest(TestIdentifier testDef) {
         mTests.add(testDef);
+        mTestClasses.add(testDef.getClassName());
     }
 
     /**
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/CtsTestTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/CtsTestTest.java
index a8c2b8d..e884365 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/CtsTestTest.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/CtsTestTest.java
@@ -87,13 +87,14 @@
      * Test normal case {@link CtsTest#run(java.util.List)} when running a plan.
      */
     @SuppressWarnings("unchecked")
-    public void testRun__plan() throws DeviceNotAvailableException, ParseException {
+    public void testRun_plan() throws DeviceNotAvailableException, ParseException {
         setParsePlanExceptations();
 
         ITestPackageDef mockPackageDef = EasyMock.createMock(ITestPackageDef.class);
         IRemoteTest mockTest = EasyMock.createMock(IRemoteTest.class);
         EasyMock.expect(mMockRepo.getTestPackage(PACKAGE_NAME)).andReturn(mockPackageDef);
-        EasyMock.expect(mockPackageDef.createTest((File)EasyMock.anyObject())).andReturn(mockTest);
+        EasyMock.expect(mockPackageDef.createTest((File)EasyMock.anyObject(),
+                (String)EasyMock.anyObject(), (String)EasyMock.anyObject())).andReturn(mockTest);
         mockTest.run((ITestInvocationListener)EasyMock.anyObject());
 
         replayMocks(mockTest, mockPackageDef);
@@ -107,12 +108,37 @@
      * Test normal case {@link CtsTest#run(java.util.List)} when running a package.
      */
     @SuppressWarnings("unchecked")
-    public void testRun__package() throws DeviceNotAvailableException {
+    public void testRun_package() throws DeviceNotAvailableException {
         mCtsTest.addPackageName(PACKAGE_NAME);
         ITestPackageDef mockPackageDef = EasyMock.createMock(ITestPackageDef.class);
         IRemoteTest mockTest = EasyMock.createMock(IRemoteTest.class);
         EasyMock.expect(mMockRepo.getTestPackage(PACKAGE_NAME)).andReturn(mockPackageDef);
-        EasyMock.expect(mockPackageDef.createTest((File)EasyMock.anyObject())).andReturn(mockTest);
+        EasyMock.expect(mockPackageDef.createTest((File)EasyMock.anyObject(),
+                (String)EasyMock.anyObject(), (String)EasyMock.anyObject())).andReturn(mockTest);
+        mockTest.run((ITestInvocationListener)EasyMock.anyObject());
+
+        replayMocks(mockTest, mockPackageDef);
+        mCtsTest.run(mMockListener);
+        verifyMocks(mockTest, mockPackageDef);
+    }
+
+    /**
+     * Test normal case {@link CtsTest#run(java.util.List)} when running a class.
+     */
+    @SuppressWarnings("unchecked")
+    public void testRun_class() throws DeviceNotAvailableException {
+        final String className = "className";
+        final String methodName = "methodName";
+        mCtsTest.setClassName(className);
+        mCtsTest.setMethodName(methodName);
+
+
+        EasyMock.expect(mMockRepo.findPackageForTest(className)).andReturn(PACKAGE_NAME);
+        ITestPackageDef mockPackageDef = EasyMock.createMock(ITestPackageDef.class);
+        EasyMock.expect(mMockRepo.getTestPackage(PACKAGE_NAME)).andReturn(mockPackageDef);
+        IRemoteTest mockTest = EasyMock.createMock(IRemoteTest.class);
+        EasyMock.expect(mockPackageDef.createTest((File)EasyMock.anyObject(),
+                EasyMock.eq(className), EasyMock.eq(methodName))).andReturn(mockTest);
         mockTest.run((ITestInvocationListener)EasyMock.anyObject());
 
         replayMocks(mockTest, mockPackageDef);
@@ -173,6 +199,52 @@
         }
     }
 
+    /**
+     * Test {@link CtsTest#run(java.util.List)} when --plan and --class options have been
+     * specified
+     */
+    public void testRun_planClass() throws DeviceNotAvailableException {
+        mCtsTest.setPlanName(PLAN_NAME);
+        mCtsTest.setClassName("class");
+        try {
+            mCtsTest.run(mMockListener);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Test {@link CtsTest#run(java.util.List)} when --package and --class options have been
+     * specified
+     */
+    public void testRun_packageClass() throws DeviceNotAvailableException {
+        mCtsTest.addPackageName(PACKAGE_NAME);
+        mCtsTest.setClassName("class");
+        try {
+            mCtsTest.run(mMockListener);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Test {@link CtsTest#run(java.util.List)} when --plan, --package and --class options have been
+     * specified
+     */
+    public void testRun_planPackageClass() throws DeviceNotAvailableException {
+        mCtsTest.setPlanName(PLAN_NAME);
+        mCtsTest.addPackageName(PACKAGE_NAME);
+        mCtsTest.setClassName("class");
+        try {
+            mCtsTest.run(mMockListener);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
     private void replayMocks(Object... mocks) {
         EasyMock.replay(mMockRepo, mMockPlanParser, mMockDevice, mMockListener);
         EasyMock.replay(mocks);