| # Copyright 2016 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections |
| import logging |
| import re |
| import sys |
| import urllib |
| |
| from telemetry import decorators |
| from telemetry import story as story_module |
| # TODO(perezju): Remove references to telementry.internal when |
| # https://github.com/catapult-project/catapult/issues/2102 is resolved. |
| from telemetry.internal.browser import browser_finder |
| from telemetry.internal.browser import browser_finder_exceptions |
| from telemetry.util import wpr_modes |
| |
| from page_sets.top_10_mobile import URL_LIST |
| |
| |
| GOOGLE_SEARCH = 'https://www.google.com/search?' |
| |
| SEARCH_QUERIES = [ |
| 'science', |
| 'cat pictures', |
| '1600 Amphitheatre Pkwy, Mountain View, CA', |
| 'tom hanks', |
| 'weather 94110', |
| 'goog', |
| 'population of california', |
| 'sfo jfk flights', |
| 'movies 94110', |
| 'tip on 100 bill' |
| ] |
| |
| |
| def _OptionsForBrowser(browser_type, finder_options): |
| """Return options used to get a browser of the given type. |
| |
| TODO(perezju): Currently this clones the finder_options passed via the |
| command line to telemetry. When browser_options are split appart from |
| finder_options (crbug.com/570348) we will be able to construct our own |
| browser_options as needed. |
| """ |
| finder_options = finder_options.Copy() |
| finder_options.browser_type = browser_type |
| finder_options.browser_executable = None |
| finder_options.browser_options.browser_type = browser_type |
| return finder_options |
| |
| |
| class MultiBrowserSharedState(story_module.SharedState): |
| def __init__(self, test, finder_options, story_set): |
| """A shared state to run a test involving multiple browsers. |
| |
| The story_set is expected to include SinglePage instances (class defined |
| below) mapping each page to a browser on which to run. The state |
| requires at least one page to run on the 'default' browser, i.e. the |
| browser selected from the command line by the user. |
| """ |
| super(MultiBrowserSharedState, self).__init__( |
| test, finder_options, story_set) |
| self._platform = None |
| self._story_set = story_set |
| self._possible_browsers = {} |
| # We use an ordered dict to request memory dumps in a deterministic order. |
| self._browsers = collections.OrderedDict() |
| self._current_story = None |
| self._current_browser = None |
| self._current_tab = None |
| |
| possible_browser = self._PrepareBrowser('default', finder_options) |
| if not possible_browser: |
| raise browser_finder_exceptions.BrowserFinderException( |
| 'No browser found.\n\nAvailable browsers:\n%s\n' % |
| '\n'.join(browser_finder.GetAllAvailableBrowserTypes(finder_options))) |
| if not finder_options.run_disabled_tests: |
| self._CheckTestEnabled(test, possible_browser) |
| |
| extra_browser_types = set(story.browser_type for story in story_set) |
| extra_browser_types.remove('default') # Must include 'default' browser. |
| for browser_type in extra_browser_types: |
| options = _OptionsForBrowser(browser_type, finder_options) |
| if not self._PrepareBrowser(browser_type, options): |
| logging.warning('Skipping %s (%s) because %s browser is not available', |
| test.__name__, str(test), browser_type) |
| logging.warning('Install %s to be able to run the test.', browser_type) |
| sys.exit(0) |
| |
| # TODO(crbug/404771): Move network controller options out of |
| # browser_options and into finder_options. |
| browser_options = finder_options.browser_options |
| if finder_options.use_live_sites: |
| wpr_mode = wpr_modes.WPR_OFF |
| elif browser_options.wpr_mode == wpr_modes.WPR_RECORD: |
| wpr_mode = wpr_modes.WPR_RECORD |
| else: |
| wpr_mode = wpr_modes.WPR_REPLAY |
| |
| self.platform.network_controller.Open(wpr_mode, |
| browser_options.extra_wpr_args) |
| |
| # Make sure all browsers are open before starting. |
| for story in story_set: |
| if story.browser_type not in self._browsers: |
| self._SetCurrentBrowser(story.browser_type) |
| |
| @property |
| def current_tab(self): |
| return self._current_tab |
| |
| @property |
| def platform(self): |
| return self._platform |
| |
| def _CheckTestEnabled(self, test, possible_browser): |
| should_skip, msg = decorators.ShouldSkip(test, possible_browser) |
| if should_skip: |
| logging.warning(msg) |
| logging.warning('You are trying to run a disabled test.') |
| logging.warning( |
| 'Pass --also-run-disabled-tests to squelch this message.') |
| sys.exit(0) |
| |
| def _PrepareBrowser(self, browser_type, options): |
| """Add a browser to the dict of possible browsers. |
| |
| TODO(perezju): When available, use the GetBrowserForPlatform API instead. |
| See: crbug.com/570348 |
| |
| Returns: |
| The possible browser if found, or None otherwise. |
| """ |
| possible_browser = browser_finder.FindBrowser(options) |
| if possible_browser is None: |
| return None |
| |
| if self._platform is None: |
| self._platform = possible_browser.platform |
| else: |
| assert self._platform is possible_browser.platform |
| self._possible_browsers[browser_type] = (possible_browser, options) |
| return possible_browser |
| |
| def _SetCurrentBrowser(self, browser_type): |
| """Select a browser of the given type and bring it to the foreground. |
| |
| This launches the browser if it does not exist already or, otherwise, moves |
| the existing browser to the foreground. |
| """ |
| if not browser_type in self._browsers: |
| possible_browser, options = self._possible_browsers[browser_type] |
| self._current_browser = possible_browser.Create(options) |
| self._browsers[browser_type] = self._current_browser |
| else: |
| self._current_browser = self._browsers[browser_type] |
| self._current_browser.Foreground() |
| |
| def CanRunStory(self, _): |
| return True |
| |
| def WillRunStory(self, story): |
| self._current_story = story |
| |
| self.platform.network_controller.StartReplay( |
| self._story_set.WprFilePathForStory(story), |
| story.make_javascript_deterministic) |
| |
| self._SetCurrentBrowser(self._current_story.browser_type) |
| self._current_tab = self._current_browser.foreground_tab |
| |
| def RunStory(self, _): |
| self._current_story.Run(self) |
| |
| def DidRunStory(self, _): |
| self._current_story = None |
| |
| def TakeMemoryMeasurement(self): |
| self.current_tab.action_runner.ForceGarbageCollection() |
| self.platform.FlushEntireSystemCache() |
| if not self.platform.tracing_controller.is_tracing_running: |
| return # Tracing is not running, e.g., when recording a WPR archive. |
| for browser_type, browser in self._browsers.iteritems(): |
| if not browser.DumpMemory(): |
| logging.error('Unable to dump memory for %s', browser_type) |
| |
| def TearDownState(self): |
| self.platform.network_controller.Close() |
| |
| for browser_type, browser in self._browsers.iteritems(): |
| try: |
| browser.Close() |
| except Exception: |
| logging.exception('Error while closing browser: %s', browser_type) |
| self._browsers = None # Not usable after tearing down. |
| |
| |
| class SinglePage(story_module.Story): |
| def __init__(self, name, url, browser_type, phase): |
| """A story that associates a particular page with a browser to view it. |
| |
| Args: |
| name: A string with the name of the page as it will appear reported, |
| e.g., on results and dashboards. |
| url: A string with the url of the page to load. |
| browser_type: A string identifying the browser where this page should be |
| displayed. Accepts the same strings as the command line --browser |
| option (e.g. 'android-webview'), and the special value 'default' to |
| select the browser chosen by the user on the command line. |
| """ |
| super(SinglePage, self).__init__(MultiBrowserSharedState, name=name) |
| self._url = url |
| self._browser_type = browser_type |
| self.grouping_keys['phase'] = phase |
| |
| @property |
| def url(self): |
| return self._url |
| |
| @property |
| def browser_type(self): |
| return self._browser_type |
| |
| def Run(self, shared_state): |
| shared_state.current_tab.Navigate(self._url) |
| shared_state.current_tab.WaitForDocumentReadyStateToBeComplete() |
| shared_state.TakeMemoryMeasurement() |
| |
| |
| class DualBrowserStorySet(story_module.StorySet): |
| """A story set that switches back and forth between two browsers.""" |
| |
| def __init__(self): |
| super(DualBrowserStorySet, self).__init__( |
| archive_data_file='data/dual_browser_story.json', |
| cloud_storage_bucket=story_module.PARTNER_BUCKET) |
| |
| for query, url in zip(SEARCH_QUERIES, URL_LIST): |
| # Stories that run on the android-webview browser. |
| self.AddStory(SinglePage( |
| name='google_%s' % re.sub('\W+', '_', query.lower()), |
| url=GOOGLE_SEARCH + urllib.urlencode({'q': query}), |
| browser_type='android-webview', |
| phase='on_webview')) |
| |
| # Stories that run on the browser selected by command line options. |
| self.AddStory(SinglePage( |
| name=re.sub('\W+', '_', url), |
| url=url, |
| browser_type='default', |
| phase='on_chrome')) |