1# Copyright 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import copy
6import logging
7import optparse
8import os
9import shlex
10import socket
11import sys
12
13from catapult_base import cloud_storage  # pylint: disable=import-error
14
15from telemetry.core import platform
16from telemetry.core import util
17from telemetry.internal.browser import browser_finder
18from telemetry.internal.browser import browser_finder_exceptions
19from telemetry.internal.browser import profile_types
20from telemetry.internal.platform import device_finder
21from telemetry.internal.platform.profiler import profiler_finder
22from telemetry.internal.util import binary_manager
23from telemetry.util import wpr_modes
24
25
26class BrowserFinderOptions(optparse.Values):
27  """Options to be used for discovering a browser."""
28
29  def __init__(self, browser_type=None):
30    optparse.Values.__init__(self)
31
32    self.browser_type = browser_type
33    self.browser_executable = None
34    self.chrome_root = None  # Path to src/
35    self.chromium_output_dir = None  # E.g.: out/Debug
36    self.device = None
37    self.cros_ssh_identity = None
38
39    self.extensions_to_load = []
40
41    # If set, copy the generated profile to this path on exit.
42    self.output_profile_path = None
43
44    self.cros_remote = None
45
46    self.profiler = None
47    self.verbosity = 0
48
49    self.browser_options = BrowserOptions()
50    self.output_file = None
51
52    self.android_blacklist_file = None
53    self.no_performance_mode = False
54
55  def __repr__(self):
56    return str(sorted(self.__dict__.items()))
57
58  def Copy(self):
59    return copy.deepcopy(self)
60
61  def CreateParser(self, *args, **kwargs):
62    parser = optparse.OptionParser(*args, **kwargs)
63
64    # Selection group
65    group = optparse.OptionGroup(parser, 'Which browser to use')
66    group.add_option('--browser',
67        dest='browser_type',
68        default=None,
69        help='Browser type to run, '
70             'in order of priority. Supported values: list,%s' %
71             ','.join(browser_finder.FindAllBrowserTypes(self)))
72    group.add_option('--browser-executable',
73        dest='browser_executable',
74        help='The exact browser to run.')
75    group.add_option('--chrome-root',
76        dest='chrome_root',
77        help='Where to look for chrome builds. '
78             'Defaults to searching parent dirs by default.')
79    group.add_option('--chromium-output-directory',
80        dest='chromium_output_dir',
81        help='Where to look for build artifacts. '
82             'Can also be specified by setting environment variable '
83             'CHROMIUM_OUTPUT_DIR.')
84    group.add_option('--device',
85        dest='device',
86        help='The device ID to use. '
87             'If not specified, only 0 or 1 connected devices are supported. '
88             'If specified as "android", all available Android devices are '
89             'used.')
90    group.add_option(
91        '--remote',
92        dest='cros_remote',
93        help='The hostname of a remote ChromeOS device to use.')
94    group.add_option(
95        '--remote-ssh-port',
96        type=int,
97        default=socket.getservbyname('ssh'),
98        dest='cros_remote_ssh_port',
99        help='The SSH port of the remote ChromeOS device (requires --remote).')
100    identity = None
101    testing_rsa = os.path.join(
102        util.GetTelemetryThirdPartyDir(), 'chromite', 'ssh_keys', 'testing_rsa')
103    if os.path.exists(testing_rsa):
104      identity = testing_rsa
105    group.add_option('--identity',
106        dest='cros_ssh_identity',
107        default=identity,
108        help='The identity file to use when ssh\'ing into the ChromeOS device')
109    parser.add_option_group(group)
110
111    # Debugging options
112    group = optparse.OptionGroup(parser, 'When things go wrong')
113    profiler_choices = profiler_finder.GetAllAvailableProfilers()
114    group.add_option(
115        '--profiler', default=None, type='choice',
116        choices=profiler_choices,
117        help='Record profiling data using this tool. Supported values: %s. '
118             '(Notice: this flag cannot be used for Timeline Based Measurement '
119             'benchmarks.)' % ', '.join(profiler_choices))
120    group.add_option(
121        '-v', '--verbose', action='count', dest='verbosity',
122        help='Increase verbosity level (repeat as needed)')
123    group.add_option('--print-bootstrap-deps',
124                     action='store_true',
125                     help='Output bootstrap deps list.')
126    parser.add_option_group(group)
127
128    # Platform options
129    group = optparse.OptionGroup(parser, 'Platform options')
130    group.add_option('--no-performance-mode', action='store_true',
131        help='Some platforms run on "full performance mode" where the '
132        'test is executed at maximum CPU speed in order to minimize noise '
133        '(specially important for dashboards / continuous builds). '
134        'This option prevents Telemetry from tweaking such platform settings.')
135    group.add_option('--android-blacklist-file',
136                     help='Device blacklist JSON file.')
137    parser.add_option_group(group)
138
139    # Browser options.
140    self.browser_options.AddCommandLineArgs(parser)
141
142    real_parse = parser.parse_args
143    def ParseArgs(args=None):
144      defaults = parser.get_default_values()
145      for k, v in defaults.__dict__.items():
146        if k in self.__dict__ and self.__dict__[k] != None:
147          continue
148        self.__dict__[k] = v
149      ret = real_parse(args, self) # pylint: disable=E1121
150
151      if self.verbosity >= 2:
152        logging.getLogger().setLevel(logging.DEBUG)
153      elif self.verbosity:
154        logging.getLogger().setLevel(logging.INFO)
155      else:
156        logging.getLogger().setLevel(logging.WARNING)
157
158      if self.chromium_output_dir:
159        os.environ['CHROMIUM_OUTPUT_DIR'] = self.chromium_output_dir
160
161      if self.device == 'list':
162        if binary_manager.NeedsInit():
163          binary_manager.InitDependencyManager(None)
164        devices = device_finder.GetDevicesMatchingOptions(self)
165        print 'Available devices:'
166        for device in devices:
167          print ' ', device.name
168        sys.exit(0)
169
170      if self.browser_executable and not self.browser_type:
171        self.browser_type = 'exact'
172      if self.browser_type == 'list':
173        if binary_manager.NeedsInit():
174          binary_manager.InitDependencyManager(None)
175        devices = device_finder.GetDevicesMatchingOptions(self)
176        if not devices:
177          sys.exit(0)
178        browser_types = {}
179        for device in devices:
180          try:
181            possible_browsers = browser_finder.GetAllAvailableBrowsers(self,
182                                                                       device)
183            browser_types[device.name] = sorted(
184              [browser.browser_type for browser in possible_browsers])
185          except browser_finder_exceptions.BrowserFinderException as ex:
186            print >> sys.stderr, 'ERROR: ', ex
187            sys.exit(1)
188        print 'Available browsers:'
189        if len(browser_types) == 0:
190          print '  No devices were found.'
191        for device_name in sorted(browser_types.keys()):
192          print '  ', device_name
193          for browser_type in browser_types[device_name]:
194            print '    ', browser_type
195        sys.exit(0)
196
197      # Parse browser options.
198      self.browser_options.UpdateFromParseResults(self)
199
200      return ret
201    parser.parse_args = ParseArgs
202    return parser
203
204  def AppendExtraBrowserArgs(self, args):
205    self.browser_options.AppendExtraBrowserArgs(args)
206
207  def MergeDefaultValues(self, defaults):
208    for k, v in defaults.__dict__.items():
209      self.ensure_value(k, v)
210
211class BrowserOptions(object):
212  """Options to be used for launching a browser."""
213  def __init__(self):
214    self.browser_type = None
215    self.show_stdout = False
216
217    # When set to True, the browser will use the default profile.  Telemetry
218    # will not provide an alternate profile directory.
219    self.dont_override_profile = False
220    self.profile_dir = None
221    self.profile_type = None
222    self._extra_browser_args = set()
223    self.extra_wpr_args = []
224    self.wpr_mode = wpr_modes.WPR_OFF
225    self.full_performance_mode = True
226
227    # The amount of time Telemetry should wait for the browser to start.
228    # This property is not exposed as a command line option.
229    self._browser_startup_timeout = 60
230
231    self.disable_background_networking = True
232    self.no_proxy_server = False
233    self.browser_user_agent_type = None
234
235    self.clear_sytem_cache_for_browser_and_profile_on_start = False
236    self.startup_url = 'about:blank'
237
238    # Background pages of built-in component extensions can interfere with
239    # performance measurements.
240    self.disable_component_extensions_with_background_pages = True
241    # Disable default apps.
242    self.disable_default_apps = True
243
244    self.enable_logging = False
245    # The cloud storage bucket & path for uploading logs data produced by the
246    # browser to.
247    # If logs_cloud_remote_path is None, a random remote path is generated every
248    # time the logs data is uploaded.
249    self.logs_cloud_bucket = cloud_storage.TELEMETRY_OUTPUT
250    self.logs_cloud_remote_path = None
251
252    # TODO(danduong): Find a way to store target_os here instead of
253    # finder_options.
254    self._finder_options = None
255
256    # Whether to take screen shot for failed page & put them in telemetry's
257    # profiling results.
258    self.take_screenshot_for_failed_page = False
259
260  def __repr__(self):
261    # This works around the infinite loop caused by the introduction of a
262    # circular reference with _finder_options.
263    obj = self.__dict__.copy()
264    del obj['_finder_options']
265    return str(sorted(obj.items()))
266
267  def IsCrosBrowserOptions(self):
268    return False
269
270  @classmethod
271  def AddCommandLineArgs(cls, parser):
272
273    ############################################################################
274    # Please do not add any more options here without first discussing with    #
275    # a telemetry owner. This is not the right place for platform-specific     #
276    # options.                                                                 #
277    ############################################################################
278
279    group = optparse.OptionGroup(parser, 'Browser options')
280    profile_choices = profile_types.GetProfileTypes()
281    group.add_option('--profile-type',
282        dest='profile_type',
283        type='choice',
284        default='clean',
285        choices=profile_choices,
286        help=('The user profile to use. A clean profile is used by default. '
287              'Supported values: ' + ', '.join(profile_choices)))
288    group.add_option('--profile-dir',
289        dest='profile_dir',
290        help='Profile directory to launch the browser with. '
291             'A clean profile is used by default')
292    group.add_option('--extra-browser-args',
293        dest='extra_browser_args_as_string',
294        help='Additional arguments to pass to the browser when it starts')
295    group.add_option('--extra-wpr-args',
296        dest='extra_wpr_args_as_string',
297        help=('Additional arguments to pass to Web Page Replay. '
298              'See third_party/webpagereplay/replay.py for usage.'))
299    group.add_option('--show-stdout',
300        action='store_true',
301        help='When possible, will display the stdout of the process')
302    group.add_option('--enable-browser-logging',
303        dest='enable_logging',
304        action='store_true',
305        help=('Enable browser logging. The log file is saved in temp directory.'
306              "Note that enabling this flag affects the browser's "
307              'performance'))
308    parser.add_option_group(group)
309
310    group = optparse.OptionGroup(parser, 'Compatibility options')
311    group.add_option('--gtest_output',
312        help='Ignored argument for compatibility with runtest.py harness')
313    parser.add_option_group(group)
314
315  def UpdateFromParseResults(self, finder_options):
316    """Copies our options from finder_options"""
317    browser_options_list = [
318        'extra_browser_args_as_string',
319        'extra_wpr_args_as_string',
320        'enable_logging',
321        'profile_dir',
322        'profile_type',
323        'show_stdout',
324        ]
325    for o in browser_options_list:
326      a = getattr(finder_options, o, None)
327      if a is not None:
328        setattr(self, o, a)
329        delattr(finder_options, o)
330
331    self.browser_type = finder_options.browser_type
332    self._finder_options = finder_options
333
334    if hasattr(self, 'extra_browser_args_as_string'):
335      tmp = shlex.split(
336        self.extra_browser_args_as_string)
337      self.AppendExtraBrowserArgs(tmp)
338      delattr(self, 'extra_browser_args_as_string')
339    if hasattr(self, 'extra_wpr_args_as_string'):
340      tmp = shlex.split(
341        self.extra_wpr_args_as_string)
342      self.extra_wpr_args.extend(tmp)
343      delattr(self, 'extra_wpr_args_as_string')
344    if self.profile_type == 'default':
345      self.dont_override_profile = True
346
347    if self.profile_dir and self.profile_type != 'clean':
348      logging.critical(
349          "It's illegal to specify both --profile-type and --profile-dir.\n"
350          "For more information see: http://goo.gl/ngdGD5")
351      sys.exit(1)
352
353    if self.profile_dir and not os.path.isdir(self.profile_dir):
354      logging.critical(
355          "Directory specified by --profile-dir (%s) doesn't exist "
356          "or isn't a directory.\n"
357          "For more information see: http://goo.gl/ngdGD5" % self.profile_dir)
358      sys.exit(1)
359
360    if not self.profile_dir:
361      self.profile_dir = profile_types.GetProfileDir(self.profile_type)
362
363    # This deferred import is necessary because browser_options is imported in
364    # telemetry/telemetry/__init__.py.
365    finder_options.browser_options = CreateChromeBrowserOptions(self)
366
367  @property
368  def finder_options(self):
369    return self._finder_options
370
371  @property
372  def extra_browser_args(self):
373    return self._extra_browser_args
374
375  @property
376  def browser_startup_timeout(self):
377    return self._browser_startup_timeout
378
379  @browser_startup_timeout.setter
380  def browser_startup_timeout(self, value):
381    self._browser_startup_timeout = value
382
383  def AppendExtraBrowserArgs(self, args):
384    if isinstance(args, list):
385      self._extra_browser_args.update(args)
386    else:
387      self._extra_browser_args.add(args)
388
389
390def CreateChromeBrowserOptions(br_options):
391  browser_type = br_options.browser_type
392
393  if (platform.GetHostPlatform().GetOSName() == 'chromeos' or
394      (browser_type and browser_type.startswith('cros'))):
395    return CrosBrowserOptions(br_options)
396
397  return br_options
398
399
400class ChromeBrowserOptions(BrowserOptions):
401  """Chrome-specific browser options."""
402
403  def __init__(self, br_options):
404    super(ChromeBrowserOptions, self).__init__()
405    # Copy to self.
406    self.__dict__.update(br_options.__dict__)
407
408
409class CrosBrowserOptions(ChromeBrowserOptions):
410  """ChromeOS-specific browser options."""
411
412  def __init__(self, br_options):
413    super(CrosBrowserOptions, self).__init__(br_options)
414    # Create a browser with oobe property.
415    self.create_browser_with_oobe = False
416    # Clear enterprise policy before logging in.
417    self.clear_enterprise_policy = True
418    # Disable GAIA/enterprise services.
419    self.disable_gaia_services = True
420
421    self.auto_login = True
422    self.gaia_login = False
423    self.username = 'test@test.test'
424    self.password = ''
425    self.gaia_id = '12345'
426
427  def IsCrosBrowserOptions(self):
428    return True
429