Make ClassPathScanner optionally work with class directories

IDEA runs tests from class directories rather than JARs. The
ClassPathScanner only works for JARs and so tests that rely on
the ClassPathScanner do not work within IDEA. This adds a system
property vogar-scan-directories-for-test which when set to true
will cause the ClassPathScanner to handle class directories.

Bug: 27940141
Change-Id: Ia1a09e1c20fc11c1389ac524570e4a986cd7cacb
diff --git a/src/vogar/target/ClassPathScanner.java b/src/vogar/target/ClassPathScanner.java
index af65b15..af079e5 100644
--- a/src/vogar/target/ClassPathScanner.java
+++ b/src/vogar/target/ClassPathScanner.java
@@ -49,9 +49,18 @@
 
     ClassPathScanner() {
         classPath = getClassPath();
-        classFinder = "Dalvik".equals(System.getProperty("java.vm.name"))
-                ? new ApkClassFinder()
-                : new JarClassFinder();
+        if ("Dalvik".equals(System.getProperty("java.vm.name"))) {
+            classFinder = new ApkClassFinder();
+        } else {
+            // When running vogar tests under an IDE the classes are not held in a .jar file.
+            // This system properties can be set to make it possible to run the vogar tests from an
+            // IDE. It is not intended for normal usage.
+            if (Boolean.parseBoolean(System.getProperty("vogar-scan-directories-for-tests"))) {
+                classFinder = new DirectoryClassFinder();
+            } else {
+                classFinder = new JarClassFinder();
+            }
+        }
     }
 
     /**
@@ -59,9 +68,9 @@
      * {@code packageName}.
      */
     public Package scan(String packageName) throws IOException {
-        Set<String> subpackageNames = new TreeSet<String>();
-        Set<String> classNames = new TreeSet<String>();
-        Set<Class<?>> topLevelClasses = new TreeSet<Class<?>>(ORDER_CLASS_BY_NAME);
+        Set<String> subpackageNames = new TreeSet<>();
+        Set<String> classNames = new TreeSet<>();
+        Set<Class<?>> topLevelClasses = new TreeSet<>(ORDER_CLASS_BY_NAME);
         findClasses(packageName, classNames, subpackageNames);
         for (String className : classNames) {
             try {
@@ -84,7 +93,7 @@
         String pathPrefix = packagePrefix.replace('.', '/');
         for (String entry : classPath) {
             File entryFile = new File(entry);
-            if (entryFile.exists() && !entryFile.isDirectory()) {
+            if (entryFile.exists()) {
                 classFinder.find(entryFile, pathPrefix, packageName, classNames, subpackageNames);
             }
         }
@@ -99,9 +108,13 @@
      * Finds all classes and subpackages that are below the packageName and
      * add them to the respective sets. Searches the package in a single jar file.
      */
-    static class JarClassFinder implements ClassFinder {
+    private static class JarClassFinder implements ClassFinder {
         public void find(File classPathEntry, String pathPrefix, String packageName,
                 Set<String> classNames, Set<String> subpackageNames) throws IOException {
+            if (classPathEntry.isDirectory()) {
+                return;
+            }
+
             Set<String> entryNames = getJarEntries(classPathEntry);
             // check if the Jar contains the package.
             if (!entryNames.contains(pathPrefix)) {
@@ -129,7 +142,7 @@
          * Gets the class and package entries from a Jar.
          */
         private Set<String> getJarEntries(File jarFile) throws IOException {
-            Set<String> entryNames = new HashSet<String>();
+            Set<String> entryNames = new HashSet<>();
             ZipFile zipFile = new ZipFile(jarFile);
             for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
                 String entryName = e.nextElement().getName();
@@ -164,15 +177,44 @@
     }
 
     /**
+     * Finds all classes and subpackages that are below the packageName and
+     * add them to the respective sets. Searches the package from a class directory.
+     */
+    private static class DirectoryClassFinder implements ClassFinder {
+        public void find(File classPathEntry, String pathPrefix, String packageName,
+                Set<String> classNames, Set<String> subpackageNames) throws IOException {
+
+            File subDir = new File(classPathEntry, pathPrefix);
+            if (subDir.exists() && subDir.isDirectory()) {
+                File[] files = subDir.listFiles();
+                if (files != null) {
+                    for (File subFile : files) {
+                        String fileName = subFile.getName();
+                        if (fileName.endsWith(DOT_CLASS)) {
+                            classNames.add(packageName + "." + getClassName(fileName));
+                        } else if (subFile.isDirectory()) {
+                            subpackageNames.add(packageName + "." + fileName);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
      * Finds all classes and sub packages that are below the packageName and
      * add them to the respective sets. Searches the package in a single APK.
      *
      * <p>This class uses the Android-only class DexFile. This class will fail
      * to load on non-Android VMs.
      */
-    static class ApkClassFinder implements ClassFinder {
+    private static class ApkClassFinder implements ClassFinder {
         public void find(File classPathEntry, String pathPrefix, String packageName,
                 Set<String> classNames, Set<String> subpackageNames) {
+            if (classPathEntry.isDirectory()) {
+                return;
+            }
+
             DexFile dexFile = null;
             try {
                 dexFile = new DexFile(classPathEntry);
diff --git a/test/vogar/target/TestRunnerTest.java b/test/vogar/target/TestRunnerTest.java
index d3317c3..0c8e227 100644
--- a/test/vogar/target/TestRunnerTest.java
+++ b/test/vogar/target/TestRunnerTest.java
@@ -75,7 +75,8 @@
 
     /**
      * If this fails with a "No classes in package: vogar.target.mixture;" error then the tests are
-     * not being run from a JAR. This is usually only a problem in IDEs.
+     * not being run from a JAR, add {@code -Dvogar-scan-directories-for-tests=true} to the
+     * command line to get it working properly. This is usually only a problem in IDEs.
      */
     @TestRunnerProperties(testClassOrPackage = "vogar.target.mixture")
     @Test