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