• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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