android_test_mapping_format: Allow test config to have a `preferred_targets` attribute

The value of the `preferred_targets` attribute can only be a list of strings.

Bug: 113352287
Test: unitest
Change-Id: I29fde5cc325135c43911b71598b9ce706afd4df6
diff --git a/tools/android_test_mapping_format.py b/tools/android_test_mapping_format.py
index c42fd61..a5d68be 100755
--- a/tools/android_test_mapping_format.py
+++ b/tools/android_test_mapping_format.py
@@ -23,6 +23,8 @@
    import TEST_MAPPING files.
 """
 
+from __future__ import print_function
+
 import argparse
 import json
 import os
@@ -33,6 +35,10 @@
 OPTIONS = 'options'
 PATH = 'path'
 HOST = 'host'
+PREFERRED_TARGETS = 'preferred_targets'
+TEST_MAPPING_URL = (
+    'https://source.android.com/compatibility/tests/development/'
+    'test-mapping')
 
 
 class Error(Exception):
@@ -85,6 +91,13 @@
             'Invalid test config in test mapping file %s. `host` setting in '
             'test config can only have boolean value of `true` or `false`. '
             'Failed test config: %s' % (test_mapping_file, test))
+    preferred_targets = test.get(PREFERRED_TARGETS, [])
+    if (not isinstance(preferred_targets, list) or
+            any(not isinstance(t, basestring) for t in preferred_targets)):
+        raise InvalidTestMappingError(
+            'Invalid test config in test mapping file %s. `preferred_targets` '
+            'setting in test config can only be a list of strings. Failed test '
+            'config: %s' % (test_mapping_file, test))
     for option in test.get(OPTIONS, []):
         if len(option) != 1:
             raise InvalidTestMappingError(
@@ -100,8 +113,10 @@
             return json.load(file_obj)
     except ValueError as e:
         # The file is not a valid JSON file.
-        raise InvalidTestMappingError(
-            'Failed to parse JSON file %s, error: %s' % (test_mapping_file, e))
+        print(
+            'Failed to parse JSON file %s, error: %s' % (test_mapping_file, e),
+            file=sys.stderr)
+        raise
 
 
 def process_file(test_mapping_file):
@@ -128,8 +143,13 @@
 def main(argv):
     parser = get_parser()
     opts = parser.parse_args(argv)
-    for filename in opts.files:
-        process_file(os.path.join(opts.project_dir, filename))
+    try:
+        for filename in opts.files:
+            process_file(os.path.join(opts.project_dir, filename))
+    except:
+        print('Visit %s for details about the format of TEST_MAPPING '
+              'file.' % TEST_MAPPING_URL, file=sys.stderr)
+        raise
 
 
 if __name__ == '__main__':
diff --git a/tools/android_test_mapping_format_unittest.py b/tools/android_test_mapping_format_unittest.py
index 3b88514..ffd1160 100755
--- a/tools/android_test_mapping_format_unittest.py
+++ b/tools/android_test_mapping_format_unittest.py
@@ -37,7 +37,8 @@
   "postsubmit": [
     {
       "name": "CtsWindowManagerDeviceTestCases",
-      "host": true
+      "host": true,
+      "preferred_targets": ["a", "b"]
     }
   ],
   "imports": [
@@ -59,7 +60,7 @@
 {
   "presubmit": [
     {
-      "bad_name": "CtsWindowManagerDeviceTestCases",
+      "bad_name": "CtsWindowManagerDeviceTestCases"
     }
   ]
 }
@@ -76,6 +77,29 @@
 }
 """
 
+
+BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_NONE_LIST = """
+{
+  "presubmit": [
+    {
+      "name": "CtsWindowManagerDeviceTestCases",
+      "preferred_targets": "bad_value"
+    }
+  ]
+}
+"""
+
+BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_WRONG_TYPE = """
+{
+  "presubmit": [
+    {
+      "name": "CtsWindowManagerDeviceTestCases",
+      "preferred_targets": ["bad_value", 123]
+    }
+  ]
+}
+"""
+
 BAD_TEST_WRONG_OPTION = """
 {
   "presubmit": [
@@ -88,7 +112,7 @@
         }
       ]
     }
-  ],
+  ]
 }
 """
 
@@ -131,6 +155,14 @@
             f.write(VALID_TEST_MAPPING)
         android_test_mapping_format.process_file(self.test_mapping_file)
 
+    def test_invalid_test_mapping_bad_json(self):
+        """Verify that TEST_MAPPING file with bad json can be detected."""
+        with open(self.test_mapping_file, 'w') as f:
+            f.write(BAD_JSON)
+        self.assertRaises(
+            ValueError, android_test_mapping_format.process_file,
+            self.test_mapping_file)
+
     def test_invalid_test_mapping_wrong_test_key(self):
         """Verify that test config using wrong key can be detected."""
         with open(self.test_mapping_file, 'w') as f:
@@ -149,6 +181,21 @@
             android_test_mapping_format.process_file,
             self.test_mapping_file)
 
+    def test_invalid_test_mapping_wrong_preferred_targets_value(self):
+        """Verify invalid preferred_targets are rejected."""
+        with open(self.test_mapping_file, 'w') as f:
+            f.write(BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_NONE_LIST)
+        self.assertRaises(
+            android_test_mapping_format.InvalidTestMappingError,
+            android_test_mapping_format.process_file,
+            self.test_mapping_file)
+        with open(self.test_mapping_file, 'w') as f:
+            f.write(BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_WRONG_TYPE)
+        self.assertRaises(
+            android_test_mapping_format.InvalidTestMappingError,
+            android_test_mapping_format.process_file,
+            self.test_mapping_file)
+
     def test_invalid_test_mapping_wrong_test_option(self):
         """Verify that test config using wrong option can be detected."""
         with open(self.test_mapping_file, 'w') as f: