|  | # 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', 'chromeos', 'ios', 'linux', 'mac', | 
|  | 'win'] | 
|  | 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)) |