Improve BaseTestSuite and ModuleLoader

Allow moduleloader to be overriden for suite specific
checking and loading.
Fix setUpFilter in BaseTestSuite to load from subdirectory
too.
This will allow also to support cts sandboxing.

Test: unit tests,
run cts-suite -m CtsGesture
run cts-suite
Bug: 70778109
Bug: 65303193

Change-Id: I35f1f60db76f4f89a6d61363158314a9da8f57ac
diff --git a/src/com/android/tradefed/config/ConfigurationUtil.java b/src/com/android/tradefed/config/ConfigurationUtil.java
index 6e452ca..790914e 100644
--- a/src/com/android/tradefed/config/ConfigurationUtil.java
+++ b/src/com/android/tradefed/config/ConfigurationUtil.java
@@ -26,6 +26,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.lang.reflect.Field;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -167,6 +168,22 @@
      * @return the set of {@link File} that were found.
      */
     public static Set<File> getConfigNamesFileFromDirs(String subPath, List<File> dirs) {
+        List<String> patterns = new ArrayList<>();
+        patterns.add(".*.config");
+        patterns.add(".*.xml");
+        return getConfigNamesFileFromDirs(subPath, dirs, patterns);
+    }
+
+    /**
+     * Search a particular pattern of in the given directories.
+     *
+     * @param subPath The location where to look for configuration. Can be null.
+     * @param dirs A list of {@link File} of extra directories to search for test configs
+     * @param configNamePatterns the list of patterns for files to be found.
+     * @return the set of {@link File} that were found.
+     */
+    public static Set<File> getConfigNamesFileFromDirs(
+            String subPath, List<File> dirs, List<String> configNamePatterns) {
         Set<File> configNames = new HashSet<>();
         for (File dir : dirs) {
             if (subPath != null) {
@@ -177,8 +194,9 @@
                 continue;
             }
             try {
-                configNames.addAll(FileUtil.findFilesObject(dir, ".*.config"));
-                configNames.addAll(FileUtil.findFilesObject(dir, ".*.xml"));
+                for (String configNamePattern : configNamePatterns) {
+                    configNames.addAll(FileUtil.findFilesObject(dir, configNamePattern));
+                }
             } catch (IOException e) {
                 CLog.w("Failed to get test config files from directory %s", dir.getAbsolutePath());
             }
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index 7d81917..f79201f 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.ArrayUtil;
 
 import java.io.File;
@@ -131,7 +132,7 @@
                             + "Test Args:%s\nModule Args:%s\nIncludes:%s\nExcludes:%s",
                     abis, mTestArgs, mModuleArgs, mIncludeFiltersParsed, mExcludeFiltersParsed);
             mModuleRepo =
-                    new SuiteModuleLoader(
+                    createModuleLoader(
                             mIncludeFiltersParsed, mExcludeFiltersParsed, mTestArgs, mModuleArgs);
             // Actual loading of the configurations.
             return loadingStrategy(abis, testsDir, mSuitePrefix, mSuiteTag);
@@ -177,24 +178,63 @@
         super.setBuild(buildInfo);
     }
 
+    /** Sets include-filters for the compatibility test */
+    public void setIncludeFilter(Set<String> includeFilters) {
+        mIncludeFilters.addAll(includeFilters);
+    }
+
+    /** Sets exclude-filters for the compatibility test */
+    public void setExcludeFilter(Set<String> excludeFilters) {
+        mExcludeFilters.addAll(excludeFilters);
+    }
+
+    /** Returns the current {@link SuiteModuleLoader}. */
+    public SuiteModuleLoader getModuleLoader() {
+        return mModuleRepo;
+    }
+
+    /**
+     * Create the {@link SuiteModuleLoader} responsible to load the {@link IConfiguration} and
+     * assign them some of the options.
+     *
+     * @param includeFiltersFormatted The formatted and parsed include filters.
+     * @param excludeFiltersFormatted The formatted and parsed exclude filters.
+     * @param testArgs the list of test ({@link IRemoteTest}) arguments.
+     * @param moduleArgs the list of module arguments.
+     * @return the created {@link SuiteModuleLoader}.
+     */
+    public SuiteModuleLoader createModuleLoader(
+            Map<String, List<SuiteTestFilter>> includeFiltersFormatted,
+            Map<String, List<SuiteTestFilter>> excludeFiltersFormatted,
+            List<String> testArgs,
+            List<String> moduleArgs) {
+        return new SuiteModuleLoader(
+                includeFiltersFormatted, excludeFiltersFormatted, testArgs, moduleArgs);
+    }
+
     /**
      * Sets the include/exclude filters up based on if a module name was given.
      *
      * @throws FileNotFoundException if any file is not found.
      */
-    protected void setupFilters(File testDir) throws FileNotFoundException {
+    protected void setupFilters(File testsDir) throws FileNotFoundException {
         if (mModuleName != null) {
-            List<String> modules = SuiteModuleLoader.getModuleNamesMatching(testDir, mModuleName);
+            // If this option (-m / --module) is set only the matching unique module should run.
+            Set<File> modules =
+                    SuiteModuleLoader.getModuleNamesMatching(
+                            testsDir, mSuitePrefix, String.format("%s.*.config", mModuleName));
             if (modules.size() == 0) {
                 throw new IllegalArgumentException(
                         String.format("No modules found matching %s", mModuleName));
             } else if (modules.size() > 1) {
                 throw new IllegalArgumentException(
                         String.format(
-                                "Multiple modules found matching %s:\n%s\nWhich one did you mean?\n",
+                                "Multiple modules found matching %s:\n%s\nWhich one did you "
+                                        + "mean?\n",
                                 mModuleName, ArrayUtil.join("\n", modules)));
             } else {
-                String moduleName = modules.get(0);
+                File mod = modules.iterator().next();
+                String moduleName = mod.getName().replace(".config", "");
                 checkFilters(mIncludeFilters, moduleName);
                 checkFilters(mExcludeFilters, moduleName);
                 mIncludeFilters.add(
@@ -217,19 +257,4 @@
         filters.clear();
         filters.addAll(cleanedFilters);
     }
-
-    /** Sets include-filters for the compatibility test */
-    public void setIncludeFilter(Set<String> includeFilters) {
-        mIncludeFilters.addAll(includeFilters);
-    }
-
-    /** Sets exclude-filters for the compatibility test */
-    public void setExcludeFilter(Set<String> excludeFilters) {
-        mExcludeFilters.addAll(excludeFilters);
-    }
-
-    /** Returns the current {@link SuiteModuleLoader}. */
-    public SuiteModuleLoader getModuleLoader() {
-        return mModuleRepo;
-    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 3129097..6438730 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -120,6 +120,38 @@
     }
 
     /**
+     * Pass the filters to the {@link IRemoteTest}. Default behavior is to ignore if the IRemoteTest
+     * does not implements {@link ITestFileFilterReceiver}. This can be overriden to create a more
+     * restrictive behavior.
+     *
+     * @param test The {@link IRemoteTest} that is being considered.
+     * @param abi The Abi we are currently working on.
+     * @param name The name of the module.
+     * @param includeFilters The formatted and parsed include filters.
+     * @param excludeFilters The formatted and parsed exclude filters.
+     */
+    public void addFiltersToTest(
+            IRemoteTest test,
+            IAbi abi,
+            String name,
+            Map<String, List<SuiteTestFilter>> includeFilters,
+            Map<String, List<SuiteTestFilter>> excludeFilters) {
+        String moduleId = AbiUtils.createId(abi.getName(), name);
+        if (!(test instanceof ITestFilterReceiver)) {
+            CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId);
+            return;
+        }
+        List<SuiteTestFilter> mdIncludes = getFilterList(includeFilters, moduleId);
+        List<SuiteTestFilter> mdExcludes = getFilterList(excludeFilters, moduleId);
+        if (!mdIncludes.isEmpty()) {
+            addTestIncludes((ITestFilterReceiver) test, mdIncludes, name);
+        }
+        if (!mdExcludes.isEmpty()) {
+            addTestExcludes((ITestFilterReceiver) test, mdExcludes, name);
+        }
+    }
+
+    /**
      * Load a single config location (file or on TF classpath). It can results in several {@link
      * IConfiguration}. If a single configuration get expanded in different ways.
      *
@@ -181,7 +213,7 @@
                         testArgsMap.putAll(mTestArgs.get(className));
                     }
                     injectOptionsToConfig(testArgsMap, config);
-                    addFiltersToTest(test, abi, name);
+                    addFiltersToTest(test, abi, name, mIncludeFilters, mExcludeFilters);
                     if (test instanceof IAbiReceiver) {
                         ((IAbiReceiver) test).setAbi(abi);
                     }
@@ -218,30 +250,15 @@
         }
     }
 
-    /** @return the {@link List} of modules whose name contains the given pattern. */
-    public static List<String> getModuleNamesMatching(File directory, String pattern) {
-        String[] names =
-                directory.list(
-                        new FilenameFilter() {
-                            @Override
-                            public boolean accept(File dir, String name) {
-                                return name.contains(pattern) && name.endsWith(CONFIG_EXT);
-                            }
-                        });
-        List<String> modules = new ArrayList<String>(names.length);
-        for (String name : names) {
-            int index = name.indexOf(CONFIG_EXT);
-            if (index > 0) {
-                String module = name.substring(0, index);
-                if (module.equals(pattern)) {
-                    // Pattern represents a single module, just return a single-item list
-                    modules = new ArrayList<>(1);
-                    modules.add(module);
-                    return modules;
-                }
-                modules.add(module);
-            }
-        }
+    /** @return the {@link Set} of modules whose name contains the given pattern. */
+    public static Set<File> getModuleNamesMatching(
+            File directory, String suitePrefix, String pattern) {
+        List<File> extraTestCasesDirs = Arrays.asList(directory);
+        List<String> patterns = new ArrayList<>();
+        patterns.add(pattern);
+        Set<File> modules =
+                ConfigurationUtil.getConfigNamesFileFromDirs(
+                        suitePrefix, extraTestCasesDirs, patterns);
         return modules;
     }
 
@@ -282,22 +299,6 @@
         return fs;
     }
 
-    private void addFiltersToTest(IRemoteTest test, IAbi abi, String name) {
-        String moduleId = AbiUtils.createId(abi.getName(), name);
-        if (!(test instanceof ITestFilterReceiver)) {
-            CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId);
-            return;
-        }
-        List<SuiteTestFilter> mdIncludes = getFilterList(mIncludeFilters, moduleId);
-        List<SuiteTestFilter> mdExcludes = getFilterList(mExcludeFilters, moduleId);
-        if (!mdIncludes.isEmpty()) {
-            addTestIncludes((ITestFilterReceiver) test, mdIncludes, name);
-        }
-        if (!mdExcludes.isEmpty()) {
-            addTestExcludes((ITestFilterReceiver) test, mdExcludes, name);
-        }
-    }
-
     private boolean shouldRunModule(String moduleId) {
         List<SuiteTestFilter> mdIncludes = getFilterList(mIncludeFilters, moduleId);
         List<SuiteTestFilter> mdExcludes = getFilterList(mExcludeFilters, moduleId);
diff --git a/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java b/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
index f43e6d3..76cd8be 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
@@ -103,4 +103,33 @@
             FileUtil.recursiveDelete(tmpDir);
         }
     }
+
+    /**
+     * Test {@link ConfigurationUtil#getConfigNamesFileFromDirs(String, List, List)} can search for
+     * file in directories based on patterns.
+     */
+    @Test
+    public void testGetConfigNamesFromDirs_patterns() throws Exception {
+        File tmpDir = null;
+        try {
+            tmpDir = FileUtil.createTempDir("test_configs_dir");
+            // Random file located in the root directory
+            FileUtil.createTempFile("whatevername", ".whateverextension", tmpDir);
+            // Other file located in a sub directory
+            File subDir = FileUtil.getFileForPath(tmpDir, "sub");
+            FileUtil.mkdirsRWX(subDir);
+            File config2 = FileUtil.createTempFile("config", ".otherext", subDir);
+
+            List<String> patterns = new ArrayList<>();
+            patterns.add(".*.other.*");
+            // Test getConfigNamesFileFromDirs only locate configs under subPath.
+            Set<File> configs =
+                    ConfigurationUtil.getConfigNamesFileFromDirs(
+                            "sub", Arrays.asList(tmpDir), patterns);
+            assertEquals(1, configs.size());
+            assertTrue(configs.contains(config2));
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
 }