Adding support for loading classes/methods from file

Added testFile argument to AndroidJunitRunner which allows to run all tests listed in a file:
adb shell am instrument -w -e testFile /sdcard/tmp/testFile.txt com.android.foo/com.android.test.runner.AndroidJUnitRunner

The file should contain a list of line separated test classes and methods.

bug: 16031984

Change-Id: Idf3e5e12f65ed670f31dad54bf3f0e303114c755
(cherry picked from commit da785cf4556f8bfc347c8118b42c74dc0ec59f58)
diff --git a/support/src/android/support/test/internal/runner/ClassPathScanner.java b/support/src/android/support/test/internal/runner/ClassPathScanner.java
index 300c7de..ac34406 100644
--- a/support/src/android/support/test/internal/runner/ClassPathScanner.java
+++ b/support/src/android/support/test/internal/runner/ClassPathScanner.java
@@ -44,7 +44,7 @@
          * Tests whether or not the specified abstract pathname should be included in a class path
          * entry list.
          *
-         * @param pathName the relative path of the class path entry
+         * @param className the relative path of the class path entry
          */
         boolean accept(String className);
     }
diff --git a/support/src/android/support/test/internal/runner/TestRequestBuilder.java b/support/src/android/support/test/internal/runner/TestRequestBuilder.java
index 82f007e..4a8383f 100644
--- a/support/src/android/support/test/internal/runner/TestRequestBuilder.java
+++ b/support/src/android/support/test/internal/runner/TestRequestBuilder.java
@@ -267,7 +267,7 @@
     }
 
     /**
-     * Class that filters out tests annotated with {@link RequestDevice} when running on emulator
+     * Class that filters out tests annotated with {@link RequiresDevice} when running on emulator
      */
     private class RequiresDeviceFilter extends AnnotationExclusionFilter {
 
@@ -518,6 +518,7 @@
      * @param instr the {@link Instrumentation} to inject into any tests that require it
      * @param bundle the {@link Bundle} of command line args to inject into any tests that require
      *         it
+     * @param skipExecution whether or not to skip actual test execution
      * @param computer Helps construct Runners from classes
      * @param classes the classes containing the tests
      * @return a <code>Request</code> that will cause all tests in the classes to be run
diff --git a/support/src/android/support/test/runner/AndroidJUnitRunner.java b/support/src/android/support/test/runner/AndroidJUnitRunner.java
index 42cdc73..f07bf0c 100644
--- a/support/src/android/support/test/runner/AndroidJUnitRunner.java
+++ b/support/src/android/support/test/runner/AndroidJUnitRunner.java
@@ -46,7 +46,12 @@
 import org.junit.runner.Result;
 import org.junit.runner.notification.RunListener;
 
+import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
 import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -88,6 +93,11 @@
  * -e class com.android.foo.FooTest,com.android.foo.TooTest
  * com.android.foo/android.support.test.runner.AndroidJUnitRunner
  * <p/>
+ * <b>Running all tests listed in a file:</b> adb shell am instrument -w
+ * -e testFile /sdcard/tmp/testFile.txt com.android.foo/com.android.test.runner.AndroidJUnitRunner
+ * The file should contain a list of line separated test classes and optionally methods (expected
+ * format: com.android.foo.FooClassName#testMethodName).
+ * <p/>
  * <b>Running all tests in a java package:</b> adb shell am instrument -w
  * -e package com.android.foo.bar
  * com.android.foo/android.support.test.runner.AndroidJUnitRunner
@@ -168,16 +178,22 @@
     private static final String ARGUMENT_DEBUG = "debug";
     private static final String ARGUMENT_LISTENER = "listener";
     private static final String ARGUMENT_TEST_PACKAGE = "package";
+    static final String ARGUMENT_TEST_FILE = "testFile";
     // TODO: consider supporting 'count' from InstrumentationTestRunner
 
     private static final String LOG_TAG = "AndroidJUnitRunner";
 
+    // used to separate multiple fully-qualified test case class names
+    private static final char CLASS_SEPARATOR = ',';
+    // used to separate fully-qualified test case class name, and one of its methods
+    private static final char METHOD_SEPARATOR = '#';
+
     private Bundle mArguments;
 
     @Override
     public void onCreate(Bundle arguments) {
         super.onCreate(arguments);
-        mArguments = arguments;
+        setArguments(arguments);
         specifyDexMakerCacheProperty();
 
         start();
@@ -411,11 +427,16 @@
 
         String testClassName = arguments.getString(ARGUMENT_TEST_CLASS);
         if (testClassName != null) {
-            for (String className : testClassName.split(",")) {
+            for (String className : testClassName.split(String.valueOf(CLASS_SEPARATOR))) {
                 parseTestClass(className, builder);
             }
         }
 
+        String testFilePath = arguments.getString(ARGUMENT_TEST_FILE);
+        if (testFilePath != null) {
+            parseTestClassesFromFile(testFilePath, builder);
+        }
+
         String testPackage = arguments.getString(ARGUMENT_TEST_PACKAGE);
         if (testPackage != null) {
             builder.addTestPackageFilter(testPackage);
@@ -475,10 +496,10 @@
      *
      * @param testClassName - full package name of test class and optionally method to add.
      *        Expected format: com.android.TestClass#testMethod
-     * @param testSuiteBuilder - builder to add tests to
+     * @param testRequestBuilder - builder to add tests to
      */
     private void parseTestClass(String testClassName, TestRequestBuilder testRequestBuilder) {
-        int methodSeparatorIndex = testClassName.indexOf('#');
+        int methodSeparatorIndex = testClassName.indexOf(METHOD_SEPARATOR);
 
         if (methodSeparatorIndex > 0) {
             String testMethodName = testClassName.substring(methodSeparatorIndex + 1);
@@ -489,6 +510,38 @@
         }
     }
 
+    /**
+     * Parse and load the content of a test file
+     *
+     * @param filePath  path to test file contaitnig full package names of test classes and
+     *                  optionally methods to add.
+     * @param testRequestBuilder - builder to add tests to
+     */
+    private void parseTestClassesFromFile(String filePath, TestRequestBuilder testRequestBuilder) {
+        List<String> classes = new ArrayList<String>();
+        BufferedReader br = null;
+        String line;
+        try {
+            br = new BufferedReader(new FileReader(new File(filePath)));
+            while ((line = br.readLine()) != null) {
+                classes.add(line);
+            }
+        } catch (FileNotFoundException e) {
+            Log.e(LOG_TAG, String.format("File not found: %s", filePath), e);
+        } catch (IOException e) {
+            Log.e(LOG_TAG,
+                    String.format("Something went wrong reading %s, ignoring file", filePath), e);
+        } finally {
+            if (br != null) {
+                try { br.close(); } catch (IOException e) { /* ignore */ }
+            }
+        }
+
+        for (String className : classes) {
+            parseTestClass(className, testRequestBuilder);
+        }
+    }
+
     private void setupDexmakerClassloader() {
         ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
         // must set the context classloader for apps that use a shared uid, see
diff --git a/support/tests/src/android/support/test/internal/runner/TestRequestBuilderTest.java b/support/tests/src/android/support/test/internal/runner/TestRequestBuilderTest.java
index f28c894..f58f39f 100644
--- a/support/tests/src/android/support/test/internal/runner/TestRequestBuilderTest.java
+++ b/support/tests/src/android/support/test/internal/runner/TestRequestBuilderTest.java
@@ -627,7 +627,7 @@
     }
 
     /**
-     * Test that {@link RequiresDevuce} filters tests as appropriate
+     * Test that {@link RequiresDevice} filters tests as appropriate
      */
     @Test
     public void testRequiresDevice() {
diff --git a/support/tests/src/android/support/test/runner/AndroidJUnitRunnerTest.java b/support/tests/src/android/support/test/runner/AndroidJUnitRunnerTest.java
index 7952980..245612a 100644
--- a/support/tests/src/android/support/test/runner/AndroidJUnitRunnerTest.java
+++ b/support/tests/src/android/support/test/runner/AndroidJUnitRunnerTest.java
@@ -18,17 +18,25 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.support.test.internal.runner.TestRequestBuilder;
-import android.support.test.runner.AndroidJUnitRunner;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
 import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StringReader;
 
 /**
  * Unit tests for {@link AndroidJUnitRunner}.
@@ -99,4 +107,45 @@
         mAndroidJUnitRunner.buildRequest(b, mStubStream);
         Mockito.verify(mMockBuilder).addTestMethod("ClassName1", "method");
     }
+
+    /**
+     * Test {@link AndroidJUnitRunner#buildRequest(Bundle, PrintStream)} when
+     * class name and method name is provided along with an additional class name.
+     */
+    @Test
+    public void testBuildRequest_classAndMethodCombo() {
+        Bundle b = new Bundle();
+        b.putString(AndroidJUnitRunner.ARGUMENT_TEST_CLASS, "ClassName1#method,ClassName2");
+        mAndroidJUnitRunner.buildRequest(b, mStubStream);
+        Mockito.verify(mMockBuilder).addTestMethod("ClassName1", "method");
+        Mockito.verify(mMockBuilder).addTestClass("ClassName2");
+    }
+
+    /**
+     * Temp file used for testing
+     */
+    @Rule
+    public TemporaryFolder mTmpFolder = new TemporaryFolder();
+
+    /**
+     * Test {@link AndroidJUnitRunner#buildRequest(Bundle, PrintStream)} when
+     * multiple class and method names are provided within a test file
+     */
+    @Test
+    public void testBuildRequest_testFile() throws IOException {
+        final File file = mTmpFolder.newFile("myTestFile.txt");
+        BufferedWriter out = new BufferedWriter(new FileWriter(file));
+        out.write("ClassName3\n");
+        out.write("ClassName4#method2\n");
+        out.close();
+
+        Bundle b = new Bundle();
+        b.putString(AndroidJUnitRunner.ARGUMENT_TEST_FILE, file.getPath());
+        b.putString(AndroidJUnitRunner.ARGUMENT_TEST_CLASS, "ClassName1#method1,ClassName2");
+        mAndroidJUnitRunner.buildRequest(b, mStubStream);
+        Mockito.verify(mMockBuilder).addTestMethod("ClassName1", "method1");
+        Mockito.verify(mMockBuilder).addTestClass("ClassName2");
+        Mockito.verify(mMockBuilder).addTestClass("ClassName3");
+        Mockito.verify(mMockBuilder).addTestMethod("ClassName4", "method2");
+    }
 }