Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(513)

Side by Side Diff: experimental/telemetry_mini/telemetry_mini.py

Issue 2996123002: [telemetry_mini] Add AndroidActions class (Closed)
Patch Set: setdefault Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « experimental/telemetry_mini/android_go_stories.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 # This is intended to be a very trimmed down, single-file, hackable, and easy 6 # This is intended to be a very trimmed down, single-file, hackable, and easy
7 # to understand version of Telemetry. It's able to run simple user stories on 7 # to understand version of Telemetry. It's able to run simple user stories on
8 # Android, grab traces, and extract metrics from them. May be useful to 8 # Android, grab traces, and extract metrics from them. May be useful to
9 # diagnose issues with Chrome, reproduce regressions or prototype new user 9 # diagnose issues with Chrome, reproduce regressions or prototype new user
10 # stories. 10 # stories.
(...skipping 12 matching lines...) Expand all
23 import posixpath 23 import posixpath
24 import re 24 import re
25 import socket 25 import socket
26 import subprocess 26 import subprocess
27 import tempfile 27 import tempfile
28 import time 28 import time
29 import websocket # pylint: disable=import-error 29 import websocket # pylint: disable=import-error
30 from xml.etree import ElementTree as element_tree 30 from xml.etree import ElementTree as element_tree
31 31
32 32
33 KEYCODE_BACK = 4
34
33 # Parse rectangle bounds given as: '[left,top][right,bottom]'. 35 # Parse rectangle bounds given as: '[left,top][right,bottom]'.
34 RE_BOUNDS = re.compile( 36 RE_BOUNDS = re.compile(
35 r'\[(?P<left>\d+),(?P<top>\d+)\]\[(?P<right>\d+),(?P<bottom>\d+)\]') 37 r'\[(?P<left>\d+),(?P<top>\d+)\]\[(?P<right>\d+),(?P<bottom>\d+)\]')
36 38
37 # TODO: Maybe replace with a true on-device temp file. 39 # TODO: Maybe replace with a true on-device temp file.
38 UI_DUMP_TEMP = '/data/local/tmp/tm_ui_dump.xml' 40 UI_DUMP_TEMP = '/data/local/tmp/tm_ui_dump.xml'
39 41
40 42
41 def RetryOn(exc_type=(), returns_falsy=False, retries=5): 43 def RetryOn(exc_type=(), returns_falsy=False, retries=5):
42 """Decorator to retry a function in case of errors or falsy values. 44 """Decorator to retry a function in case of errors or falsy values.
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after
135 row = line.split(None, 8) 137 row = line.split(None, 8)
136 try: 138 try:
137 pid = int(row[1]) 139 pid = int(row[1])
138 process_name = row[-1] 140 process_name = row[-1]
139 except StandardError: 141 except StandardError:
140 continue 142 continue
141 result[process_name].append(pid) 143 result[process_name].append(pid)
142 return result 144 return result
143 145
144 @RetryOn(AdbCommandError) 146 @RetryOn(AdbCommandError)
145 def GetUiDump(self): 147 def GetUiScreenDump(self):
146 """Return the root XML node with screen captured from the device.""" 148 """Return the root XML node with screen captured from the device."""
147 self.RunShellCommand('rm', '-f', UI_DUMP_TEMP) 149 self.RunShellCommand('rm', '-f', UI_DUMP_TEMP)
148 output = self.RunShellCommand('uiautomator', 'dump', UI_DUMP_TEMP).strip() 150 output = self.RunShellCommand('uiautomator', 'dump', UI_DUMP_TEMP).strip()
149 151
150 if output.startswith('ERROR:'): 152 if output.startswith('ERROR:'):
151 # uiautomator may fail if device is not in idle state, e.g. animations 153 # uiautomator may fail if device is not in idle state, e.g. animations
152 # or video playing. Retry if that's the case. 154 # or video playing. Retry if that's the case.
153 raise AdbCommandError(output) 155 raise AdbCommandError(output)
154 156
155 with tempfile.NamedTemporaryFile(suffix='.xml') as f: 157 with tempfile.NamedTemporaryFile(suffix='.xml') as f:
156 f.close() 158 f.close()
157 self.RunCommand('pull', UI_DUMP_TEMP, f.name) 159 self.RunCommand('pull', UI_DUMP_TEMP, f.name)
158 return element_tree.parse(f.name) 160 return element_tree.parse(f.name)
159 161
160 @RetryOn(LookupError) 162 @RetryOn(LookupError)
161 def FindUiNode(self, attr_values): 163 def FindUiElement(self, attr_values):
162 """Find a UI node on screen capture, retrying if not yet visible.""" 164 """Find a UI element on screen capture, retrying if not yet visible."""
163 root = self.GetUiDump() 165 root = self.GetUiScreenDump()
164 for node in root.iter(): 166 for node in root.iter():
165 if all(node.get(k) == v for k, v in attr_values): 167 if all(node.get(k) == v for k, v in attr_values):
166 return node 168 return node
167 raise LookupError('Specified UI node not found') 169 raise LookupError('Specified UI element not found')
168 170
169 def TapUiNode(self, *args, **kwargs): 171 def TapUiElement(self, *args, **kwargs):
170 node = self.FindUiNode(*args, **kwargs) 172 node = self.FindUiElement(*args, **kwargs)
171 m = RE_BOUNDS.match(node.get('bounds')) 173 m = RE_BOUNDS.match(node.get('bounds'))
172 left, top, right, bottom = (int(v) for v in m.groups()) 174 left, top, right, bottom = (int(v) for v in m.groups())
173 x, y = (left + right) / 2, (top + bottom) / 2 175 x, y = (left + right) / 2, (top + bottom) / 2
174 self.RunShellCommand('input', 'tap', str(x), str(y)) 176 self.RunShellCommand('input', 'tap', str(x), str(y))
175 177
176 178
179 def _UserAction(f):
180 @functools.wraps(f)
181 def Wrapper(self, *args, **kwargs):
182 repeat = kwargs.setdefault('repeat', 1)
183 action_delay = kwargs.setdefault('action_delay', None)
184 for _ in xrange(repeat):
185 f(self, *args, **kwargs)
186 self.Idle(action_delay)
187 return Wrapper
188
189
190 class AndroidActions(object):
191 def __init__(self, device, user_action_delay=1):
192 self.device = device
193 self.user_action_delay = user_action_delay
194
195 def Idle(self, duration=None):
196 if duration is None:
197 duration = self.user_action_delay
198 if duration:
199 time.sleep(duration)
200
201 @_UserAction
202 def GoBack(self, **kwargs):
nednguyen 2017/08/17 10:28:58 why do you add these **kwargs to just delete them
203 del kwargs
204 self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_BACK))
205
206 @_UserAction
207 def StartActivity(
208 self, data_uri, action='android.intent.action.VIEW', **kwargs):
209 del kwargs
210 self.device.RunShellCommand('am', 'start', '-a', action, '-d', data_uri)
211
212 @_UserAction
213 def TapUiElement(self, attr_values, **kwargs):
214 del kwargs
215 self.device.TapUiElement(attr_values)
216
217 @_UserAction
218 def SwipeUp(self, **kwargs):
219 del kwargs
220 # Hardcoded values for 480x854 screen size; should work reasonably on
221 # other screen sizes.
222 # Command args: swipe <x1> <y1> <x2> <y2> [duration(ms)]
223 self.device.RunShellCommand(
224 'input', 'swipe', '240', '568', '240', '284', '400')
225
226
177 class DevToolsWebSocket(object): 227 class DevToolsWebSocket(object):
178 def __init__(self, url): 228 def __init__(self, url):
179 self._url = url 229 self._url = url
180 self._socket = None 230 self._socket = None
181 self._cmdid = 0 231 self._cmdid = 0
182 232
183 def __enter__(self): 233 def __enter__(self):
184 self.Open() 234 self.Open()
185 return self 235 return self
186 236
(...skipping 200 matching lines...) Expand 10 before | Expand all | Expand 10 after
387 def Uninstall(self): 437 def Uninstall(self):
388 # System Chrome app cannot be (un)installed, so we enable/disable instead. 438 # System Chrome app cannot be (un)installed, so we enable/disable instead.
389 logging.warning('Disabling %s', self.PACKAGE_NAME) 439 logging.warning('Disabling %s', self.PACKAGE_NAME)
390 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME) 440 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME)
391 441
392 442
393 class UserStory(object): 443 class UserStory(object):
394 def __init__(self, browser): 444 def __init__(self, browser):
395 self.device = browser.device 445 self.device = browser.device
396 self.browser = browser 446 self.browser = browser
447 self.actions = AndroidActions(self.device)
397 448
398 def GetExtraStoryApps(self): 449 def GetExtraStoryApps(self):
399 """Sequence of AndroidApp's, other than the browser, used in the story.""" 450 """Sequence of AndroidApp's, other than the browser, used in the story."""
400 return () 451 return ()
401 452
402 def EnsureExtraStoryAppsClosed(self): 453 def EnsureExtraStoryAppsClosed(self):
403 running_processes = self.device.ProcessStatus() 454 running_processes = self.device.ProcessStatus()
404 for app in self.GetExtraStoryApps(): 455 for app in self.GetExtraStoryApps():
405 if app.PACKAGE_NAME in running_processes: 456 if app.PACKAGE_NAME in running_processes:
406 app.ForceStop() 457 app.ForceStop()
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
439 if event['ph'] == 'v': 490 if event['ph'] == 'v':
440 # Extract any metrics you may need from the trace. 491 # Extract any metrics you may need from the trace.
441 value = event['args']['dumps']['allocators'][ 492 value = event['args']['dumps']['allocators'][
442 'java_heap/allocated_objects']['attrs']['size'] 493 'java_heap/allocated_objects']['attrs']['size']
443 assert value['units'] == 'bytes' 494 assert value['units'] == 'bytes'
444 processes[event['pid']]['java_heap'] = int(value['value'], 16) 495 processes[event['pid']]['java_heap'] = int(value['value'], 16)
445 elif event['ph'] == 'M' and event['name'] == 'process_name': 496 elif event['ph'] == 'M' and event['name'] == 'process_name':
446 processes[event['pid']]['name'] = event['args']['name'] 497 processes[event['pid']]['name'] = event['args']['name']
447 498
448 return processes.values() 499 return processes.values()
OLDNEW
« no previous file with comments | « experimental/telemetry_mini/android_go_stories.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698