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"""Validate 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 30 31_path = os.path.realpath(__file__ + '/../..') 32if sys.path[0] != _path: 33 sys.path.insert(0, _path) 34del _path 35 36# We have to import our local modules after the sys.path tweak. We can't use 37# relative imports because this is an executable program, not a module. 38# pylint: disable=wrong-import-position 39import rh.git 40 41IMPORTS = 'imports' 42NAME = 'name' 43OPTIONS = 'options' 44PATH = 'path' 45HOST = 'host' 46PREFERRED_TARGETS = 'preferred_targets' 47FILE_PATTERNS = 'file_patterns' 48TEST_MAPPING_URL = ( 49 'https://source.android.com/compatibility/tests/development/' 50 'test-mapping') 51 52# Pattern used to identify line-level '//'-format comment in TEST_MAPPING file. 53_COMMENTS_RE = re.compile(r'^\s*//') 54 55 56if sys.version_info.major < 3: 57 # pylint: disable=basestring-builtin,undefined-variable 58 string_types = basestring 59else: 60 string_types = str 61 62 63class Error(Exception): 64 """Base exception for all custom exceptions in this module.""" 65 66 67class InvalidTestMappingError(Error): 68 """Exception to raise when detecting an invalid TEST_MAPPING file.""" 69 70 71def filter_comments(json_data): 72 """Remove '//'-format comments in TEST_MAPPING file to valid format. 73 74 Args: 75 json_data: TEST_MAPPING file content (as a string). 76 77 Returns: 78 Valid json string without comments. 79 """ 80 return ''.join('\n' if _COMMENTS_RE.match(x) else x for x in 81 json_data.splitlines()) 82 83 84def _validate_import(entry, test_mapping_file): 85 """Validate an import setting. 86 87 Args: 88 entry: A dictionary of an import setting. 89 test_mapping_file: Path to the TEST_MAPPING file to be validated. 90 91 Raises: 92 InvalidTestMappingError: if the import setting is invalid. 93 """ 94 if len(entry) != 1: 95 raise InvalidTestMappingError( 96 'Invalid import config in test mapping file %s. each import can ' 97 'only have one `path` setting. Failed entry: %s' % 98 (test_mapping_file, entry)) 99 if list(entry.keys())[0] != PATH: 100 raise InvalidTestMappingError( 101 'Invalid import config in test mapping file %s. import can only ' 102 'have one `path` setting. Failed entry: %s' % 103 (test_mapping_file, entry)) 104 105 106def _validate_test(test, test_mapping_file): 107 """Validate a test declaration. 108 109 Args: 110 entry: A dictionary of a test declaration. 111 test_mapping_file: Path to the TEST_MAPPING file to be validated. 112 113 Raises: 114 InvalidTestMappingError: if the a test declaration is invalid. 115 """ 116 if NAME not in test: 117 raise InvalidTestMappingError( 118 'Invalid test config in test mapping file %s. test config must ' 119 'a `name` setting. Failed test config: %s' % 120 (test_mapping_file, test)) 121 if not isinstance(test.get(HOST, False), bool): 122 raise InvalidTestMappingError( 123 'Invalid test config in test mapping file %s. `host` setting in ' 124 'test config can only have boolean value of `true` or `false`. ' 125 'Failed test config: %s' % (test_mapping_file, test)) 126 preferred_targets = test.get(PREFERRED_TARGETS, []) 127 if (not isinstance(preferred_targets, list) or 128 any(not isinstance(t, string_types) for t in preferred_targets)): 129 raise InvalidTestMappingError( 130 'Invalid test config in test mapping file %s. `preferred_targets` ' 131 'setting in test config can only be a list of strings. Failed test ' 132 'config: %s' % (test_mapping_file, test)) 133 file_patterns = test.get(FILE_PATTERNS, []) 134 if (not isinstance(file_patterns, list) or 135 any(not isinstance(p, string_types) for p in file_patterns)): 136 raise InvalidTestMappingError( 137 'Invalid test config in test mapping file %s. `file_patterns` ' 138 'setting in test config can only be a list of strings. Failed test ' 139 'config: %s' % (test_mapping_file, test)) 140 for option in test.get(OPTIONS, []): 141 if len(option) != 1: 142 raise InvalidTestMappingError( 143 'Invalid option setting in test mapping file %s. each option ' 144 'setting can only have one key-val setting. Failed entry: %s' % 145 (test_mapping_file, option)) 146 147 148def _load_file(test_mapping_file): 149 """Load a TEST_MAPPING file as a json file.""" 150 try: 151 return json.loads(filter_comments(test_mapping_file)) 152 except ValueError as e: 153 # The file is not a valid JSON file. 154 print( 155 'Failed to parse JSON file %s, error: %s' % (test_mapping_file, e), 156 file=sys.stderr) 157 raise 158 159 160def process_file(test_mapping_file): 161 """Validate a TEST_MAPPING file.""" 162 test_mapping = _load_file(test_mapping_file) 163 # Validate imports. 164 for import_entry in test_mapping.get(IMPORTS, []): 165 _validate_import(import_entry, test_mapping_file) 166 # Validate tests. 167 all_tests = [test for group, tests in test_mapping.items() 168 if group != IMPORTS for test in tests] 169 for test in all_tests: 170 _validate_test(test, test_mapping_file) 171 172 173def get_parser(): 174 """Return a command line parser.""" 175 parser = argparse.ArgumentParser(description=__doc__) 176 parser.add_argument('--commit', type=str, 177 help='Specify the commit to validate.') 178 parser.add_argument('project_dir') 179 parser.add_argument('files', nargs='+') 180 return parser 181 182 183def main(argv): 184 parser = get_parser() 185 opts = parser.parse_args(argv) 186 try: 187 for filename in opts.files: 188 if opts.commit: 189 json_data = rh.git.get_file_content(opts.commit, filename) 190 else: 191 with open(os.path.join(opts.project_dir, filename)) as f: 192 json_data = f.read() 193 process_file(json_data) 194 except: 195 print('Visit %s for details about the format of TEST_MAPPING ' 196 'file.' % TEST_MAPPING_URL, file=sys.stderr) 197 raise 198 199 200if __name__ == '__main__': 201 sys.exit(main(sys.argv[1:])) 202