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 collections
6import copy
7import logging
8import optparse
9import os
10import random
11import sys
12import tempfile
13import time
14
15from telemetry import decorators
16from telemetry.core import browser_finder
17from telemetry.core import browser_info
18from telemetry.core import exceptions
19from telemetry.core import util
20from telemetry.core import wpr_modes
21from telemetry.core.platform.profiler import profiler_finder
22from telemetry.page import cloud_storage
23from telemetry.page import page_filter
24from telemetry.page import page_runner_repeat
25from telemetry.page import page_test
26from telemetry.page.actions import navigate
27from telemetry.page.actions import page_action
28from telemetry.results import results_options
29from telemetry.util import exception_formatter
30
31
32class _RunState(object):
33  def __init__(self):
34    self.browser = None
35
36    self._append_to_existing_wpr = False
37    self._last_archive_path = None
38    self._first_browser = True
39    self.first_page = collections.defaultdict(lambda: True)
40    self.profiler_dir = None
41    self.repeat_state = None
42
43  def StartBrowserIfNeeded(self, test, page_set, page, possible_browser,
44                           credentials_path, archive_path, finder_options):
45    started_browser = not self.browser
46    # Create a browser.
47    if not self.browser:
48      test.CustomizeBrowserOptionsForSinglePage(page, finder_options)
49      self.browser = possible_browser.Create()
50      self.browser.credentials.credentials_path = credentials_path
51
52      # Set up WPR path on the new browser.
53      self.browser.SetReplayArchivePath(archive_path,
54                                        self._append_to_existing_wpr,
55                                        page_set.make_javascript_deterministic)
56      self._last_archive_path = page.archive_path
57
58      test.WillStartBrowser(self.browser)
59      self.browser.Start()
60      test.DidStartBrowser(self.browser)
61
62      if self._first_browser:
63        self._first_browser = False
64        self.browser.credentials.WarnIfMissingCredentials(page_set)
65        logging.info('OS: %s %s',
66                     self.browser.platform.GetOSName(),
67                     self.browser.platform.GetOSVersionName())
68        if self.browser.supports_system_info:
69          system_info = self.browser.GetSystemInfo()
70          if system_info.model_name:
71            logging.info('Model: %s', system_info.model_name)
72          if system_info.gpu:
73            for i, device in enumerate(system_info.gpu.devices):
74              logging.info('GPU device %d: %s', i, device)
75            if system_info.gpu.aux_attributes:
76              logging.info('GPU Attributes:')
77              for k, v in sorted(system_info.gpu.aux_attributes.iteritems()):
78                logging.info('  %-20s: %s', k, v)
79            if system_info.gpu.feature_status:
80              logging.info('Feature Status:')
81              for k, v in sorted(system_info.gpu.feature_status.iteritems()):
82                logging.info('  %-20s: %s', k, v)
83            if system_info.gpu.driver_bug_workarounds:
84              logging.info('Driver Bug Workarounds:')
85              for workaround in system_info.gpu.driver_bug_workarounds:
86                logging.info('  %s', workaround)
87          else:
88            logging.info('No GPU devices')
89    else:
90      # Set up WPR path if it changed.
91      if page.archive_path and self._last_archive_path != page.archive_path:
92        self.browser.SetReplayArchivePath(
93            page.archive_path,
94            self._append_to_existing_wpr,
95            page_set.make_javascript_deterministic)
96        self._last_archive_path = page.archive_path
97
98    if self.browser.supports_tab_control and test.close_tabs_before_run:
99      # Create a tab if there's none.
100      if len(self.browser.tabs) == 0:
101        self.browser.tabs.New()
102
103      # Ensure only one tab is open, unless the test is a multi-tab test.
104      if not test.is_multi_tab_test:
105        while len(self.browser.tabs) > 1:
106          self.browser.tabs[-1].Close()
107
108      # Must wait for tab to commit otherwise it can commit after the next
109      # navigation has begun and RenderFrameHostManager::DidNavigateMainFrame()
110      # will cancel the next navigation because it's pending. This manifests as
111      # the first navigation in a PageSet freezing indefinitly because the
112      # navigation was silently cancelled when |self.browser.tabs[0]| was
113      # committed. Only do this when we just started the browser, otherwise
114      # there are cases where previous pages in a PageSet never complete
115      # loading so we'll wait forever.
116      if started_browser:
117        self.browser.tabs[0].WaitForDocumentReadyStateToBeComplete()
118
119  def StopBrowser(self):
120    if self.browser:
121      self.browser.Close()
122      self.browser = None
123
124      # Restarting the state will also restart the wpr server. If we're
125      # recording, we need to continue adding into the same wpr archive,
126      # not overwrite it.
127      self._append_to_existing_wpr = True
128
129  def StartProfiling(self, page, finder_options):
130    if not self.profiler_dir:
131      self.profiler_dir = tempfile.mkdtemp()
132    output_file = os.path.join(self.profiler_dir, page.file_safe_name)
133    is_repeating = (finder_options.page_repeat != 1 or
134                    finder_options.pageset_repeat != 1)
135    if is_repeating:
136      output_file = util.GetSequentialFileName(output_file)
137    self.browser.StartProfiling(finder_options.profiler, output_file)
138
139  def StopProfiling(self):
140    if self.browser:
141      self.browser.StopProfiling()
142
143
144class PageState(object):
145  def __init__(self, page, tab):
146    self.page = page
147    self.tab = tab
148
149    self._did_login = False
150
151  def PreparePage(self, test=None):
152    if self.page.is_file:
153      server_started = self.tab.browser.SetHTTPServerDirectories(
154        self.page.page_set.serving_dirs | set([self.page.serving_dir]))
155      if server_started and test:
156        test.DidStartHTTPServer(self.tab)
157
158    if self.page.credentials:
159      if not self.tab.browser.credentials.LoginNeeded(
160          self.tab, self.page.credentials):
161        raise page_test.Failure('Login as ' + self.page.credentials + ' failed')
162      self._did_login = True
163
164    if test:
165      if test.clear_cache_before_each_run:
166        self.tab.ClearCache(force=True)
167
168  def ImplicitPageNavigation(self, test=None):
169    """Executes the implicit navigation that occurs for every page iteration.
170
171    This function will be called once per page before any actions are executed.
172    """
173    if test:
174      test.WillNavigateToPage(self.page, self.tab)
175      test.RunNavigateSteps(self.page, self.tab)
176      test.DidNavigateToPage(self.page, self.tab)
177    else:
178      i = navigate.NavigateAction()
179      i.RunAction(self.page, self.tab, None)
180
181  def CleanUpPage(self, test):
182    test.CleanUpAfterPage(self.page, self.tab)
183    if self.page.credentials and self._did_login:
184      self.tab.browser.credentials.LoginNoLongerNeeded(
185          self.tab, self.page.credentials)
186
187
188def AddCommandLineArgs(parser):
189  page_filter.PageFilter.AddCommandLineArgs(parser)
190  results_options.AddResultsOptions(parser)
191
192  # Page set options
193  group = optparse.OptionGroup(parser, 'Page set ordering and repeat options')
194  group.add_option('--pageset-shuffle', action='store_true',
195      dest='pageset_shuffle',
196      help='Shuffle the order of pages within a pageset.')
197  group.add_option('--pageset-shuffle-order-file',
198      dest='pageset_shuffle_order_file', default=None,
199      help='Filename of an output of a previously run test on the current '
200      'pageset. The tests will run in the same order again, overriding '
201      'what is specified by --page-repeat and --pageset-repeat.')
202  group.add_option('--page-repeat', default=1, type='int',
203                   help='Number of times to repeat each individual page '
204                   'before proceeding with the next page in the pageset.')
205  group.add_option('--pageset-repeat', default=1, type='int',
206                   help='Number of times to repeat the entire pageset.')
207  parser.add_option_group(group)
208
209  # WPR options
210  group = optparse.OptionGroup(parser, 'Web Page Replay options')
211  group.add_option('--use-live-sites',
212      dest='use_live_sites', action='store_true',
213      help='Run against live sites and ignore the Web Page Replay archives.')
214  parser.add_option_group(group)
215
216
217def ProcessCommandLineArgs(parser, args):
218  page_filter.PageFilter.ProcessCommandLineArgs(parser, args)
219
220  # Page set options
221  if args.pageset_shuffle_order_file and not args.pageset_shuffle:
222    parser.error('--pageset-shuffle-order-file requires --pageset-shuffle.')
223
224  if args.page_repeat < 1:
225    parser.error('--page-repeat must be a positive integer.')
226  if args.pageset_repeat < 1:
227    parser.error('--pageset-repeat must be a positive integer.')
228
229
230def _PrepareAndRunPage(test, page_set, expectations, finder_options,
231                       browser_options, page, credentials_path,
232                       possible_browser, results, state):
233  if finder_options.use_live_sites:
234    browser_options.wpr_mode = wpr_modes.WPR_OFF
235  elif browser_options.wpr_mode != wpr_modes.WPR_RECORD:
236    browser_options.wpr_mode = (
237        wpr_modes.WPR_REPLAY
238        if page.archive_path and os.path.isfile(page.archive_path)
239        else wpr_modes.WPR_OFF)
240
241  tries = test.attempts
242  while tries:
243    tries -= 1
244    try:
245      results_for_current_run = copy.copy(results)
246      results_for_current_run.StartTest(page)
247      if test.RestartBrowserBeforeEachPage() or page.startup_url:
248        state.StopBrowser()
249        # If we are restarting the browser for each page customize the per page
250        # options for just the current page before starting the browser.
251      state.StartBrowserIfNeeded(test, page_set, page, possible_browser,
252                                 credentials_path, page.archive_path,
253                                 finder_options)
254      if not page.CanRunOnBrowser(browser_info.BrowserInfo(state.browser)):
255        logging.info('Skip test for page %s because browser is not supported.'
256                     % page.url)
257        results_for_current_run.StopTest(page)
258        return results
259
260      expectation = expectations.GetExpectationForPage(state.browser, page)
261
262      _WaitForThermalThrottlingIfNeeded(state.browser.platform)
263
264      if finder_options.profiler:
265        state.StartProfiling(page, finder_options)
266
267      try:
268        _RunPage(test, page, state, expectation,
269                 results_for_current_run, finder_options)
270        _CheckThermalThrottling(state.browser.platform)
271      except exceptions.TabCrashException as e:
272        if test.is_multi_tab_test:
273          logging.error('Aborting multi-tab test after tab %s crashed',
274                        page.url)
275          raise
276        logging.warning(e)
277        state.StopBrowser()
278
279      if finder_options.profiler:
280        state.StopProfiling()
281
282      if (test.StopBrowserAfterPage(state.browser, page)):
283        state.StopBrowser()
284
285      results_for_current_run.StopTest(page)
286
287      if state.first_page[page]:
288        state.first_page[page] = False
289        if test.discard_first_result:
290          return results
291      return results_for_current_run
292    except exceptions.BrowserGoneException as e:
293      state.StopBrowser()
294      if not tries:
295        logging.error('Aborting after too many retries')
296        raise
297      if test.is_multi_tab_test:
298        logging.error('Aborting multi-tab test after browser crashed')
299        raise
300      logging.warning(e)
301
302
303def _UpdatePageSetArchivesIfChanged(page_set):
304  # Attempt to download the credentials file.
305  if page_set.credentials_path:
306    try:
307      cloud_storage.GetIfChanged(
308          os.path.join(page_set.base_dir, page_set.credentials_path))
309    except (cloud_storage.CredentialsError, cloud_storage.PermissionError,
310            cloud_storage.CloudStorageError) as e:
311      logging.warning('Cannot retrieve credential file %s due to cloud storage '
312                      'error %s', page_set.credentials_path, str(e))
313
314  # Scan every serving directory for .sha1 files
315  # and download them from Cloud Storage. Assume all data is public.
316  all_serving_dirs = page_set.serving_dirs.copy()
317  # Add individual page dirs to all serving dirs.
318  for page in page_set:
319    if page.is_file:
320      all_serving_dirs.add(page.serving_dir)
321  # Scan all serving dirs.
322  for serving_dir in all_serving_dirs:
323    if os.path.splitdrive(serving_dir)[1] == '/':
324      raise ValueError('Trying to serve root directory from HTTP server.')
325    for dirpath, _, filenames in os.walk(serving_dir):
326      for filename in filenames:
327        path, extension = os.path.splitext(
328            os.path.join(dirpath, filename))
329        if extension != '.sha1':
330          continue
331        cloud_storage.GetIfChanged(path)
332
333
334def Run(test, page_set, expectations, finder_options):
335  """Runs a given test against a given page_set with the given options."""
336  results = results_options.PrepareResults(test, finder_options)
337
338  test.ValidatePageSet(page_set)
339
340  # Create a possible_browser with the given options.
341  try:
342    possible_browser = browser_finder.FindBrowser(finder_options)
343  except browser_finder.BrowserTypeRequiredException, e:
344    sys.stderr.write(str(e) + '\n')
345    sys.exit(-1)
346  if not possible_browser:
347    sys.stderr.write(
348        'No browser found. Available browsers:\n' +
349        '\n'.join(browser_finder.GetAllAvailableBrowserTypes(finder_options)) +
350        '\n')
351    sys.exit(-1)
352
353  browser_options = possible_browser.finder_options.browser_options
354  browser_options.browser_type = possible_browser.browser_type
355  browser_options.platform = possible_browser.platform
356  test.CustomizeBrowserOptions(browser_options)
357
358  if not decorators.IsEnabled(
359      test, browser_options.browser_type, browser_options.platform):
360    return results
361
362  # Reorder page set based on options.
363  pages = _ShuffleAndFilterPageSet(page_set, finder_options)
364
365  if (not finder_options.use_live_sites and
366      browser_options.wpr_mode != wpr_modes.WPR_RECORD):
367    _UpdatePageSetArchivesIfChanged(page_set)
368    pages = _CheckArchives(page_set, pages, results)
369
370  # Verify credentials path.
371  credentials_path = None
372  if page_set.credentials_path:
373    credentials_path = os.path.join(os.path.dirname(page_set.file_path),
374                                    page_set.credentials_path)
375    if not os.path.exists(credentials_path):
376      credentials_path = None
377
378  # Set up user agent.
379  browser_options.browser_user_agent_type = page_set.user_agent_type or None
380
381  if finder_options.profiler:
382    profiler_class = profiler_finder.FindProfiler(finder_options.profiler)
383    profiler_class.CustomizeBrowserOptions(browser_options.browser_type,
384                                           finder_options)
385
386  for page in list(pages):
387    if not test.CanRunForPage(page):
388      logging.debug('Skipping test: it cannot run for %s', page.url)
389      results.AddSkip(page, 'Test cannot run')
390      pages.remove(page)
391
392  if not pages:
393    return results
394
395  state = _RunState()
396  # TODO(dtu): Move results creation and results_for_current_run into RunState.
397
398  try:
399    test.WillRunTest(finder_options)
400    state.repeat_state = page_runner_repeat.PageRunnerRepeatState(
401        finder_options)
402
403    state.repeat_state.WillRunPageSet()
404    while state.repeat_state.ShouldRepeatPageSet() and not test.IsExiting():
405      for page in pages:
406        state.repeat_state.WillRunPage()
407        test.WillRunPageRepeats(page)
408        while state.repeat_state.ShouldRepeatPage():
409          results = _PrepareAndRunPage(
410              test, page_set, expectations, finder_options, browser_options,
411              page, credentials_path, possible_browser, results, state)
412          state.repeat_state.DidRunPage()
413        test.DidRunPageRepeats(page)
414        if (not test.max_failures is None and
415            len(results.failures) > test.max_failures):
416          logging.error('Too many failures. Aborting.')
417          test.RequestExit()
418        if (not test.max_errors is None and
419            len(results.errors) > test.max_errors):
420          logging.error('Too many errors. Aborting.')
421          test.RequestExit()
422        if test.IsExiting():
423          break
424      state.repeat_state.DidRunPageSet()
425
426    test.DidRunTest(state.browser, results)
427  finally:
428    state.StopBrowser()
429
430  return results
431
432
433def _ShuffleAndFilterPageSet(page_set, finder_options):
434  if finder_options.pageset_shuffle_order_file:
435    return page_set.ReorderPageSet(finder_options.pageset_shuffle_order_file)
436
437  pages = [page for page in page_set.pages[:]
438           if not page.disabled and page_filter.PageFilter.IsSelected(page)]
439
440  if finder_options.pageset_shuffle:
441    random.Random().shuffle(pages)
442
443  return pages
444
445
446def _CheckArchives(page_set, pages, results):
447  """Returns a subset of pages that are local or have WPR archives.
448
449  Logs warnings if any are missing."""
450  page_set_has_live_sites = False
451  for page in pages:
452    if not page.is_local:
453      page_set_has_live_sites = True
454      break
455
456  # Potential problems with the entire page set.
457  if page_set_has_live_sites:
458    if not page_set.archive_data_file:
459      logging.warning('The page set is missing an "archive_data_file" '
460                      'property. Skipping any live sites. To include them, '
461                      'pass the flag --use-live-sites.')
462    if not page_set.wpr_archive_info:
463      logging.warning('The archive info file is missing. '
464                      'To fix this, either add svn-internal to your '
465                      '.gclient using http://goto/read-src-internal, '
466                      'or create a new archive using record_wpr.')
467
468  # Potential problems with individual pages.
469  pages_missing_archive_path = []
470  pages_missing_archive_data = []
471
472  for page in pages:
473    if page.is_local:
474      continue
475
476    if not page.archive_path:
477      pages_missing_archive_path.append(page)
478    elif not os.path.isfile(page.archive_path):
479      pages_missing_archive_data.append(page)
480
481  if pages_missing_archive_path:
482    logging.warning('The page set archives for some pages do not exist. '
483                    'Skipping those pages. To fix this, record those pages '
484                    'using record_wpr. To ignore this warning and run '
485                    'against live sites, pass the flag --use-live-sites.')
486  if pages_missing_archive_data:
487    logging.warning('The page set archives for some pages are missing. '
488                    'Someone forgot to check them in, or they were deleted. '
489                    'Skipping those pages. To fix this, record those pages '
490                    'using record_wpr. To ignore this warning and run '
491                    'against live sites, pass the flag --use-live-sites.')
492
493  for page in pages_missing_archive_path + pages_missing_archive_data:
494    results.StartTest(page)
495    results.AddErrorMessage(page, 'Page set archive doesn\'t exist.')
496    results.StopTest(page)
497
498  return [page for page in pages if page not in
499          pages_missing_archive_path + pages_missing_archive_data]
500
501
502def _RunPage(test, page, state, expectation, results, finder_options):
503  if expectation == 'skip':
504    logging.debug('Skipping test: Skip expectation for %s', page.url)
505    results.AddSkip(page, 'Skipped by test expectations')
506    return
507
508  logging.info('Running %s', page.url)
509
510  page_state = PageState(page, test.TabForPage(page, state.browser))
511
512  def ProcessError():
513    if expectation == 'fail':
514      msg = 'Expected exception while running %s' % page.url
515      results.AddSuccess(page)
516    else:
517      msg = 'Exception while running %s' % page.url
518      results.AddError(page, sys.exc_info())
519    exception_formatter.PrintFormattedException(msg=msg)
520
521  try:
522    page_state.PreparePage(test)
523    if state.repeat_state.ShouldNavigate(
524        finder_options.skip_navigate_on_repeat):
525      page_state.ImplicitPageNavigation(test)
526    test.RunPage(page, page_state.tab, results)
527    util.CloseConnections(page_state.tab)
528  except page_test.TestNotSupportedOnPlatformFailure:
529    raise
530  except page_test.Failure:
531    if expectation == 'fail':
532      exception_formatter.PrintFormattedException(
533          msg='Expected failure while running %s' % page.url)
534      results.AddSuccess(page)
535    else:
536      exception_formatter.PrintFormattedException(
537          msg='Failure while running %s' % page.url)
538      results.AddFailure(page, sys.exc_info())
539  except (util.TimeoutException, exceptions.LoginException,
540          exceptions.ProfilingException):
541    ProcessError()
542  except (exceptions.TabCrashException, exceptions.BrowserGoneException):
543    ProcessError()
544    # Run() catches these exceptions to relaunch the tab/browser, so re-raise.
545    raise
546  except page_action.PageActionNotSupported as e:
547    results.AddSkip(page, 'Unsupported page action: %s' % e)
548  except Exception:
549    exception_formatter.PrintFormattedException(
550        msg='Unhandled exception while running %s' % page.url)
551    results.AddFailure(page, sys.exc_info())
552  else:
553    if expectation == 'fail':
554      logging.warning('%s was expected to fail, but passed.\n', page.url)
555    results.AddSuccess(page)
556  finally:
557    page_state.CleanUpPage(test)
558
559
560def _WaitForThermalThrottlingIfNeeded(platform):
561  if not platform.CanMonitorThermalThrottling():
562    return
563  thermal_throttling_retry = 0
564  while (platform.IsThermallyThrottled() and
565         thermal_throttling_retry < 3):
566    logging.warning('Thermally throttled, waiting (%d)...',
567                    thermal_throttling_retry)
568    thermal_throttling_retry += 1
569    time.sleep(thermal_throttling_retry * 2)
570
571  if thermal_throttling_retry and platform.IsThermallyThrottled():
572    logging.warning('Device is thermally throttled before running '
573                    'performance tests, results will vary.')
574
575
576def _CheckThermalThrottling(platform):
577  if not platform.CanMonitorThermalThrottling():
578    return
579  if platform.HasBeenThermallyThrottled():
580    logging.warning('Device has been thermally throttled during '
581                    'performance tests, results will vary.')
582