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