1#!/usr/bin/env python
2# Copyright 2015 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import json
7import os.path
8import sys
9import optparse
10
11_script_path = os.path.realpath(__file__)
12
13sys.path.insert(0, os.path.normpath(_script_path + "/../../json_comment_eater"))
14try:
15  import json_comment_eater
16finally:
17  sys.path.pop(0)
18
19sys.path.insert(0, os.path.normpath(_script_path + "/../../json_to_struct"))
20try:
21  import json_to_struct
22finally:
23  sys.path.pop(0)
24
25sys.path.insert(
26    0,
27    os.path.normpath(_script_path + "/../../../components/variations/service"))
28try:
29  import generate_ui_string_overrider
30finally:
31  sys.path.pop(0)
32
33_platforms = [
34    'android',
35    'android_weblayer',
36    'android_webview',
37    'chromeos',
38    'freebsd',
39    'fuchsia',
40    'ios',
41    'linux',
42    'mac',
43    'windows',
44]
45
46_form_factors = [
47    'desktop',
48    'phone',
49    'tablet',
50]
51
52# Convert a platform argument to the matching Platform enum value in
53# components/variations/proto/study.proto.
54def _PlatformEnumValue(platform):
55  assert platform in _platforms
56  return 'Study::PLATFORM_' + platform.upper()
57
58def _FormFactorEnumValue(form_factor):
59  assert form_factor in _form_factors
60  return 'Study::' + form_factor.upper()
61
62def _Load(filename):
63  """Loads a JSON file into a Python object and return this object."""
64  with open(filename, 'r') as handle:
65    result = json.loads(json_comment_eater.Nom(handle.read()))
66  return result
67
68def _LoadFieldTrialConfig(filename, platforms):
69  """Loads a field trial config JSON and converts it into a format that can be
70  used by json_to_struct.
71  """
72  return _FieldTrialConfigToDescription(_Load(filename), platforms)
73
74def _ConvertOverrideUIStrings(override_ui_strings):
75  """Converts override_ui_strings to formatted dicts."""
76  overrides = []
77  for ui_string, override in override_ui_strings.iteritems():
78    overrides.append({
79        'name_hash': generate_ui_string_overrider.HashName(ui_string),
80        'value': override
81    })
82  return overrides
83
84def _CreateExperiment(experiment_data,
85                      platforms,
86                      form_factors,
87                      is_low_end_device):
88  """Creates an experiment dictionary with all necessary information.
89
90  Args:
91    experiment_data: An experiment json config.
92    platforms: A list of platforms for this trial. This should be
93      a subset of |_platforms|.
94    form_factors: A list of form factors for this trial. This should be
95      a subset of |_form_factors|.
96    is_low_end_device: An optional parameter. This can either be True or
97      False. None if not specified.
98
99  Returns:
100    An experiment dict.
101  """
102  experiment = {
103    'name': experiment_data['name'],
104    'platforms': [_PlatformEnumValue(p) for p in platforms],
105    'form_factors': [_FormFactorEnumValue(f) for f in form_factors],
106  }
107  if is_low_end_device is not None:
108    experiment['is_low_end_device'] = str(is_low_end_device).lower()
109  forcing_flags_data = experiment_data.get('forcing_flag')
110  if forcing_flags_data:
111    experiment['forcing_flag'] = forcing_flags_data
112  params_data = experiment_data.get('params')
113  if (params_data):
114    experiment['params'] = [{'key': param, 'value': params_data[param]}
115                          for param in sorted(params_data.keys())];
116  enable_features_data = experiment_data.get('enable_features')
117  if enable_features_data:
118    experiment['enable_features'] = enable_features_data
119  disable_features_data = experiment_data.get('disable_features')
120  if disable_features_data:
121    experiment['disable_features'] = disable_features_data
122  override_ui_strings = experiment_data.get('override_ui_strings')
123  if override_ui_strings:
124    experiment['override_ui_string'] = _ConvertOverrideUIStrings(
125        override_ui_strings)
126  return experiment
127
128def _CreateTrial(study_name, experiment_configs, platforms):
129  """Returns the applicable experiments for |study_name| and |platforms|.
130
131  This iterates through all of the experiment_configs for |study_name|
132  and picks out the applicable experiments based off of the valid platforms
133  and device type settings if specified.
134  """
135  experiments = []
136  for config in experiment_configs:
137    platform_intersection = [p for p in platforms if p in config['platforms']]
138
139    if platform_intersection:
140      experiments += [_CreateExperiment(
141                          e,
142                          platform_intersection,
143                          config.get('form_factors', []),
144                          config.get('is_low_end_device'))
145                      for e in config['experiments']]
146  return {
147    'name': study_name,
148    'experiments': experiments,
149  }
150
151def _GenerateTrials(config, platforms):
152  for study_name in sorted(config.keys()):
153    study = _CreateTrial(study_name, config[study_name], platforms)
154    # To avoid converting studies with empty experiments (e.g. the study doesn't
155    # apply to the target platforms), this generator only yields studies that
156    # have non-empty experiments.
157    if study['experiments']:
158      yield study
159
160def ConfigToStudies(config, platforms):
161  """Returns the applicable studies from config for the platforms."""
162  return [study for study in _GenerateTrials(config, platforms)]
163
164def _FieldTrialConfigToDescription(config, platforms):
165  return {
166    'elements': {
167      'kFieldTrialConfig': {
168        'studies': ConfigToStudies(config, platforms)
169      }
170    }
171  }
172
173def main(arguments):
174  parser = optparse.OptionParser(
175      description='Generates a struct from a JSON description.',
176      usage='usage: %prog [option] -s schema -p platform description')
177  parser.add_option('-b', '--destbase',
178      help='base directory of generated files.')
179  parser.add_option('-d', '--destdir',
180      help='directory to output generated files, relative to destbase.')
181  parser.add_option('-n', '--namespace',
182      help='C++ namespace for generated files. e.g search_providers.')
183  parser.add_option('-p', '--platform', action='append', choices=_platforms,
184      help='target platform for the field trial, mandatory.')
185  parser.add_option('-s', '--schema', help='path to the schema file, '
186      'mandatory.')
187  parser.add_option('-o', '--output', help='output filename, '
188      'mandatory.')
189  parser.add_option('-y', '--year',
190      help='year to put in the copy-right.')
191  (opts, args) = parser.parse_args(args=arguments)
192
193  if not opts.schema:
194    parser.error('You must specify a --schema.')
195
196  if not opts.platform:
197    parser.error('You must specify at least 1 --platform.')
198
199  description_filename = os.path.normpath(args[0])
200  shortroot = opts.output
201  if opts.destdir:
202    output_root = os.path.join(os.path.normpath(opts.destdir), shortroot)
203  else:
204    output_root = shortroot
205
206  if opts.destbase:
207    basepath = os.path.normpath(opts.destbase)
208  else:
209    basepath = ''
210
211  schema = _Load(opts.schema)
212  description = _LoadFieldTrialConfig(description_filename, opts.platform)
213  json_to_struct.GenerateStruct(
214      basepath, output_root, opts.namespace, schema, description,
215      os.path.split(description_filename)[1], os.path.split(opts.schema)[1],
216      opts.year)
217
218if __name__ == '__main__':
219  main(sys.argv[1:])
220