v8/infra/testing/PRESUBMIT.py
Tamer Tas 3404608ee9 [infra] migrate infra/testing/PRESUBMIT script
R=machenbach@chromium.org,alexschulze@chromium.org,almuthanna@chromium.org,liviurau@chromium.org

Bug: chromium:1298869
Change-Id: Ia08f5069bacf5134ba56265d64eff527d7dd96fb
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3525134
Commit-Queue: Tamer Tas <tmrts@chromium.org>
Auto-Submit: Tamer Tas <tmrts@chromium.org>
Commit-Queue: Michael Achenbach <machenbach@chromium.org>
Reviewed-by: Michael Achenbach <machenbach@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79480}
2022-03-15 13:35:59 +00:00

193 lines
5.9 KiB
Python

# Copyright 2018 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Presubmit checks for the validity of V8-side test specifications in pyl files.
For simplicity, we check all pyl files on any changes in this folder.
"""
import ast
import os
# This line is 'magic' in that git-cl looks for it to decide whether to
# use Python3 instead of Python2 when running the code in this file.
USE_PYTHON3 = True
SUPPORTED_BUILDER_SPEC_KEYS = [
'swarming_dimensions',
'swarming_task_attrs',
'tests',
]
# This is not an exhaustive list. It only reflects what we currently use. If
# there's need to specify a different dimension, just add it here.
SUPPORTED_SWARMING_DIMENSIONS = [
'cores',
'cpu',
'device_os',
'device_type',
'gpu',
'os',
'pool',
]
# This is not an exhaustive list. It only reflects what we currently use. If
# there's need to specify a different property, add it here and update the
# properties passed to swarming in:
# //build/scripts/slave/recipe_modules/v8/testing.py.
SUPPORTED_SWARMING_TASK_ATTRS = [
'expiration',
'hard_timeout',
'priority',
]
SUPPORTED_TEST_KEYS = [
'name',
'shards',
'suffix',
'swarming_dimensions',
'swarming_task_attrs',
'test_args',
'variant',
]
def check_keys(error_msg, src_dict, supported_keys):
errors = []
for key in src_dict.keys():
if key not in supported_keys:
errors += error_msg(f'Key "{key}" must be one of {supported_keys}')
return errors
def _check_properties(error_msg, src_dict, prop_name, supported_keys):
properties = src_dict.get(prop_name, {})
if not isinstance(properties, dict):
return error_msg(f'Value for {prop_name} must be a dict')
return check_keys(error_msg, properties, supported_keys)
def _check_int_range(error_msg, src_dict, prop_name, lower_bound=None,
upper_bound=None):
if prop_name not in src_dict:
# All properties are optional.
return []
try:
value = int(src_dict[prop_name])
except ValueError:
return error_msg(f'If specified, {prop_name} must be an int')
if lower_bound is not None and value < lower_bound:
return error_msg(f'If specified, {prop_name} must be >={lower_bound}')
if upper_bound is not None and value > upper_bound:
return error_msg(f'If specified, {prop_name} must be <={upper_bound}')
return []
def _check_swarming_task_attrs(error_msg, src_dict):
errors = []
task_attrs = src_dict.get('swarming_task_attrs', {})
errors += _check_int_range(
error_msg, task_attrs, 'priority', lower_bound=25, upper_bound=100)
errors += _check_int_range(
error_msg, task_attrs, 'expiration', lower_bound=1)
errors += _check_int_range(
error_msg, task_attrs, 'hard_timeout', lower_bound=1)
return errors
def _check_swarming_config(error_msg, src_dict):
errors = []
errors += _check_properties(
error_msg, src_dict, 'swarming_dimensions',
SUPPORTED_SWARMING_DIMENSIONS)
errors += _check_properties(
error_msg, src_dict, 'swarming_task_attrs',
SUPPORTED_SWARMING_TASK_ATTRS)
errors += _check_swarming_task_attrs(error_msg, src_dict)
return errors
def _check_test(error_msg, test):
if not isinstance(test, dict):
return error_msg('Each test must be specified with a dict')
errors = check_keys(error_msg, test, SUPPORTED_TEST_KEYS)
if not test.get('name'):
errors += error_msg('A test requires a name')
errors += _check_swarming_config(error_msg, test)
test_args = test.get('test_args', [])
if not isinstance(test_args, list):
errors += error_msg('If specified, test_args must be a list of arguments')
if not all(isinstance(x, str) for x in test_args):
errors += error_msg('If specified, all test_args must be strings')
# Limit shards to 14 to avoid erroneous resource exhaustion.
errors += _check_int_range(
error_msg, test, 'shards', lower_bound=1, upper_bound=14)
variant = test.get('variant', 'default')
if not variant or not isinstance(variant, str):
errors += error_msg('If specified, variant must be a non-empty string')
return errors
def _check_test_spec(file_path, raw_pyl):
def error_msg(msg):
return [f'Error in {file_path}:\n{msg}']
try:
# Eval python literal file.
full_test_spec = ast.literal_eval(raw_pyl)
except SyntaxError as e:
return error_msg(f'Pyl parsing failed with:\n{e}')
if not isinstance(full_test_spec, dict):
return error_msg('Test spec must be a dict')
errors = []
for buildername, builder_spec in full_test_spec.items():
def error_msg(msg):
return [f'Error in {file_path} for builder {buildername}:\n{msg}']
if not isinstance(buildername, str) or not buildername:
errors += error_msg('Buildername must be a non-empty string')
if not isinstance(builder_spec, dict) or not builder_spec:
errors += error_msg('Value must be a non-empty dict')
continue
errors += check_keys(error_msg, builder_spec, SUPPORTED_BUILDER_SPEC_KEYS)
errors += _check_swarming_config(error_msg, builder_spec)
for test in builder_spec.get('tests', []):
errors += _check_test(error_msg, test)
return errors
def CheckChangeOnCommit(input_api, output_api):
def file_filter(regexp):
return lambda f: input_api.FilterSourceFile(f, files_to_check=(regexp,))
# Calculate which files are affected.
if input_api.AffectedFiles(False, file_filter(r'.*PRESUBMIT\.py')):
# If PRESUBMIT.py itself was changed, check also the test spec.
affected_files = [
os.path.join(input_api.PresubmitLocalPath(), 'builders.pyl'),
]
else:
# Otherwise, check test spec only when changed.
affected_files = [
f.AbsoluteLocalPath()
for f in input_api.AffectedFiles(False, file_filter(r'.*builders\.pyl'))
]
errors = []
for file_path in affected_files:
with open(file_path) as f:
errors += _check_test_spec(file_path, f.read())
return [output_api.PresubmitError(r) for r in errors]