Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(617)

Side by Side Diff: testing/variations/PRESUBMIT.py

Issue 2296493002: Merge all Field Trial Testing Configuration Together (Closed)
Patch Set: Add Comment Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/common/variations/BUILD.gn ('k') | testing/variations/fieldtrial_testing_config.json » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2015 The Chromium Authors. All rights reserved. 1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 """Presubmit script validating field trial configs. 4 """Presubmit script validating field trial configs.
5 5
6 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts 6 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
7 for more details on the presubmit API built into depot_tools. 7 for more details on the presubmit API built into depot_tools.
8 """ 8 """
9 9
10 import copy
10 import json 11 import json
11 import sys 12 import sys
12 13
13 VALID_GROUP_KEYS = ['group_name', 14 from collections import OrderedDict
14 'params', 15
15 'enable_features', 16 VALID_EXPERIMENT_KEYS = ['name',
16 'disable_features', 17 'params',
17 '//0', 18 'enable_features',
18 '//1', 19 'disable_features',
19 '//2', 20 '//0',
20 '//3', 21 '//1',
21 '//4', 22 '//2',
22 '//5', 23 '//3',
23 '//6', 24 '//4',
24 '//7', 25 '//5',
25 '//8', 26 '//6',
26 '//9'] 27 '//7',
28 '//8',
29 '//9']
27 30
28 def PrettyPrint(contents): 31 def PrettyPrint(contents):
29 """Pretty prints a fieldtrial configuration. 32 """Pretty prints a fieldtrial configuration.
30 33
31 Args: 34 Args:
32 contents: File contents as a string. 35 contents: File contents as a string.
33 36
34 Returns: 37 Returns:
35 Pretty printed file contents. 38 Pretty printed file contents.
36 """ 39 """
37 return json.dumps(json.loads(contents), 40
38 sort_keys=True, indent=4, 41 # We have a preferred ordering of the fields (e.g. platforms on top). This
42 # code loads everything into OrderedDicts and then tells json to dump it out.
43 # The JSON dumper will respect the dict ordering.
44 #
45 # The ordering is as follows:
46 # {
47 # 'StudyName Alphabetical': [
48 # {
49 # 'platforms': [sorted platforms]
50 # 'groups': [
51 # {
52 # name: ...
53 # params: {sorted dict}
54 # enable_features: [sorted features]
55 # disable_features: [sorted features]
56 # (Unexpected extra keys will be caught by the validator)
57 # }
58 # ],
59 # ....
60 # },
61 # ...
62 # ]
63 # ...
64 # }
65 config = json.loads(contents)
66 ordered_config = OrderedDict()
67 for key in sorted(config.keys()):
68 study = copy.deepcopy(config[key])
69 ordered_study = []
70 for experiment_config in study:
71 ordered_experiment_config = OrderedDict([
72 ('platforms', experiment_config['platforms']),
73 ('experiments', [])])
74 for experiment in experiment_config['experiments']:
75 ordered_experiment = OrderedDict()
76 for index in xrange(0, 10):
77 comment_key = '//' + str(index)
78 if comment_key in experiment:
79 ordered_experiment[comment_key] = experiment[comment_key]
80 ordered_experiment['name'] = experiment['name']
81 if 'params' in experiment:
82 ordered_experiment['params'] = OrderedDict(
83 sorted(experiment['params'].items(), key=lambda t: t[0]))
84 if 'enable_features' in experiment:
85 ordered_experiment['enable_features'] = \
86 sorted(experiment['enable_features'])
87 if 'disable_features' in experiment:
88 ordered_experiment['disable_features'] = \
89 sorted(experiment['disable_features'])
90 ordered_experiment_config['experiments'].append(ordered_experiment)
91 ordered_study.append(ordered_experiment_config)
92 ordered_config[key] = ordered_study
93 return json.dumps(ordered_config,
94 sort_keys=False, indent=4,
39 separators=(',', ': ')) + '\n' 95 separators=(',', ': ')) + '\n'
40 96
41 def ValidateData(json_data, file_path, message_type): 97 def ValidateData(json_data, file_path, message_type):
42 """Validates the format of a fieldtrial configuration. 98 """Validates the format of a fieldtrial configuration.
43 99
44 Args: 100 Args:
45 json_data: Parsed JSON object representing the fieldtrial config. 101 json_data: Parsed JSON object representing the fieldtrial config.
46 file_path: String representing the path to the JSON file. 102 file_path: String representing the path to the JSON file.
47 message_type: Type of message from |output_api| to return in the case of 103 message_type: Type of message from |output_api| to return in the case of
48 errors/warnings. 104 errors/warnings.
49 105
50 Returns: 106 Returns:
51 A list of |message_type| messages. In the case of all tests passing with no 107 A list of |message_type| messages. In the case of all tests passing with no
52 warnings/errors, this will return []. 108 warnings/errors, this will return [].
53 """ 109 """
54 if not isinstance(json_data, dict): 110 if not isinstance(json_data, dict):
55 return [message_type( 111 return _CreateMalformedConfigMessage(message_type, file_path,
56 'Malformed config file %s: Expecting dict' % file_path)] 112 'Expecting dict')
57 for (study, groups) in json_data.iteritems(): 113 for (study, experiment_configs) in json_data.iteritems():
58 if not isinstance(study, unicode): 114 if not isinstance(study, unicode):
59 return [message_type( 115 return _CreateMalformedConfigMessage(message_type, file_path,
60 'Malformed config file %s: Expecting keys to be string, got %s' 116 'Expecting keys to be string, got %s', type(study))
61 % (file_path, type(study)))] 117 if not isinstance(experiment_configs, list):
62 if not isinstance(groups, list): 118 return _CreateMalformedConfigMessage(message_type, file_path,
63 return [message_type( 119 'Expecting list for study %s', study)
64 'Malformed config file %s: Expecting list for study %s' 120 for experiment_config in experiment_configs:
65 % (file_path, study))] 121 if not isinstance(experiment_config, dict):
66 for group in groups: 122 return _CreateMalformedConfigMessage(message_type, file_path,
67 if not isinstance(group, dict): 123 'Expecting dict for experiment config in Study[%s]', study)
68 return [message_type( 124 if not 'experiments' in experiment_config:
69 'Malformed config file %s: Expecting dict for group in ' 125 return _CreateMalformedConfigMessage(message_type, file_path,
70 'Study[%s]' % (file_path, study))] 126 'Missing valid experiments for experiment config in Study[%s]',
71 if not 'group_name' in group or not isinstance(group['group_name'], 127 study)
72 unicode): 128 if not isinstance(experiment_config['experiments'], list):
73 return [message_type( 129 return _CreateMalformedConfigMessage(message_type, file_path,
74 'Malformed config file %s: Missing valid group_name for group' 130 'Expecting list for experiments in Study[%s]', study)
75 ' in Study[%s]' % (file_path, study))] 131 for experiment in experiment_config['experiments']:
76 if 'params' in group: 132 if not 'name' in experiment or not isinstance(experiment['name'],
77 params = group['params'] 133 unicode):
78 if not isinstance(params, dict): 134 return _CreateMalformedConfigMessage(message_type, file_path,
79 return [message_type( 135 'Missing valid name for experiment in Study[%s]', study)
80 'Malformed config file %s: Invalid params for Group[%s]' 136 if 'params' in experiment:
81 ' in Study[%s]' % (file_path, group['group_name'], 137 params = experiment['params']
82 study))] 138 if not isinstance(params, dict):
83 for (key, value) in params.iteritems(): 139 return _CreateMalformedConfigMessage(message_type, file_path,
84 if not isinstance(key, unicode) or not isinstance(value, 140 'Expected dict for params for Experiment[%s] in Study[%s]',
85 unicode): 141 experiment['name'], study)
86 return [message_type( 142 for (key, value) in params.iteritems():
87 'Malformed config file %s: Invalid params for Group[%s]' 143 if not isinstance(key, unicode) or not isinstance(value, unicode):
88 ' in Study[%s]' % (file_path, group['group_name'], 144 return _CreateMalformedConfigMessage(message_type, file_path,
89 study))] 145 '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
90 for key in group.keys(): 146 key, value, experiment['name'], study)
91 if key not in VALID_GROUP_KEYS: 147 for key in experiment.keys():
92 return [message_type( 148 if key not in VALID_EXPERIMENT_KEYS:
93 'Malformed config file %s: Key[%s] in Group[%s] in Study[%s] ' 149 return _CreateMalformedConfigMessage(message_type, file_path,
94 'is not a valid key.' % ( 150 'Key[%s] in Experiment[%s] in Study[%s] is not a valid key.',
95 file_path, key, group['group_name'], study))] 151 key, experiment['name'], study)
152 if not 'platforms' in experiment_config:
153 return _CreateMalformedConfigMessage(message_type, file_path,
154 'Missing valid platforms for experiment config in Study[%s]', study)
155 if not isinstance(experiment_config['platforms'], list):
156 return _CreateMalformedConfigMessage(message_type, file_path,
157 'Expecting list for platforms in Study[%s]', study)
158 supported_platforms = ['android', 'chromeos', 'ios', 'linux', 'mac',
159 'win']
160 experiment_platforms = experiment_config['platforms']
161 unsupported_platforms = list(set(experiment_platforms).difference(
162 supported_platforms))
163 if unsupported_platforms:
164 return _CreateMalformedConfigMessage(message_type, file_path,
165 'Unsupported platforms %s in Study[%s]',
166 unsupported_platforms, study)
96 167
97 return [] 168 return []
98 169
170 def _CreateMalformedConfigMessage(message_type, file_path, message_format,
171 *args):
172 """Returns a list containing one |message_type| with the error message.
173
174 Args:
175 message_type: Type of message from |output_api| to return in the case of
176 errors/warnings.
177 message_format: The error message format string.
178 file_path: The path to the config file.
179 *args: The args for message_format.
180
181 Returns:
182 A list containing a message_type with a formatted error message and
183 'Malformed config file [file]: ' prepended to it.
184 """
185 error_message_format = 'Malformed config file %s: ' + message_format
186 format_args = (file_path,) + args
187 return [message_type(error_message_format % format_args)]
188
99 def CheckPretty(contents, file_path, message_type): 189 def CheckPretty(contents, file_path, message_type):
100 """Validates the pretty printing of fieldtrial configuration. 190 """Validates the pretty printing of fieldtrial configuration.
101 191
102 Args: 192 Args:
103 contents: File contents as a string. 193 contents: File contents as a string.
104 file_path: String representing the path to the JSON file. 194 file_path: String representing the path to the JSON file.
105 message_type: Type of message from |output_api| to return in the case of 195 message_type: Type of message from |output_api| to return in the case of
106 errors/warnings. 196 errors/warnings.
107 197
108 Returns: 198 Returns:
109 A list of |message_type| messages. In the case of all tests passing with no 199 A list of |message_type| messages. In the case of all tests passing with no
110 warnings/errors, this will return []. 200 warnings/errors, this will return [].
111 """ 201 """
112 pretty = PrettyPrint(contents) 202 pretty = PrettyPrint(contents)
113 if contents != pretty: 203 if contents != pretty:
114 return [message_type( 204 return [message_type(
115 'Pretty printing error: Run ' 205 'Pretty printing error: Run '
116 'python testing/variations/PRESUBMIT.py %s' % file_path)] 206 'python testing/variations/PRESUBMIT.py %s' % file_path)]
117 return [] 207 return []
118 208
119 def CommonChecks(input_api, output_api): 209 def CommonChecks(input_api, output_api):
120 affected_files = input_api.AffectedFiles( 210 affected_files = input_api.AffectedFiles(
121 include_deletes=False, 211 include_deletes=False,
122 file_filter=lambda x: x.LocalPath().endswith('.json')) 212 file_filter=lambda x: x.LocalPath().endswith('.json'))
123 for f in affected_files: 213 for f in affected_files:
124 contents = input_api.ReadFile(f) 214 contents = input_api.ReadFile(f)
125 try: 215 try:
126 json_data = input_api.json.loads(contents) 216 json_data = input_api.json.loads(contents)
217 result = ValidateData(json_data, f.LocalPath(), output_api.PresubmitError)
218 if len(result):
219 return result
127 result = CheckPretty(contents, f.LocalPath(), output_api.PresubmitError) 220 result = CheckPretty(contents, f.LocalPath(), output_api.PresubmitError)
128 if len(result): 221 if len(result):
129 return result 222 return result
130 result = ValidateData(json_data, f.LocalPath(),
131 output_api.PresubmitError)
132 if len(result):
133 return result
134 except ValueError: 223 except ValueError:
135 return [output_api.PresubmitError( 224 return [output_api.PresubmitError(
136 'Malformed JSON file: %s' % f.LocalPath())] 225 'Malformed JSON file: %s' % f.LocalPath())]
137 return [] 226 return []
138 227
139 def CheckChangeOnUpload(input_api, output_api): 228 def CheckChangeOnUpload(input_api, output_api):
140 return CommonChecks(input_api, output_api) 229 return CommonChecks(input_api, output_api)
141 230
142 def CheckChangeOnCommit(input_api, output_api): 231 def CheckChangeOnCommit(input_api, output_api):
143 return CommonChecks(input_api, output_api) 232 return CommonChecks(input_api, output_api)
144 233
145 234
146 def main(argv): 235 def main(argv):
147 content = open(argv[1]).read() 236 content = open(argv[1]).read()
148 pretty = PrettyPrint(content) 237 pretty = PrettyPrint(content)
149 open(argv[1],'w').write(pretty) 238 open(argv[1],'w').write(pretty)
150 239
151 if __name__ == "__main__": 240 if __name__ == "__main__":
152 sys.exit(main(sys.argv)) 241 sys.exit(main(sys.argv))
OLDNEW
« no previous file with comments | « chrome/common/variations/BUILD.gn ('k') | testing/variations/fieldtrial_testing_config.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698