page_runner.py revision 3551c9c881056c480085172ff9840cab31610854
1# Copyright (c) 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.
4import collections
5import glob
6import logging
7import os
8import sys
9import tempfile
10import time
11import traceback
12import random
13
14from telemetry.core import browser_finder
15from telemetry.core import exceptions
16from telemetry.core import util
17from telemetry.core import wpr_modes
18from telemetry.core.platform.profiler import profiler_finder
19from telemetry.page import page_filter as page_filter_module
20from telemetry.page import page_measurement_results
21from telemetry.page import page_runner_repeat
22from telemetry.page import page_test
23from telemetry.page import results_options
24from telemetry.page.actions import navigate
25
26
27class _RunState(object):
28  def __init__(self):
29    self.browser = None
30    self.tab = None
31
32    self._append_to_existing_wpr = False
33    self._last_archive_path = None
34    self._first_browser = True
35    self.first_page = collections.defaultdict(lambda: True)
36    self.profiler_dir = None
37    self.repeat_state = None
38
39  def StartBrowser(self, test, page_set, page, possible_browser,
40                   credentials_path, archive_path):
41    # Create a browser.
42    if not self.browser:
43      assert not self.tab
44      self.browser = possible_browser.Create()
45      self.browser.credentials.credentials_path = credentials_path
46
47      test.WillStartBrowser(self.browser)
48      self.browser.Start()
49      test.DidStartBrowser(self.browser)
50
51      if self._first_browser:
52        self._first_browser = False
53        self.browser.credentials.WarnIfMissingCredentials(page_set)
54
55      # Set up WPR path on the new browser.
56      self.browser.SetReplayArchivePath(archive_path,
57                                        self._append_to_existing_wpr,
58                                        page_set.make_javascript_deterministic)
59      self._last_archive_path = page.archive_path
60    else:
61      # Set up WPR path if it changed.
62      if page.archive_path and self._last_archive_path != page.archive_path:
63        self.browser.SetReplayArchivePath(
64            page.archive_path,
65            self._append_to_existing_wpr,
66            page_set.make_javascript_deterministic)
67        self._last_archive_path = page.archive_path
68
69    if self.browser.supports_tab_control:
70      # Create a tab if there's none.
71      if len(self.browser.tabs) == 0:
72        self.browser.tabs.New()
73
74      # Ensure only one tab is open.
75      while len(self.browser.tabs) > 1:
76        self.browser.tabs[-1].Close()
77
78    if not self.tab:
79      self.tab = self.browser.tabs[0]
80
81    if self.first_page[page]:
82      self.first_page[page] = False
83
84  def StopBrowser(self):
85    if self.tab:
86      self.tab.Disconnect()
87      self.tab = None
88
89    if self.browser:
90      self.browser.Close()
91      self.browser = None
92
93      # Restarting the state will also restart the wpr server. If we're
94      # recording, we need to continue adding into the same wpr archive,
95      # not overwrite it.
96      self._append_to_existing_wpr = True
97
98  def StartProfiling(self, page, options):
99    if not self.profiler_dir:
100      self.profiler_dir = tempfile.mkdtemp()
101    output_file = os.path.join(self.profiler_dir, page.url_as_file_safe_name)
102    if options.repeat_options.IsRepeating():
103      output_file = _GetSequentialFileName(output_file)
104    self.browser.StartProfiling(options.profiler, output_file)
105
106  def StopProfiling(self):
107    self.browser.StopProfiling()
108
109
110class PageState(object):
111  def __init__(self):
112    self._did_login = False
113
114  def PreparePage(self, page, tab, test=None):
115    if page.is_file:
116      serving_dirs = page.serving_dirs_and_file[0]
117      if tab.browser.SetHTTPServerDirectories(serving_dirs) and test:
118        test.DidStartHTTPServer(tab)
119
120    if page.credentials:
121      if not tab.browser.credentials.LoginNeeded(tab, page.credentials):
122        raise page_test.Failure('Login as ' + page.credentials + ' failed')
123      self._did_login = True
124
125    if test:
126      if test.clear_cache_before_each_run:
127        tab.ClearCache()
128
129  def ImplicitPageNavigation(self, page, tab, test=None):
130    """Executes the implicit navigation that occurs for every page iteration.
131
132    This function will be called once per page before any actions are executed.
133    """
134    if test:
135      test.WillNavigateToPage(page, tab)
136      test.RunNavigateSteps(page, tab)
137      test.DidNavigateToPage(page, tab)
138    else:
139      i = navigate.NavigateAction()
140      i.RunAction(page, tab, None)
141      page.WaitToLoad(tab, 60)
142
143  def CleanUpPage(self, page, tab):
144    if page.credentials and self._did_login:
145      tab.browser.credentials.LoginNoLongerNeeded(tab, page.credentials)
146
147
148def AddCommandLineOptions(parser):
149  page_filter_module.PageFilter.AddCommandLineOptions(parser)
150  results_options.AddResultsOptions(parser)
151
152
153def _LogStackTrace(title, browser):
154  if browser:
155    stack_trace = browser.GetStackTrace()
156  else:
157    stack_trace = 'Browser object is empty, no stack trace.'
158  stack_trace = (('\nStack Trace:\n') +
159            ('*' * 80) +
160            '\n\t' + stack_trace.replace('\n', '\n\t') + '\n' +
161            ('*' * 80))
162  logging.warning('%s%s', title, stack_trace)
163
164
165def _PrepareAndRunPage(test, page_set, expectations, options, page,
166                       credentials_path, possible_browser, results, state):
167  if options.wpr_mode != wpr_modes.WPR_RECORD:
168    if page.archive_path and os.path.isfile(page.archive_path):
169      possible_browser.options.wpr_mode = wpr_modes.WPR_REPLAY
170    else:
171      possible_browser.options.wpr_mode = wpr_modes.WPR_OFF
172  results_for_current_run = results
173  if state.first_page[page] and test.discard_first_result:
174    # If discarding results, substitute a dummy object.
175    results_for_current_run = page_measurement_results.PageMeasurementResults()
176  results_for_current_run.StartTest(page)
177  tries = 3
178  while tries:
179    try:
180      state.StartBrowser(test, page_set, page, possible_browser,
181                         credentials_path, page.archive_path)
182
183      expectation = expectations.GetExpectationForPage(state.browser, page)
184
185      _WaitForThermalThrottlingIfNeeded(state.browser.platform)
186
187      if options.profiler:
188        state.StartProfiling(page, options)
189
190      try:
191        _RunPage(test, page, state, expectation,
192                 results_for_current_run, options)
193        _CheckThermalThrottling(state.browser.platform)
194      except exceptions.TabCrashException:
195        _LogStackTrace('Tab crashed: %s' % page.url, state.browser)
196        state.StopBrowser()
197
198      if options.profiler:
199        state.StopProfiling()
200
201      if test.NeedsBrowserRestartAfterEachRun(state.tab):
202        state.StopBrowser()
203
204      break
205    except exceptions.BrowserGoneException:
206      _LogStackTrace('Browser crashed', state.browser)
207      logging.warning('Lost connection to browser. Retrying.')
208      state.StopBrowser()
209      tries -= 1
210      if not tries:
211        logging.error('Lost connection to browser 3 times. Failing.')
212        raise
213  results_for_current_run.StopTest(page)
214
215
216def Run(test, page_set, expectations, options):
217  """Runs a given test against a given page_set with the given options."""
218  results = results_options.PrepareResults(test, options)
219
220  # Create a possible_browser with the given options.
221  test.CustomizeBrowserOptions(options)
222  if options.profiler:
223    profiler_class = profiler_finder.FindProfiler(options.profiler)
224    profiler_class.CustomizeBrowserOptions(options)
225  try:
226    possible_browser = browser_finder.FindBrowser(options)
227  except browser_finder.BrowserTypeRequiredException, e:
228    sys.stderr.write(str(e) + '\n')
229    sys.exit(1)
230  if not possible_browser:
231    sys.stderr.write(
232        'No browser found. Available browsers:\n' +
233        '\n'.join(browser_finder.GetAllAvailableBrowserTypes(options)) + '\n')
234    sys.exit(1)
235
236  # Reorder page set based on options.
237  pages = _ShuffleAndFilterPageSet(page_set, options)
238
239  if (not options.allow_live_sites and
240      options.wpr_mode != wpr_modes.WPR_RECORD):
241    pages = _CheckArchives(page_set, pages, results)
242
243  # Verify credentials path.
244  credentials_path = None
245  if page_set.credentials_path:
246    credentials_path = os.path.join(os.path.dirname(page_set.file_path),
247                                    page_set.credentials_path)
248    if not os.path.exists(credentials_path):
249      credentials_path = None
250
251  # Set up user agent.
252  if page_set.user_agent_type:
253    options.browser_user_agent_type = page_set.user_agent_type
254
255  for page in pages:
256    test.CustomizeBrowserOptionsForPage(page, possible_browser.options)
257
258  for page in list(pages):
259    if not test.CanRunForPage(page):
260      logging.warning('Skipping test: it cannot run for %s', page.url)
261      results.AddSkip(page, 'Test cannot run')
262      pages.remove(page)
263
264  if not pages:
265    return results
266
267  state = _RunState()
268  # TODO(dtu): Move results creation and results_for_current_run into RunState.
269
270  try:
271    test.WillRunTest(state.tab)
272    state.repeat_state = page_runner_repeat.PageRunnerRepeatState(
273                             options.repeat_options)
274
275    state.repeat_state.WillRunPageSet()
276    while state.repeat_state.ShouldRepeatPageSet():
277      for page in pages:
278        state.repeat_state.WillRunPage()
279        test.WillRunPageRepeats(page, state.tab)
280        while state.repeat_state.ShouldRepeatPage():
281          # execute test on page
282          _PrepareAndRunPage(test, page_set, expectations, options, page,
283                             credentials_path, possible_browser, results, state)
284          state.repeat_state.DidRunPage()
285        test.DidRunPageRepeats(page, state.tab)
286      state.repeat_state.DidRunPageSet()
287
288    test.DidRunTest(state.tab, results)
289  finally:
290    state.StopBrowser()
291
292  return results
293
294
295def _ShuffleAndFilterPageSet(page_set, options):
296  if options.pageset_shuffle_order_file and not options.pageset_shuffle:
297    raise Exception('--pageset-shuffle-order-file requires --pageset-shuffle.')
298
299  if options.pageset_shuffle_order_file:
300    return page_set.ReorderPageSet(options.pageset_shuffle_order_file)
301
302  page_filter = page_filter_module.PageFilter(options)
303  pages = [page for page in page_set.pages[:]
304           if not page.disabled and page_filter.IsSelected(page)]
305
306  if options.pageset_shuffle:
307    random.Random().shuffle(pages)
308
309  return pages
310
311
312def _CheckArchives(page_set, pages, results):
313  """Returns a subset of pages that are local or have WPR archives.
314
315  Logs warnings if any are missing."""
316  page_set_has_live_sites = False
317  for page in pages:
318    if not page.is_local:
319      page_set_has_live_sites = True
320      break
321
322  # Potential problems with the entire page set.
323  if page_set_has_live_sites:
324    if not page_set.archive_data_file:
325      logging.warning('The page set is missing an "archive_data_file" '
326                      'property. Skipping any live sites. To include them, '
327                      'pass the flag --allow-live-sites.')
328    if not page_set.wpr_archive_info:
329      logging.warning('The archive info file is missing. '
330                      'To fix this, either add svn-internal to your '
331                      '.gclient using http://goto/read-src-internal, '
332                      'or create a new archive using record_wpr.')
333
334  # Potential problems with individual pages.
335  pages_missing_archive_path = []
336  pages_missing_archive_data = []
337
338  for page in pages:
339    if page.is_local:
340      continue
341
342    if not page.archive_path:
343      pages_missing_archive_path.append(page)
344    elif not os.path.isfile(page.archive_path):
345      pages_missing_archive_data.append(page)
346
347  if pages_missing_archive_path:
348    logging.warning('The page set archives for some pages do not exist. '
349                    'Skipping those pages. To fix this, record those pages '
350                    'using record_wpr. To ignore this warning and run '
351                    'against live sites, pass the flag --allow-live-sites.')
352  if pages_missing_archive_data:
353    logging.warning('The page set archives for some pages are missing. '
354                    'Someone forgot to check them in, or they were deleted. '
355                    'Skipping those pages. To fix this, record those pages '
356                    'using record_wpr. To ignore this warning and run '
357                    'against live sites, pass the flag --allow-live-sites.')
358
359  for page in pages_missing_archive_path + pages_missing_archive_data:
360    results.StartTest(page)
361    results.AddErrorMessage(page, 'Page set archive doesn\'t exist.')
362    results.StopTest(page)
363
364  return [page for page in pages if page not in
365          pages_missing_archive_path + pages_missing_archive_data]
366
367
368def _RunPage(test, page, state, expectation, results, options):
369  logging.info('Running %s' % page.url)
370
371  page_state = PageState()
372  tab = state.tab
373
374  def ProcessError():
375    logging.error('%s:\n%s', page.url, traceback.format_exc())
376    if expectation == 'fail':
377      logging.info('Error was expected\n')
378      results.AddSuccess(page)
379    else:
380      results.AddError(page, sys.exc_info())
381
382  try:
383    page_state.PreparePage(page, tab, test)
384    if state.repeat_state.ShouldNavigate(options.skip_navigate_on_repeat):
385      page_state.ImplicitPageNavigation(page, tab, test)
386    test.Run(options, page, tab, results)
387    util.CloseConnections(tab)
388  except page_test.Failure:
389    logging.warning('%s:\n%s', page.url, traceback.format_exc())
390    if expectation == 'fail':
391      logging.info('Failure was expected\n')
392      results.AddSuccess(page)
393    else:
394      results.AddFailure(page, sys.exc_info())
395  except (util.TimeoutException, exceptions.LoginException,
396          exceptions.ProfilingException):
397    ProcessError()
398  except (exceptions.TabCrashException, exceptions.BrowserGoneException):
399    ProcessError()
400    # Run() catches these exceptions to relaunch the tab/browser, so re-raise.
401    raise
402  except Exception:
403    raise
404  else:
405    if expectation == 'fail':
406      logging.warning('%s was expected to fail, but passed.\n', page.url)
407    results.AddSuccess(page)
408  finally:
409    page_state.CleanUpPage(page, tab)
410
411
412def _GetSequentialFileName(base_name):
413  """Returns the next sequential file name based on |base_name| and the
414  existing files."""
415  index = 0
416  while True:
417    output_name = '%s_%03d' % (base_name, index)
418    if not glob.glob(output_name + '.*'):
419      break
420    index = index + 1
421  return output_name
422
423
424def _WaitForThermalThrottlingIfNeeded(platform):
425  if not platform.CanMonitorThermalThrottling():
426    return
427  thermal_throttling_retry = 0
428  while (platform.IsThermallyThrottled() and
429         thermal_throttling_retry < 3):
430    logging.warning('Thermally throttled, waiting (%d)...',
431                    thermal_throttling_retry)
432    thermal_throttling_retry += 1
433    time.sleep(thermal_throttling_retry * 2)
434
435  if thermal_throttling_retry and platform.IsThermallyThrottled():
436    logging.error('Device is thermally throttled before running '
437                  'performance tests, results will vary.')
438
439
440def _CheckThermalThrottling(platform):
441  if not platform.CanMonitorThermalThrottling():
442    return
443  if platform.HasBeenThermallyThrottled():
444    logging.error('Device has been thermally throttled during '
445                  'performance tests, results will vary.')
446