Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2017 The Chromium Authors. All rights reserved. | 2 # Copyright 2017 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 import argparse | 6 import argparse |
| 7 import fnmatch | |
| 7 import logging | 8 import logging |
| 8 import os | 9 import os |
| 9 import sys | 10 import sys |
| 10 | 11 |
| 11 import telemetry_mini | 12 import telemetry_mini |
| 12 | 13 |
| 13 | 14 |
| 14 BROWSER_FLAGS = [ | 15 BROWSER_FLAGS = [ |
| 15 '--enable-remote-debugging', | 16 '--enable-remote-debugging', |
| 16 '--disable-fre', | 17 '--disable-fre', |
| 17 '--no-default-browser-check', | 18 '--no-default-browser-check', |
| 18 '--no-first-run', | 19 '--no-first-run', |
| 19 ] | 20 ] |
| 20 | 21 |
| 21 TRACE_CONFIG = { | 22 TRACE_CONFIG = { |
| 22 'excludedCategories': ['*'], | 23 'excludedCategories': ['*'], |
| 23 'includedCategories': ['rails', 'toplevel', 'startup', 'blink.user_timing'], | 24 'includedCategories': ['rails', 'toplevel', 'startup', 'blink.user_timing'], |
| 24 'memoryDumpConfig': {'triggers': []} | 25 'memoryDumpConfig': {'triggers': []} |
| 25 } | 26 } |
| 26 | 27 |
| 27 BROWSERS = { | 28 BROWSERS = { |
| 28 'android-chrome': telemetry_mini.ChromeApp, | 29 'android-chrome': telemetry_mini.ChromeApp, |
| 29 'android-chromium': telemetry_mini.ChromiumApp, | 30 'android-chromium': telemetry_mini.ChromiumApp, |
| 30 'android-system-chrome': telemetry_mini.SystemChromeApp, | 31 'android-system-chrome': telemetry_mini.SystemChromeApp, |
| 31 } | 32 } |
| 32 | 33 |
| 33 | 34 |
| 34 class ProcessWatcher(object): | |
|
perezju
2017/08/18 11:30:33
Did not changed any of this code. Just moved it a
| |
| 35 def __init__(self, device): | |
| 36 self.device = device | |
| 37 self._process_pid = {} | |
| 38 | |
| 39 def StartWatching(self, process_name): | |
| 40 """Register a process or android app to keep track of its PID.""" | |
| 41 if isinstance(process_name, telemetry_mini.AndroidApp): | |
| 42 process_name = process_name.PACKAGE_NAME | |
| 43 | |
| 44 @telemetry_mini.RetryOn(returns_falsy=True) | |
| 45 def GetPids(): | |
| 46 # Returns an empty list if the process name is not found. | |
| 47 return self.device.ProcessStatus()[process_name] | |
| 48 | |
| 49 assert process_name not in self._process_pid | |
| 50 pids = GetPids() | |
| 51 assert pids, 'PID for %s not found' % process_name | |
| 52 assert len(pids) == 1, 'Single PID for %s expected, but found: %s' % ( | |
| 53 process_name, pids) | |
| 54 logging.info('Started watching %s (PID=%d)', process_name, pids[0]) | |
| 55 self._process_pid[process_name] = pids[0] | |
| 56 | |
| 57 def AssertAllAlive(self): | |
| 58 """Check that all watched processes remain alive and were not restarted.""" | |
| 59 status = self.device.ProcessStatus() | |
| 60 all_alive = True | |
| 61 for process_name, old_pid in sorted(self._process_pid.iteritems()): | |
| 62 new_pids = status[process_name] | |
| 63 if not new_pids: | |
| 64 all_alive = False | |
| 65 logging.error('Process %s died (PID=%d).', process_name, old_pid) | |
| 66 elif new_pids != [old_pid]: | |
| 67 all_alive = False | |
| 68 logging.error( | |
| 69 'Process %s restarted (PID=%d -> %s).', process_name, | |
| 70 old_pid, new_pids) | |
| 71 else: | |
| 72 logging.info('Process %s still alive (PID=%d)', process_name, old_pid) | |
| 73 assert all_alive, 'Some watched processes died or got restarted' | |
| 74 | |
| 75 | |
| 76 def EnsureSingleBrowser(device, browser_name, force_install=False): | |
| 77 """Ensure a single Chrome browser is installed and available on the device. | |
| 78 | |
| 79 Having more than one Chrome browser available may produce results which are | |
| 80 confusing or unreliable (e.g. unclear which browser will respond by default | |
| 81 to intents triggered by other apps). | |
| 82 | |
| 83 This function ensures only the selected browser is available, installing it | |
| 84 if necessary, and uninstalling/disabling others. | |
| 85 """ | |
| 86 browser = BROWSERS[browser_name](device) | |
| 87 available_browsers = set(device.ListPackages('chrome', only_enabled=True)) | |
| 88 | |
| 89 # Install or enable if needed. | |
| 90 if force_install or browser.PACKAGE_NAME not in available_browsers: | |
| 91 browser.Install() | |
| 92 | |
| 93 # Uninstall disable other browser apps. | |
| 94 for other_browser in BROWSERS.itervalues(): | |
| 95 if (other_browser.PACKAGE_NAME != browser.PACKAGE_NAME and | |
| 96 other_browser.PACKAGE_NAME in available_browsers): | |
| 97 other_browser(device).Uninstall() | |
| 98 | |
| 99 # Finally check that only the selected browser is actually available. | |
| 100 available_browsers = device.ListPackages('chrome', only_enabled=True) | |
| 101 assert browser.PACKAGE_NAME in available_browsers, ( | |
| 102 'Unable to make %s available' % browser.PACKAGE_NAME) | |
| 103 available_browsers.remove(browser.PACKAGE_NAME) | |
| 104 assert not available_browsers, ( | |
| 105 'Other browsers may intefere with the test: %s' % available_browsers) | |
| 106 return browser | |
| 107 | |
| 108 | |
| 109 class TwitterApp(telemetry_mini.AndroidApp): | 35 class TwitterApp(telemetry_mini.AndroidApp): |
| 110 PACKAGE_NAME = 'com.twitter.android' | 36 PACKAGE_NAME = 'com.twitter.android' |
| 111 | 37 |
| 112 | 38 |
| 113 class InstagramApp(telemetry_mini.AndroidApp): | 39 class InstagramApp(telemetry_mini.AndroidApp): |
| 114 PACKAGE_NAME = 'com.instagram.android' | 40 PACKAGE_NAME = 'com.instagram.android' |
| 115 | 41 |
| 116 | 42 |
| 117 class TwitterFlipkartStory(telemetry_mini.UserStory): | 43 class TwitterFlipkartStory(telemetry_mini.UserStory): |
| 44 """Load Chrome Custom Tab from another application. | |
| 45 | |
| 46 The flow of the story is: | |
| 47 - Start Twitter app to view the @flipkart profile. | |
| 48 - Tap on a link to open Flipkart in a Chrome Custom Tab. | |
| 49 - Return to Twitter app. | |
| 50 """ | |
| 51 NAME = 'twitter_flipkart' | |
| 118 FLIPKART_TWITTER_LINK = [ | 52 FLIPKART_TWITTER_LINK = [ |
| 119 ('package', 'com.twitter.android'), | 53 ('package', 'com.twitter.android'), |
| 120 ('class', 'android.widget.TextView'), | 54 ('class', 'android.widget.TextView'), |
| 121 ('text', 'flipkart.com') | 55 ('text', 'flipkart.com') |
| 122 ] | 56 ] |
| 123 | 57 |
| 124 def __init__(self, *args, **kwargs): | 58 def __init__(self, *args, **kwargs): |
| 125 super(TwitterFlipkartStory, self).__init__(*args, **kwargs) | 59 super(TwitterFlipkartStory, self).__init__(*args, **kwargs) |
| 126 self.watcher = ProcessWatcher(self.device) | 60 self.watcher = ProcessWatcher(self.device) |
| 127 self.twitter = TwitterApp(self.device) | 61 self.twitter = TwitterApp(self.device) |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 142 | 76 |
| 143 # Return to Twitter app. | 77 # Return to Twitter app. |
| 144 self.actions.GoBack() | 78 self.actions.GoBack() |
| 145 self.watcher.AssertAllAlive() | 79 self.watcher.AssertAllAlive() |
| 146 | 80 |
| 147 def RunCleanupSteps(self): | 81 def RunCleanupSteps(self): |
| 148 self.twitter.ForceStop() | 82 self.twitter.ForceStop() |
| 149 | 83 |
| 150 | 84 |
| 151 class FlipkartInstagramStory(telemetry_mini.UserStory): | 85 class FlipkartInstagramStory(telemetry_mini.UserStory): |
| 86 """Interaction between Chrome, PWAs and a WebView-based app. | |
| 87 | |
| 88 The flow of the story is: | |
| 89 - Launch the Flipkart PWA. | |
| 90 - Go back home and launch the Instagram app. | |
| 91 - Use the app switcher to return to Flipkart. | |
| 92 - Go back home and launch Cricbuzz from a shortcut. | |
| 93 """ | |
| 94 NAME = 'flipkart_instagram' | |
| 95 | |
| 152 def __init__(self, *args, **kwargs): | 96 def __init__(self, *args, **kwargs): |
| 153 super(FlipkartInstagramStory, self).__init__(*args, **kwargs) | 97 super(FlipkartInstagramStory, self).__init__(*args, **kwargs) |
| 154 self.watcher = ProcessWatcher(self.device) | 98 self.watcher = ProcessWatcher(self.device) |
| 155 self.instagram = InstagramApp(self.device) | 99 self.instagram = InstagramApp(self.device) |
| 156 | 100 |
| 157 def RunPrepareSteps(self): | 101 def RunPrepareSteps(self): |
| 158 self.instagram.ForceStop() | 102 self.instagram.ForceStop() |
| 159 self.actions.ClearRecentApps() | 103 self.actions.ClearRecentApps() |
| 160 | 104 |
| 161 def RunStorySteps(self): | 105 def RunStorySteps(self): |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 180 self.actions.GoHome() | 124 self.actions.GoHome() |
| 181 self.actions.TapHomeScreenShortcut('Cricbuzz') | 125 self.actions.TapHomeScreenShortcut('Cricbuzz') |
| 182 self.browser.WaitForCurrentPageReady() | 126 self.browser.WaitForCurrentPageReady() |
| 183 self.actions.SwipeUp() | 127 self.actions.SwipeUp() |
| 184 self.watcher.AssertAllAlive() | 128 self.watcher.AssertAllAlive() |
| 185 | 129 |
| 186 def RunCleanupSteps(self): | 130 def RunCleanupSteps(self): |
| 187 self.instagram.ForceStop() | 131 self.instagram.ForceStop() |
| 188 | 132 |
| 189 | 133 |
| 134 STORIES = ( | |
| 135 TwitterFlipkartStory, | |
| 136 FlipkartInstagramStory | |
| 137 ) | |
| 138 | |
| 139 | |
| 140 class ProcessWatcher(object): | |
|
perezju
2017/08/18 11:30:33
Code removed from above pasted back here.
| |
| 141 def __init__(self, device): | |
| 142 self.device = device | |
| 143 self._process_pid = {} | |
| 144 | |
| 145 def StartWatching(self, process_name): | |
| 146 """Register a process or android app to keep track of its PID.""" | |
| 147 if isinstance(process_name, telemetry_mini.AndroidApp): | |
| 148 process_name = process_name.PACKAGE_NAME | |
| 149 | |
| 150 @telemetry_mini.RetryOn(returns_falsy=True) | |
| 151 def GetPids(): | |
| 152 # Returns an empty list if the process name is not found. | |
| 153 return self.device.ProcessStatus()[process_name] | |
| 154 | |
| 155 assert process_name not in self._process_pid | |
| 156 pids = GetPids() | |
| 157 assert pids, 'PID for %s not found' % process_name | |
| 158 assert len(pids) == 1, 'Single PID for %s expected, but found: %s' % ( | |
| 159 process_name, pids) | |
| 160 logging.info('Started watching %s (PID=%d)', process_name, pids[0]) | |
| 161 self._process_pid[process_name] = pids[0] | |
| 162 | |
| 163 def AssertAllAlive(self): | |
| 164 """Check that all watched processes remain alive and were not restarted.""" | |
| 165 status = self.device.ProcessStatus() | |
| 166 all_alive = True | |
| 167 for process_name, old_pid in sorted(self._process_pid.iteritems()): | |
| 168 new_pids = status[process_name] | |
| 169 if not new_pids: | |
| 170 all_alive = False | |
| 171 logging.error('Process %s died (PID=%d).', process_name, old_pid) | |
| 172 elif new_pids != [old_pid]: | |
| 173 all_alive = False | |
| 174 logging.error( | |
| 175 'Process %s restarted (PID=%d -> %s).', process_name, | |
| 176 old_pid, new_pids) | |
| 177 else: | |
| 178 logging.info('Process %s still alive (PID=%d)', process_name, old_pid) | |
| 179 assert all_alive, 'Some watched processes died or got restarted' | |
| 180 | |
| 181 | |
| 182 def EnsureSingleBrowser(device, browser_name, force_install=False): | |
| 183 """Ensure a single Chrome browser is installed and available on the device. | |
| 184 | |
| 185 Having more than one Chrome browser available may produce results which are | |
| 186 confusing or unreliable (e.g. unclear which browser will respond by default | |
| 187 to intents triggered by other apps). | |
| 188 | |
| 189 This function ensures only the selected browser is available, installing it | |
| 190 if necessary, and uninstalling/disabling others. | |
| 191 """ | |
| 192 browser = BROWSERS[browser_name](device) | |
| 193 available_browsers = set(device.ListPackages('chrome', only_enabled=True)) | |
| 194 | |
| 195 # Install or enable if needed. | |
| 196 if force_install or browser.PACKAGE_NAME not in available_browsers: | |
| 197 browser.Install() | |
| 198 | |
| 199 # Uninstall disable other browser apps. | |
| 200 for other_browser in BROWSERS.itervalues(): | |
| 201 if (other_browser.PACKAGE_NAME != browser.PACKAGE_NAME and | |
| 202 other_browser.PACKAGE_NAME in available_browsers): | |
| 203 other_browser(device).Uninstall() | |
| 204 | |
| 205 # Finally check that only the selected browser is actually available. | |
| 206 available_browsers = device.ListPackages('chrome', only_enabled=True) | |
| 207 assert browser.PACKAGE_NAME in available_browsers, ( | |
| 208 'Unable to make %s available' % browser.PACKAGE_NAME) | |
| 209 available_browsers.remove(browser.PACKAGE_NAME) | |
| 210 assert not available_browsers, ( | |
| 211 'Other browsers may intefere with the test: %s' % available_browsers) | |
| 212 return browser | |
| 213 | |
| 214 | |
| 190 def main(): | 215 def main(): |
| 191 browser_names = sorted(BROWSERS) | 216 browser_names = sorted(BROWSERS) |
| 192 default_browser = 'android-chrome' | 217 default_browser = 'android-chrome' |
| 193 parser = argparse.ArgumentParser() | 218 parser = argparse.ArgumentParser() |
| 194 parser.add_argument('--serial', | 219 parser.add_argument('--serial', |
| 195 help='device serial on which to run user stories' | 220 help='device serial on which to run user stories' |
| 196 ' (defaults to first device found)') | 221 ' (defaults to first device found)') |
| 197 parser.add_argument('--adb-bin', default='adb', metavar='PATH', | 222 parser.add_argument('--adb-bin', default='adb', metavar='PATH', |
| 198 help='path to adb binary to use (default: %(default)s)') | 223 help='path to adb binary to use (default: %(default)s)') |
| 199 parser.add_argument('--browser', default=default_browser, metavar='NAME', | 224 parser.add_argument('--browser', default=default_browser, metavar='NAME', |
| 200 choices=browser_names, | 225 choices=browser_names, |
| 201 help='one of: %s' % ', '.join( | 226 help='one of: %s' % ', '.join( |
| 202 '%s (default)' % b if b == default_browser else b | 227 '%s (default)' % b if b == default_browser else b |
| 203 for b in browser_names)) | 228 for b in browser_names)) |
| 229 parser.add_argument('--story-filter', metavar='PATTERN', default='*', | |
| 230 help='run the matching stories only (allows Unix' | |
| 231 ' shell-style wildcards)') | |
| 232 parser.add_argument('--storyset-repeat', metavar='NUM', type=int, default=1, | |
| 233 help='repeat the story set a number of times' | |
| 234 ' (default: %(default)d)') | |
|
nednguyen
2017/08/18 14:00:32
I would just name this to "--repeat" for less typi
perezju
2017/08/18 14:09:05
Good point. Done!
| |
| 235 parser.add_argument('--output-dir', metavar='PATH', | |
| 236 help='path to directory for placing output trace files' | |
| 237 ' (defaults to current directory)') | |
| 204 parser.add_argument('--force-install', action='store_true', | 238 parser.add_argument('--force-install', action='store_true', |
| 205 help='install APK even if browser is already available') | 239 help='install APK even if browser is already available') |
| 206 parser.add_argument('--apks-dir', metavar='PATH', | 240 parser.add_argument('--apks-dir', metavar='PATH', |
| 207 help='path where to find APKs to install') | 241 help='path where to find APKs to install') |
| 208 parser.add_argument('--port', type=int, default=1234, | 242 parser.add_argument('--port', type=int, default=1234, |
| 209 help='port for connection with device' | 243 help='port for connection with device' |
| 210 ' (default: %(default)s)') | 244 ' (default: %(default)s)') |
| 211 parser.add_argument('-v', '--verbose', action='store_true') | 245 parser.add_argument('-v', '--verbose', action='store_true') |
| 212 args = parser.parse_args() | 246 args = parser.parse_args() |
| 213 | 247 |
| 214 logging.basicConfig() | 248 logging.basicConfig() |
| 215 if args.verbose: | 249 if args.verbose: |
| 216 logging.getLogger().setLevel(logging.INFO) | 250 logging.getLogger().setLevel(logging.INFO) |
| 217 | 251 |
| 252 stories = [s for s in STORIES if fnmatch.fnmatch(s.NAME, args.story_filter)] | |
| 253 if not stories: | |
| 254 return 'No matching stories' | |
| 255 | |
| 256 if args.output_dir is None: | |
| 257 args.output_dir = os.getcwd() | |
| 258 else: | |
| 259 args.output_dir = os.path.realpath(args.output_dir) | |
| 260 if not os.path.isdir(args.output_dir): | |
| 261 return 'Output directory does not exit' | |
| 262 | |
| 218 if args.apks_dir is None: | 263 if args.apks_dir is None: |
| 219 args.apks_dir = os.path.realpath(os.path.join( | 264 args.apks_dir = os.path.realpath(os.path.join( |
| 220 os.path.dirname(__file__), '..', '..', '..', '..', | 265 os.path.dirname(__file__), '..', '..', '..', '..', |
| 221 'out', 'Release', 'apks')) | 266 'out', 'Release', 'apks')) |
| 222 telemetry_mini.AndroidApp.APKS_DIR = args.apks_dir | 267 telemetry_mini.AndroidApp.APKS_DIR = args.apks_dir |
| 223 | 268 |
| 224 telemetry_mini.AdbMini.ADB_BIN = args.adb_bin | 269 telemetry_mini.AdbMini.ADB_BIN = args.adb_bin |
| 225 if args.serial is None: | 270 if args.serial is None: |
| 226 device = next(telemetry_mini.AdbMini.GetDevices()) | 271 device = next(telemetry_mini.AdbMini.GetDevices()) |
| 227 logging.warning('Connected to first device found: %s', device.serial) | 272 logging.warning('Connected to first device found: %s', device.serial) |
| 228 else: | 273 else: |
| 229 device = telemetry_mini.AdbMini(args.serial) | 274 device = telemetry_mini.AdbMini(args.serial) |
| 230 | 275 |
| 231 # Some operations may require a rooted device. | 276 # Some operations may require a rooted device. |
| 232 device.RunCommand('root') | 277 device.RunCommand('root') |
| 233 device.RunCommand('wait-for-device') | 278 device.RunCommand('wait-for-device') |
| 234 | 279 |
| 235 browser = EnsureSingleBrowser(device, args.browser, args.force_install) | 280 browser = EnsureSingleBrowser(device, args.browser, args.force_install) |
| 281 browser.SetBrowserFlags(BROWSER_FLAGS) | |
| 282 browser.SetTraceConfig(TRACE_CONFIG) | |
| 236 browser.SetDevToolsLocalPort(args.port) | 283 browser.SetDevToolsLocalPort(args.port) |
| 237 | 284 telemetry_mini.RunStories( |
| 238 story = FlipkartInstagramStory(browser) | 285 browser, stories, args.storyset_repeat, args.output_dir) |
| 239 story.Run(BROWSER_FLAGS, TRACE_CONFIG, 'trace.json') | |
| 240 | 286 |
| 241 | 287 |
| 242 if __name__ == '__main__': | 288 if __name__ == '__main__': |
| 243 sys.exit(main()) | 289 sys.exit(main()) |
| OLD | NEW |