Add source information in TestMapping's TestInfo

The source defines where the test is originated in TEST_MAPPING file located
in the source tree. The information will be saved to invocation context and
used in reporting.

Bug: 116817899
Test: unittest
Change-Id: Id5cfc76cc571720e4828d391f62b2b83844ce2ad
diff --git a/src/com/android/tradefed/util/testmapping/TestInfo.java b/src/com/android/tradefed/util/testmapping/TestInfo.java
index 37d5e97..8d1e521 100644
--- a/src/com/android/tradefed/util/testmapping/TestInfo.java
+++ b/src/com/android/tradefed/util/testmapping/TestInfo.java
@@ -32,9 +32,12 @@
 
     private String mName = null;
     private List<TestOption> mOptions = new ArrayList<TestOption>();
+    // A list of locations with TEST_MAPPING files that containing the test.
+    private Set<String> mSources = new HashSet<String>();
 
-    public TestInfo(String name) {
+    public TestInfo(String name, String source) {
         mName = name;
+        mSources.add(source);
     }
 
     public String getName() {
@@ -49,6 +52,14 @@
         return mOptions;
     }
 
+    public void addSources(Set<String> sources) {
+        mSources.addAll(sources);
+    }
+
+    public Set<String> getSources() {
+        return mSources;
+    }
+
     /**
      * Merge with another test.
      *
@@ -194,6 +205,7 @@
             mergedOptions.add(option);
         }
         this.mOptions = mergedOptions;
+        this.addSources(test.getSources());
         CLog.d("Options are merged, updated test: %s.", this);
     }
 
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index 3ff8116..0baab43 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -59,9 +59,11 @@
      * Constructor to create a {@link TestMapping} object from a path to TEST_MAPPING file.
      *
      * @param path The {@link Path} to a TEST_MAPPING file.
+     * @param testMappingsDir The {@link Path} to the folder of all TEST_MAPPING files for a build.
      */
-    public TestMapping(Path path) {
+    public TestMapping(Path path, Path testMappingsDir) {
         mTestCollection = new LinkedHashMap<>();
+        String relativePath = testMappingsDir.relativize(path.getParent()).toString();
         String errorMessage = null;
         try {
             String content = String.join("", Files.readAllLines(path, StandardCharsets.UTF_8));
@@ -81,7 +83,7 @@
                     JSONArray arr = root.getJSONArray(group);
                     for (int i = 0; i < arr.length(); i++) {
                         JSONObject testObject = arr.getJSONObject(i);
-                        TestInfo test = new TestInfo(testObject.getString(KEY_NAME));
+                        TestInfo test = new TestInfo(testObject.getString(KEY_NAME), relativePath);
                         if (testObject.has(KEY_OPTIONS)) {
                             JSONArray optionObjects = testObject.getJSONArray(KEY_OPTIONS);
                             for (int j = 0; j < optionObjects.length(); j++) {
@@ -179,12 +181,14 @@
         Stream<Path> stream = null;
         try {
             testMappingsDir = ZipUtil2.extractZipToTemp(testMappingsZip, TEST_MAPPINGS_ZIP);
-            stream =
-                    Files.walk(
-                            Paths.get(testMappingsDir.getAbsolutePath()),
-                            FileVisitOption.FOLLOW_LINKS);
+            Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
+            stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS);
             stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
-                    .forEach(path -> tests.addAll((new TestMapping(path)).getTests(testGroup)));
+                    .forEach(
+                            path ->
+                                    tests.addAll(
+                                            (new TestMapping(path, testMappingsRootPath))
+                                                    .getTests(testGroup)));
         } catch (IOException e) {
             RuntimeException runtimeException =
                     new RuntimeException(
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index 05d175b..a10ace5 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -52,11 +52,18 @@
             tempDir = FileUtil.createTempDir("test_mapping");
             String srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
             InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
-            testMappingFile = FileUtil.saveResourceFile(resourceStream, tempDir, TEST_MAPPING);
-            List<TestInfo> tests = new TestMapping(testMappingFile.toPath()).getTests("presubmit");
+            File testMappingRootDir = FileUtil.createTempDir("subdir", tempDir);
+            String rootDirName = testMappingRootDir.getName();
+            testMappingFile =
+                    FileUtil.saveResourceFile(resourceStream, testMappingRootDir, TEST_MAPPING);
+            List<TestInfo> tests =
+                    new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
+                            .getTests("presubmit");
             assertEquals(1, tests.size());
             assertEquals("test1", tests.get(0).getName());
-            tests = new TestMapping(testMappingFile.toPath()).getTests("postsubmit");
+            tests =
+                    new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
+                            .getTests("postsubmit");
             assertEquals(3, tests.size());
             assertEquals("test2", tests.get(0).getName());
             TestOption option = tests.get(0).getOptions().get(0);
@@ -64,9 +71,13 @@
             assertEquals(
                     "annotation=android.platform.test.annotations.Presubmit", option.getValue());
             assertEquals("instrument", tests.get(1).getName());
-            tests = new TestMapping(testMappingFile.toPath()).getTests("othertype");
+            tests =
+                    new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
+                            .getTests("othertype");
             assertEquals(1, tests.size());
             assertEquals("test3", tests.get(0).getName());
+            assertEquals(1, tests.get(0).getSources().size());
+            assertTrue(tests.get(0).getSources().contains(rootDirName));
         } finally {
             FileUtil.recursiveDelete(tempDir);
         }
@@ -81,7 +92,9 @@
             tempDir = FileUtil.createTempDir("test_mapping");
             File testMappingFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPING).toFile();
             FileUtil.writeToFile("bad format json file", testMappingFile);
-            List<TestInfo> tests = new TestMapping(testMappingFile.toPath()).getTests("presubmit");
+            List<TestInfo> tests =
+                    new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
+                            .getTests("presubmit");
         } finally {
             FileUtil.recursiveDelete(tempDir);
         }
@@ -131,8 +144,8 @@
      */
     @Test(expected = RuntimeException.class)
     public void testMergeFailByName() throws Exception {
-        TestInfo test1 = new TestInfo("test1");
-        TestInfo test2 = new TestInfo("test2");
+        TestInfo test1 = new TestInfo("test1", "folder1");
+        TestInfo test2 = new TestInfo("test2", "folder1");
         test1.merge(test2);
     }
 
@@ -143,14 +156,14 @@
     @Test
     public void testMergeSuccess() throws Exception {
         // Check that the test without any option should be the merge result.
-        TestInfo test1 = new TestInfo("test1");
-        TestInfo test2 = new TestInfo("test1");
+        TestInfo test1 = new TestInfo("test1", "folder1");
+        TestInfo test2 = new TestInfo("test1", "folder1");
         test2.addOption(new TestOption("include-filter", "value"));
         test1.merge(test2);
         assertTrue(test1.getOptions().isEmpty());
 
-        test1 = new TestInfo("test1");
-        test2 = new TestInfo("test1");
+        test1 = new TestInfo("test1", "folder1");
+        test2 = new TestInfo("test1", "folder1");
         test1.addOption(new TestOption("include-filter", "value"));
         test1.merge(test2);
         assertTrue(test1.getOptions().isEmpty());
@@ -163,8 +176,8 @@
     @Test
     public void testMergeSuccess_2Filters() throws Exception {
         // Check that the test without any option should be the merge result.
-        TestInfo test1 = new TestInfo("test1");
-        TestInfo test2 = new TestInfo("test1");
+        TestInfo test1 = new TestInfo("test1", "folder1");
+        TestInfo test2 = new TestInfo("test1", "folder2");
         TestOption option1 = new TestOption("include-filter", "value1");
         test1.addOption(option1);
         TestOption option2 = new TestOption("include-filter", "value2");
@@ -173,6 +186,9 @@
         assertEquals(2, test1.getOptions().size());
         assertTrue(new HashSet<TestOption>(test1.getOptions()).contains(option1));
         assertTrue(new HashSet<TestOption>(test1.getOptions()).contains(option2));
+        assertEquals(2, test1.getSources().size());
+        assertTrue(test1.getSources().contains("folder1"));
+        assertTrue(test1.getSources().contains("folder2"));
     }
 
     /**
@@ -182,8 +198,8 @@
     @Test
     public void testMergeSuccess_multiFilters() throws Exception {
         // Check that the test without any option should be the merge result.
-        TestInfo test1 = new TestInfo("test1");
-        TestInfo test2 = new TestInfo("test1");
+        TestInfo test1 = new TestInfo("test1", "folder1");
+        TestInfo test2 = new TestInfo("test1", "folder2");
         TestOption inclusiveOption1 = new TestOption("include-filter", "value1");
         test1.addOption(inclusiveOption1);
         TestOption exclusiveOption1 = new TestOption("exclude-filter", "exclude-value1");
@@ -213,6 +229,10 @@
         // Options from test2.
         assertTrue(mergedOptions.contains(inclusiveOption2));
         assertTrue(mergedOptions.contains(otherOption2));
+        // Both folders are in sources
+        assertEquals(2, test1.getSources().size());
+        assertTrue(test1.getSources().contains("folder1"));
+        assertTrue(test1.getSources().contains("folder2"));
     }
 
     /**
@@ -223,8 +243,8 @@
     public void testMergeSuccess_MultiFilters_dropIncludeAnnotation() throws Exception {
         // Check that the test without all options except include-annotation option should be the
         // merge result.
-        TestInfo test1 = new TestInfo("test1");
-        TestInfo test2 = new TestInfo("test1");
+        TestInfo test1 = new TestInfo("test1", "folder1");
+        TestInfo test2 = new TestInfo("test1", "folder1");
         TestOption option1 = new TestOption("include-filter", "value1");
         test1.addOption(option1);
         TestOption optionIncludeAnnotation =
@@ -246,8 +266,8 @@
     public void testMergeSuccess_MultiFilters_keepExcludeAnnotation() throws Exception {
         // Check that the test without all options including exclude-annotation option should be the
         // merge result.
-        TestInfo test1 = new TestInfo("test1");
-        TestInfo test2 = new TestInfo("test1");
+        TestInfo test1 = new TestInfo("test1", "folder1");
+        TestInfo test2 = new TestInfo("test1", "folder1");
         TestOption option1 = new TestOption("include-filter", "value1");
         test1.addOption(option1);
         TestOption optionExcludeAnnotation1 =