Run plan with excluded tests.

Also add support for excluded class names, and stop running extra tesst when
--class and --method are specified.

Bug 5412014,5171576

Change-Id: I1f76b206d6b6487dfb3711433d2ab27fdd6f247e
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 37b407a..79d5732 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
@@ -24,7 +24,6 @@
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
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 a825805..e2c0a02 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
@@ -36,6 +36,8 @@
 import com.android.tradefed.testtype.IShardableTest;
 import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
 
+import junit.framework.Test;
+
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -51,8 +53,6 @@
 import java.util.Queue;
 import java.util.Set;
 
-import junit.framework.Test;
-
 /**
  * A {@link Test} for running CTS tests.
  * <p/>
@@ -302,16 +302,15 @@
      * @return
      */
     private List<TestPackage> buildTestsToRun() {
-        List<TestPackage> testList = new LinkedList<TestPackage>();
+        List<TestPackage> testPkgList = new LinkedList<TestPackage>();
         try {
             ITestCaseRepo testRepo = createTestCaseRepo();
-            Collection<String> testUris = getTestPackageUrisToRun(testRepo);
+            Collection<ITestPackageDef> testPkgDefs = getTestPackagesToRun(testRepo);
 
-            for (String testUri : testUris) {
-                ITestPackageDef testPackage = testRepo.getTestPackage(testUri);
-                addTestPackage(testList, testUri, testPackage);
+            for (ITestPackageDef testPkgDef : testPkgDefs) {
+                addTestPackage(testPkgList, testPkgDef);
             }
-            if (testList.isEmpty()) {
+            if (testPkgList.isEmpty()) {
                 Log.logAndDisplay(LogLevel.WARN, LOG_TAG, "No tests to run");
             }
         } catch (FileNotFoundException e) {
@@ -319,57 +318,67 @@
         } catch (ParseException e) {
             throw new IllegalArgumentException("failed to parse CTS plan file", e);
         }
-        return testList;
+        return testPkgList;
     }
 
     /**
      * Adds a test package to the list of packages to test
      *
      * @param testList
-     * @param testUri
-     * @param testPackage
+     * @param testPkgDef
      */
-    private void addTestPackage(List<TestPackage> testList, String testUri,
-            ITestPackageDef testPackage) {
-        if (testPackage != null) {
-            IRemoteTest testForPackage = testPackage.createTest(mCtsBuild.getTestCasesDir(),
-                    mClassName, mMethodName);
-            if (testForPackage != null) {
-                Collection<TestIdentifier> knownTests = testPackage.getTests();
-                testList.add(new TestPackage(testPackage, testForPackage, knownTests));
-            }
-        } else {
-            Log.e(LOG_TAG, String.format("Could not find test package uri %s", testUri));
+    private void addTestPackage(List<TestPackage> testList, ITestPackageDef testPkgDef) {
+        IRemoteTest testForPackage = testPkgDef.createTest(mCtsBuild.getTestCasesDir());
+        if (testForPackage != null) {
+            Collection<TestIdentifier> knownTests = testPkgDef.getTests();
+            testList.add(new TestPackage(testPkgDef, testForPackage, knownTests));
         }
     }
 
     /**
-     * Return the list of test package uris to run
+     * Return the list of test package defs to run
      *
-     * @return the list of test package uris to run
+     * @return the list of test package defs to run
      * @throws ParseException
      * @throws FileNotFoundException
      */
-    private Collection<String> getTestPackageUrisToRun(ITestCaseRepo testRepo)
+    private Collection<ITestPackageDef> getTestPackagesToRun(ITestCaseRepo testRepo)
             throws ParseException, FileNotFoundException {
         // use LinkedHashSet to have predictable iteration order
-        Set<String> testUris = new LinkedHashSet<String>();
+        Set<ITestPackageDef> testPkgDefs = new LinkedHashSet<ITestPackageDef>();
         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(mCtsBuild.getTestPlansDir(), ctsPlanRelativePath);
             IPlanXmlParser parser = createXmlParser();
             parser.parse(createXmlStream(ctsPlanFile));
-            testUris.addAll(parser.getTestUris());
+            for (String uri : parser.getTestUris()) {
+                if (!mExcludedPackageNames.contains(uri)) {
+                    ITestPackageDef testPackage = testRepo.getTestPackage(uri);
+                    testPackage.setExcludedTestFilter(parser.getExcludedTestFilter(uri));
+                    testPkgDefs.add(testPackage);
+                }
+            }
         } else if (mPackageNames.size() > 0){
             Log.i(LOG_TAG, String.format("Executing CTS test packages %s", mPackageNames));
-            testUris.addAll(mPackageNames);
+            for (String uri : mPackageNames) {
+                ITestPackageDef testPackage = testRepo.getTestPackage(uri);
+                if (testPackage != null) {
+                    testPkgDefs.add(testPackage);
+                } else {
+                    throw new IllegalArgumentException(String.format(
+                            "Could not find test package %s. " +
+                            "Use 'list packages' to see available packages." , uri));
+                }
+            }
         } 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) {
-                testUris.add(packageUri);
+                ITestPackageDef testPackageDef = testRepo.getTestPackage(packageUri);
+                testPackageDef.setClassName(mClassName, mMethodName);
+                testPkgDefs.add(testPackageDef);
             } else {
                 Log.logAndDisplay(LogLevel.WARN, LOG_TAG, String.format(
                         "Could not find package for test class %s", mClassName));
@@ -378,8 +387,7 @@
             // should never get here - was checkFields() not called?
             throw new IllegalStateException("nothing to run?");
         }
-        testUris.removeAll(mExcludedPackageNames);
-        return testUris;
+        return testPkgDefs;
     }
 
     /**
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/IPlanXmlParser.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/IPlanXmlParser.java
index b9fbfe2..f2a0f74 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/IPlanXmlParser.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/IPlanXmlParser.java
@@ -16,7 +16,6 @@
 
 package com.android.cts.tradefed.testtype;
 
-import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
 
 import java.io.InputStream;
@@ -42,9 +41,9 @@
     public Collection<String> getTestUris();
 
     /**
-     * Gets the list of {@link TestIdentifier} that should be excluded from given package uri.
+     * Gets the {@link TestFilter} that should be used to exclude tests from given package.
      * <p/>
      * Must be called after {@link IPlanXmlParser#parse(InputStream)}.
      */
-    public Collection<TestIdentifier> getExcludedTests(String uri);
+    public TestFilter getExcludedTestFilter(String uri);
 }
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 0348660..1893a93 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
@@ -46,7 +46,7 @@
      * @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, String className, String methodName);
+    public IRemoteTest createTest(File testCaseDir);
 
     /**
      * Determine if given test is defined in this package.
@@ -84,4 +84,20 @@
      */
     public String getName();
 
+    /**
+     * Set the filter to use to exclude tests
+     *
+     * @param excludedTestFilter
+     */
+    public void setExcludedTestFilter(TestFilter excludedTestFilter);
+
+    /**
+     * Restrict this test package to run a specific class and method name
+     *
+     * @param className the test class to restrict this run to
+     * @param methodName the optional test method to restrict this run to, or <code>null</code> to
+     *            run all tests in class
+     */
+    public void setClassName(String className, String methodName);
+
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/PlanXmlParser.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/PlanXmlParser.java
index eaa8b69..e311c78 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/PlanXmlParser.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/PlanXmlParser.java
@@ -17,14 +17,12 @@
 package com.android.cts.tradefed.testtype;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.xml.AbstractXmlParser;
 
 import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
 import org.xml.sax.helpers.DefaultHandler;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -37,7 +35,7 @@
     /**
      * Map of uri names found in plan, and their excluded tests
      */
-    private Map<String, Collection<TestIdentifier>> mUriExcludedTestsMap;
+    private Map<String, TestFilter> mUriExcludedTestsMap;
 
     /**
      * SAX callback object. Handles parsing data from the xml tags.
@@ -51,39 +49,41 @@
                 throws SAXException {
             if (ENTRY_TAG.equals(localName)) {
                 final String entryUriValue = attributes.getValue("uri");
-                Collection<TestIdentifier> excludedTests = parseExcludedTests(
-                        attributes.getValue("exclude"));
-                mUriExcludedTestsMap.put(entryUriValue, excludedTests);
+                TestFilter filter = parseExcludedTests(attributes.getValue("exclude"));
+                mUriExcludedTestsMap.put(entryUriValue, filter);
             }
         }
 
         /**
-         * Parse the semi colon separated list of {@link TestIdentifier}s
+         * Parse the semi colon separated list of tests to exclude.
+         * <p/>
+         * Expected format:
+         * testClassName[#testMethodName][;testClassName2...]
          *
          * @param excludedString the excluded string list
          * @return
          */
-        private Collection<TestIdentifier> parseExcludedTests(String excludedString) {
-            Collection<TestIdentifier> tests = new ArrayList<TestIdentifier>();
+        private TestFilter parseExcludedTests(String excludedString) {
+            TestFilter filter = new TestFilter();
             if (excludedString != null) {
                 String[] testStrings = excludedString.split(";");
                 for (String testString : testStrings) {
                     String[] classMethodPair = testString.split("#");
                     if (classMethodPair.length == 2) {
-                        tests.add(new TestIdentifier(classMethodPair[0], classMethodPair[1]));
+                        filter.addExcludedTest(new TestIdentifier(classMethodPair[0],
+                                classMethodPair[1]));
                     } else {
-                        CLog.w("Unrecognized test name: %s. Expected format: class#method",
-                                testString);
+                        filter.addExcludedClass(testString);
                     }
                 }
             }
-            return tests;
+            return filter;
         }
     }
 
     PlanXmlParser() {
         // Uses a LinkedHashMap to have predictable iteration order
-        mUriExcludedTestsMap = new LinkedHashMap<String, Collection<TestIdentifier>>();
+        mUriExcludedTestsMap = new LinkedHashMap<String, TestFilter>();
     }
 
     /**
@@ -98,7 +98,7 @@
      * {@inheritDoc}
      */
     @Override
-    public Collection<TestIdentifier> getExcludedTests(String uri) {
+    public TestFilter getExcludedTestFilter(String uri) {
         return mUriExcludedTestsMap.get(uri);
     }
 
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestFilter.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestFilter.java
new file mode 100644
index 0000000..6dfb52c
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestFilter.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2011 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.cts.tradefed.testtype;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Filter for {@link TestIdentifier}s.
+ */
+class TestFilter {
+
+    private final Set<String> mExcludedClasses;
+    private final Set<TestIdentifier> mExcludedTests;
+    private String mIncludedClass = null;
+    private String mIncludedMethod = null;
+
+    /**
+     * Creates a {@link TestFilter}
+     */
+    public TestFilter() {
+        mExcludedClasses = new HashSet<String>();
+        mExcludedTests = new HashSet<TestIdentifier>();
+    }
+
+    /**
+     * Adds a test class to the filter.
+     * <p/>
+     * All tests in this class should be filtered.
+     */
+    public void addExcludedClass(String className) {
+        mExcludedClasses.add(className);
+    }
+
+    /**
+     * Adds a test class to the filter. All tests in this class should be excluded.
+     */
+    public void addExcludedTest(TestIdentifier test) {
+        mExcludedTests.add(test);
+    }
+
+    /**
+     * Get the test classes to exclude.
+     * <p/>
+     * Exposed for unit testing
+     */
+    Set<String> getExcludedClasses() {
+        return mExcludedClasses;
+    }
+
+    /**
+     * Get the tests to exclude.
+     * <p/>
+     * Exposed for unit testing
+     */
+    Set<TestIdentifier> getExcludedTests() {
+        return mExcludedTests;
+    }
+
+    /**
+     * Sets the class name and optionally method that should pass this filter. If non-null, all
+     * other tests will be excluded.
+     *
+     * @param className the test class name to exclusively include
+     * @param method the test method name to exclusively include
+     */
+    public void setTestInclusion(String className, String method) {
+        mIncludedClass = className;
+        mIncludedMethod = method;
+    }
+
+    /**
+     * Filter the list of tests based on rules in this filter
+     *
+     * @param tests the list of tests to filter
+     * @return a new list of tests that passed the filter
+     */
+    public Collection<TestIdentifier> filter(Collection<TestIdentifier > tests) {
+        Collection<TestIdentifier> filteredTests = new ArrayList<TestIdentifier>(tests.size());
+        for (TestIdentifier test : tests) {
+            if (mIncludedClass != null && !test.getClassName().equals(mIncludedClass)) {
+                // skip
+                continue;
+            }
+            if (mIncludedMethod != null && !test.getTestName().equals(mIncludedMethod)) {
+                // skip
+                continue;
+            }
+            if (mExcludedClasses.contains(test.getClassName())) {
+                // skip
+                continue;
+            }
+            if (mExcludedTests.contains(test)) {
+                // skip
+                continue;
+            }
+            filteredTests.add(test);
+        }
+        return filteredTests;
+    }
+
+    /**
+     * Return true if there are exclusions rules defined.
+     */
+    public boolean hasExclusion() {
+        return !mExcludedClasses.isEmpty() || !mExcludedTests.isEmpty();
+    }
+}
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 9d70a61..2b91053 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
@@ -31,7 +31,6 @@
 import java.security.DigestInputStream;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashSet;
 
@@ -65,6 +64,11 @@
     // also maintain an index of known test classes
     private Collection<String> mTestClasses = new LinkedHashSet<String>();
 
+    // dynamic options, not parsed from package xml
+    private String mClassName;
+    private String mMethodName;
+    private TestFilter mExcludedTestFilter = new TestFilter();
+
     void setUri(String uri) {
         mUri = uri;
     }
@@ -160,13 +164,34 @@
     /**
      * {@inheritDoc}
      */
-    public IRemoteTest createTest(File testCaseDir, String className, String methodName) {
+    @Override
+    public void setExcludedTestFilter(TestFilter excludeFilter) {
+        mExcludedTestFilter = excludeFilter;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setClassName(String className, String methodName) {
+        mClassName = className;
+        mMethodName = methodName;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IRemoteTest createTest(File testCaseDir) {
+        mExcludedTestFilter.setTestInclusion(mClassName, mMethodName);
+        mTests = filterTests();
+
         if (mIsHostSideTest) {
             Log.d(LOG_TAG, String.format("Creating host test for %s", mName));
             JarHostTest hostTest = new JarHostTest();
             hostTest.setRunName(getUri());
             hostTest.setJarFileName(mJarPath);
-            hostTest.setTests(filterTests(mTests, className, methodName));
+            hostTest.setTests(mTests);
             mDigest = generateDigest(testCaseDir, mJarPath);
             return hostTest;
         } else if (mIsVMHostTest) {
@@ -174,7 +199,7 @@
             VMHostTest vmHostTest = new VMHostTest();
             vmHostTest.setRunName(getUri());
             vmHostTest.setJarFileName(mJarPath);
-            vmHostTest.setTests(filterTests(mTests, className, methodName));
+            vmHostTest.setTests(mTests);
             mDigest = generateDigest(testCaseDir, mJarPath);
             return vmHostTest;
         } else if (mIsSignatureTest) {
@@ -197,16 +222,14 @@
             // a reference app test is just a InstrumentationTest with one extra apk to install
             InstrumentationApkTest instrTest = new InstrumentationApkTest();
             instrTest.addInstallApk(String.format("%s.apk", mApkToTestName), mPackageToTest);
-            return setInstrumentationTest(className, methodName, instrTest, testCaseDir, mTests);
+            return setInstrumentationTest(instrTest, testCaseDir);
         } else {
             Log.d(LOG_TAG, String.format("Creating instrumentation test for %s", mName));
             InstrumentationApkTest instrTest = new InstrumentationApkTest();
-            return setInstrumentationTest(className, methodName, instrTest, testCaseDir, mTests);
+            return setInstrumentationTest(instrTest, testCaseDir);
         }
     }
 
-
-
     /**
      * Populates given {@link InstrumentationApkTest} with data from the package xml
      *
@@ -216,16 +239,17 @@
      * @param instrTest
      * @return the populated {@link InstrumentationTest} or <code>null</code>
      */
-    private InstrumentationTest setInstrumentationTest(String className,
-            String methodName, InstrumentationApkTest instrTest, File testCaseDir,
-            Collection<TestIdentifier> testsToRun) {
+    private InstrumentationTest setInstrumentationTest(InstrumentationApkTest instrTest,
+            File testCaseDir) {
         instrTest.setRunName(getUri());
         instrTest.setPackageName(mAppNameSpace);
         instrTest.setRunnerName(mRunner);
         instrTest.setTestPackageName(mTestPackageName);
-        instrTest.setClassName(className);
-        instrTest.setMethodName(methodName);
-        instrTest.setTestsToRun(testsToRun, true /* force batch mode */);
+        instrTest.setClassName(mClassName);
+        instrTest.setMethodName(mMethodName);
+        instrTest.setTestsToRun(mTests,
+                !mExcludedTestFilter.hasExclusion()
+                /* only force batch mode if no tests are excluded */);
         // mName means 'apk file name' for instrumentation tests
         instrTest.addInstallApk(String.format("%s.apk", mName), mAppNameSpace);
         mDigest = generateDigest(testCaseDir, String.format("%s.apk", mName));
@@ -237,24 +261,13 @@
     }
 
     /**
-     * Filter the tests to run based on class and method name
+     * Filter the tests to run based on list of excluded tests, 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;
+    private Collection<TestIdentifier> filterTests() {
+        mExcludedTestFilter.setTestInclusion(mClassName, mMethodName);
+        return mExcludedTestFilter.filter(mTests);
     }
 
     /**
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 157e85e..71e0eb8 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
@@ -47,6 +47,8 @@
     private ITestDevice mMockDevice;
     private ITestInvocationListener mMockListener;
     private StubCtsBuildHelper mStubBuildHelper;
+    private ITestPackageDef mMockPackageDef;
+    private IRemoteTest mMockTest;
 
     private static final String PLAN_NAME = "CTS";
 
@@ -61,6 +63,8 @@
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         mMockListener = EasyMock.createNiceMock(ITestInvocationListener.class);
         mStubBuildHelper = new StubCtsBuildHelper();
+        mMockPackageDef = EasyMock.createMock(ITestPackageDef.class);
+        mMockTest = EasyMock.createMock(IRemoteTest.class);
 
         mCtsTest = new CtsTest() {
             @Override
@@ -83,6 +87,8 @@
         mCtsTest.setBuildHelper(mStubBuildHelper);
         // turn off device collection for simplicity
         mCtsTest.setCollectDeviceInfo(false);
+
+
     }
 
     /**
@@ -92,17 +98,11 @@
     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(),
-                (String)EasyMock.anyObject(), (String)EasyMock.anyObject())).andReturn(mockTest);
-        EasyMock.expect(mockPackageDef.getTests()).andReturn(new ArrayList<TestIdentifier>());
-        mockTest.run((ITestInvocationListener)EasyMock.anyObject());
+        setCreateAndRunTestExpectations();
 
-        replayMocks(mockTest, mockPackageDef);
+        replayMocks();
         mCtsTest.run(mMockListener);
-        verifyMocks(mockTest, mockPackageDef);
+        verifyMocks();
     }
 
     /**
@@ -111,17 +111,12 @@
     @SuppressWarnings("unchecked")
     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(),
-                (String)EasyMock.anyObject(), (String)EasyMock.anyObject())).andReturn(mockTest);
-        EasyMock.expect(mockPackageDef.getTests()).andReturn(new ArrayList<TestIdentifier>());
-        mockTest.run((ITestInvocationListener)EasyMock.anyObject());
 
-        replayMocks(mockTest, mockPackageDef);
+        setCreateAndRunTestExpectations();
+
+        replayMocks();
         mCtsTest.run(mMockListener);
-        verifyMocks(mockTest, mockPackageDef);
+        verifyMocks();
     }
 
     /**
@@ -130,21 +125,17 @@
     @SuppressWarnings("unchecked")
     public void testRun_resume() 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(),
-                (String)EasyMock.anyObject(), (String)EasyMock.anyObject())).andReturn(mockTest);
-        EasyMock.expect(mockPackageDef.getTests()).andReturn(new ArrayList<TestIdentifier>());
 
-        mockTest.run((ITestInvocationListener)EasyMock.anyObject());
+        setCreateAndRunTestExpectations();
         // abort the first run
         EasyMock.expectLastCall().andThrow(new DeviceNotAvailableException());
 
         // now expect test to be resumed
-        mockTest.run((ITestInvocationListener)EasyMock.anyObject());
+        mMockTest.run((ITestInvocationListener)EasyMock.anyObject());
+        EasyMock.expect(mMockPackageDef.getName()).andReturn(PACKAGE_NAME);
+        EasyMock.expect(mMockPackageDef.getDigest()).andReturn("digest");
 
-        replayMocks(mockTest, mockPackageDef);
+        replayMocks();
         try {
             mCtsTest.run(mMockListener);
             fail("Did not throw DeviceNotAvailableException");
@@ -153,7 +144,7 @@
         }
         // now resume, and expect same test's run method to be called again
         mCtsTest.run(mMockListener);
-        verifyMocks(mockTest, mockPackageDef);
+        verifyMocks();
     }
 
     /**
@@ -166,26 +157,25 @@
         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);
-        EasyMock.expect(mockPackageDef.getTests()).andReturn(new ArrayList<TestIdentifier>());
-        mockTest.run((ITestInvocationListener)EasyMock.anyObject());
+        mMockPackageDef.setClassName(className, methodName);
 
-        replayMocks(mockTest, mockPackageDef);
+        setCreateAndRunTestExpectations();
+
+        replayMocks();
         mCtsTest.run(mMockListener);
-        verifyMocks(mockTest, mockPackageDef);
+        verifyMocks();
     }
 
     /**
      * Test {@link CtsTest#run(java.util.List)} when --excluded-package is specified
      */
     public void testRun_excludedPackage() throws DeviceNotAvailableException, ParseException {
-        setParsePlanExceptations();
+        mCtsTest.setPlanName(PLAN_NAME);
+        mMockPlanParser.parse((InputStream)EasyMock.anyObject());
+        Collection<String> uris = new ArrayList<String>(1);
+        uris.add(PACKAGE_NAME);
+        EasyMock.expect(mMockPlanParser.getTestUris()).andReturn(uris);
 
         mCtsTest.addExcludedPackageName(PACKAGE_NAME);
 
@@ -204,6 +194,25 @@
         Collection<String> uris = new ArrayList<String>(1);
         uris.add(PACKAGE_NAME);
         EasyMock.expect(mMockPlanParser.getTestUris()).andReturn(uris);
+        TestFilter filter = new TestFilter();
+        EasyMock.expect(mMockPlanParser.getExcludedTestFilter(PACKAGE_NAME)).andReturn(
+                filter);
+        mMockPackageDef.setExcludedTestFilter(filter);
+    }
+
+    /**
+     * Set EasyMock expectations for creating and running a package with PACKAGE_NAME
+     */
+    private void setCreateAndRunTestExpectations() throws DeviceNotAvailableException {
+        EasyMock.expect(mMockRepo.getTestPackage(PACKAGE_NAME)).andReturn(mMockPackageDef);
+        EasyMock.expect(mMockPackageDef.createTest((File)EasyMock.anyObject())).andReturn(
+                mMockTest);
+        EasyMock.expect(mMockPackageDef.getTests()).andReturn(new ArrayList<TestIdentifier>());
+        EasyMock.expect(mMockPackageDef.getUri()).andStubReturn(PACKAGE_NAME);
+        EasyMock.expect(mMockPackageDef.getName()).andReturn(PACKAGE_NAME);
+        EasyMock.expect(mMockPackageDef.getDigest()).andReturn("digest");
+
+        mMockTest.run((ITestInvocationListener)EasyMock.anyObject());
     }
 
     /**
@@ -281,12 +290,14 @@
     }
 
     private void replayMocks(Object... mocks) {
-        EasyMock.replay(mMockRepo, mMockPlanParser, mMockDevice, mMockListener);
+        EasyMock.replay(mMockRepo, mMockPlanParser, mMockDevice, mMockListener, mMockPackageDef,
+                mMockTest);
         EasyMock.replay(mocks);
     }
 
     private void verifyMocks(Object... mocks) {
-        EasyMock.verify(mMockRepo, mMockPlanParser, mMockDevice, mMockListener);
+        EasyMock.verify(mMockRepo, mMockPlanParser, mMockDevice, mMockListener, mMockPackageDef,
+                mMockTest);
         EasyMock.verify(mocks);
     }
 }
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/PlanXmlParserTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/PlanXmlParserTest.java
index 0a07b49..387c7c5 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/PlanXmlParserTest.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/PlanXmlParserTest.java
@@ -19,13 +19,12 @@
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
 
+import junit.framework.TestCase;
+
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
-import java.util.Collection;
 import java.util.Iterator;
 
-import junit.framework.TestCase;
-
 /**
  * Unit tests for {@link PlanXmlParser}.
  */
@@ -49,13 +48,19 @@
                     EXCLUDE_TEST_METHOD) +
         "</TestPlan>";
 
-    static final String TEST_EXCLUDED2_DATA =
+    static final String TEST_MULTI_EXCLUDED_DATA =
         "<TestPlan version=\"1.0\">" +
             String.format("<Entry uri=\"%s\" exclude=\"%s#%s;%s#%s\" />", TEST_URI1,
                     EXCLUDE_TEST_CLASS, EXCLUDE_TEST_METHOD, EXCLUDE_TEST_CLASS,
                     EXCLUDE_TEST_METHOD2) +
         "</TestPlan>";
 
+    static final String TEST_CLASS_EXCLUDED_DATA =
+        "<TestPlan version=\"1.0\">" +
+            String.format("<Entry uri=\"%s\" exclude=\"%s\" />", TEST_URI1,
+                    EXCLUDE_TEST_CLASS) +
+        "</TestPlan>";
+
     /**
      * Simple test for parsing a plan containing two uris
      */
@@ -67,8 +72,8 @@
         // assert uris in order
         assertEquals(TEST_URI1, iter.next());
         assertEquals(TEST_URI2, iter.next());
-        assertTrue(parser.getExcludedTests(TEST_URI1).isEmpty());
-        assertTrue(parser.getExcludedTests(TEST_URI2).isEmpty());
+        assertFalse(parser.getExcludedTestFilter(TEST_URI1).hasExclusion());
+        assertFalse(parser.getExcludedTestFilter(TEST_URI2).hasExclusion());
     }
 
     /**
@@ -78,10 +83,9 @@
         PlanXmlParser parser = new PlanXmlParser();
         parser.parse(getStringAsStream(TEST_EXCLUDED_DATA));
         assertEquals(1, parser.getTestUris().size());
-        Collection<TestIdentifier> excludedTests = parser.getExcludedTests(TEST_URI1);
-        TestIdentifier test = excludedTests.iterator().next();
-        assertEquals(EXCLUDE_TEST_CLASS, test.getClassName());
-        assertEquals(EXCLUDE_TEST_METHOD, test.getTestName());
+        TestFilter filter = parser.getExcludedTestFilter(TEST_URI1);
+        assertTrue(filter.getExcludedTests().contains(new TestIdentifier(EXCLUDE_TEST_CLASS,
+                EXCLUDE_TEST_METHOD)));
     }
 
     /**
@@ -89,16 +93,24 @@
      */
     public void testParse_multiExclude() throws ParseException  {
         PlanXmlParser parser = new PlanXmlParser();
-        parser.parse(getStringAsStream(TEST_EXCLUDED2_DATA));
+        parser.parse(getStringAsStream(TEST_MULTI_EXCLUDED_DATA));
         assertEquals(1, parser.getTestUris().size());
-        Iterator<TestIdentifier> iter = parser.getExcludedTests(TEST_URI1).iterator();
-        TestIdentifier test = iter.next();
-        assertEquals(EXCLUDE_TEST_CLASS, test.getClassName());
-        assertEquals(EXCLUDE_TEST_METHOD, test.getTestName());
-        TestIdentifier test2 = iter.next();
-        assertEquals(EXCLUDE_TEST_CLASS, test2.getClassName());
-        assertEquals(EXCLUDE_TEST_METHOD2, test2.getTestName());
+        TestFilter filter = parser.getExcludedTestFilter(TEST_URI1);
+        assertTrue(filter.getExcludedTests().contains(new TestIdentifier(EXCLUDE_TEST_CLASS,
+                EXCLUDE_TEST_METHOD)));
+        assertTrue(filter.getExcludedTests().contains(new TestIdentifier(EXCLUDE_TEST_CLASS,
+                EXCLUDE_TEST_METHOD2)));
+    }
 
+    /**
+     * Test parsing a plan containing an excluded class
+     */
+    public void testParse_classExclude() throws ParseException  {
+        PlanXmlParser parser = new PlanXmlParser();
+        parser.parse(getStringAsStream(TEST_CLASS_EXCLUDED_DATA));
+        assertEquals(1, parser.getTestUris().size());
+        TestFilter filter = parser.getExcludedTestFilter(TEST_URI1);
+        assertTrue(filter.getExcludedClasses().contains(EXCLUDE_TEST_CLASS));
     }
 
     private InputStream getStringAsStream(String input) {
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/TestFilterTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/TestFilterTest.java
new file mode 100644
index 0000000..676b912
--- /dev/null
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/TestFilterTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2011 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.cts.tradefed.testtype;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ *
+ */
+public class TestFilterTest extends TestCase {
+
+    private TestFilter mFilter;
+    private List<TestIdentifier> mTestList;
+
+    private static final TestIdentifier TEST1 = new TestIdentifier("FooTest", "testFoo");
+    private static final TestIdentifier TEST2 = new TestIdentifier("FooTest", "testFoo2");
+    private static final TestIdentifier TEST3 = new TestIdentifier("FooTest2", "testFoo3");
+
+    @Override
+    protected void setUp() throws Exception {
+        mFilter = new TestFilter();
+        mTestList = new ArrayList<TestIdentifier>();
+        mTestList.add(TEST1);
+        mTestList.add(TEST2);
+        mTestList.add(TEST3);
+    }
+
+    /**
+     * Test {@link TestFilter#filter(java.util.Collection)} with no rules defined
+     */
+    public void testFilter_empty() {
+        assertEquals(mTestList.size(), mFilter.filter(mTestList).size());
+    }
+
+    /**
+     * Test {@link TestFilter#filter(java.util.Collection)} with an excluded test filter
+     */
+    public void testFilter_excludeTest() {
+        mFilter.addExcludedTest(TEST1);
+        Collection<TestIdentifier> filteredList = mFilter.filter(mTestList);
+        assertEquals(2, filteredList.size());
+        Iterator<TestIdentifier> iter = filteredList.iterator();
+        assertEquals(TEST2, iter.next());
+        assertEquals(TEST3, iter.next());
+    }
+
+    /**
+     * Test {@link TestFilter#filter(java.util.Collection)} with an excluded test filter
+     */
+    public void testFilter_excludeClass() {
+        mFilter.addExcludedClass(TEST1.getClassName());
+        Collection<TestIdentifier> filteredList = mFilter.filter(mTestList);
+        assertEquals(1, filteredList.size());
+        assertEquals(TEST3, filteredList.iterator().next());
+    }
+
+    /**
+     * Test {@link TestFilter#filter(java.util.Collection)} with a class inclusion rule
+     */
+    public void testFilter_includeClass() {
+        mFilter.setTestInclusion(TEST1.getClassName(), null);
+        Collection<TestIdentifier> filteredList = mFilter.filter(mTestList);
+        assertEquals(2, filteredList.size());
+        Iterator<TestIdentifier> iter = filteredList.iterator();
+        assertEquals(TEST1, iter.next());
+        assertEquals(TEST2, iter.next());
+    }
+
+    /**
+     * Test {@link TestFilter#filter(java.util.Collection)} with at class
+     */
+    public void testFilter_includeTest() {
+        mFilter.setTestInclusion(TEST1.getClassName(), TEST1.getTestName());
+        Collection<TestIdentifier> filteredList = mFilter.filter(mTestList);
+        assertEquals(1, filteredList.size());
+        Iterator<TestIdentifier> iter = filteredList.iterator();
+        assertEquals(TEST1, iter.next());
+    }
+}