1#!/usr/bin/env python3 2# Copyright 2018 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Validates TEST_MAPPING files in Android source code. 17 18The goal of this script is to validate the format of TEST_MAPPING files: 191. It must be a valid json file. 202. Each test group must have a list of test that containing name and options. 213. Each import must have only one key `path` and one value for the path to 22 import TEST_MAPPING files. 23""" 24 25import argparse 26import json 27import os 28import re 29import sys 30from typing import Any, Dict 31 32_path = os.path.realpath(__file__ + '/../..') 33if sys.path[0] != _path: 34 sys.path.insert(0, _path) 35del _path 36 37# We have to import our local modules after the sys.path tweak. We can't use 38# relative imports because this is an executable program, not a module. 39# pylint: disable=wrong-import-position 40import rh.git 41 42_IMPORTS = 'imports' 43_NAME = 'name' 44_OPTIONS = 'options' 45_PATH = 'path' 46_HOST = 'host' 47_PREFERRED_TARGETS = 'preferred_targets' 48_FILE_PATTERNS = 'file_patterns' 49_INVALID_IMPORT_CONFIG = 'Invalid import config in TEST_MAPPING file' 50_INVALID_TEST_CONFIG = 'Invalid test config in TEST_MAPPING file' 51_TEST_MAPPING_URL = ( 52 'https://source.android.com/compatibility/tests/development/' 53 'test-mapping') 54 55# Pattern used to identify line-level '//'-format comment in TEST_MAPPING file. 56_COMMENTS_RE = re.compile(r'^\s*//') 57 58 59class Error(Exception): 60 """Base exception for all custom exceptions in this module.""" 61 62 63class InvalidTestMappingError(Error): 64 """Exception to raise when detecting an invalid TEST_MAPPING file.""" 65 66 67def _filter_comments(json_data: str) -> str: 68 """Removes '//'-format comments in TEST_MAPPING file to valid format. 69 70 Args: 71 json_data: TEST_MAPPING file content (as a string). 72 73 Returns: 74 Valid json string without comments. 75 """ 76 return ''.join( 77 '\n' if _COMMENTS_RE.match(x) else x for x in json_data.splitlines()) 78 79 80def _validate_import(entry: Dict[str, Any], test_mapping_file: str): 81 """Validates an import setting. 82 83 Args: 84 entry: A dictionary of an import setting. 85 test_mapping_file: Path to the TEST_MAPPING file to be validated. 86 87 Raises: 88 InvalidTestMappingError: if the import setting is invalid. 89 """ 90 if len(entry) != 1: 91 raise InvalidTestMappingError( 92 f'{_INVALID_IMPORT_CONFIG} {test_mapping_file}. Each import can ' 93 f'only have one `path` setting. Failed entry: {entry}') 94 if _PATH not in entry: 95 raise InvalidTestMappingError( 96 f'{_INVALID_IMPORT_CONFIG} {test_mapping_file}. Import can ' 97 f'only have one `path` setting. Failed entry: {entry}') 98 99 100def _validate_test(test: Dict[str, Any], test_mapping_file: str) -> bool: 101 """Returns whether a test declaration is valid. 102 103 Args: 104 test: A dictionary of a test declaration. 105 test_mapping_file: Path to the TEST_MAPPING file to be validated. 106 107 Raises: 108 InvalidTestMappingError: if the a test declaration is invalid. 109 """ 110 if _NAME not in test: 111 raise InvalidTestMappingError( 112 113 f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Test config must ' 114 f'have a `name` setting. Failed test config: {test}') 115 116 if not isinstance(test.get(_HOST, False), bool): 117 raise InvalidTestMappingError( 118 f'{_INVALID_TEST_CONFIG} {test_mapping_file}. `host` setting in ' 119 f'test config can only have boolean value of `true` or `false`. ' 120 f'Failed test config: {test}') 121 122 for key in (_PREFERRED_TARGETS, _FILE_PATTERNS): 123 value = test.get(key, []) 124 if (not isinstance(value, list) or 125 any(not isinstance(t, str) for t in value)): 126 raise InvalidTestMappingError( 127 f'{_INVALID_TEST_CONFIG} {test_mapping_file}. `{key}` setting ' 128 f'in test config can only be a list of strings. ' 129 f'Failed test config: {test}') 130 131 for option in test.get(_OPTIONS, []): 132 if not isinstance(option, dict): 133 raise InvalidTestMappingError( 134 f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Option setting ' 135 f'in test config can only be a dictionary of key-val setting. ' 136 f'Failed entry: {option}') 137 if len(option) != 1: 138 raise InvalidTestMappingError( 139 f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Each option ' 140 f'setting can only have one key-val setting. ' 141 f'Failed entry: {option}') 142 143 144def process_file(test_mapping_file: str): 145 """Validates a TEST_MAPPING file content.""" 146 try: 147 test_mapping_data = json.loads(_filter_comments(test_mapping_file)) 148 except ValueError as exception: 149 # The file is not a valid JSON file. 150 print( 151 f'Invalid JSON data in TEST_MAPPING file ' 152 f'Failed to parse JSON data: {test_mapping_file}, ' 153 f'error: {exception}', 154 file=sys.stderr) 155 raise 156 157 for group, value in test_mapping_data.items(): 158 if group == _IMPORTS: 159 # Validate imports. 160 for test in value: 161 _validate_import(test, test_mapping_file) 162 else: 163 # Validate tests. 164 for test in value: 165 _validate_test(test, test_mapping_file) 166 167 168def get_parser(): 169 """Returns a command line parser.""" 170 parser = argparse.ArgumentParser(description=__doc__) 171 parser.add_argument('--commit', type=str, 172 help='Specify the commit to validate.') 173 parser.add_argument('project_dir') 174 parser.add_argument('files', nargs='+') 175 return parser 176 177 178def main(argv): 179 """Main function.""" 180 parser = get_parser() 181 opts = parser.parse_args(argv) 182 try: 183 for filename in opts.files: 184 if opts.commit: 185 json_data = rh.git.get_file_content(opts.commit, filename) 186 else: 187 with open(os.path.join(opts.project_dir, filename)) as file: 188 json_data = file.read() 189 process_file(json_data) 190 except: 191 print('Visit %s for details about the format of TEST_MAPPING ' 192 'file.' % _TEST_MAPPING_URL, file=sys.stderr) 193 raise 194 195 196if __name__ == '__main__': 197 sys.exit(main(sys.argv[1:])) 198