| #!/usr/bin/env python3 |
| # 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. |
| |
| """Validates 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 re |
| import sys |
| from typing import Any, Dict |
| |
| _path = os.path.realpath(__file__ + '/../..') |
| if sys.path[0] != _path: |
| sys.path.insert(0, _path) |
| del _path |
| |
| # We have to import our local modules after the sys.path tweak. We can't use |
| # relative imports because this is an executable program, not a module. |
| # pylint: disable=wrong-import-position |
| import rh.git |
| |
| _IMPORTS = 'imports' |
| _NAME = 'name' |
| _OPTIONS = 'options' |
| _PATH = 'path' |
| _HOST = 'host' |
| _PREFERRED_TARGETS = 'preferred_targets' |
| _FILE_PATTERNS = 'file_patterns' |
| _INVALID_IMPORT_CONFIG = 'Invalid import config in TEST_MAPPING file' |
| _INVALID_TEST_CONFIG = 'Invalid test config in TEST_MAPPING file' |
| _TEST_MAPPING_URL = ( |
| 'https://source.android.com/compatibility/tests/development/' |
| 'test-mapping') |
| |
| # Pattern used to identify line-level '//'-format comment in TEST_MAPPING file. |
| _COMMENTS_RE = re.compile(r'^\s*//') |
| |
| |
| 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 _filter_comments(json_data: str) -> str: |
| """Removes '//'-format comments in TEST_MAPPING file to valid format. |
| |
| Args: |
| json_data: TEST_MAPPING file content (as a string). |
| |
| Returns: |
| Valid json string without comments. |
| """ |
| return ''.join( |
| '\n' if _COMMENTS_RE.match(x) else x for x in json_data.splitlines()) |
| |
| |
| def _validate_import(entry: Dict[str, Any], test_mapping_file: str): |
| """Validates 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( |
| f'{_INVALID_IMPORT_CONFIG} {test_mapping_file}. Each import can ' |
| f'only have one `path` setting. Failed entry: {entry}') |
| if _PATH not in entry: |
| raise InvalidTestMappingError( |
| f'{_INVALID_IMPORT_CONFIG} {test_mapping_file}. Import can ' |
| f'only have one `path` setting. Failed entry: {entry}') |
| |
| |
| def _validate_test(test: Dict[str, Any], test_mapping_file: str) -> bool: |
| """Returns whether a test declaration is valid. |
| |
| Args: |
| test: 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( |
| |
| f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Test config must ' |
| f'have a `name` setting. Failed test config: {test}') |
| |
| if not isinstance(test.get(_HOST, False), bool): |
| raise InvalidTestMappingError( |
| f'{_INVALID_TEST_CONFIG} {test_mapping_file}. `host` setting in ' |
| f'test config can only have boolean value of `true` or `false`. ' |
| f'Failed test config: {test}') |
| |
| for key in (_PREFERRED_TARGETS, _FILE_PATTERNS): |
| value = test.get(key, []) |
| if (not isinstance(value, list) or |
| any(not isinstance(t, str) for t in value)): |
| raise InvalidTestMappingError( |
| f'{_INVALID_TEST_CONFIG} {test_mapping_file}. `{key}` setting ' |
| f'in test config can only be a list of strings. ' |
| f'Failed test config: {test}') |
| |
| for option in test.get(_OPTIONS, []): |
| if not isinstance(option, dict): |
| raise InvalidTestMappingError( |
| f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Option setting ' |
| f'in test config can only be a dictionary of key-val setting. ' |
| f'Failed entry: {option}') |
| if len(option) != 1: |
| raise InvalidTestMappingError( |
| f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Each option ' |
| f'setting can only have one key-val setting. ' |
| f'Failed entry: {option}') |
| |
| |
| def process_file(test_mapping_file: str): |
| """Validates a TEST_MAPPING file content.""" |
| try: |
| test_mapping_data = json.loads(_filter_comments(test_mapping_file)) |
| except ValueError as exception: |
| # The file is not a valid JSON file. |
| print( |
| f'Invalid JSON data in TEST_MAPPING file ' |
| f'Failed to parse JSON data: {test_mapping_file}, ' |
| f'error: {exception}', |
| file=sys.stderr) |
| raise |
| |
| for group, value in test_mapping_data.items(): |
| if group == _IMPORTS: |
| # Validate imports. |
| for test in value: |
| _validate_import(test, test_mapping_file) |
| else: |
| # Validate tests. |
| for test in value: |
| _validate_test(test, test_mapping_file) |
| |
| |
| def get_parser(): |
| """Returns a command line parser.""" |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument('--commit', type=str, |
| help='Specify the commit to validate.') |
| parser.add_argument('project_dir') |
| parser.add_argument('files', nargs='+') |
| return parser |
| |
| |
| def main(argv): |
| """Main function.""" |
| parser = get_parser() |
| opts = parser.parse_args(argv) |
| try: |
| for filename in opts.files: |
| if opts.commit: |
| json_data = rh.git.get_file_content(opts.commit, filename) |
| else: |
| with open(os.path.join(opts.project_dir, filename), |
| encoding='utf-8') as file: |
| json_data = file.read() |
| process_file(json_data) |
| except: |
| print(f'Visit {_TEST_MAPPING_URL} for details about the format of ' |
| 'TEST_MAPPING file.', file=sys.stderr) |
| raise |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |