| # Copyright 2012 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 logging |
| import six |
| |
| from py_utils import cloud_storage |
| from py_utils import exc_util |
| |
| from telemetry.core import exceptions |
| from telemetry import decorators |
| from telemetry.internal import app |
| from telemetry.internal.backends import browser_backend |
| from telemetry.internal.backends.chrome_inspector import tracing_backend |
| from telemetry.internal.browser import extension_dict |
| from telemetry.internal.browser import tab_list |
| from telemetry.internal.browser import ui_devtools |
| from telemetry.internal.browser import web_contents |
| from telemetry.testing import test_utils |
| |
| |
| class Browser(app.App): |
| """A running browser instance that can be controlled in a limited way. |
| |
| To create a browser instance, use browser_finder.FindBrowser, e.g: |
| |
| possible_browser = browser_finder.FindBrowser(finder_options) |
| with possible_browser.BrowserSession( |
| finder_options.browser_options) as browser: |
| # Do all your operations on browser here. |
| |
| See telemetry.internal.browser.possible_browser for more details and use |
| cases. |
| """ |
| def __init__(self, backend, platform_backend, startup_args, |
| find_existing=False): |
| super(Browser, self).__init__(app_backend=backend, |
| platform_backend=platform_backend) |
| try: |
| self._browser_backend = backend |
| self._platform_backend = platform_backend |
| self._startup_args = startup_args |
| self._tabs = tab_list.TabList(backend.tab_list_backend) |
| self._browser_backend.SetBrowser(self) |
| self._supports_inspecting_webui = False |
| if find_existing: |
| self._browser_backend.BindDevToolsClient() |
| else: |
| self._browser_backend.Start(startup_args) |
| self._LogBrowserInfo() |
| except Exception: |
| self.DumpStateUponFailure() |
| self.Close() |
| raise |
| |
| @property |
| def browser_type(self): |
| return self.app_type |
| |
| @property |
| def startup_args(self): |
| return self._startup_args |
| |
| @property |
| def supports_extensions(self): |
| return self._browser_backend.supports_extensions |
| |
| @property |
| def supports_tab_control(self): |
| return self._browser_backend.supports_tab_control |
| |
| @property |
| def tabs(self): |
| return self._tabs |
| |
| @property |
| def foreground_tab(self): |
| for tab in self._tabs: |
| # The foreground tab is the first (only) one that isn't hidden. |
| # This only works through luck on Android, due to crbug.com/322544 |
| # which means that tabs that have never been in the foreground return |
| # document.hidden as false; however in current code the Android foreground |
| # tab is always tab 0, which will be the first one that isn't hidden |
| if tab.EvaluateJavaScript('!document.hidden'): |
| return tab |
| raise exceptions.TabMissingError("No foreground tab found") |
| |
| @property |
| @decorators.Cache |
| def extensions(self): |
| if not self.supports_extensions: |
| raise browser_backend.ExtensionsNotSupportedException( |
| 'Extensions not supported') |
| return extension_dict.ExtensionDict(self._browser_backend.extension_backend) |
| |
| def GetTypExpectationsTags(self): |
| tags = self.platform.GetTypExpectationsTags() |
| return tags + test_utils.sanitizeTypExpectationsTags([self.browser_type]) |
| |
| def _LogBrowserInfo(self): |
| trim_logs = self._browser_backend.browser_options.trim_logs |
| logs = [] |
| logs.append(' Browser started (pid=%s).' % self._browser_backend.GetPid()) |
| logs.append(' OS: %s %s' % ( |
| self._platform_backend.platform.GetOSName(), |
| self._platform_backend.platform.GetOSVersionName())) |
| os_detail = self._platform_backend.platform.GetOSVersionDetailString() |
| if os_detail: |
| logs.append(' Detailed OS version: %s' % os_detail) |
| system_info = self.GetSystemInfo() |
| if system_info: |
| if system_info.model_name: |
| logs.append(' Model: %s' % system_info.model_name) |
| if not trim_logs: |
| if system_info.command_line: |
| logs.append(' Browser command line: %s' % system_info.command_line) |
| if system_info.gpu: |
| for i, device in enumerate(system_info.gpu.devices): |
| logs.append(' GPU device %d: %s' % (i, device)) |
| if not trim_logs: |
| if system_info.gpu.aux_attributes: |
| logs.append(' GPU Attributes:') |
| for k, v in sorted(six.iteritems(system_info.gpu.aux_attributes)): |
| logs.append(' %-20s: %s' % (k, v)) |
| if system_info.gpu.feature_status: |
| logs.append(' Feature Status:') |
| for k, v in sorted(six.iteritems(system_info.gpu.feature_status)): |
| logs.append(' %-20s: %s' % (k, v)) |
| if system_info.gpu.driver_bug_workarounds: |
| logs.append(' Driver Bug Workarounds:') |
| for workaround in system_info.gpu.driver_bug_workarounds: |
| logs.append(' %s' % workaround) |
| else: |
| logs.append(' No GPU devices') |
| else: |
| logging.warning('System info not supported') |
| logging.info('Browser information:\n%s', '\n'.join(logs)) |
| |
| @exc_util.BestEffort |
| def Close(self): |
| """Closes this browser.""" |
| try: |
| if self._browser_backend.IsBrowserRunning(): |
| logging.info( |
| 'Closing browser (pid=%s) ...', self._browser_backend.GetPid()) |
| |
| if self._browser_backend.supports_uploading_logs: |
| try: |
| self._browser_backend.UploadLogsToCloudStorage() |
| except cloud_storage.CloudStorageError as e: |
| logging.error('Cannot upload browser log: %s' % str(e)) |
| finally: |
| self._browser_backend.Close() |
| if self._browser_backend.IsBrowserRunning(): |
| logging.error( |
| 'Browser is still running (pid=%s).' |
| , self._browser_backend.GetPid()) |
| else: |
| logging.info('Browser is closed.') |
| |
| def Foreground(self): |
| """Ensure the browser application is moved to the foreground.""" |
| return self._browser_backend.Foreground() |
| |
| def Background(self): |
| """Ensure the browser application is moved to the background.""" |
| return self._browser_backend.Background() |
| |
| def GetStandardOutput(self): |
| return self._browser_backend.GetStandardOutput() |
| |
| def GetLogFileContents(self): |
| return self._browser_backend.GetLogFileContents() |
| |
| def GetMostRecentMinidumpPath(self): |
| """Returns the path to the most recent minidump.""" |
| return self._browser_backend.GetMostRecentMinidumpPath() |
| |
| def GetAllMinidumpPaths(self): |
| """Returns all minidump paths available in the backend.""" |
| return self._browser_backend.GetAllMinidumpPaths() |
| |
| def GetAllUnsymbolizedMinidumpPaths(self): |
| """Returns paths to all minidumps that have not already been |
| symbolized.""" |
| return self._browser_backend.GetAllUnsymbolizedMinidumpPaths() |
| |
| def SymbolizeMinidump(self, minidump_path): |
| """Given a minidump path, this method returns a tuple with the |
| first value being whether or not the minidump was able to be |
| symbolized and the second being that symbolized dump when true |
| and error message when false.""" |
| return self._browser_backend.SymbolizeMinidump(minidump_path) |
| |
| @property |
| def supports_app_ui_interactions(self): |
| """True if the browser supports Android app UI interactions.""" |
| return self._browser_backend.supports_app_ui_interactions |
| |
| def GetAppUi(self): |
| """Returns an AppUi object to interact with app's UI. |
| |
| See devil.android.app_ui for the documentation of the API provided.""" |
| assert self.supports_app_ui_interactions |
| return self._browser_backend.GetAppUi() |
| |
| def GetSystemInfo(self): |
| """Returns low-level information about the system, if available. |
| |
| See the documentation of the SystemInfo class for more details.""" |
| return self._browser_backend.GetSystemInfo() |
| |
| @property |
| def supports_memory_dumping(self): |
| return self._browser_backend.supports_memory_dumping |
| |
| def DumpMemory(self, timeout=None): |
| try: |
| return self._browser_backend.DumpMemory(timeout=timeout) |
| except tracing_backend.TracingUnrecoverableException: |
| logging.exception('Failed to record memory dump due to exception:') |
| # Re-raise as an AppCrashException to obtain further debug information |
| # about the browser state. |
| raise exceptions.AppCrashException( |
| app=self, msg='Browser failed to record memory dump.') |
| |
| @property |
| def supports_java_heap_garbage_collection( # pylint: disable=invalid-name |
| self): |
| return hasattr(self._browser_backend, 'ForceJavaHeapGarbageCollection') |
| |
| def ForceJavaHeapGarbageCollection(self): |
| """Forces java heap GC on supported platforms.""" |
| return self._browser_backend.ForceJavaHeapGarbageCollection() |
| |
| @property |
| # pylint: disable=invalid-name |
| def supports_overriding_memory_pressure_notifications(self): |
| # pylint: enable=invalid-name |
| return ( |
| self._browser_backend.supports_overriding_memory_pressure_notifications) |
| |
| def SetMemoryPressureNotificationsSuppressed( |
| self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): |
| self._browser_backend.SetMemoryPressureNotificationsSuppressed( |
| suppressed, timeout) |
| |
| def SimulateMemoryPressureNotification( |
| self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): |
| self._browser_backend.SimulateMemoryPressureNotification( |
| pressure_level, timeout) |
| |
| @property |
| def supports_overview_mode(self): # pylint: disable=invalid-name |
| return self._browser_backend.supports_overview_mode |
| |
| def EnterOverviewMode( |
| self, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): |
| self._browser_backend.EnterOverviewMode(timeout) |
| |
| def ExitOverviewMode( |
| self, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): |
| self._browser_backend.ExitOverviewMode(timeout) |
| |
| @property |
| def supports_cpu_metrics(self): |
| return self._browser_backend.supports_cpu_metrics |
| |
| @property |
| def supports_memory_metrics(self): |
| return self._browser_backend.supports_memory_metrics |
| |
| def CollectDebugData(self, log_level): |
| """Collects various information that may be useful for debugging. |
| |
| Specifically: |
| 1. Captures a screenshot. |
| 2. Collects stdout and system logs. |
| 3. Attempts to symbolize all currently unsymbolized minidumps. |
| |
| All collected information is stored as artifacts, and everything but the |
| screenshot is also included in the return value. |
| |
| Platforms may override this to provide other debug information in addition |
| to the above set of information. |
| |
| Args: |
| log_level: The logging level to use from the logging module, e.g. |
| logging.ERROR. |
| |
| Returns: |
| A debug_data.DebugData object containing the collected data. |
| """ |
| return self._browser_backend.CollectDebugData(log_level) |
| |
| @exc_util.BestEffort |
| def DumpStateUponFailure(self): |
| logging.info('*************** BROWSER STANDARD OUTPUT ***************') |
| try: |
| logging.info(self.GetStandardOutput()) |
| except Exception: # pylint: disable=broad-except |
| logging.exception('Failed to get browser standard output:') |
| logging.info('*********** END OF BROWSER STANDARD OUTPUT ************') |
| |
| logging.info('********************* BROWSER LOG *********************') |
| try: |
| logging.info(self.GetLogFileContents()) |
| except Exception: # pylint: disable=broad-except |
| logging.exception('Failed to get browser log:') |
| logging.info('***************** END OF BROWSER LOG ******************') |
| |
| logging.info( |
| '********************* SYMBOLIZED MINIDUMP *********************') |
| try: |
| self.CollectDebugData(logging.INFO) |
| except Exception: # pylint: disable=broad-except |
| logging.exception('Failed to get symbolized minidump:') |
| logging.info( |
| '***************** END OF SYMBOLIZED MINIDUMP ******************') |
| |
| def CleanupUnsymbolizedMinidumps(self, fatal=False): |
| """Cleans up any unsymbolized minidumps so they aren't found later. |
| |
| Args: |
| fatal: Whether the presence of unsymbolized minidumps should be considered |
| a fatal error or not. Typically, before a test should be non-fatal, |
| while after a test should be fatal. |
| """ |
| self._browser_backend.CleanupUnsymbolizedMinidumps(fatal=fatal) |
| |
| def IgnoreMinidump(self, path): |
| """Ignores the given minidump, treating it as already symbolized. |
| |
| Args: |
| path: The path to the minidump to ignore. |
| """ |
| self._browser_backend.IgnoreMinidump(path) |
| |
| def ExecuteBrowserCommand( |
| self, command_id, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): |
| self._browser_backend.ExecuteBrowserCommand(command_id, timeout) |
| |
| def StartCollectingPeriodicScreenshots(self, frequency_ms): |
| self._browser_backend.StartCollectingPeriodicScreenshots(frequency_ms) |
| |
| def StopCollectingPeriodicScreenshots(self): |
| self._browser_backend.StopCollectingPeriodicScreenshots() |
| |
| @property |
| def supports_inspecting_webui(self): |
| '''If this flag is enabled, any inspectable targets with chrome:// will |
| pass through browser.tabs |
| |
| This is mainly used for inspecting non-content area browser WebUI. |
| (e.g. Tab Search, WebUI TabStrip) |
| ''' |
| return self._supports_inspecting_webui |
| |
| @supports_inspecting_webui.setter |
| def supports_inspecting_webui(self, value): |
| self._supports_inspecting_webui = value |
| |
| def GetUIDevtools(self, port=None): |
| '''UI Devtools is mainly used to interact with native UI''' |
| return ui_devtools.UIDevTools( |
| self._browser_backend.GetUIDevtoolsBackend(port)) |