Skip to content

Commit aeac406

Browse files
committed
Update culebratester API invocations
- Update display real size - Update click via UiObject and UiDevice - Update wait for idle - Add set text - Add debug to canvas (not showed on macOS) - Remove call to populate view tree - Add checked and focused properties (not yet in WindowHierarchyChild) - Catch exception trying to access unittest.TestProgram.USAGE - Remove line escapes in print to generate code as they were producing the wrong format
1 parent 97c9d37 commit aeac406

5 files changed

Lines changed: 102 additions & 63 deletions

File tree

src/com/dtmilano/android/adb/adbclient.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@ def __getattr__(self, attr):
11771177
if attr in ['v', 'd', 'i', 'w', 'e']:
11781178
return lambda tag, message, verbose: self.adbClient.log(tag, message, priority=attr.upper(),
11791179
verbose=verbose)
1180-
raise AttributeError(self.__class__.__name__ + ' has no attribute "%s"' % attr)
1180+
raise AttributeError('__Log: %s has not attribute "%s"' % (self.__class__.__name__, attr))
11811181

11821182
def getSystemService(self, name):
11831183
if name == WIFI_SERVICE:

src/com/dtmilano/android/culebron.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import time
2727

2828
import numpy
29+
from culebratester_client import WindowHierarchy
2930

3031
from com.dtmilano.android.common import profileEnd
3132
from com.dtmilano.android.common import profileStart
@@ -350,16 +351,21 @@ def takeScreenshotAndShowItOnWindow(self):
350351
self.image = self.image.resize((scaledWidth, scaledHeight), PIL.Image.ANTIALIAS)
351352
(width, height) = self.image.size
352353
if self.isDarwin and 14 < self.sdkVersion < 23:
353-
stream = io.StringIO()
354+
if sys.version_info[0] < 3:
355+
stream = io.StringIO()
356+
else:
357+
stream = io.BytesIO()
354358
self.image.save(stream, 'GIF')
355359
import base64
356360
gif = base64.b64encode(stream.getvalue())
357361
stream.close()
358362
if self.canvas is None:
359363
if DEBUG:
360-
print("Creating canvas", width, 'x', height, file=sys.stderr)
364+
print("⬜️ Creating canvas", width, 'x', height, file=sys.stderr)
361365
self.placeholder.grid_forget()
362366
self.canvas = tkinter.Canvas(self.mainFrame, width=width, height=height)
367+
if DEBUG:
368+
print("⬜️ canvas", self.canvas, file=sys.stderr)
363369
self.canvas.focus_set()
364370
self.enableEvents()
365371
self.createMessageArea(width, height)
@@ -375,26 +381,26 @@ def takeScreenshotAndShowItOnWindow(self):
375381
self.screenshot = ImageTk.PhotoImage(self.image)
376382
if self.imageId is not None:
377383
self.canvas.delete(self.imageId)
378-
self.imageId = self.canvas.create_image(0, 0, anchor=tkinter.NW, image=self.screenshot)
384+
self.imageId = self.canvas.create_image(0, 0, anchor=tkinter.NW, image=self.screenshot, tag="screenshot")
379385
if DEBUG:
380386
try:
381-
print("Grid info", self.canvas.grid_info(), file=sys.stderr)
387+
print("⬜️ Grid info", self.canvas.grid_info(), file=sys.stderr)
382388
except:
383-
print("Exception getting grid info", file=sys.stderr)
389+
print("⬜️ Exception getting grid info", file=sys.stderr)
384390
gridInfo = None
385391
try:
386392
gridInfo = self.canvas.grid_info()
387393
except:
388394
if DEBUG:
389-
print("Adding canvas to grid (1,1)", file=sys.stderr)
395+
print("⬜️ Adding canvas to grid (1,1)", file=sys.stderr)
390396
self.canvas.grid(row=1, column=1, rowspan=4)
391397
if not gridInfo:
392398
self.canvas.grid(row=1, column=1, rowspan=4)
393399
try:
394400
self.findTargets()
395401
self.hideVignette()
396402
except Exception as ex:
397-
print(ex, file=sys.stderr)
403+
print("⛔️ %s" % ex, file=sys.stderr)
398404
if DEBUG:
399405
try:
400406
self.printGridInfo()
@@ -447,7 +453,7 @@ def toast(self, text, background=None, timeout=5):
447453

448454
def createVignette(self, width, height):
449455
if DEBUG:
450-
print("createVignette(%d, %d)" % (width, height), file=sys.stderr)
456+
print("🟪 createVignette(%d, %d)" % (width, height), file=sys.stderr)
451457
self.vignetteId = self.canvas.create_rectangle(0, 0, width, height, fill=Color.MAGENTA,
452458
stipple='gray50')
453459
if sys.version_info > (3, 0):
@@ -463,32 +469,40 @@ def createVignette(self, width, height):
463469

464470
def showVignette(self):
465471
if DEBUG:
466-
print("showVignette()", file=sys.stderr)
472+
print("🟪 showVignette()", file=sys.stderr)
467473
if self.canvas is None:
468474
return
469475
if self.vignetteId:
470476
if DEBUG:
471-
print(" showing vignette", file=sys.stderr)
477+
print("🟪 showing vignette id=%d" % self.vignetteId, file=sys.stderr)
472478
# disable events while we are processing one
473479
self.disableEvents()
480+
self.canvas.tag_lower('screenshot')
474481
self.canvas.lift(self.vignetteId)
475482
self.canvas.lift(self.waitMessageShadowId)
476483
self.canvas.lift(self.waitMessageId)
477484
self.canvas.update_idletasks()
478485

479486
def hideVignette(self):
480487
if DEBUG:
481-
print("hideVignette()", file=sys.stderr)
488+
print("🟪 hideVignette()", file=sys.stderr)
482489
if self.canvas is None:
483490
return
484491
if self.vignetteId:
485492
if DEBUG:
486-
print(" hiding vignette", file=sys.stderr)
487-
self.canvas.lift(self.imageId)
493+
print("🟪 hiding vignette", file=sys.stderr)
494+
try:
495+
self.canvas.tag_lift('screenshot')
496+
except Exception as ex:
497+
if DEBUG:
498+
print("🟪 exception=%s" % ex, file=sys.stderr)
499+
self.canvas.lift(self.imageId)
488500
self.canvas.update_idletasks()
489501
self.enableEvents()
490502

491503
def deleteVignette(self):
504+
if DEBUG:
505+
print("🟪 deleteVignette()", file=sys.stderr)
492506
if self.canvas is not None:
493507
self.canvas.delete(self.vignetteId)
494508
self.vignetteId = None
@@ -571,13 +585,38 @@ def populateViewTree(self, view):
571585
Populates the View tree.
572586
'''
573587

574-
vuid = view.getUniqueId()
575-
text = view.__smallStr__()
576-
if view.getParent() is None:
588+
print('culebron.populateViewTree: getting unique id for %s' % view.__class__.__name__, file=sys.stderr)
589+
if type(view) == WindowHierarchy:
590+
print('culebron.populateViewTree: skipping HierarchyView', file=sys.stderr)
591+
return
592+
else:
593+
print('culebron.populateViewTree: %s' % type(view), file=sys.stderr)
594+
595+
parent_unique_id = ''
596+
try:
597+
vuid = view.getUniqueId()
598+
text = view.__smallStr__()
599+
parent = view.getParent()
600+
is_target = view.isTarget()
601+
if parent:
602+
parent_unique_id = parent.getUniqueId()
603+
except AttributeError:
604+
vuid = view.unique_id
605+
#FIXME
606+
#is_target = view.is_target
607+
is_target = False
608+
text = view.text
609+
parent = view.parent
610+
if parent:
611+
#FIXME:
612+
# parent is an int
613+
#parent_unique_id = parent.unique_id
614+
parent_unique_id = 0
615+
if parent is None:
577616
self.viewTree.insert('', tkinter.END, vuid, text=text)
578617
else:
579-
self.viewTree.insert(view.getParent().getUniqueId(), tkinter.END, vuid, text=text, tags=('ttk'))
580-
self.viewTree.set(vuid, 'T', '*' if view.isTarget() else ' ')
618+
self.viewTree.insert(parent_unique_id, tkinter.END, vuid, text=text, tags=('ttk'))
619+
self.viewTree.set(vuid, 'T', '*' if is_target else ' ')
581620
self.viewTree.tag_bind('ttk', '<1>', self.viewTreeItemClicked)
582621

583622
def findTargets(self):
@@ -632,8 +671,10 @@ def findTargets(self):
632671
else:
633672
target = False
634673

635-
if self.vc:
636-
self.vc.traverse(transform=self.populateViewTree)
674+
# FIXME: we are not populating the view tree now
675+
# there are some problems with culebratester2
676+
#if self.vc:
677+
# self.vc.traverse(transform=self.populateViewTree)
637678

638679
def getViewContainingPointAndGenerateTestCondition(self, x, y):
639680
if DEBUG:

src/com/dtmilano/android/uiautomator/uiautomatorhelper.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class UiAutomatorHelper:
9090
TEST_CLASS = PACKAGE + '.test'
9191
TEST_RUNNER = 'com.dtmilano.android.uiautomatorhelper.UiAutomatorHelperTestRunner'
9292

93-
def __init__(self, adbclient, adb=None, localport=9999, remoteport=9999, hostname='localhost'):
93+
def __init__(self, adbclient, adb=None, localport=9987, remoteport=9987, hostname='localhost'):
9494
self.adbClient = adbclient
9595
''' The adb client (a.k.a. device) '''
9696
# instrumentation = self.adbClient.shell('pm list instrumentation %s' % self.PACKAGE)
@@ -118,6 +118,7 @@ def __init__(self, adbclient, adb=None, localport=9999, remoteport=9999, hostnam
118118
# self.thread.forceStop()
119119
# raise ex
120120
print('⚠️ CulebraTester2 server should have been started and port redirected.', file=sys.stderr)
121+
# TODO: localport should be in ApiClient configuration
121122
self.api_instance = culebratester_client.DefaultApi(culebratester_client.ApiClient())
122123

123124
def __connectSession(self):
@@ -187,7 +188,7 @@ def __httpCommand(self, url, params=None, method='GET'):
187188
# Device
188189
#
189190
def getDisplayRealSize(self):
190-
return self.__httpCommand('/Device/getDisplayRealSize')
191+
return self.api_instance.device_display_real_size_get()
191192

192193
#
193194
# UiAutomatorHelper internal commands
@@ -208,9 +209,12 @@ def click(self, **kwargs):
208209
if not (('x' in params and 'y' in params) or 'oid' in params):
209210
raise RuntimeError('click: (x, y) or oid must have a value')
210211
if 'oid' in params:
211-
return self.__httpCommand('/UiObject2/%d/click' % params['oid'])
212+
oid = int(params['oid'])
213+
return self.api_instance.ui_object2_oid_click_get(oid)
212214
else:
213-
return self.__httpCommand('/UiDevice/click', params)
215+
x = int(params['x'])
216+
y = int(params['y'])
217+
return self.api_instance.ui_device_click_get(x=x, y=y)
214218

215219
def dumpWindowHierarchy(self):
216220
return self.api_instance.ui_device_dump_window_hierarchy_get(format='JSON')
@@ -260,21 +264,15 @@ def takeScreenshot(self, scale=1.0, quality=90):
260264

261265
def waitForIdle(self, timeout):
262266
params = {'timeout': timeout}
263-
return self.__httpCommand('/UiDevice/waitForIdle')
264-
265-
#
266-
# UiObject
267-
#
268-
def setText(self, uiObject, text):
269-
# NOTICE: uiObject can receive UiObject or UiObject2
270-
element = uiObject.__class__.__name__
271-
_f = {'UiObject': '0x%x', 'UiObject2': '%d'}[element]
272-
params = {'text': text}
273-
return self.__httpCommand(('/%s/' + _f + '/setText') % (element, uiObject.oid), params)
267+
return self.api_instance.ui_device_wait_for_idle_get(**params)
274268

275269
#
276270
# UiObject2
277271
#
272+
def setText(self, oid, text):
273+
body = {'text': text}
274+
return self.api_instance.ui_object2_oid_set_text_post(body, oid)
275+
278276
def clickAndWait(self, uiObject2, eventCondition, timeout):
279277
params = {'eventCondition': eventCondition, 'timeout': timeout}
280278
return self.__httpCommand('/UiObject2/%d/clickAndWait' % uiObject2.oid, params)

src/com/dtmilano/android/viewclient.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -993,13 +993,15 @@ def touch(self, eventType=adbclient.DOWN_AND_UP, deltaX=0, deltaY=0):
993993
else:
994994
if self.uiAutomatorHelper:
995995
selector = self.obtainSelectorForView()
996+
if DEBUG_UI_AUTOMATOR_HELPER:
997+
print('using selector="%s"' % selector, file=sys.stderr)
996998
if selector:
997999
try:
998-
oid = self.uiAutomatorHelper.findObject(by_selector=selector)
1000+
object_ref = self.uiAutomatorHelper.findObject(by_selector=selector)
9991001
if DEBUG_UI_AUTOMATOR_HELPER:
1000-
print("oid=", oid, file=sys.stderr)
1002+
print("oid=%d" % object_ref.oid, file=sys.stderr)
10011003
print("ignoring click delta to click View as UiObject", file=sys.stderr)
1002-
oid.click()
1004+
self.uiAutomatorHelper.click(oid=object_ref.oid)
10031005
except RuntimeError as e:
10041006
print(e.message, file=sys.stderr)
10051007
print("UiObject click failed, using co-ordinates", file=sys.stderr)
@@ -3268,20 +3270,22 @@ def __treeFromWindowHierarchy(self, windowHierarchy):
32683270
@staticmethod
32693271
# python 3
32703272
# def __attributesFromWindowHierarchyChild(unique_id, child: WindowHierarchyChild):
3271-
def __attributesFromWindowHierarchyChild(unique_id, child):
3273+
def __attributesFromWindowHierarchyChild(child):
32723274
bounds = ((int(child.bounds[0]), int(child.bounds[1])), (int(child.bounds[2]), int(child.bounds[3])))
32733275
return {'index': child.index, 'text': child.text, 'resource-id': child.resource_id, 'class': child.clazz,
32743276
'package': child.package, 'content-desc': child.content_description, 'checkable': child.checkable,
3275-
'checked': None, 'clickable': child.clickable, 'enabled': child.enabled, 'focusable': child.focusable,
3276-
'focused': None, 'scrollable': child.scrollable, 'long-clickable': child.long_clickable,
3277+
'checked': False, # FIXME
3278+
'clickable': child.clickable, 'enabled': child.enabled, 'focusable': child.focusable,
3279+
'focused': False, # FIXME
3280+
'scrollable': child.scrollable, 'long-clickable': child.long_clickable,
32773281
'password': child.password, 'selected': child.selected, 'bounds': bounds,
3278-
'uniqueId': unique_id}
3282+
'uniqueId': child.unique_id}
32793283

32803284
def __processWindowHierarchyChild(self, node, idCount, version):
32813285
if node.id != 'hierarchy':
3286+
# NOTE: we are extending WindowHierarchyChild adding unique_id
32823287
node.unique_id = 'id/no_id/%d' % idCount
3283-
# FIXME: unique_id is not needed as a param
3284-
attributes = ViewClient.__attributesFromWindowHierarchyChild(node.unique_id, node)
3288+
attributes = ViewClient.__attributesFromWindowHierarchyChild(node)
32853289
view = View.factory(attributes, self.device, version=version, uiAutomatorHelper=self.uiAutomatorHelper)
32863290
self.views.append(view)
32873291
idCount += 1
@@ -3346,16 +3350,6 @@ def traverse(self, root="ROOT", indent="", transform=None, stream=sys.stdout):
33463350

33473351
return ViewClient.__traverse(root, indent, transform, stream)
33483352

3349-
# if not root:
3350-
# return
3351-
#
3352-
# s = transform(root)
3353-
# if s:
3354-
# print >>stream, "%s%s" % (indent, s)
3355-
#
3356-
# for ch in root.children:
3357-
# self.traverse(ch, indent=indent+" ", transform=transform, stream=stream)
3358-
33593353
@staticmethod
33603354
def __traverse(root, indent="", transform=View.__str__, stream=sys.stdout):
33613355
if not root:
@@ -4007,9 +4001,12 @@ def touch(self, x=-1, y=-1, selector=None):
40074001
if self.uiAutomatorHelper:
40084002
if selector:
40094003
if DEBUG_UI_AUTOMATOR_HELPER:
4010-
print("Touching View by selector=%s through UiAutomatorHelper" % (selector), file=sys.stderr)
4004+
print("Touching View by selector=%s through UiAutomatorHelper" % selector, file=sys.stderr)
40114005
# FIXME: is `selector` a `bySlector`?
4012-
self.uiAutomatorHelper.findObject(bySelector=selector).click()
4006+
object_ref = self.uiAutomatorHelper.findObject(by_selector=selector)
4007+
if DEBUG_UI_AUTOMATOR_HELPER:
4008+
print("♦️ object_ref=%s" % object_ref, file=sys.stderr)
4009+
self.uiAutomatorHelper.click(oid=object_ref.oid)
40134010
else:
40144011
if DEBUG_UI_AUTOMATOR_HELPER:
40154012
print("Touching (%d, %d) through UiAutomatorHelper" % (x, y), file=sys.stderr)
@@ -4886,7 +4883,10 @@ def main(*args, **kwargs):
48864883
ser = ['-s', '--serialno']
48874884
old = '%(failfast)'
48884885
new = ' %s s The serial number[s] to connect to or \'all\'\n%s' % (', '.join(ser), old)
4889-
unittest.TestProgram.USAGE = unittest.TestProgram.USAGE.replace(old, new)
4886+
try:
4887+
unittest.TestProgram.USAGE = unittest.TestProgram.USAGE.replace(old, new)
4888+
except AttributeError as ex:
4889+
print(ex, file=sys.stderr)
48904890
argsToRemove = []
48914891
i = 0
48924892
while i < len(sys.argv):

tools/culebra

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,29 +1322,29 @@ class CulebraTests(CulebraTestCase):
13221322
13231323
''' % (kwargs1, kwargs2, options), end=' ')
13241324

1325-
print('''\
1325+
print('''
13261326
def setUp(self):
13271327
super(CulebraTests, self).setUp()
13281328
''')
13291329

1330-
print('''\
1330+
print('''
13311331
def tearDown(self):
13321332
super(CulebraTests, self).tearDown()
13331333
''')
13341334

1335-
print('''\
1335+
print('''
13361336
def preconditions(self):
13371337
if not super(CulebraTests, self).preconditions():
13381338
return False
13391339
''', end=' ')
13401340

13411341
if options[CulebraOptions.INSTALL_APK]:
1342-
print('''\
1342+
print('''
13431343
if self.vc.installPackage(\"%s\") != 0:
13441344
return False
13451345
''' % (options[CulebraOptions.INSTALL_APK]), end=' ')
13461346

1347-
print('''\
1347+
print('''
13481348
return True
13491349
13501350
def %s(self):

0 commit comments

Comments
 (0)