xref: /aosp_15_r20/external/cronet/testing/variations/PRESUBMIT.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1*6777b538SAndroid Build Coastguard Worker# Copyright 2015 The Chromium Authors
2*6777b538SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
3*6777b538SAndroid Build Coastguard Worker# found in the LICENSE file.
4*6777b538SAndroid Build Coastguard Worker"""Presubmit script validating field trial configs.
5*6777b538SAndroid Build Coastguard Worker
6*6777b538SAndroid Build Coastguard WorkerSee http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
7*6777b538SAndroid Build Coastguard Workerfor more details on the presubmit API built into depot_tools.
8*6777b538SAndroid Build Coastguard Worker"""
9*6777b538SAndroid Build Coastguard Worker
10*6777b538SAndroid Build Coastguard Workerimport copy
11*6777b538SAndroid Build Coastguard Workerimport io
12*6777b538SAndroid Build Coastguard Workerimport json
13*6777b538SAndroid Build Coastguard Workerimport re
14*6777b538SAndroid Build Coastguard Workerimport sys
15*6777b538SAndroid Build Coastguard Worker
16*6777b538SAndroid Build Coastguard Workerfrom collections import OrderedDict
17*6777b538SAndroid Build Coastguard Worker
18*6777b538SAndroid Build Coastguard WorkerVALID_EXPERIMENT_KEYS = [
19*6777b538SAndroid Build Coastguard Worker    'name', 'forcing_flag', 'params', 'enable_features', 'disable_features',
20*6777b538SAndroid Build Coastguard Worker    'min_os_version', 'hardware_classes', 'exclude_hardware_classes', '//0',
21*6777b538SAndroid Build Coastguard Worker    '//1', '//2', '//3', '//4', '//5', '//6', '//7', '//8', '//9'
22*6777b538SAndroid Build Coastguard Worker]
23*6777b538SAndroid Build Coastguard Worker
24*6777b538SAndroid Build Coastguard WorkerFIELDTRIAL_CONFIG_FILE_NAME = 'fieldtrial_testing_config.json'
25*6777b538SAndroid Build Coastguard Worker
26*6777b538SAndroid Build Coastguard WorkerBASE_FEATURE_PATTERN = r"BASE_FEATURE\((.*?),(.*?),(.*?)\);"
27*6777b538SAndroid Build Coastguard WorkerBASE_FEATURE_RE = re.compile(BASE_FEATURE_PATTERN, flags=re.MULTILINE+re.DOTALL)
28*6777b538SAndroid Build Coastguard Worker
29*6777b538SAndroid Build Coastguard Workerdef PrettyPrint(contents):
30*6777b538SAndroid Build Coastguard Worker  """Pretty prints a fieldtrial configuration.
31*6777b538SAndroid Build Coastguard Worker
32*6777b538SAndroid Build Coastguard Worker  Args:
33*6777b538SAndroid Build Coastguard Worker    contents: File contents as a string.
34*6777b538SAndroid Build Coastguard Worker
35*6777b538SAndroid Build Coastguard Worker  Returns:
36*6777b538SAndroid Build Coastguard Worker    Pretty printed file contents.
37*6777b538SAndroid Build Coastguard Worker  """
38*6777b538SAndroid Build Coastguard Worker
39*6777b538SAndroid Build Coastguard Worker  # We have a preferred ordering of the fields (e.g. platforms on top). This
40*6777b538SAndroid Build Coastguard Worker  # code loads everything into OrderedDicts and then tells json to dump it out.
41*6777b538SAndroid Build Coastguard Worker  # The JSON dumper will respect the dict ordering.
42*6777b538SAndroid Build Coastguard Worker  #
43*6777b538SAndroid Build Coastguard Worker  # The ordering is as follows:
44*6777b538SAndroid Build Coastguard Worker  # {
45*6777b538SAndroid Build Coastguard Worker  #     'StudyName Alphabetical': [
46*6777b538SAndroid Build Coastguard Worker  #         {
47*6777b538SAndroid Build Coastguard Worker  #             'platforms': [sorted platforms]
48*6777b538SAndroid Build Coastguard Worker  #             'groups': [
49*6777b538SAndroid Build Coastguard Worker  #                 {
50*6777b538SAndroid Build Coastguard Worker  #                     name: ...
51*6777b538SAndroid Build Coastguard Worker  #                     forcing_flag: "forcing flag string"
52*6777b538SAndroid Build Coastguard Worker  #                     params: {sorted dict}
53*6777b538SAndroid Build Coastguard Worker  #                     enable_features: [sorted features]
54*6777b538SAndroid Build Coastguard Worker  #                     disable_features: [sorted features]
55*6777b538SAndroid Build Coastguard Worker  #                     min_os_version: "version string"
56*6777b538SAndroid Build Coastguard Worker  #                     hardware_classes: [sorted classes]
57*6777b538SAndroid Build Coastguard Worker  #                     exclude_hardware_classes: [sorted classes]
58*6777b538SAndroid Build Coastguard Worker  #                     (Unexpected extra keys will be caught by the validator)
59*6777b538SAndroid Build Coastguard Worker  #                 }
60*6777b538SAndroid Build Coastguard Worker  #             ],
61*6777b538SAndroid Build Coastguard Worker  #             ....
62*6777b538SAndroid Build Coastguard Worker  #         },
63*6777b538SAndroid Build Coastguard Worker  #         ...
64*6777b538SAndroid Build Coastguard Worker  #     ]
65*6777b538SAndroid Build Coastguard Worker  #     ...
66*6777b538SAndroid Build Coastguard Worker  # }
67*6777b538SAndroid Build Coastguard Worker  config = json.loads(contents)
68*6777b538SAndroid Build Coastguard Worker  ordered_config = OrderedDict()
69*6777b538SAndroid Build Coastguard Worker  for key in sorted(config.keys()):
70*6777b538SAndroid Build Coastguard Worker    study = copy.deepcopy(config[key])
71*6777b538SAndroid Build Coastguard Worker    ordered_study = []
72*6777b538SAndroid Build Coastguard Worker    for experiment_config in study:
73*6777b538SAndroid Build Coastguard Worker      ordered_experiment_config = OrderedDict([('platforms',
74*6777b538SAndroid Build Coastguard Worker                                                experiment_config['platforms']),
75*6777b538SAndroid Build Coastguard Worker                                               ('experiments', [])])
76*6777b538SAndroid Build Coastguard Worker      for experiment in experiment_config['experiments']:
77*6777b538SAndroid Build Coastguard Worker        ordered_experiment = OrderedDict()
78*6777b538SAndroid Build Coastguard Worker        for index in range(0, 10):
79*6777b538SAndroid Build Coastguard Worker          comment_key = '//' + str(index)
80*6777b538SAndroid Build Coastguard Worker          if comment_key in experiment:
81*6777b538SAndroid Build Coastguard Worker            ordered_experiment[comment_key] = experiment[comment_key]
82*6777b538SAndroid Build Coastguard Worker        ordered_experiment['name'] = experiment['name']
83*6777b538SAndroid Build Coastguard Worker        if 'forcing_flag' in experiment:
84*6777b538SAndroid Build Coastguard Worker          ordered_experiment['forcing_flag'] = experiment['forcing_flag']
85*6777b538SAndroid Build Coastguard Worker        if 'params' in experiment:
86*6777b538SAndroid Build Coastguard Worker          ordered_experiment['params'] = OrderedDict(
87*6777b538SAndroid Build Coastguard Worker              sorted(experiment['params'].items(), key=lambda t: t[0]))
88*6777b538SAndroid Build Coastguard Worker        if 'enable_features' in experiment:
89*6777b538SAndroid Build Coastguard Worker          ordered_experiment['enable_features'] = \
90*6777b538SAndroid Build Coastguard Worker              sorted(experiment['enable_features'])
91*6777b538SAndroid Build Coastguard Worker        if 'disable_features' in experiment:
92*6777b538SAndroid Build Coastguard Worker          ordered_experiment['disable_features'] = \
93*6777b538SAndroid Build Coastguard Worker              sorted(experiment['disable_features'])
94*6777b538SAndroid Build Coastguard Worker        if 'min_os_version' in experiment:
95*6777b538SAndroid Build Coastguard Worker          ordered_experiment['min_os_version'] = experiment['min_os_version']
96*6777b538SAndroid Build Coastguard Worker        if 'hardware_classes' in experiment:
97*6777b538SAndroid Build Coastguard Worker          ordered_experiment['hardware_classes'] = \
98*6777b538SAndroid Build Coastguard Worker              sorted(experiment['hardware_classes'])
99*6777b538SAndroid Build Coastguard Worker        if 'exclude_hardware_classes' in experiment:
100*6777b538SAndroid Build Coastguard Worker          ordered_experiment['exclude_hardware_classes'] = \
101*6777b538SAndroid Build Coastguard Worker              sorted(experiment['exclude_hardware_classes'])
102*6777b538SAndroid Build Coastguard Worker        ordered_experiment_config['experiments'].append(ordered_experiment)
103*6777b538SAndroid Build Coastguard Worker      ordered_study.append(ordered_experiment_config)
104*6777b538SAndroid Build Coastguard Worker    ordered_config[key] = ordered_study
105*6777b538SAndroid Build Coastguard Worker  return json.dumps(
106*6777b538SAndroid Build Coastguard Worker      ordered_config, sort_keys=False, indent=4, separators=(',', ': ')) + '\n'
107*6777b538SAndroid Build Coastguard Worker
108*6777b538SAndroid Build Coastguard Worker
109*6777b538SAndroid Build Coastguard Workerdef ValidateData(json_data, file_path, message_type):
110*6777b538SAndroid Build Coastguard Worker  """Validates the format of a fieldtrial configuration.
111*6777b538SAndroid Build Coastguard Worker
112*6777b538SAndroid Build Coastguard Worker  Args:
113*6777b538SAndroid Build Coastguard Worker    json_data: Parsed JSON object representing the fieldtrial config.
114*6777b538SAndroid Build Coastguard Worker    file_path: String representing the path to the JSON file.
115*6777b538SAndroid Build Coastguard Worker    message_type: Type of message from |output_api| to return in the case of
116*6777b538SAndroid Build Coastguard Worker      errors/warnings.
117*6777b538SAndroid Build Coastguard Worker
118*6777b538SAndroid Build Coastguard Worker  Returns:
119*6777b538SAndroid Build Coastguard Worker    A list of |message_type| messages. In the case of all tests passing with no
120*6777b538SAndroid Build Coastguard Worker    warnings/errors, this will return [].
121*6777b538SAndroid Build Coastguard Worker  """
122*6777b538SAndroid Build Coastguard Worker
123*6777b538SAndroid Build Coastguard Worker  def _CreateMessage(message_format, *args):
124*6777b538SAndroid Build Coastguard Worker    return _CreateMalformedConfigMessage(message_type, file_path,
125*6777b538SAndroid Build Coastguard Worker                                         message_format, *args)
126*6777b538SAndroid Build Coastguard Worker
127*6777b538SAndroid Build Coastguard Worker  if not isinstance(json_data, dict):
128*6777b538SAndroid Build Coastguard Worker    return _CreateMessage('Expecting dict')
129*6777b538SAndroid Build Coastguard Worker  for (study, experiment_configs) in iter(json_data.items()):
130*6777b538SAndroid Build Coastguard Worker    warnings = _ValidateEntry(study, experiment_configs, _CreateMessage)
131*6777b538SAndroid Build Coastguard Worker    if warnings:
132*6777b538SAndroid Build Coastguard Worker      return warnings
133*6777b538SAndroid Build Coastguard Worker
134*6777b538SAndroid Build Coastguard Worker  return []
135*6777b538SAndroid Build Coastguard Worker
136*6777b538SAndroid Build Coastguard Worker
137*6777b538SAndroid Build Coastguard Workerdef _ValidateEntry(study, experiment_configs, create_message_fn):
138*6777b538SAndroid Build Coastguard Worker  """Validates one entry of the field trial configuration."""
139*6777b538SAndroid Build Coastguard Worker  if not isinstance(study, str):
140*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Expecting keys to be string, got %s', type(study))
141*6777b538SAndroid Build Coastguard Worker  if not isinstance(experiment_configs, list):
142*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Expecting list for study %s', study)
143*6777b538SAndroid Build Coastguard Worker
144*6777b538SAndroid Build Coastguard Worker  # Add context to other messages.
145*6777b538SAndroid Build Coastguard Worker  def _CreateStudyMessage(message_format, *args):
146*6777b538SAndroid Build Coastguard Worker    suffix = ' in Study[%s]' % study
147*6777b538SAndroid Build Coastguard Worker    return create_message_fn(message_format + suffix, *args)
148*6777b538SAndroid Build Coastguard Worker
149*6777b538SAndroid Build Coastguard Worker  for experiment_config in experiment_configs:
150*6777b538SAndroid Build Coastguard Worker    warnings = _ValidateExperimentConfig(experiment_config, _CreateStudyMessage)
151*6777b538SAndroid Build Coastguard Worker    if warnings:
152*6777b538SAndroid Build Coastguard Worker      return warnings
153*6777b538SAndroid Build Coastguard Worker  return []
154*6777b538SAndroid Build Coastguard Worker
155*6777b538SAndroid Build Coastguard Worker
156*6777b538SAndroid Build Coastguard Workerdef _ValidateExperimentConfig(experiment_config, create_message_fn):
157*6777b538SAndroid Build Coastguard Worker  """Validates one config in a configuration entry."""
158*6777b538SAndroid Build Coastguard Worker  if not isinstance(experiment_config, dict):
159*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Expecting dict for experiment config')
160*6777b538SAndroid Build Coastguard Worker  if not 'experiments' in experiment_config:
161*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Missing valid experiments for experiment config')
162*6777b538SAndroid Build Coastguard Worker  if not isinstance(experiment_config['experiments'], list):
163*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Expecting list for experiments')
164*6777b538SAndroid Build Coastguard Worker  for experiment_group in experiment_config['experiments']:
165*6777b538SAndroid Build Coastguard Worker    warnings = _ValidateExperimentGroup(experiment_group, create_message_fn)
166*6777b538SAndroid Build Coastguard Worker    if warnings:
167*6777b538SAndroid Build Coastguard Worker      return warnings
168*6777b538SAndroid Build Coastguard Worker  if not 'platforms' in experiment_config:
169*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Missing valid platforms for experiment config')
170*6777b538SAndroid Build Coastguard Worker  if not isinstance(experiment_config['platforms'], list):
171*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Expecting list for platforms')
172*6777b538SAndroid Build Coastguard Worker  supported_platforms = [
173*6777b538SAndroid Build Coastguard Worker      'android', 'android_weblayer', 'android_webview', 'chromeos',
174*6777b538SAndroid Build Coastguard Worker      'chromeos_lacros', 'fuchsia', 'ios', 'linux', 'mac', 'windows'
175*6777b538SAndroid Build Coastguard Worker  ]
176*6777b538SAndroid Build Coastguard Worker  experiment_platforms = experiment_config['platforms']
177*6777b538SAndroid Build Coastguard Worker  unsupported_platforms = list(
178*6777b538SAndroid Build Coastguard Worker      set(experiment_platforms).difference(supported_platforms))
179*6777b538SAndroid Build Coastguard Worker  if unsupported_platforms:
180*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Unsupported platforms %s', unsupported_platforms)
181*6777b538SAndroid Build Coastguard Worker  return []
182*6777b538SAndroid Build Coastguard Worker
183*6777b538SAndroid Build Coastguard Worker
184*6777b538SAndroid Build Coastguard Workerdef _ValidateExperimentGroup(experiment_group, create_message_fn):
185*6777b538SAndroid Build Coastguard Worker  """Validates one group of one config in a configuration entry."""
186*6777b538SAndroid Build Coastguard Worker  name = experiment_group.get('name', '')
187*6777b538SAndroid Build Coastguard Worker  if not name or not isinstance(name, str):
188*6777b538SAndroid Build Coastguard Worker    return create_message_fn('Missing valid name for experiment')
189*6777b538SAndroid Build Coastguard Worker
190*6777b538SAndroid Build Coastguard Worker  # Add context to other messages.
191*6777b538SAndroid Build Coastguard Worker  def _CreateGroupMessage(message_format, *args):
192*6777b538SAndroid Build Coastguard Worker    suffix = ' in Group[%s]' % name
193*6777b538SAndroid Build Coastguard Worker    return create_message_fn(message_format + suffix, *args)
194*6777b538SAndroid Build Coastguard Worker
195*6777b538SAndroid Build Coastguard Worker  if 'params' in experiment_group:
196*6777b538SAndroid Build Coastguard Worker    params = experiment_group['params']
197*6777b538SAndroid Build Coastguard Worker    if not isinstance(params, dict):
198*6777b538SAndroid Build Coastguard Worker      return _CreateGroupMessage('Expected dict for params')
199*6777b538SAndroid Build Coastguard Worker    for (key, value) in iter(params.items()):
200*6777b538SAndroid Build Coastguard Worker      if not isinstance(key, str) or not isinstance(value, str):
201*6777b538SAndroid Build Coastguard Worker        return _CreateGroupMessage('Invalid param (%s: %s)', key, value)
202*6777b538SAndroid Build Coastguard Worker  for key in experiment_group.keys():
203*6777b538SAndroid Build Coastguard Worker    if key not in VALID_EXPERIMENT_KEYS:
204*6777b538SAndroid Build Coastguard Worker      return _CreateGroupMessage('Key[%s] is not a valid key', key)
205*6777b538SAndroid Build Coastguard Worker  return []
206*6777b538SAndroid Build Coastguard Worker
207*6777b538SAndroid Build Coastguard Worker
208*6777b538SAndroid Build Coastguard Workerdef _CreateMalformedConfigMessage(message_type, file_path, message_format,
209*6777b538SAndroid Build Coastguard Worker                                  *args):
210*6777b538SAndroid Build Coastguard Worker  """Returns a list containing one |message_type| with the error message.
211*6777b538SAndroid Build Coastguard Worker
212*6777b538SAndroid Build Coastguard Worker  Args:
213*6777b538SAndroid Build Coastguard Worker    message_type: Type of message from |output_api| to return in the case of
214*6777b538SAndroid Build Coastguard Worker      errors/warnings.
215*6777b538SAndroid Build Coastguard Worker    message_format: The error message format string.
216*6777b538SAndroid Build Coastguard Worker    file_path: The path to the config file.
217*6777b538SAndroid Build Coastguard Worker    *args: The args for message_format.
218*6777b538SAndroid Build Coastguard Worker
219*6777b538SAndroid Build Coastguard Worker  Returns:
220*6777b538SAndroid Build Coastguard Worker    A list containing a message_type with a formatted error message and
221*6777b538SAndroid Build Coastguard Worker    'Malformed config file [file]: ' prepended to it.
222*6777b538SAndroid Build Coastguard Worker  """
223*6777b538SAndroid Build Coastguard Worker  error_message_format = 'Malformed config file %s: ' + message_format
224*6777b538SAndroid Build Coastguard Worker  format_args = (file_path,) + args
225*6777b538SAndroid Build Coastguard Worker  return [message_type(error_message_format % format_args)]
226*6777b538SAndroid Build Coastguard Worker
227*6777b538SAndroid Build Coastguard Worker
228*6777b538SAndroid Build Coastguard Workerdef CheckPretty(contents, file_path, message_type):
229*6777b538SAndroid Build Coastguard Worker  """Validates the pretty printing of fieldtrial configuration.
230*6777b538SAndroid Build Coastguard Worker
231*6777b538SAndroid Build Coastguard Worker  Args:
232*6777b538SAndroid Build Coastguard Worker    contents: File contents as a string.
233*6777b538SAndroid Build Coastguard Worker    file_path: String representing the path to the JSON file.
234*6777b538SAndroid Build Coastguard Worker    message_type: Type of message from |output_api| to return in the case of
235*6777b538SAndroid Build Coastguard Worker      errors/warnings.
236*6777b538SAndroid Build Coastguard Worker
237*6777b538SAndroid Build Coastguard Worker  Returns:
238*6777b538SAndroid Build Coastguard Worker    A list of |message_type| messages. In the case of all tests passing with no
239*6777b538SAndroid Build Coastguard Worker    warnings/errors, this will return [].
240*6777b538SAndroid Build Coastguard Worker  """
241*6777b538SAndroid Build Coastguard Worker  pretty = PrettyPrint(contents)
242*6777b538SAndroid Build Coastguard Worker  if contents != pretty:
243*6777b538SAndroid Build Coastguard Worker    return [
244*6777b538SAndroid Build Coastguard Worker        message_type('Pretty printing error: Run '
245*6777b538SAndroid Build Coastguard Worker                     'python3 testing/variations/PRESUBMIT.py %s' % file_path)
246*6777b538SAndroid Build Coastguard Worker    ]
247*6777b538SAndroid Build Coastguard Worker  return []
248*6777b538SAndroid Build Coastguard Worker
249*6777b538SAndroid Build Coastguard Workerdef _GetStudyConfigFeatures(study_config):
250*6777b538SAndroid Build Coastguard Worker  """Gets the set of features overridden in a study config."""
251*6777b538SAndroid Build Coastguard Worker  features = set()
252*6777b538SAndroid Build Coastguard Worker  for experiment in study_config.get("experiments", []):
253*6777b538SAndroid Build Coastguard Worker    features.update(experiment.get("enable_features", []))
254*6777b538SAndroid Build Coastguard Worker    features.update(experiment.get("disable_features", []))
255*6777b538SAndroid Build Coastguard Worker  return features
256*6777b538SAndroid Build Coastguard Worker
257*6777b538SAndroid Build Coastguard Workerdef _GetDuplicatedFeatures(study1, study2):
258*6777b538SAndroid Build Coastguard Worker  """Gets the set of features that are overridden in two overlapping studies."""
259*6777b538SAndroid Build Coastguard Worker  duplicated_features = set()
260*6777b538SAndroid Build Coastguard Worker  for study_config1 in study1:
261*6777b538SAndroid Build Coastguard Worker    features = _GetStudyConfigFeatures(study_config1)
262*6777b538SAndroid Build Coastguard Worker    platforms = set(study_config1.get("platforms", []))
263*6777b538SAndroid Build Coastguard Worker    for study_config2 in study2:
264*6777b538SAndroid Build Coastguard Worker      # If the study configs do not specify any common platform, they do not
265*6777b538SAndroid Build Coastguard Worker      # overlap, so we can skip them.
266*6777b538SAndroid Build Coastguard Worker      if platforms.isdisjoint(set(study_config2.get("platforms", []))):
267*6777b538SAndroid Build Coastguard Worker        continue
268*6777b538SAndroid Build Coastguard Worker
269*6777b538SAndroid Build Coastguard Worker      common_features = features & _GetStudyConfigFeatures(study_config2)
270*6777b538SAndroid Build Coastguard Worker      duplicated_features.update(common_features)
271*6777b538SAndroid Build Coastguard Worker
272*6777b538SAndroid Build Coastguard Worker  return duplicated_features
273*6777b538SAndroid Build Coastguard Worker
274*6777b538SAndroid Build Coastguard Workerdef CheckDuplicatedFeatures(new_json_data, old_json_data, message_type):
275*6777b538SAndroid Build Coastguard Worker  """Validates that features are not specified in multiple studies.
276*6777b538SAndroid Build Coastguard Worker
277*6777b538SAndroid Build Coastguard Worker  Note that a feature may be specified in different studies that do not overlap.
278*6777b538SAndroid Build Coastguard Worker  For example, if they specify different platforms. In such a case, this will
279*6777b538SAndroid Build Coastguard Worker  not give a warning/error. However, it is possible that this incorrectly
280*6777b538SAndroid Build Coastguard Worker  gives an error, as it is possible for studies to have complex filters (e.g.,
281*6777b538SAndroid Build Coastguard Worker  if they make use of additional filters such as form_factors,
282*6777b538SAndroid Build Coastguard Worker  is_low_end_device, etc.). In those cases, the PRESUBMIT check can be bypassed.
283*6777b538SAndroid Build Coastguard Worker  Since this will only check for studies that were changed in this particular
284*6777b538SAndroid Build Coastguard Worker  commit, bypassing the PRESUBMIT check will not block future commits.
285*6777b538SAndroid Build Coastguard Worker
286*6777b538SAndroid Build Coastguard Worker  Args:
287*6777b538SAndroid Build Coastguard Worker    new_json_data: Parsed JSON object representing the new fieldtrial config.
288*6777b538SAndroid Build Coastguard Worker    old_json_data: Parsed JSON object representing the old fieldtrial config.
289*6777b538SAndroid Build Coastguard Worker    message_type: Type of message from |output_api| to return in the case of
290*6777b538SAndroid Build Coastguard Worker      errors/warnings.
291*6777b538SAndroid Build Coastguard Worker
292*6777b538SAndroid Build Coastguard Worker  Returns:
293*6777b538SAndroid Build Coastguard Worker    A list of |message_type| messages. In the case of all tests passing with no
294*6777b538SAndroid Build Coastguard Worker    warnings/errors, this will return [].
295*6777b538SAndroid Build Coastguard Worker  """
296*6777b538SAndroid Build Coastguard Worker  # Get list of studies that changed.
297*6777b538SAndroid Build Coastguard Worker  changed_studies = []
298*6777b538SAndroid Build Coastguard Worker  for study_name in new_json_data:
299*6777b538SAndroid Build Coastguard Worker    if (study_name not in old_json_data or
300*6777b538SAndroid Build Coastguard Worker          new_json_data[study_name] != old_json_data[study_name]):
301*6777b538SAndroid Build Coastguard Worker      changed_studies.append(study_name)
302*6777b538SAndroid Build Coastguard Worker
303*6777b538SAndroid Build Coastguard Worker  # A map between a feature name and the name of studies that use it. E.g.,
304*6777b538SAndroid Build Coastguard Worker  # duplicated_features_to_studies_map["FeatureA"] = {"StudyA", "StudyB"}.
305*6777b538SAndroid Build Coastguard Worker  # Only features that are defined in multiple studies are added to this map.
306*6777b538SAndroid Build Coastguard Worker  duplicated_features_to_studies_map = dict()
307*6777b538SAndroid Build Coastguard Worker
308*6777b538SAndroid Build Coastguard Worker  # Compare the changed studies against all studies defined.
309*6777b538SAndroid Build Coastguard Worker  for changed_study_name in changed_studies:
310*6777b538SAndroid Build Coastguard Worker    for study_name in new_json_data:
311*6777b538SAndroid Build Coastguard Worker      if changed_study_name == study_name:
312*6777b538SAndroid Build Coastguard Worker        continue
313*6777b538SAndroid Build Coastguard Worker
314*6777b538SAndroid Build Coastguard Worker      duplicated_features = _GetDuplicatedFeatures(
315*6777b538SAndroid Build Coastguard Worker          new_json_data[changed_study_name], new_json_data[study_name])
316*6777b538SAndroid Build Coastguard Worker
317*6777b538SAndroid Build Coastguard Worker      for feature in duplicated_features:
318*6777b538SAndroid Build Coastguard Worker        if feature not in duplicated_features_to_studies_map:
319*6777b538SAndroid Build Coastguard Worker          duplicated_features_to_studies_map[feature] = set()
320*6777b538SAndroid Build Coastguard Worker        duplicated_features_to_studies_map[feature].update(
321*6777b538SAndroid Build Coastguard Worker            [changed_study_name, study_name])
322*6777b538SAndroid Build Coastguard Worker
323*6777b538SAndroid Build Coastguard Worker  if len(duplicated_features_to_studies_map) == 0:
324*6777b538SAndroid Build Coastguard Worker    return []
325*6777b538SAndroid Build Coastguard Worker
326*6777b538SAndroid Build Coastguard Worker  duplicated_features_strings = [
327*6777b538SAndroid Build Coastguard Worker      "%s (in studies %s)" % (feature, ', '.join(studies))
328*6777b538SAndroid Build Coastguard Worker      for feature, studies in duplicated_features_to_studies_map.items()
329*6777b538SAndroid Build Coastguard Worker  ]
330*6777b538SAndroid Build Coastguard Worker
331*6777b538SAndroid Build Coastguard Worker  return [
332*6777b538SAndroid Build Coastguard Worker    message_type('The following feature(s) were specified in multiple '
333*6777b538SAndroid Build Coastguard Worker                  'studies: %s' % ', '.join(duplicated_features_strings))
334*6777b538SAndroid Build Coastguard Worker  ]
335*6777b538SAndroid Build Coastguard Worker
336*6777b538SAndroid Build Coastguard Worker
337*6777b538SAndroid Build Coastguard Workerdef CheckUndeclaredFeatures(input_api, output_api, json_data, changed_lines):
338*6777b538SAndroid Build Coastguard Worker  """Checks that feature names are all valid declared features.
339*6777b538SAndroid Build Coastguard Worker
340*6777b538SAndroid Build Coastguard Worker  There have been more than one instance of developers accidentally mistyping
341*6777b538SAndroid Build Coastguard Worker  a feature name in the fieldtrial_testing_config.json file, which leads
342*6777b538SAndroid Build Coastguard Worker  to the config silently doing nothing.
343*6777b538SAndroid Build Coastguard Worker
344*6777b538SAndroid Build Coastguard Worker  This check aims to catch these errors by validating that the feature name
345*6777b538SAndroid Build Coastguard Worker  is defined somewhere in the Chrome source code.
346*6777b538SAndroid Build Coastguard Worker
347*6777b538SAndroid Build Coastguard Worker  Args:
348*6777b538SAndroid Build Coastguard Worker    input_api: Presubmit InputApi
349*6777b538SAndroid Build Coastguard Worker    output_api: Presubmit OutputApi
350*6777b538SAndroid Build Coastguard Worker    json_data: The parsed fieldtrial_testing_config.json
351*6777b538SAndroid Build Coastguard Worker    changed_lines: The AffectedFile.ChangedContents() of the json file
352*6777b538SAndroid Build Coastguard Worker
353*6777b538SAndroid Build Coastguard Worker  Returns:
354*6777b538SAndroid Build Coastguard Worker    List of validation messages - empty if there are no errors.
355*6777b538SAndroid Build Coastguard Worker  """
356*6777b538SAndroid Build Coastguard Worker
357*6777b538SAndroid Build Coastguard Worker  declared_features = set()
358*6777b538SAndroid Build Coastguard Worker  # I was unable to figure out how to do a proper top-level include that did
359*6777b538SAndroid Build Coastguard Worker  # not depend on getting the path from input_api. I found this pattern
360*6777b538SAndroid Build Coastguard Worker  # elsewhere in the code base. Please change to a top-level include if you
361*6777b538SAndroid Build Coastguard Worker  # know how.
362*6777b538SAndroid Build Coastguard Worker  old_sys_path = sys.path[:]
363*6777b538SAndroid Build Coastguard Worker  try:
364*6777b538SAndroid Build Coastguard Worker    sys.path.append(input_api.os_path.join(
365*6777b538SAndroid Build Coastguard Worker            input_api.PresubmitLocalPath(), 'presubmit'))
366*6777b538SAndroid Build Coastguard Worker    # pylint: disable=import-outside-toplevel
367*6777b538SAndroid Build Coastguard Worker    import find_features
368*6777b538SAndroid Build Coastguard Worker    # pylint: enable=import-outside-toplevel
369*6777b538SAndroid Build Coastguard Worker    declared_features = find_features.FindDeclaredFeatures(input_api)
370*6777b538SAndroid Build Coastguard Worker  finally:
371*6777b538SAndroid Build Coastguard Worker    sys.path = old_sys_path
372*6777b538SAndroid Build Coastguard Worker
373*6777b538SAndroid Build Coastguard Worker  if not declared_features:
374*6777b538SAndroid Build Coastguard Worker    return [message_type("Presubmit unable to find any declared flags "
375*6777b538SAndroid Build Coastguard Worker                         "in source. Please check PRESUBMIT.py for errors.")]
376*6777b538SAndroid Build Coastguard Worker
377*6777b538SAndroid Build Coastguard Worker  messages = []
378*6777b538SAndroid Build Coastguard Worker  # Join all changed lines into a single string. This will be used to check
379*6777b538SAndroid Build Coastguard Worker  # if feature names are present in the changed lines by substring search.
380*6777b538SAndroid Build Coastguard Worker  changed_contents = " ".join([x[1].strip() for x in changed_lines])
381*6777b538SAndroid Build Coastguard Worker  for study_name in json_data:
382*6777b538SAndroid Build Coastguard Worker    study = json_data[study_name]
383*6777b538SAndroid Build Coastguard Worker    for config in study:
384*6777b538SAndroid Build Coastguard Worker      features = set(_GetStudyConfigFeatures(config))
385*6777b538SAndroid Build Coastguard Worker      # Determine if a study has been touched by the current change by checking
386*6777b538SAndroid Build Coastguard Worker      # if any of the features are part of the changed lines of the file.
387*6777b538SAndroid Build Coastguard Worker      # This limits the noise from old configs that are no longer valid.
388*6777b538SAndroid Build Coastguard Worker      probably_affected = False
389*6777b538SAndroid Build Coastguard Worker      for feature in features:
390*6777b538SAndroid Build Coastguard Worker        if feature in changed_contents:
391*6777b538SAndroid Build Coastguard Worker          probably_affected = True
392*6777b538SAndroid Build Coastguard Worker          break
393*6777b538SAndroid Build Coastguard Worker
394*6777b538SAndroid Build Coastguard Worker      if probably_affected and not declared_features.issuperset(features):
395*6777b538SAndroid Build Coastguard Worker        missing_features = features - declared_features
396*6777b538SAndroid Build Coastguard Worker        # CrOS has external feature declarations starting with this prefix
397*6777b538SAndroid Build Coastguard Worker        # (checked by build tools in base/BUILD.gn).
398*6777b538SAndroid Build Coastguard Worker        # Warn, but don't break, if they are present in the CL
399*6777b538SAndroid Build Coastguard Worker        cros_late_boot_features = {s for s in missing_features if
400*6777b538SAndroid Build Coastguard Worker                                          s.startswith("CrOSLateBoot")}
401*6777b538SAndroid Build Coastguard Worker        missing_features = missing_features - cros_late_boot_features
402*6777b538SAndroid Build Coastguard Worker        if cros_late_boot_features:
403*6777b538SAndroid Build Coastguard Worker          msg = ("CrOSLateBoot features added to "
404*6777b538SAndroid Build Coastguard Worker                 "study %s are not checked by presubmit."
405*6777b538SAndroid Build Coastguard Worker                 "\nPlease manually check that they exist in the code base."
406*6777b538SAndroid Build Coastguard Worker                ) % study_name
407*6777b538SAndroid Build Coastguard Worker          messages.append(output_api.PresubmitResult(msg,
408*6777b538SAndroid Build Coastguard Worker                                                     cros_late_boot_features))
409*6777b538SAndroid Build Coastguard Worker
410*6777b538SAndroid Build Coastguard Worker        if missing_features:
411*6777b538SAndroid Build Coastguard Worker          msg = ("Presubmit was unable to verify existence of features in "
412*6777b538SAndroid Build Coastguard Worker                  "study %s.\nThis happens most commonly if the feature is "
413*6777b538SAndroid Build Coastguard Worker                  "defined by code generation.\n"
414*6777b538SAndroid Build Coastguard Worker                  "Please verify that the feature names have been spelled "
415*6777b538SAndroid Build Coastguard Worker                  "correctly before submitting. The affected features are:"
416*6777b538SAndroid Build Coastguard Worker              ) % study_name
417*6777b538SAndroid Build Coastguard Worker          messages.append(output_api.PresubmitResult(msg, missing_features))
418*6777b538SAndroid Build Coastguard Worker
419*6777b538SAndroid Build Coastguard Worker  return messages
420*6777b538SAndroid Build Coastguard Worker
421*6777b538SAndroid Build Coastguard Worker
422*6777b538SAndroid Build Coastguard Workerdef CommonChecks(input_api, output_api):
423*6777b538SAndroid Build Coastguard Worker  affected_files = input_api.AffectedFiles(
424*6777b538SAndroid Build Coastguard Worker      include_deletes=False,
425*6777b538SAndroid Build Coastguard Worker      file_filter=lambda x: x.LocalPath().endswith('.json'))
426*6777b538SAndroid Build Coastguard Worker  for f in affected_files:
427*6777b538SAndroid Build Coastguard Worker    if not f.LocalPath().endswith(FIELDTRIAL_CONFIG_FILE_NAME):
428*6777b538SAndroid Build Coastguard Worker      return [
429*6777b538SAndroid Build Coastguard Worker          output_api.PresubmitError(
430*6777b538SAndroid Build Coastguard Worker              '%s is the only json file expected in this folder. If new jsons '
431*6777b538SAndroid Build Coastguard Worker              'are added, please update the presubmit process with proper '
432*6777b538SAndroid Build Coastguard Worker              'validation. ' % FIELDTRIAL_CONFIG_FILE_NAME
433*6777b538SAndroid Build Coastguard Worker          )
434*6777b538SAndroid Build Coastguard Worker      ]
435*6777b538SAndroid Build Coastguard Worker    contents = input_api.ReadFile(f)
436*6777b538SAndroid Build Coastguard Worker    try:
437*6777b538SAndroid Build Coastguard Worker      json_data = input_api.json.loads(contents)
438*6777b538SAndroid Build Coastguard Worker      result = ValidateData(
439*6777b538SAndroid Build Coastguard Worker          json_data,
440*6777b538SAndroid Build Coastguard Worker          f.AbsoluteLocalPath(),
441*6777b538SAndroid Build Coastguard Worker          output_api.PresubmitError)
442*6777b538SAndroid Build Coastguard Worker      if result:
443*6777b538SAndroid Build Coastguard Worker        return result
444*6777b538SAndroid Build Coastguard Worker      result = CheckPretty(contents, f.LocalPath(), output_api.PresubmitError)
445*6777b538SAndroid Build Coastguard Worker      if result:
446*6777b538SAndroid Build Coastguard Worker        return result
447*6777b538SAndroid Build Coastguard Worker      result = CheckDuplicatedFeatures(
448*6777b538SAndroid Build Coastguard Worker          json_data,
449*6777b538SAndroid Build Coastguard Worker          input_api.json.loads('\n'.join(f.OldContents())),
450*6777b538SAndroid Build Coastguard Worker          output_api.PresubmitError)
451*6777b538SAndroid Build Coastguard Worker      if result:
452*6777b538SAndroid Build Coastguard Worker        return result
453*6777b538SAndroid Build Coastguard Worker      result = CheckUndeclaredFeatures(input_api, output_api, json_data,
454*6777b538SAndroid Build Coastguard Worker                                       f.ChangedContents())
455*6777b538SAndroid Build Coastguard Worker      if result:
456*6777b538SAndroid Build Coastguard Worker        return result
457*6777b538SAndroid Build Coastguard Worker    except ValueError:
458*6777b538SAndroid Build Coastguard Worker      return [
459*6777b538SAndroid Build Coastguard Worker          output_api.PresubmitError('Malformed JSON file: %s' % f.LocalPath())
460*6777b538SAndroid Build Coastguard Worker      ]
461*6777b538SAndroid Build Coastguard Worker  return []
462*6777b538SAndroid Build Coastguard Worker
463*6777b538SAndroid Build Coastguard Worker
464*6777b538SAndroid Build Coastguard Workerdef CheckChangeOnUpload(input_api, output_api):
465*6777b538SAndroid Build Coastguard Worker  return CommonChecks(input_api, output_api)
466*6777b538SAndroid Build Coastguard Worker
467*6777b538SAndroid Build Coastguard Worker
468*6777b538SAndroid Build Coastguard Workerdef CheckChangeOnCommit(input_api, output_api):
469*6777b538SAndroid Build Coastguard Worker  return CommonChecks(input_api, output_api)
470*6777b538SAndroid Build Coastguard Worker
471*6777b538SAndroid Build Coastguard Worker
472*6777b538SAndroid Build Coastguard Workerdef main(argv):
473*6777b538SAndroid Build Coastguard Worker  with io.open(argv[1], encoding='utf-8') as f:
474*6777b538SAndroid Build Coastguard Worker    content = f.read()
475*6777b538SAndroid Build Coastguard Worker  pretty = PrettyPrint(content)
476*6777b538SAndroid Build Coastguard Worker  io.open(argv[1], 'wb').write(pretty.encode('utf-8'))
477*6777b538SAndroid Build Coastguard Worker
478*6777b538SAndroid Build Coastguard Worker
479*6777b538SAndroid Build Coastguard Workerif __name__ == '__main__':
480*6777b538SAndroid Build Coastguard Worker  sys.exit(main(sys.argv))
481