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): | |
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): |
| 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('--repeat', metavar='NUM', type=int, default=1, |
| 233 help='repeat the story set a number of times' |
| 234 ' (default: %(default)d)') |
| 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(browser, stories, args.repeat, args.output_dir) |
238 story = FlipkartInstagramStory(browser) | |
239 story.Run(BROWSER_FLAGS, TRACE_CONFIG, 'trace.json') | |
240 | 285 |
241 | 286 |
242 if __name__ == '__main__': | 287 if __name__ == '__main__': |
243 sys.exit(main()) | 288 sys.exit(main()) |
OLD | NEW |