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