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 |