Merge "Add ability to run tests restricted to given annotation."
diff --git a/test-runner/src/android/test/InstrumentationTestRunner.java b/test-runner/src/android/test/InstrumentationTestRunner.java
index 4ae98e6..e586010 100644
--- a/test-runner/src/android/test/InstrumentationTestRunner.java
+++ b/test-runner/src/android/test/InstrumentationTestRunner.java
@@ -19,6 +19,7 @@
 import static android.test.suitebuilder.TestPredicates.REJECT_PERFORMANCE;
 
 import com.android.internal.util.Predicate;
+import com.android.internal.util.Predicates;
 
 import android.app.Activity;
 import android.app.Instrumentation;
@@ -31,11 +32,13 @@
 import android.test.suitebuilder.TestMethod;
 import android.test.suitebuilder.TestPredicates;
 import android.test.suitebuilder.TestSuiteBuilder;
+import android.test.suitebuilder.annotation.HasAnnotation;
 import android.util.Log;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.PrintStream;
+import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -93,6 +96,18 @@
  * -e size large
  * com.android.foo/android.test.InstrumentationTestRunner
  * <p/>
+ * <b>Filter test run to tests with given annotation:</b> adb shell am instrument -w
+ * -e annotation com.android.foo.MyAnnotation
+ * com.android.foo/android.test.InstrumentationTestRunner
+ * <p/>
+ * If used with other options, the resulting test run will contain the union of the two options.
+ * e.g. "-e size large -e annotation com.android.foo.MyAnnotation" will run only tests with both
+ * the {@link LargeTest} and "com.android.foo.MyAnnotation" annotations.
+ * <p/>
+ * <b>Filter test run to tests <i>without</i> given annotation:</b> adb shell am instrument -w
+ * -e notAnnotation com.android.foo.MyAnnotation
+ * com.android.foo/android.test.InstrumentationTestRunner
+ * <p/>
  * <b>Running a single testcase:</b> adb shell am instrument -w
  * -e class com.android.foo.FooTest
  * com.android.foo/android.test.InstrumentationTestRunner
@@ -161,6 +176,10 @@
     private static final String LARGE_SUITE = "large";
 
     private static final String ARGUMENT_LOG_ONLY = "log";
+    /** @hide */
+    static final String ARGUMENT_ANNOTATION = "annotation";
+    /** @hide */
+    static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation";
 
     /**
      * This constant defines the maximum allowed runtime (in ms) for a test included in the "small"
@@ -274,6 +293,8 @@
         ClassPathPackageInfoSource.setApkPaths(apkPaths);
 
         Predicate<TestMethod> testSizePredicate = null;
+        Predicate<TestMethod> testAnnotationPredicate = null;
+        Predicate<TestMethod> testNotAnnotationPredicate = null;
         boolean includePerformance = false;
         String testClassesArg = null;
         boolean logOnly = false;
@@ -287,6 +308,11 @@
             mPackageOfTests = arguments.getString(ARGUMENT_TEST_PACKAGE);
             testSizePredicate = getSizePredicateFromArg(
                     arguments.getString(ARGUMENT_TEST_SIZE_PREDICATE));
+            testAnnotationPredicate = getAnnotationPredicate(
+                    arguments.getString(ARGUMENT_ANNOTATION));
+            testNotAnnotationPredicate = getNotAnnotationPredicate(
+                    arguments.getString(ARGUMENT_NOT_ANNOTATION));
+
             includePerformance = getBooleanArgument(arguments, ARGUMENT_INCLUDE_PERF);
             logOnly = getBooleanArgument(arguments, ARGUMENT_LOG_ONLY);
             mCoverage = getBooleanArgument(arguments, "coverage");
@@ -306,6 +332,12 @@
         if (testSizePredicate != null) {
             testSuiteBuilder.addRequirements(testSizePredicate);
         }
+        if (testAnnotationPredicate != null) {
+            testSuiteBuilder.addRequirements(testAnnotationPredicate);
+        }
+        if (testNotAnnotationPredicate != null) {
+            testSuiteBuilder.addRequirements(testNotAnnotationPredicate);
+        }
         if (!includePerformance) {
             testSuiteBuilder.addRequirements(REJECT_PERFORMANCE);
         }
@@ -406,6 +438,59 @@
         }
     }
 
+   /**
+    * Returns the test predicate object, corresponding to the annotation class value provided via
+    * the {@link ARGUMENT_ANNOTATION} argument.
+    *
+    * @return the predicate or <code>null</code>
+    */
+    private Predicate<TestMethod> getAnnotationPredicate(String annotationClassName) {
+        Class<? extends Annotation> annotationClass = getAnnotationClass(annotationClassName);
+        if (annotationClass != null) {
+            return new HasAnnotation(annotationClass);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the negative test predicate object, corresponding to the annotation class value
+     * provided via the {@link ARGUMENT_NOT_ANNOTATION} argument.
+     *
+     * @return the predicate or <code>null</code>
+     */
+     private Predicate<TestMethod> getNotAnnotationPredicate(String annotationClassName) {
+         Class<? extends Annotation> annotationClass = getAnnotationClass(annotationClassName);
+         if (annotationClass != null) {
+             return Predicates.not(new HasAnnotation(annotationClass));
+         }
+         return null;
+     }
+
+    /**
+     * Helper method to return the annotation class with specified name
+     *
+     * @param annotationClassName the fully qualified name of the class
+     * @return the annotation class or <code>null</code>
+     */
+    private Class<? extends Annotation> getAnnotationClass(String annotationClassName) {
+        if (annotationClassName == null) {
+            return null;
+        }
+        try {
+           Class<?> annotationClass = Class.forName(annotationClassName);
+           if (annotationClass.isAnnotation()) {
+               return (Class<? extends Annotation>)annotationClass;
+           } else {
+               Log.e(LOG_TAG, String.format("Provided annotation value %s is not an Annotation",
+                       annotationClassName));
+           }
+        } catch (ClassNotFoundException e) {
+            Log.e(LOG_TAG, String.format("Could not find class for specified annotation %s",
+                    annotationClassName));
+        }
+        return null;
+    }
+
     @Override
     public void onStart() {
         Looper.prepare();
@@ -471,7 +556,7 @@
         String coverageFilePath = getCoverageFilePath();
         java.io.File coverageFile = new java.io.File(coverageFilePath);
         try {
-            Class emmaRTClass = Class.forName("com.vladium.emma.rt.RT");
+            Class<?> emmaRTClass = Class.forName("com.vladium.emma.rt.RT");
             Method dumpCoverageMethod = emmaRTClass.getMethod("dumpCoverageData",
                     coverageFile.getClass(), boolean.class, boolean.class);
 
diff --git a/test-runner/tests/src/android/test/InstrumentationTestRunnerTest.java b/test-runner/tests/src/android/test/InstrumentationTestRunnerTest.java
index d9afd54..6db72ad 100644
--- a/test-runner/tests/src/android/test/InstrumentationTestRunnerTest.java
+++ b/test-runner/tests/src/android/test/InstrumentationTestRunnerTest.java
@@ -109,6 +109,33 @@
         assertTrue(mStubAndroidTestRunner.isRun());
     }
 
+    /**
+     * Test that the -e {@link InstrumentationTestRunner.ARGUMENT_ANNOTATION} parameter properly
+     * selects tests.
+     */
+    public void testAnnotationParameter() throws Exception {
+        String expectedTestClassName = AnnotationTest.class.getName();
+        Bundle args = new Bundle();
+        args.putString(InstrumentationTestRunner.ARGUMENT_TEST_CLASS, expectedTestClassName);
+        args.putString(InstrumentationTestRunner.ARGUMENT_ANNOTATION, FlakyTest.class.getName());
+        mInstrumentationTestRunner.onCreate(args);
+        assertTestRunnerCalledWithExpectedParameters(expectedTestClassName, "testAnnotated");
+    }
+    
+    /**
+     * Test that the -e {@link InstrumentationTestRunner.ARGUMENT_NOT_ANNOTATION} parameter
+     * properly excludes tests.
+     */
+    public void testNotAnnotationParameter() throws Exception {
+        String expectedTestClassName = AnnotationTest.class.getName();
+        Bundle args = new Bundle();
+        args.putString(InstrumentationTestRunner.ARGUMENT_TEST_CLASS, expectedTestClassName);
+        args.putString(InstrumentationTestRunner.ARGUMENT_NOT_ANNOTATION,
+                FlakyTest.class.getName());
+        mInstrumentationTestRunner.onCreate(args);
+        assertTestRunnerCalledWithExpectedParameters(expectedTestClassName, "testNotAnnotated");
+    }
+
     private void assertContentsInOrder(List<TestDescriptor> actual, TestDescriptor... source) {
         TestDescriptor[] clonedSource = source.clone();
         assertEquals("Unexpected number of items.", clonedSource.length, actual.size());
@@ -269,4 +296,17 @@
 
         }
     }
+
+    /**
+     * Annotated test used for validation.
+     */
+    public static class AnnotationTest extends TestCase {
+
+        public void testNotAnnotated() throws Exception {
+        }
+
+        @FlakyTest
+        public void testAnnotated() throws Exception {
+        }
+    }
 }