aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Shi <dshi@google.com>2018-08-20 16:13:10 -0700
committerDan Shi <dshi@google.com>2018-08-27 10:19:23 -0700
commit45c9c682dc469c039dd07a69d01ca6adaaf8da8f (patch)
treefd462969148ac11efceffc0b5dee98e8e405f3c0
parent2ee871c35817d3c1f7b8964d2e9c62c033cd8785 (diff)
downloadrepohooks-45c9c682dc469c039dd07a69d01ca6adaaf8da8f.tar.gz
TEST_MAPPING is a json file can exist anywhere in the Android Source tree. It has a specific format to list tests to run in presubmit or postsubmit (go/test-mapping). Instead of waiting for presubmit to fail for an invalid TEST_MAPPING file, a repo hook is way more efficient on validating the format. Bug: 77805721 Test: unittest Change-Id: Ia1440ce46029d7a59a915d22ccbb585bdf98298e
-rw-r--r--PREUPLOAD.cfg1
-rw-r--r--README.md4
-rw-r--r--rh/hooks.py17
-rwxr-xr-xrh/hooks_unittest.py14
-rwxr-xr-xtools/android_test_mapping_format.py130
-rwxr-xr-xtools/android_test_mapping_format_unittest.py160
6 files changed, 326 insertions, 0 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index e82319c..96b1dfc 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -3,6 +3,7 @@
config_unittest = ./rh/config_unittest.py
hooks_unittest = ./rh/hooks_unittest.py
shell_unittest = ./rh/shell_unittest.py
+android_test_mapping_format_unittest = ./tools/android_test_mapping_format_unittest.py
[Builtin Hooks]
commit_msg_bug_field = true
diff --git a/README.md b/README.md
index a320e99..faab1cf 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,8 @@ canned hooks already included geared towards AOSP style guidelines.
* `jsonlint`: Verify JSON code is sane.
* `pylint`: Run Python code through `pylint`.
* `xmllint`: Run XML code through `xmllint`.
+* `android_test_mapping_format`: Validate TEST_MAPPING files in Android source
+ code. Refer to go/test-mapping for more details.
Note: Builtin hooks tend to match specific filenames (e.g. `.json`). If no
files match in a specific commit, then the hook will be skipped for that commit.
@@ -200,6 +202,8 @@ distros/versions. The following tools are recognized:
* `google-java-format`: used for the `google_java_format` builtin hook.
* `google-java-format-diff`: used for the `google_java_format` builtin hook.
* `pylint`: used for the `pylint` builtin hook.
+* `android-test-mapping-format`: used for the `android_test_mapping_format`
+ builtin hook.
See [Placeholders](#Placeholders) for variables you can expand automatically.
diff --git a/rh/hooks.py b/rh/hooks.py
index 1898364..bf70643 100644
--- a/rh/hooks.py
+++ b/rh/hooks.py
@@ -596,9 +596,24 @@ def check_xmllint(project, commit, _desc, diff, options=None):
return _check_cmd('xmllint', project, commit, cmd)
+def check_android_test_mapping(project, commit, _desc, diff, options=None):
+ """Verify Android TEST_MAPPING files are valid."""
+ if options.args():
+ raise ValueError('Android TEST_MAPPING check takes no options')
+ filtered = _filter_diff(diff, [r'(^|.*/)TEST_MAPPING$'])
+ if not filtered:
+ return
+
+ testmapping_format = options.tool_path('android-test-mapping-format')
+ cmd = [testmapping_format] + options.args(
+ (project.dir, '${PREUPLOAD_FILES}',), filtered)
+ return _check_cmd('android-test-mapping-format', project, commit, cmd)
+
+
# Hooks that projects can opt into.
# Note: Make sure to keep the top level README.md up to date when adding more!
BUILTIN_HOOKS = {
+ 'android_test_mapping_format': check_android_test_mapping,
'checkpatch': check_checkpatch,
'clang_format': check_clang_format,
'commit_msg_bug_field': check_commit_msg_bug_field,
@@ -616,6 +631,8 @@ BUILTIN_HOOKS = {
# Additional tools that the hooks can call with their default values.
# Note: Make sure to keep the top level README.md up to date when adding more!
TOOL_PATHS = {
+ 'android-test-mapping-format':
+ os.path.join(TOOLS_DIR, 'android_test_mapping_format.py'),
'clang-format': 'clang-format',
'cpplint': os.path.join(TOOLS_DIR, 'cpplint.py'),
'git-clang-format': 'git-clang-format',
diff --git a/rh/hooks_unittest.py b/rh/hooks_unittest.py
index 38b8df2..b7b5c86 100755
--- a/rh/hooks_unittest.py
+++ b/rh/hooks_unittest.py
@@ -502,6 +502,20 @@ class BuiltinHooksTests(unittest.TestCase):
self._test_file_filter(mock_check, rh.hooks.check_xmllint,
('foo.xml',))
+ def test_android_test_mapping_format(self, mock_check, _mock_run):
+ """Verify the android_test_mapping_format builtin hook."""
+ # First call should do nothing as there are no files to check.
+ ret = rh.hooks.check_android_test_mapping(
+ self.project, 'commit', 'desc', (), options=self.options)
+ self.assertIsNone(ret)
+ self.assertFalse(mock_check.called)
+
+ # Second call will have some results.
+ diff = [rh.git.RawDiffEntry(file='TEST_MAPPING')]
+ ret = rh.hooks.check_android_test_mapping(
+ self.project, 'commit', 'desc', diff, options=self.options)
+ self.assertIsNotNone(ret)
+
if __name__ == '__main__':
unittest.main()
diff --git a/tools/android_test_mapping_format.py b/tools/android_test_mapping_format.py
new file mode 100755
index 0000000..49725b3
--- /dev/null
+++ b/tools/android_test_mapping_format.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+# -*- coding:utf-8 -*-
+# Copyright 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Validate TEST_MAPPING files in Android source code.
+
+The goal of this script is to validate the format of TEST_MAPPING files:
+1. It must be a valid json file.
+2. Each test group must have a list of test that containing name and options.
+3. Each import must have only one key `path` and one value for the path to
+ import TEST_MAPPING files.
+"""
+
+import argparse
+import json
+import os
+import sys
+
+IMPORTS = 'imports'
+NAME = 'name'
+OPTIONS = 'options'
+PATH = 'path'
+
+
+class Error(Exception):
+ """Base exception for all custom exceptions in this module."""
+
+
+class InvalidTestMappingError(Error):
+ """Exception to raise when detecting an invalid TEST_MAPPING file."""
+
+
+def _validate_import(entry, test_mapping_file):
+ """Validate an import setting.
+
+ Args:
+ entry: A dictionary of an import setting.
+ test_mapping_file: Path to the TEST_MAPPING file to be validated.
+
+ Raises:
+ InvalidTestMappingError: if the import setting is invalid.
+ """
+ if len(entry) != 1:
+ raise InvalidTestMappingError(
+ 'Invalid import config in test mapping file %s. each import can '
+ 'only have one `path` setting. Failed entry: %s' %
+ (test_mapping_file, entry))
+ if entry.keys()[0] != PATH:
+ raise InvalidTestMappingError(
+ 'Invalid import config in test mapping file %s. import can only '
+ 'have one `path` setting. Failed entry: %s' %
+ (test_mapping_file, entry))
+
+
+def _validate_test(test, test_mapping_file):
+ """Validate a test declaration.
+
+ Args:
+ entry: A dictionary of a test declaration.
+ test_mapping_file: Path to the TEST_MAPPING file to be validated.
+
+ Raises:
+ InvalidTestMappingError: if the a test declaration is invalid.
+ """
+ if NAME not in test:
+ raise InvalidTestMappingError(
+ 'Invalid test config in test mapping file %s. test config must '
+ 'a `name` setting. Failed test config: %s' %
+ (test_mapping_file, test))
+ for option in test.get(OPTIONS, []):
+ if len(option) != 1:
+ raise InvalidTestMappingError(
+ 'Invalid option setting in test mapping file %s. each option '
+ 'setting can only have one key-val setting. Failed entry: %s' %
+ (test_mapping_file, option))
+
+
+def _load_file(test_mapping_file):
+ """Load a TEST_MAPPING file as a json file."""
+ try:
+ with open(test_mapping_file) as file_obj:
+ 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))
+
+
+def process_file(test_mapping_file):
+ """Validate a TEST_MAPPING file."""
+ test_mapping = _load_file(test_mapping_file)
+ # Validate imports.
+ for import_entry in test_mapping.get(IMPORTS, []):
+ _validate_import(import_entry, test_mapping_file)
+ # Validate tests.
+ all_tests = [test for group, tests in test_mapping.items()
+ if group != IMPORTS for test in tests]
+ for test in all_tests:
+ _validate_test(test, test_mapping_file)
+
+
+def get_parser():
+ """Return a command line parser."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('project_dir')
+ parser.add_argument('files', nargs='+')
+ return parser
+
+
+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))
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/tools/android_test_mapping_format_unittest.py b/tools/android_test_mapping_format_unittest.py
new file mode 100755
index 0000000..d8a3167
--- /dev/null
+++ b/tools/android_test_mapping_format_unittest.py
@@ -0,0 +1,160 @@
+#!/usr/bin/python
+# -*- coding:utf-8 -*-
+# Copyright 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import shutil
+import tempfile
+import unittest
+
+import android_test_mapping_format
+
+
+VALID_TEST_MAPPING = """
+{
+ "presubmit": [
+ {
+ "name": "CtsWindowManagerDeviceTestCases",
+ "options": [
+ {
+ "include-annotation": "android.platform.test.annotations.Presubmit"
+ }
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "CtsWindowManagerDeviceTestCases"
+ }
+ ],
+ "imports": [
+ {
+ "path": "frameworks/base/services/core/java/com/android/server/am"
+ },
+ {
+ "path": "frameworks/base/services/core/java/com/android/server/wm"
+ }
+ ]
+}
+"""
+
+BAD_JSON = """
+{wrong format}
+"""
+
+BAD_TEST_WRONG_KEY = """
+{
+ "presubmit": [
+ {
+ "bad_name": "CtsWindowManagerDeviceTestCases",
+ }
+ ],
+}
+"""
+
+BAD_TEST_WRONG_OPTION = """
+{
+ "presubmit": [
+ {
+ "name": "CtsWindowManagerDeviceTestCases",
+ "options": [
+ {
+ "include-annotation": "android.platform.test.annotations.Presubmit",
+ "bad_option": "some_name"
+ }
+ ]
+ }
+ ],
+}
+"""
+
+BAD_IMPORT_WRONG_KEY = """
+{
+ "imports": [
+ {
+ "name": "frameworks/base/services/core/java/com/android/server/am"
+ }
+ ]
+}
+"""
+
+BAD_IMPORT_WRONG_IMPORT_VALUE = """
+{
+ "imports": [
+ {
+ "path": "frameworks/base/services/core/java/com/android/server/am",
+ "option": "something"
+ }
+ ]
+}
+"""
+
+
+class AndroidTestMappingFormatTests(unittest.TestCase):
+ """Unittest for android_test_mapping_format module."""
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ self.test_mapping_file = os.path.join(self.tempdir, 'TEST_MAPPING')
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_valid_test_mapping(self):
+ """Verify that the check doesn't raise any error for valid test mapping.
+ """
+ with open(self.test_mapping_file, 'w') as f:
+ f.write(VALID_TEST_MAPPING)
+ 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:
+ f.write(BAD_TEST_WRONG_KEY)
+ 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:
+ f.write(BAD_TEST_WRONG_OPTION)
+ self.assertRaises(
+ android_test_mapping_format.InvalidTestMappingError,
+ android_test_mapping_format.process_file,
+ self.test_mapping_file)
+
+ def test_invalid_test_mapping_wrong_import_key(self):
+ """Verify that import setting using wrong key can be detected."""
+ with open(self.test_mapping_file, 'w') as f:
+ f.write(BAD_IMPORT_WRONG_KEY)
+ self.assertRaises(
+ android_test_mapping_format.InvalidTestMappingError,
+ android_test_mapping_format.process_file,
+ self.test_mapping_file)
+
+ def test_invalid_test_mapping_wrong_import_value(self):
+ """Verify that import setting using wrong value can be detected."""
+ with open(self.test_mapping_file, 'w') as f:
+ f.write(BAD_IMPORT_WRONG_IMPORT_VALUE)
+ self.assertRaises(
+ android_test_mapping_format.InvalidTestMappingError,
+ android_test_mapping_format.process_file,
+ self.test_mapping_file)
+
+
+if __name__ == '__main__':
+ unittest.main()