| # Copyright 2015 The Chromium 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 script validating field trial configs. |
| |
| See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts |
| for more details on the presubmit API built into depot_tools. |
| """ |
| |
| import copy |
| import json |
| import sys |
| |
| from collections import OrderedDict |
| |
| VALID_EXPERIMENT_KEYS = ['name', |
| 'forcing_flag', |
| 'params', |
| 'enable_features', |
| 'disable_features', |
| '//0', |
| '//1', |
| '//2', |
| '//3', |
| '//4', |
| '//5', |
| '//6', |
| '//7', |
| '//8', |
| '//9'] |
| |
| def PrettyPrint(contents): |
| """Pretty prints a fieldtrial configuration. |
| |
| Args: |
| contents: File contents as a string. |
| |
| Returns: |
| Pretty printed file contents. |
| """ |
| |
| # We have a preferred ordering of the fields (e.g. platforms on top). This |
| # code loads everything into OrderedDicts and then tells json to dump it out. |
| # The JSON dumper will respect the dict ordering. |
| # |
| # The ordering is as follows: |
| # { |
| # 'StudyName Alphabetical': [ |
| # { |
| # 'platforms': [sorted platforms] |
| # 'groups': [ |
| # { |
| # name: ... |
| # forcing_flag: "forcing flag string" |
| # params: {sorted dict} |
| # enable_features: [sorted features] |
| # disable_features: [sorted features] |
| # (Unexpected extra keys will be caught by the validator) |
| # } |
| # ], |
| # .... |
| # }, |
| # ... |
| # ] |
| # ... |
| # } |
| config = json.loads(contents) |
| ordered_config = OrderedDict() |
| for key in sorted(config.keys()): |
| study = copy.deepcopy(config[key]) |
| ordered_study = [] |
| for experiment_config in study: |
| ordered_experiment_config = OrderedDict([ |
| ('platforms', experiment_config['platforms']), |
| ('experiments', [])]) |
| for experiment in experiment_config['experiments']: |
| ordered_experiment = OrderedDict() |
| for index in xrange(0, 10): |
| comment_key = '//' + str(index) |
| if comment_key in experiment: |
| ordered_experiment[comment_key] = experiment[comment_key] |
| ordered_experiment['name'] = experiment['name'] |
| if 'forcing_flag' in experiment: |
| ordered_experiment['forcing_flag'] = experiment['forcing_flag'] |
| if 'params' in experiment: |
| ordered_experiment['params'] = OrderedDict( |
| sorted(experiment['params'].items(), key=lambda t: t[0])) |
| if 'enable_features' in experiment: |
| ordered_experiment['enable_features'] = \ |
| sorted(experiment['enable_features']) |
| if 'disable_features' in experiment: |
| ordered_experiment['disable_features'] = \ |
| sorted(experiment['disable_features']) |
| ordered_experiment_config['experiments'].append(ordered_experiment) |
| ordered_study.append(ordered_experiment_config) |
| ordered_config[key] = ordered_study |
| return json.dumps(ordered_config, |
| sort_keys=False, indent=4, |
| separators=(',', ': ')) + '\n' |
| |
| def ValidateData(json_data, file_path, message_type): |
| """Validates the format of a fieldtrial configuration. |
| |
| Args: |
| json_data: Parsed JSON object representing the fieldtrial config. |
| file_path: String representing the path to the JSON file. |
| message_type: Type of message from |output_api| to return in the case of |
| errors/warnings. |
| |
| Returns: |
| A list of |message_type| messages. In the case of all tests passing with no |
| warnings/errors, this will return []. |
| """ |
| if not isinstance(json_data, dict): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Expecting dict') |
| for (study, experiment_configs) in json_data.iteritems(): |
| if not isinstance(study, unicode): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Expecting keys to be string, got %s', type(study)) |
| if not isinstance(experiment_configs, list): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Expecting list for study %s', study) |
| for experiment_config in experiment_configs: |
| if not isinstance(experiment_config, dict): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Expecting dict for experiment config in Study[%s]', study) |
| if not 'experiments' in experiment_config: |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Missing valid experiments for experiment config in Study[%s]', |
| study) |
| if not isinstance(experiment_config['experiments'], list): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Expecting list for experiments in Study[%s]', study) |
| for experiment in experiment_config['experiments']: |
| if not 'name' in experiment or not isinstance(experiment['name'], |
| unicode): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Missing valid name for experiment in Study[%s]', study) |
| if 'params' in experiment: |
| params = experiment['params'] |
| if not isinstance(params, dict): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Expected dict for params for Experiment[%s] in Study[%s]', |
| experiment['name'], study) |
| for (key, value) in params.iteritems(): |
| if not isinstance(key, unicode) or not isinstance(value, unicode): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Invalid param (%s: %s) for Experiment[%s] in Study[%s]', |
| key, value, experiment['name'], study) |
| for key in experiment.keys(): |
| if key not in VALID_EXPERIMENT_KEYS: |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Key[%s] in Experiment[%s] in Study[%s] is not a valid key.', |
| key, experiment['name'], study) |
| if not 'platforms' in experiment_config: |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Missing valid platforms for experiment config in Study[%s]', study) |
| if not isinstance(experiment_config['platforms'], list): |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Expecting list for platforms in Study[%s]', study) |
| supported_platforms = ['android', 'android_webview', 'chromeos', 'ios', |
| 'linux', 'mac', 'windows'] |
| experiment_platforms = experiment_config['platforms'] |
| unsupported_platforms = list(set(experiment_platforms).difference( |
| supported_platforms)) |
| if unsupported_platforms: |
| return _CreateMalformedConfigMessage(message_type, file_path, |
| 'Unsupported platforms %s in Study[%s]', |
| unsupported_platforms, study) |
| |
| return [] |
| |
| def _CreateMalformedConfigMessage(message_type, file_path, message_format, |
| *args): |
| """Returns a list containing one |message_type| with the error message. |
| |
| Args: |
| message_type: Type of message from |output_api| to return in the case of |
| errors/warnings. |
| message_format: The error message format string. |
| file_path: The path to the config file. |
| *args: The args for message_format. |
| |
| Returns: |
| A list containing a message_type with a formatted error message and |
| 'Malformed config file [file]: ' prepended to it. |
| """ |
| error_message_format = 'Malformed config file %s: ' + message_format |
| format_args = (file_path,) + args |
| return [message_type(error_message_format % format_args)] |
| |
| def CheckPretty(contents, file_path, message_type): |
| """Validates the pretty printing of fieldtrial configuration. |
| |
| Args: |
| contents: File contents as a string. |
| file_path: String representing the path to the JSON file. |
| message_type: Type of message from |output_api| to return in the case of |
| errors/warnings. |
| |
| Returns: |
| A list of |message_type| messages. In the case of all tests passing with no |
| warnings/errors, this will return []. |
| """ |
| pretty = PrettyPrint(contents) |
| if contents != pretty: |
| return [message_type( |
| 'Pretty printing error: Run ' |
| 'python testing/variations/PRESUBMIT.py %s' % file_path)] |
| return [] |
| |
| def CommonChecks(input_api, output_api): |
| affected_files = input_api.AffectedFiles( |
| include_deletes=False, |
| file_filter=lambda x: x.LocalPath().endswith('.json')) |
| for f in affected_files: |
| contents = input_api.ReadFile(f) |
| try: |
| json_data = input_api.json.loads(contents) |
| result = ValidateData(json_data, f.LocalPath(), output_api.PresubmitError) |
| if len(result): |
| return result |
| result = CheckPretty(contents, f.LocalPath(), output_api.PresubmitError) |
| if len(result): |
| return result |
| except ValueError: |
| return [output_api.PresubmitError( |
| 'Malformed JSON file: %s' % f.LocalPath())] |
| return [] |
| |
| def CheckChangeOnUpload(input_api, output_api): |
| return CommonChecks(input_api, output_api) |
| |
| def CheckChangeOnCommit(input_api, output_api): |
| return CommonChecks(input_api, output_api) |
| |
| |
| def main(argv): |
| content = open(argv[1]).read() |
| pretty = PrettyPrint(content) |
| open(argv[1], 'wb').write(pretty) |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv)) |