1# Copyright 2018 the V8 project authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5""" 6Presubmit checks for the validity of V8-side test specifications in pyl files. 7 8For simplicity, we check all pyl files on any changes in this folder. 9""" 10 11import ast 12import os 13 14# This line is 'magic' in that git-cl looks for it to decide whether to 15# use Python3 instead of Python2 when running the code in this file. 16USE_PYTHON3 = True 17 18SUPPORTED_BUILDER_SPEC_KEYS = [ 19 'swarming_dimensions', 20 'swarming_task_attrs', 21 'tests', 22] 23 24# This is not an exhaustive list. It only reflects what we currently use. If 25# there's need to specify a different dimension, just add it here. 26SUPPORTED_SWARMING_DIMENSIONS = [ 27 'cores', 28 'cpu', 29 'device_os', 30 'device_type', 31 'gpu', 32 'os', 33 'pool', 34] 35 36# This is not an exhaustive list. It only reflects what we currently use. If 37# there's need to specify a different property, add it here and update the 38# properties passed to swarming in: 39# //build/scripts/slave/recipe_modules/v8/testing.py. 40SUPPORTED_SWARMING_TASK_ATTRS = [ 41 'expiration', 42 'hard_timeout', 43 'priority', 44] 45 46SUPPORTED_TEST_KEYS = [ 47 'name', 48 'shards', 49 'suffix', 50 'swarming_dimensions', 51 'swarming_task_attrs', 52 'test_args', 53 'variant', 54] 55 56def check_keys(error_msg, src_dict, supported_keys): 57 errors = [] 58 for key in src_dict.keys(): 59 if key not in supported_keys: 60 errors += error_msg(f'Key "{key}" must be one of {supported_keys}') 61 return errors 62 63 64def _check_properties(error_msg, src_dict, prop_name, supported_keys): 65 properties = src_dict.get(prop_name, {}) 66 if not isinstance(properties, dict): 67 return error_msg(f'Value for {prop_name} must be a dict') 68 return check_keys(error_msg, properties, supported_keys) 69 70 71def _check_int_range(error_msg, src_dict, prop_name, lower_bound=None, 72 upper_bound=None): 73 if prop_name not in src_dict: 74 # All properties are optional. 75 return [] 76 try: 77 value = int(src_dict[prop_name]) 78 except ValueError: 79 return error_msg(f'If specified, {prop_name} must be an int') 80 if lower_bound is not None and value < lower_bound: 81 return error_msg(f'If specified, {prop_name} must be >={lower_bound}') 82 if upper_bound is not None and value > upper_bound: 83 return error_msg(f'If specified, {prop_name} must be <={upper_bound}') 84 return [] 85 86 87def _check_swarming_task_attrs(error_msg, src_dict): 88 errors = [] 89 task_attrs = src_dict.get('swarming_task_attrs', {}) 90 errors += _check_int_range( 91 error_msg, task_attrs, 'priority', lower_bound=25, upper_bound=100) 92 errors += _check_int_range( 93 error_msg, task_attrs, 'expiration', lower_bound=1) 94 errors += _check_int_range( 95 error_msg, task_attrs, 'hard_timeout', lower_bound=1) 96 return errors 97 98 99def _check_swarming_config(error_msg, src_dict): 100 errors = [] 101 errors += _check_properties( 102 error_msg, src_dict, 'swarming_dimensions', 103 SUPPORTED_SWARMING_DIMENSIONS) 104 errors += _check_properties( 105 error_msg, src_dict, 'swarming_task_attrs', 106 SUPPORTED_SWARMING_TASK_ATTRS) 107 errors += _check_swarming_task_attrs(error_msg, src_dict) 108 return errors 109 110 111def _check_test(error_msg, test): 112 if not isinstance(test, dict): 113 return error_msg('Each test must be specified with a dict') 114 errors = check_keys(error_msg, test, SUPPORTED_TEST_KEYS) 115 if not test.get('name'): 116 errors += error_msg('A test requires a name') 117 errors += _check_swarming_config(error_msg, test) 118 119 test_args = test.get('test_args', []) 120 if not isinstance(test_args, list): 121 errors += error_msg('If specified, test_args must be a list of arguments') 122 if not all(isinstance(x, str) for x in test_args): 123 errors += error_msg('If specified, all test_args must be strings') 124 125 # Limit shards to 14 to avoid erroneous resource exhaustion. 126 errors += _check_int_range( 127 error_msg, test, 'shards', lower_bound=1, upper_bound=14) 128 129 variant = test.get('variant', 'default') 130 if not variant or not isinstance(variant, str): 131 errors += error_msg('If specified, variant must be a non-empty string') 132 133 return errors 134 135 136def _check_test_spec(file_path, raw_pyl): 137 def error_msg(msg): 138 return [f'Error in {file_path}:\n{msg}'] 139 140 try: 141 # Eval python literal file. 142 full_test_spec = ast.literal_eval(raw_pyl) 143 except SyntaxError as e: 144 return error_msg(f'Pyl parsing failed with:\n{e}') 145 146 if not isinstance(full_test_spec, dict): 147 return error_msg('Test spec must be a dict') 148 149 errors = [] 150 for buildername, builder_spec in full_test_spec.items(): 151 def error_msg(msg): 152 return [f'Error in {file_path} for builder {buildername}:\n{msg}'] 153 154 if not isinstance(buildername, str) or not buildername: 155 errors += error_msg('Buildername must be a non-empty string') 156 157 if not isinstance(builder_spec, dict) or not builder_spec: 158 errors += error_msg('Value must be a non-empty dict') 159 continue 160 161 errors += check_keys(error_msg, builder_spec, SUPPORTED_BUILDER_SPEC_KEYS) 162 errors += _check_swarming_config(error_msg, builder_spec) 163 164 for test in builder_spec.get('tests', []): 165 errors += _check_test(error_msg, test) 166 167 return errors 168 169 170 171def CheckChangeOnCommit(input_api, output_api): 172 def file_filter(regexp): 173 return lambda f: input_api.FilterSourceFile(f, files_to_check=(regexp,)) 174 175 # Calculate which files are affected. 176 if input_api.AffectedFiles(False, file_filter(r'.*PRESUBMIT\.py')): 177 # If PRESUBMIT.py itself was changed, check also the test spec. 178 affected_files = [ 179 os.path.join(input_api.PresubmitLocalPath(), 'builders.pyl'), 180 ] 181 else: 182 # Otherwise, check test spec only when changed. 183 affected_files = [ 184 f.AbsoluteLocalPath() 185 for f in input_api.AffectedFiles(False, file_filter(r'.*builders\.pyl')) 186 ] 187 188 errors = [] 189 for file_path in affected_files: 190 with open(file_path) as f: 191 errors += _check_test_spec(file_path, f.read()) 192 return [output_api.PresubmitError(r) for r in errors] 193