Index: testing/variations/PRESUBMIT.py |
diff --git a/testing/variations/PRESUBMIT.py b/testing/variations/PRESUBMIT.py |
index 3865988e0e405b41210f6fd88a77cb9524d4305d..22fae124ea5ebcc0d553631c5850f4e3d3f7d3f2 100644 |
--- a/testing/variations/PRESUBMIT.py |
+++ b/testing/variations/PRESUBMIT.py |
@@ -7,23 +7,26 @@ 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 |
-VALID_GROUP_KEYS = ['group_name', |
- 'params', |
- 'enable_features', |
- 'disable_features', |
- '//0', |
- '//1', |
- '//2', |
- '//3', |
- '//4', |
- '//5', |
- '//6', |
- '//7', |
- '//8', |
- '//9'] |
+from collections import OrderedDict |
+ |
+VALID_EXPERIMENT_KEYS = ['name', |
+ 'params', |
+ 'enable_features', |
+ 'disable_features', |
+ '//0', |
+ '//1', |
+ '//2', |
+ '//3', |
+ '//4', |
+ '//5', |
+ '//6', |
+ '//7', |
+ '//8', |
+ '//9'] |
def PrettyPrint(contents): |
"""Pretty prints a fieldtrial configuration. |
@@ -34,8 +37,61 @@ def PrettyPrint(contents): |
Returns: |
Pretty printed file contents. |
""" |
- return json.dumps(json.loads(contents), |
- sort_keys=True, indent=4, |
+ |
+ # 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: ... |
+ # 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 '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): |
@@ -52,50 +108,84 @@ def ValidateData(json_data, file_path, message_type): |
warnings/errors, this will return []. |
""" |
if not isinstance(json_data, dict): |
- return [message_type( |
- 'Malformed config file %s: Expecting dict' % file_path)] |
- for (study, groups) in json_data.iteritems(): |
+ return _CreateMalformedConfigMessage(message_type, file_path, |
+ 'Expecting dict') |
+ for (study, experiment_configs) in json_data.iteritems(): |
if not isinstance(study, unicode): |
- return [message_type( |
- 'Malformed config file %s: Expecting keys to be string, got %s' |
- % (file_path, type(study)))] |
- if not isinstance(groups, list): |
- return [message_type( |
- 'Malformed config file %s: Expecting list for study %s' |
- % (file_path, study))] |
- for group in groups: |
- if not isinstance(group, dict): |
- return [message_type( |
- 'Malformed config file %s: Expecting dict for group in ' |
- 'Study[%s]' % (file_path, study))] |
- if not 'group_name' in group or not isinstance(group['group_name'], |
- unicode): |
- return [message_type( |
- 'Malformed config file %s: Missing valid group_name for group' |
- ' in Study[%s]' % (file_path, study))] |
- if 'params' in group: |
- params = group['params'] |
- if not isinstance(params, dict): |
- return [message_type( |
- 'Malformed config file %s: Invalid params for Group[%s]' |
- ' in Study[%s]' % (file_path, group['group_name'], |
- study))] |
- for (key, value) in params.iteritems(): |
- if not isinstance(key, unicode) or not isinstance(value, |
- unicode): |
- return [message_type( |
- 'Malformed config file %s: Invalid params for Group[%s]' |
- ' in Study[%s]' % (file_path, group['group_name'], |
- study))] |
- for key in group.keys(): |
- if key not in VALID_GROUP_KEYS: |
- return [message_type( |
- 'Malformed config file %s: Key[%s] in Group[%s] in Study[%s] ' |
- 'is not a valid key.' % ( |
- file_path, key, group['group_name'], study))] |
+ 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]', |
Alexei Svitkine (slow)
2016/09/19 18:57:02
Nit: I would hoist "in Study[%s]" out of the custo
robliao
2016/09/19 23:31:38
Making the file_path common made sense because it
|
+ 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. |
@@ -124,11 +214,10 @@ def CommonChecks(input_api, output_api): |
contents = input_api.ReadFile(f) |
try: |
json_data = input_api.json.loads(contents) |
- result = CheckPretty(contents, f.LocalPath(), output_api.PresubmitError) |
+ result = ValidateData(json_data, f.LocalPath(), output_api.PresubmitError) |
if len(result): |
return result |
- result = ValidateData(json_data, f.LocalPath(), |
- output_api.PresubmitError) |
+ result = CheckPretty(contents, f.LocalPath(), output_api.PresubmitError) |
if len(result): |
return result |
except ValueError: |