| # 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 |
| |
| try: |
| basestring # Python 2 |
| except NameError: # Python 3 |
| basestring = str |
| |
| 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('Key "%s" must be one of %s' % (key, 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('Value for %s must be a dict' % prop_name) |
| 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('If specified, %s must be an int' % prop_name) |
| if lower_bound is not None and value < lower_bound: |
| return error_msg('If specified, %s must be >=%d' % (prop_name, lower_bound)) |
| if upper_bound is not None and value > upper_bound: |
| return error_msg('If specified, %s must be <=%d' % (prop_name, 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, basestring) 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, basestring): |
| 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 ['Error in %s:\n%s' % (file_path, msg)] |
| |
| try: |
| # Eval python literal file. |
| full_test_spec = ast.literal_eval(raw_pyl) |
| except SyntaxError as e: |
| return error_msg('Pyl parsing failed with:\n%s' % 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.iteritems(): |
| def error_msg(msg): |
| return ['Error in %s for builder %s:\n%s' % (file_path, buildername, msg)] |
| |
| if not isinstance(buildername, basestring) 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] |