| #!/usr/bin/env python |
| # Copyright 2017 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 argparse |
| import fnmatch |
| import logging |
| import os |
| import sys |
| |
| import telemetry_mini |
| |
| |
| BROWSER_FLAGS = [ |
| '--enable-remote-debugging', |
| '--disable-fre', |
| '--no-default-browser-check', |
| '--no-first-run', |
| ] |
| |
| TRACE_CONFIG = { |
| 'excludedCategories': ['*'], |
| 'includedCategories': ['rails', 'toplevel', 'startup', 'blink.user_timing'], |
| 'memoryDumpConfig': {'triggers': []} |
| } |
| |
| BROWSERS = { |
| 'android-chrome': telemetry_mini.ChromeApp, |
| 'android-chromium': telemetry_mini.ChromiumApp, |
| 'android-system-chrome': telemetry_mini.SystemChromeApp, |
| } |
| |
| |
| class TwitterApp(telemetry_mini.AndroidApp): |
| PACKAGE_NAME = 'com.twitter.android' |
| |
| |
| class InstagramApp(telemetry_mini.AndroidApp): |
| PACKAGE_NAME = 'com.instagram.android' |
| |
| |
| class HangoutsApp(telemetry_mini.AndroidApp): |
| PACKAGE_NAME = 'com.google.android.talk' |
| |
| |
| class TwitterFlipkartStory(telemetry_mini.UserStory): |
| """Load Chrome Custom Tab from another application. |
| |
| The flow of the story is: |
| - Start Twitter app to view the @flipkart profile. |
| - Tap on a link to open Flipkart in a Chrome Custom Tab. |
| - Return to Twitter app. |
| """ |
| NAME = 'twitter_flipkart' |
| FLIPKART_TWITTER_LINK = [ |
| ('package', 'com.twitter.android'), |
| ('class', 'android.widget.TextView'), |
| ('text', 'flipkart.com') |
| ] |
| |
| def __init__(self, *args, **kwargs): |
| super(TwitterFlipkartStory, self).__init__(*args, **kwargs) |
| self.watcher = ProcessWatcher(self.device) |
| self.twitter = TwitterApp(self.device) |
| |
| def RunPrepareSteps(self): |
| self.twitter.ForceStop() |
| |
| def RunStorySteps(self): |
| # Activity will launch Twitter app on Flipkart profile. |
| self.actions.StartActivity('https://twitter.com/flipkart') |
| self.watcher.StartWatching(self.twitter) |
| |
| # Tapping on Flikpart link on Twitter app will launch Chrome. |
| self.actions.TapUiElement(self.FLIPKART_TWITTER_LINK) |
| self.watcher.StartWatching(self.browser) |
| self.browser.WaitForCurrentPageReady() |
| self.actions.SwipeUp(repeat=3) |
| |
| # Return to Twitter app. |
| self.actions.GoBack() |
| self.watcher.AssertAllAlive() |
| |
| def RunCleanupSteps(self): |
| self.twitter.ForceStop() |
| |
| |
| class FlipkartInstagramStory(telemetry_mini.UserStory): |
| """Interaction between Chrome, PWAs and a WebView-based app. |
| |
| The flow of the story is: |
| - Launch the Flipkart PWA. |
| - Go back home and launch the Instagram app. |
| - Use the app switcher to return to Flipkart. |
| - Go back home and launch Cricbuzz from a shortcut. |
| """ |
| NAME = 'flipkart_instagram' |
| |
| def __init__(self, *args, **kwargs): |
| super(FlipkartInstagramStory, self).__init__(*args, **kwargs) |
| self.watcher = ProcessWatcher(self.device) |
| self.instagram = InstagramApp(self.device) |
| |
| def RunPrepareSteps(self): |
| self.instagram.ForceStop() |
| self.actions.ClearRecentApps() |
| |
| def RunStorySteps(self): |
| # Tap on home screen shortcut to open Flipkart PWA. |
| self.actions.TapHomeScreenShortcut('Flipkart Lite') |
| self.watcher.StartWatching(self.browser) |
| self.browser.WaitForCurrentPageReady() |
| self.actions.SwipeUp(repeat=2) |
| |
| # Go back home, then launch Instagram app. |
| self.actions.GoHome() |
| self.actions.TapHomeScreenShortcut('Instagram') |
| self.watcher.StartWatching(self.instagram) |
| self.actions.SwipeUp(repeat=5) |
| |
| # Go to app switcher and return to Flipkart PWA. |
| self.actions.GoAppSwitcher() |
| self.actions.TapAppSwitcherTitle('Flipkart Lite') |
| self.actions.SwipeDown() |
| |
| # Go back home, then open Cricbuzz shortcut. |
| self.actions.GoHome() |
| self.actions.TapHomeScreenShortcut('Cricbuzz') |
| self.browser.WaitForCurrentPageReady() |
| self.actions.SwipeUp() |
| self.watcher.AssertAllAlive() |
| |
| def RunCleanupSteps(self): |
| self.instagram.ForceStop() |
| |
| |
| class HangoutsIndiaTimesStory(telemetry_mini.UserStory): |
| """Interaction between Chrome and a non-WebView-based app. |
| |
| TODO: Not sure if Hangouts is a non-WebView app. Consider using another app |
| if needed. |
| |
| The flow of the story is: |
| - Launch the Hangouts app. |
| - Open a conversation with a link to an IndiaTimes article. |
| - Click on the link to launch Chrome. |
| - Go back to the conversation. |
| """ |
| NAME = 'hangouts_indiatimes' |
| # TODO: Maybe use more specific targets, e.g. check url is in message. |
| FIRST_CONVERSATION = [ |
| ('resource-id', 'com.google.android.talk:id/conversationContent'), |
| ('index', '0'), |
| ] |
| SECOND_MESSAGE = [ |
| ('resource-id', 'com.google.android.talk:id/message_root'), |
| ('index', '1'), |
| ] |
| |
| def __init__(self, *args, **kwargs): |
| super(HangoutsIndiaTimesStory, self).__init__(*args, **kwargs) |
| self.watcher = ProcessWatcher(self.device) |
| self.hangouts = HangoutsApp(self.device) |
| |
| def RunPrepareSteps(self): |
| self.hangouts.ForceStop() |
| |
| def RunStorySteps(self): |
| # Tap on home screen shortcut to open Hangouts app. |
| self.actions.TapHomeScreenShortcut('Hangouts') |
| self.watcher.StartWatching(self.hangouts) |
| |
| # Find conversation with link to IndiaTimes and tap to launch Chrome. |
| self.actions.TapUiElement(self.FIRST_CONVERSATION) |
| self.actions.TapUiElement(self.SECOND_MESSAGE) |
| self.watcher.StartWatching(self.browser) |
| self.browser.WaitForCurrentPageReady() |
| self.actions.SwipeUp(repeat=4) |
| |
| # Go back to Hangouts, then back Home. |
| self.actions.GoBack() |
| self.actions.Idle(2) |
| self.actions.GoHome() |
| self.watcher.AssertAllAlive() |
| |
| def RunCleanupSteps(self): |
| self.hangouts.ForceStop() |
| |
| |
| STORIES = ( |
| TwitterFlipkartStory, |
| FlipkartInstagramStory, |
| HangoutsIndiaTimesStory, |
| ) |
| |
| |
| class ProcessWatcher(object): |
| def __init__(self, device): |
| self.device = device |
| self._process_pid = {} |
| |
| def StartWatching(self, process_name): |
| """Register a process or android app to keep track of its PID.""" |
| if isinstance(process_name, telemetry_mini.AndroidApp): |
| process_name = process_name.PACKAGE_NAME |
| |
| @telemetry_mini.RetryOn(returns_falsy=True) |
| def GetPids(): |
| # Returns an empty list if the process name is not found. |
| return self.device.ProcessStatus()[process_name] |
| |
| assert process_name not in self._process_pid |
| pids = GetPids() |
| assert pids, 'PID for %s not found' % process_name |
| assert len(pids) == 1, 'Single PID for %s expected, but found: %s' % ( |
| process_name, pids) |
| logging.info('Started watching %s (PID=%d)', process_name, pids[0]) |
| self._process_pid[process_name] = pids[0] |
| |
| def AssertAllAlive(self): |
| """Check that all watched processes remain alive and were not restarted.""" |
| status = self.device.ProcessStatus() |
| all_alive = True |
| for process_name, old_pid in sorted(self._process_pid.iteritems()): |
| new_pids = status[process_name] |
| if not new_pids: |
| all_alive = False |
| logging.error('Process %s died (PID=%d).', process_name, old_pid) |
| elif new_pids != [old_pid]: |
| all_alive = False |
| logging.error( |
| 'Process %s restarted (PID=%d -> %s).', process_name, |
| old_pid, new_pids) |
| else: |
| logging.info('Process %s still alive (PID=%d)', process_name, old_pid) |
| assert all_alive, 'Some watched processes died or got restarted' |
| |
| |
| def EnsureSingleBrowser(device, browser_name, force_install=False): |
| """Ensure a single Chrome browser is installed and available on the device. |
| |
| Having more than one Chrome browser available may produce results which are |
| confusing or unreliable (e.g. unclear which browser will respond by default |
| to intents triggered by other apps). |
| |
| This function ensures only the selected browser is available, installing it |
| if necessary, and uninstalling/disabling others. |
| """ |
| browser = BROWSERS[browser_name](device) |
| available_browsers = set(device.ListPackages('chrome', only_enabled=True)) |
| |
| # Install or enable if needed. |
| if force_install or browser.PACKAGE_NAME not in available_browsers: |
| browser.Install() |
| |
| # Uninstall disable other browser apps. |
| for other_browser in BROWSERS.itervalues(): |
| if (other_browser.PACKAGE_NAME != browser.PACKAGE_NAME and |
| other_browser.PACKAGE_NAME in available_browsers): |
| other_browser(device).Uninstall() |
| |
| # Finally check that only the selected browser is actually available. |
| available_browsers = device.ListPackages('chrome', only_enabled=True) |
| assert browser.PACKAGE_NAME in available_browsers, ( |
| 'Unable to make %s available' % browser.PACKAGE_NAME) |
| available_browsers.remove(browser.PACKAGE_NAME) |
| assert not available_browsers, ( |
| 'Other browsers may intefere with the test: %s' % available_browsers) |
| return browser |
| |
| |
| def main(): |
| browser_names = sorted(BROWSERS) |
| default_browser = 'android-chrome' |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--serial', |
| help='device serial on which to run user stories' |
| ' (defaults to first device found)') |
| parser.add_argument('--adb-bin', default='adb', metavar='PATH', |
| help='path to adb binary to use (default: %(default)s)') |
| parser.add_argument('--browser', default=default_browser, metavar='NAME', |
| choices=browser_names, |
| help='one of: %s' % ', '.join( |
| '%s (default)' % b if b == default_browser else b |
| for b in browser_names)) |
| parser.add_argument('--story-filter', metavar='PATTERN', default='*', |
| help='run the matching stories only (allows Unix' |
| ' shell-style wildcards)') |
| parser.add_argument('--repeat', metavar='NUM', type=int, default=1, |
| help='repeat the story set a number of times' |
| ' (default: %(default)d)') |
| parser.add_argument('--output-dir', metavar='PATH', |
| help='path to directory for placing output trace files' |
| ' (defaults to current directory)') |
| parser.add_argument('--force-install', action='store_true', |
| help='install APK even if browser is already available') |
| parser.add_argument('--apks-dir', metavar='PATH', |
| help='path where to find APKs to install') |
| parser.add_argument('--port', type=int, default=1234, |
| help='port for connection with device' |
| ' (default: %(default)s)') |
| parser.add_argument('-v', '--verbose', action='store_true') |
| args = parser.parse_args() |
| |
| logging.basicConfig() |
| if args.verbose: |
| logging.getLogger().setLevel(logging.INFO) |
| |
| stories = [s for s in STORIES if fnmatch.fnmatch(s.NAME, args.story_filter)] |
| if not stories: |
| return 'No matching stories' |
| |
| if args.output_dir is None: |
| args.output_dir = os.getcwd() |
| else: |
| args.output_dir = os.path.realpath(args.output_dir) |
| if not os.path.isdir(args.output_dir): |
| return 'Output directory does not exit' |
| |
| if args.apks_dir is None: |
| args.apks_dir = os.path.realpath(os.path.join( |
| os.path.dirname(__file__), '..', '..', '..', '..', |
| 'out', 'Release', 'apks')) |
| telemetry_mini.AndroidApp.APKS_DIR = args.apks_dir |
| |
| telemetry_mini.AdbMini.ADB_BIN = args.adb_bin |
| if args.serial is None: |
| device = next(telemetry_mini.AdbMini.GetDevices()) |
| logging.warning( |
| 'Connected to first device found: --serial %s', device.serial) |
| else: |
| device = telemetry_mini.AdbMini(args.serial) |
| |
| # Some operations may require a rooted device. |
| device.RunCommand('root') |
| device.RunCommand('wait-for-device') |
| |
| browser = EnsureSingleBrowser(device, args.browser, args.force_install) |
| browser.SetBrowserFlags(BROWSER_FLAGS) |
| browser.SetTraceConfig(TRACE_CONFIG) |
| browser.SetDevToolsLocalPort(args.port) |
| telemetry_mini.RunStories(browser, stories, args.repeat, args.output_dir) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |