TestMappingSuiteRunner: Run tests in TestMapping base on option
test-mapping-path.

This CL is to run with multiple test options defined in TEST_MAPPING file
for presubmit tests. In the past, these test options were merged in the
Test Provider Service and might lose some test coverage. The design doc:
go/run-test-mapping-presubmit-tests

Bug: 117880789
Test: unittests.
Change-Id: I0451012a36485a61c0c552ed48276baa86304de9
Merged-In: I0451012a36485a61c0c552ed48276baa86304de9
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index 8eab13f..d32a1fc 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -126,8 +126,16 @@
                     "If options --test-mapping-test-group is set, option --include-filter should "
                             + "not be set.");
         }
+        if (!includeFilter.isEmpty() && !mTestMappingPaths.isEmpty()) {
+            throw new RuntimeException(
+                    "If option --include-filter is set, option --test-mapping-path should "
+                            + "not be set.");
+        }
 
         if (mTestGroup != null) {
+            if (!mTestMappingPaths.isEmpty()) {
+                TestMapping.setTestMappingPaths(mTestMappingPaths);
+            }
             testInfosToRun =
                     TestMapping.getTests(
                             getBuildInfo(), mTestGroup, getPrioritizeHostConfig(), mKeywords);
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index a550ec0..93419c0 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -34,6 +34,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -70,6 +71,20 @@
             "(?m)[\\s\\t]*(//|#).*|(\".*?\")");
     private static final Set<String> COMMENTS = new HashSet<>(Arrays.asList("#", "//"));
 
+    private static List<String> mTestMappingRelativePaths = new ArrayList<>();
+
+    /**
+     * Set the TEST_MAPPING paths inside of TEST_MAPPINGS_ZIP to limit loading the TEST_MAPPING.
+     *
+     * @param relativePaths A {@code List<String>} of TEST_MAPPING paths relative to
+     *     TEST_MAPPINGS_ZIP.
+     */
+    public static void setTestMappingPaths(List<String> relativePaths) {
+        mTestMappingRelativePaths.clear();
+        mTestMappingRelativePaths.addAll(relativePaths);
+    }
+
+
     /**
      * Constructor to create a {@link TestMapping} object from a path to TEST_MAPPING file.
      *
@@ -246,6 +261,39 @@
     }
 
     /**
+     * Helper to get all TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
+     *
+     * @param testMappingsRootPath The {@link Path} to a test mappings zip path.
+     * @return A {@code Set<Path>} of all the TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
+     */
+    @VisibleForTesting
+    static Set<Path> getAllTestMappingPaths(Path testMappingsRootPath) {
+        Set<Path> allTestMappingPaths = new HashSet<>();
+        for (String path : mTestMappingRelativePaths) {
+            boolean hasAdded = false;
+            Path testMappingPath = testMappingsRootPath.resolve(path);
+            // Recursively find the TEST_MAPPING file until reaching to testMappingsRootPath.
+            while (!testMappingPath.equals(testMappingsRootPath)) {
+                if (testMappingPath.resolve(TEST_MAPPING).toFile().exists()) {
+                    hasAdded = true;
+                    CLog.d("Adding TEST_MAPPING path: %s", testMappingPath);
+                    allTestMappingPaths.add(testMappingPath.resolve(TEST_MAPPING));
+                }
+                testMappingPath = testMappingPath.getParent();
+            }
+            if (!hasAdded) {
+                CLog.w("Couldn't find TEST_MAPPING files from %s", path);
+            }
+        }
+        if (allTestMappingPaths.isEmpty()) {
+            throw new RuntimeException(
+                    String.format(
+                            "Couldn't find TEST_MAPPING files from %s", mTestMappingRelativePaths));
+        }
+        return allTestMappingPaths;
+    }
+
+    /**
      * Helper to find all tests in all TEST_MAPPING files. This is needed when a suite run requires
      * to run all tests in TEST_MAPPING files for a given group, e.g., presubmit.
      *
@@ -263,7 +311,12 @@
         try {
             Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
             Set<String> disabledTests = getDisabledTests(testMappingsRootPath, testGroup);
-            stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS);
+            if (mTestMappingRelativePaths.isEmpty()) {
+                stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS);
+            }
+            else {
+                stream = getAllTestMappingPaths(testMappingsRootPath).stream();
+            }
             stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
                     .forEach(
                             path ->
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index 7ac65a4..342bd93 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -163,6 +163,17 @@
         mRunner.loadTests();
     }
 
+    /**
+     * Test for {@link TestMappingSuiteRunner#loadTests()} to fail when both options include-filter
+     * and test-mapping-path are set.
+     */
+    @Test(expected = RuntimeException.class)
+    public void testLoadTests_conflictOptions() throws Exception {
+        mOptionSetter.setOptionValue("include-filter", "test1");
+        mOptionSetter.setOptionValue("test-mapping-path", "path1");
+        mRunner.loadTests();
+    }
+
     /** Test for {@link TestMappingSuiteRunner#loadTests()} to fail when no test option is set. */
     @Test(expected = RuntimeException.class)
     public void testLoadTests_noOption() throws Exception {
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index 0d69264..c1537ca 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -36,6 +36,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -228,6 +229,93 @@
     }
 
     /**
+     * Test for {@link TestMapping#getAllTestMappingPaths(Path)} to get TEST_MAPPING files from
+     * child directory.
+     */
+    @Test
+    public void testGetAllTestMappingPaths_FromChildDirectory() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            Path testMappingsRootPath = Paths.get(tempDir.getAbsolutePath());
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<String> testMappingRelativePaths = new ArrayList<>();
+            Path relPath = testMappingsRootPath.relativize(Paths.get(subDir.getAbsolutePath()));
+            testMappingRelativePaths.add(relPath.toString());
+            TestMapping.setTestMappingPaths(testMappingRelativePaths);
+            Set<Path> paths = TestMapping.getAllTestMappingPaths(testMappingsRootPath);
+            assertEquals(2, paths.size());
+        } finally {
+            TestMapping.setTestMappingPaths(new ArrayList<>());
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMapping#getAllTestMappingPaths(Path)} to get TEST_MAPPING files from
+     * parent directory.
+     */
+    @Test
+    public void testGetAllTestMappingPaths_FromParentDirectory() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            Path testMappingsRootPath = Paths.get(tempDir.getAbsolutePath());
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<String> testMappingRelativePaths = new ArrayList<>();
+            Path relPath = testMappingsRootPath.relativize(Paths.get(srcDir.getAbsolutePath()));
+            testMappingRelativePaths.add(relPath.toString());
+            TestMapping.setTestMappingPaths(testMappingRelativePaths);
+            Set<Path> paths = TestMapping.getAllTestMappingPaths(testMappingsRootPath);
+            assertEquals(1, paths.size());
+        } finally {
+            TestMapping.setTestMappingPaths(new ArrayList<>());
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMapping#getAllTestMappingPaths(Path)} to fail when no TEST_MAPPING files
+     * found.
+     */
+    @Test(expected = RuntimeException.class)
+    public void testGetAllTestMappingPaths_NoFilesFound() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            Path testMappingsRootPath = Paths.get(tempDir.getAbsolutePath());
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+
+            List<String> testMappingRelativePaths = new ArrayList<>();
+            Path relPath = testMappingsRootPath.relativize(Paths.get(srcDir.getAbsolutePath()));
+            testMappingRelativePaths.add(relPath.toString());
+            TestMapping.setTestMappingPaths(testMappingRelativePaths);
+            // No TEST_MAPPING files should be found according to the srcDir, getAllTestMappingPaths
+            // method shall raise RuntimeException.
+            TestMapping.getAllTestMappingPaths(testMappingsRootPath);
+        } finally {
+            TestMapping.setTestMappingPaths(new ArrayList<>());
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
      * Test for {@link TestInfo#merge()} for merging two TestInfo objects to fail when module names
      * are different.
      */