diff options
author | Dan Shi <dshi@google.com> | 2018-08-20 16:13:10 -0700 |
---|---|---|
committer | Dan Shi <dshi@google.com> | 2018-08-27 10:19:23 -0700 |
commit | 45c9c682dc469c039dd07a69d01ca6adaaf8da8f (patch) | |
tree | fd462969148ac11efceffc0b5dee98e8e405f3c0 | |
parent | 2ee871c35817d3c1f7b8964d2e9c62c033cd8785 (diff) | |
download | repohooks-45c9c682dc469c039dd07a69d01ca6adaaf8da8f.tar.gz |
Add a repo hook to validate TEST_MAPPING file.android-o-mr1-iot-release-smart-display-r3android-o-mr1-iot-release-1.0.5android-o-mr1-iot-release-1.0.4oreo-mr1-1.2-iot-releasemaster-cuttlefish-testing-release
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.cfg | 1 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | rh/hooks.py | 17 | ||||
-rwxr-xr-x | rh/hooks_unittest.py | 14 | ||||
-rwxr-xr-x | tools/android_test_mapping_format.py | 130 | ||||
-rwxr-xr-x | tools/android_test_mapping_format_unittest.py | 160 |
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 @@ -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() |