TestMapping: Support comments in TEST_MAPPING file.

Reason: TEST_MAPPING file uses JSON format, which doesn't support
comments. There is a request that it would be helpful if support
comments in TEST_MAPPING file.

Bug: 118691442

Test: unittests.
Change-Id: I35eae804a88cd38a9caa63000da99db80d56135e
Merged-In: I35eae804a88cd38a9caa63000da99db80d56135e
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index 79da7c2..fdc019d 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -42,6 +42,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -63,6 +65,10 @@
     private static final String DISABLED_PRESUBMIT_TESTS_FILE = "disabled-presubmit-tests";
 
     private Map<String, Set<TestInfo>> mTestCollection = null;
+    // Pattern used to identify comments start with "//" or "#" in TEST_MAPPING.
+    private static final Pattern COMMENTS_REGEX = Pattern.compile(
+            "(?m)[\\s\\t]*(//|#).*|(\".*?\")");
+    private static final Set<String> COMMENTS = new HashSet<>(Arrays.asList("#", "//"));
 
     /**
      * Constructor to create a {@link TestMapping} object from a path to TEST_MAPPING file.
@@ -75,7 +81,8 @@
         String relativePath = testMappingsDir.relativize(path.getParent()).toString();
         String errorMessage = null;
         try {
-            String content = String.join("", Files.readAllLines(path, StandardCharsets.UTF_8));
+            String content = removeComments(
+                    String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)));
             if (content != null) {
                 JSONTokener tokener = new JSONTokener(content);
                 JSONObject root = new JSONObject(tokener);
@@ -139,6 +146,26 @@
     }
 
     /**
+     * Helper to remove comments in a TEST_MAPPING file to valid format. Only "//" and "#" are
+     * regarded as comments.
+     *
+     * @param jsonContent A {@link String} of json which content is from a TEST_MAPPING file.
+     * @return A {@link String} of valid json without comments.
+     */
+    @VisibleForTesting
+    static String removeComments(String jsonContent) {
+        StringBuffer out = new StringBuffer();
+        Matcher matcher = COMMENTS_REGEX.matcher(jsonContent);
+        while (matcher.find()) {
+            if (COMMENTS.contains(matcher.group(1))) {
+                matcher.appendReplacement(out, "");
+            }
+        }
+        matcher.appendTail(out);
+        return out.toString();
+    }
+
+    /**
      * Helper to get all tests set in a TEST_MAPPING file for a given group.
      *
      * @param testGroup A {@link String} of the test group.
diff --git a/tests/res/testdata/test_mapping_golden1 b/tests/res/testdata/test_mapping_golden1
new file mode 100644
index 0000000..db3998d
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden1
@@ -0,0 +1,14 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true,
+      "include-filter": "testClass#testMethod"
+    }
+  ],
+  "imports": [
+    {
+      "path": "path1//path2//path3"
+    }
+  ]
+}
diff --git a/tests/res/testdata/test_mapping_golden2 b/tests/res/testdata/test_mapping_golden2
new file mode 100644
index 0000000..07486c0
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden2
@@ -0,0 +1,48 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true
+    },
+    {
+      "name": "suite/stub1"
+    },
+    {
+      "name": "suite/stub2",
+      "keywords": ["key_1"]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "test2",
+      "options": [
+        {
+          "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit"
+        }
+      ]
+    },
+    {
+      "name": "instrument",
+      "options": [
+        {
+          "run-name": "some-name"
+        }
+      ]
+    }
+  ],
+  "othertype": [
+    {
+      "name": "test3",
+      "options": [
+        {
+          "just-an-option": ""
+        }
+      ]
+    }
+  ],
+  "imports": [
+    {
+      "path": "folder1//folder2//folder3"
+    }
+  ]
+}
diff --git a/tests/res/testdata/test_mapping_with_comments1 b/tests/res/testdata/test_mapping_with_comments1
new file mode 100644
index 0000000..3f4083f
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments1
@@ -0,0 +1,16 @@
+{#comments1
+  "presubmit": [//comments2 // comments3 # comment4
+  #comments3
+    { #comments4
+      "name": "test1",#comments5
+//comments6
+      "host": true,//comments7
+      "include-filter": "testClass#testMethod" #comment11 // another comments
+    }#comments8
+  ],#comments9 // another comments
+  "imports": [
+    {
+      "path": "path1//path2//path3"#comment12
+    }
+  ]
+}#comments10
diff --git a/tests/res/testdata/test_mapping_with_comments2 b/tests/res/testdata/test_mapping_with_comments2
new file mode 100644
index 0000000..da5a503
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments2
@@ -0,0 +1,49 @@
+{//comment..// another comment # another comment
+  "presubmit": [ #comment //another comment #// another comment
+    { # comment
+      "name": "test1",//comment
+      "host": true#comment
+    },#comment
+    {
+      "name": "suite/stub1" // comment # comment
+    },
+    {
+      "name": "suite/stub2",
+      "keywords": ["key_1"] # comment
+    } # comment
+  ], // comment
+  "postsubmit": [
+    {
+      "name": "test2",
+      "options": [
+        {
+          "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit" //comment
+        }
+      ]
+    },
+    {
+      "name": "instrument",
+      "options": [
+        {
+          "run-name": "some-name"
+        }
+      ]
+    }
+  ],
+  "othertype": [ // another comment
+    {
+      "name": "test3",
+      "options": [
+        {
+          "just-an-option": ""##comment
+        }////another comment
+      //// another comment...
+      ]
+    }
+  ],
+  "imports": [
+    {
+      "path": "folder1//folder2//folder3"//comment... # another comment // another comment
+    }
+  ]
+}
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index 56cfa8c..8c53c17 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -32,6 +32,8 @@
 
 import java.io.File;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
@@ -504,4 +506,35 @@
             FileUtil.recursiveDelete(tempDir);
         }
     }
+
+    /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+    @Test
+    public void testRemoveComments() throws Exception {
+        String jsonString = getJsonStringByName("test_mapping_with_comments1");
+        String goldenString = getJsonStringByName("test_mapping_golden1");
+        assertEquals(TestMapping.removeComments(jsonString), goldenString);
+    }
+
+    /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+    @Test
+    public void testRemoveComments2() throws Exception {
+        String jsonString = getJsonStringByName("test_mapping_with_comments2");
+        String goldenString = getJsonStringByName("test_mapping_golden2");
+        assertEquals(TestMapping.removeComments(jsonString), goldenString);
+    }
+
+    private String getJsonStringByName(String fileName) throws Exception  {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + fileName;
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            Path file = Paths.get(srcDir.getAbsolutePath(), TEST_MAPPING);
+            return String.join("\n", Files.readAllLines(file, StandardCharsets.UTF_8));
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
 }