OLD | NEW |
---|---|
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)) |
OLD | NEW |