blob: b87b886ece00dd7bd0190b2fe83208f537f0b758 [file] [log] [blame]
#!/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.
"""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 re
import sys
_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'
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*//')
if sys.version_info.major < 3:
# pylint: disable=basestring-builtin,undefined-variable
string_types = basestring
else:
string_types = str
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):
"""Remove '//'-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, 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 list(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))
if not isinstance(test.get(HOST, False), bool):
raise InvalidTestMappingError(
'Invalid test config in test mapping file %s. `host` setting in '
'test config can only have boolean value of `true` or `false`. '
'Failed test config: %s' % (test_mapping_file, test))
preferred_targets = test.get(PREFERRED_TARGETS, [])
if (not isinstance(preferred_targets, list) or
any(not isinstance(t, string_types) for t in preferred_targets)):
raise InvalidTestMappingError(
'Invalid test config in test mapping file %s. `preferred_targets` '
'setting in test config can only be a list of strings. Failed test '
'config: %s' % (test_mapping_file, test))
file_patterns = test.get(FILE_PATTERNS, [])
if (not isinstance(file_patterns, list) or
any(not isinstance(p, string_types) for p in file_patterns)):
raise InvalidTestMappingError(
'Invalid test config in test mapping file %s. `file_patterns` '
'setting in test config can only be a list of strings. 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:
return json.loads(filter_comments(test_mapping_file))
except ValueError as e:
# The file is not a valid JSON file.
print(
'Failed to parse JSON file %s, error: %s' % (test_mapping_file, e),
file=sys.stderr)
raise
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('--commit', type=str,
help='Specify the commit to validate.')
parser.add_argument('project_dir')
parser.add_argument('files', nargs='+')
return parser
def main(argv):
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)) as f:
json_data = f.read()
process_file(json_data)
except:
print('Visit %s for details about the format of TEST_MAPPING '
'file.' % TEST_MAPPING_URL, file=sys.stderr)
raise
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))