背景
在追求测试自动化的道路上,我想让脚本具有一定的自适应能力,能自主识别出可操作(可点击、可长按、可编辑)的控件,并能自主控制脚本运行逻辑(依据策略自适应)。
能识别出可操作控件则是这一目标的第一步。Airtest使用的POCO具有获取控件属性的作用,同时Airtest封装Opencv用于图像识别。因此这是一个实现该目标较理想的平台。
逻辑
Android界面无论采用何种布局,都可以理解为是以图层的原理,逐层叠加出来的。所以利用POCO获取子节点的方法,从根节点开始依次遍历其所有子节点以及子节点的子节点(孙节点),并保留可操作(可点击、可长按、可编辑)的节点。如此即可获得想要的控件列表。
实现
首先利用Airtest的截图功能G.DEVICE.snapshot
获取当前屏幕截图(已启动需遍历的app)。然后通过offspring
获取所有子节点,为了加速遍历速度,我们使用freeze()
将界面元素冻结,以加速子节点的遍历。
拿到子节点后通过获取其属性判断是否具有可操作性,在poco中获取属性方式很简单,通过attr即可。
def getUseableAttrs(self, item):
attrsArray = []
if(item.attr('touchable')):
attrsArray.append('touchable')
if(item.attr('touchable')):
attrsArray.append('touchable')
if(item.attr('editalbe')):
attrsArray.append('editalbe')
return attrsArray
def parseFreezeLayout(self, elementsArray):
sleep(5.0)
freeze = self.poco.freeze()
screen = G.DEVICE.snapshot(os.path.join(self.datapath, "snap.jpg"))
for item in freeze().offspring():
attrs = self.getUseableAttrs(item)
if(attrs):
elementsArray.append(item)
return index
这样我们就可以快速获得当前屏幕上可操作的控件元素。
问题
遍历获取到的控件仅仅是屏幕区域内的控件,屏幕外的控件是无法获取的。换句话讲上述方面仅能获取到当前显示区域内的控件,一旦需要滑动屏幕才可显示的内容则一概获取不到。而屏幕滑动之后(无论滑动距离有多小),界面即刷新,而offspring又会从根节点出发重新依次遍历当前显示区域内的所有控件。如果能去掉重复的控件,那么我们就可以通过滑动获取到APP当前页面的所有可操作控件了。
首先看看通过attr是否可获取类似Android R.id这样全局唯一的ID。梦想很美好,现实很残酷。在某些APP中似乎并没有对每一个可操作控件定义不一样的ID,这导致无法通过ID进行控件的区分。那么控件上的文字描述呢?比如按钮上显示的确定或取消字样。这也仅对部分app有效,因为随着UX设计越来越人性化,某些功能的按钮多以图形来表示,如:添加,撤销,搜索等等。至此我们已无法单纯的利用控件基本属性作为去重的判断依据。
那么人在页面刷新后又是如何判断那些控件是新出现的,那些是之前已有的呢?这里不深入探讨视觉神经的工作原理,我们假设一个不识字的小孩可以怎样识别出新增控件的。答案很简单,依靠图形对比,在我们假设的这个小孩眼里只会有“绿色背景带有加号图形的方块”或“红色背景上面有些文字的圆圈”,并以此作为显示区域前后两次刷新对比的依据,“绿色背景带有加号图形的方块”在之前已出现过,“红色的家伙”之前没有。
有了这样的假设,我们就可以以图像识别为抓手,对原有代码进行升级改造了。
改进
思路很简单,之前的逻辑已可以识别出当前显示区域所有可操作控件,那么我们可以对每个控件做区域截图(也就是仅把控件截图保存)。并将截图与其它控件的截图做图像对比,若图像一致,则认为是同一个控件,否则为新增控件。
def saveCropScreen(self, screen, x, y, width, height, page, index):
cropscreen = aircv.crop_image(screen, [x,y,x+width,y+height])
path = self.datapath
for dir in page.split('-'):
path = os.path.join(path, dir)
if(not os.path.exists(path)):
os.makedirs(path)
path = os.path.join(path, '{}.jpg'.format(index))
aircv.imwrite(path, cropscreen, ST.SNAPSHOT_QUALITY, ST.IMAGE_MAXSIZE)
return path
而图像识别则是通过计算两张图片的哈希值,然后通过计算出汉明距离,来判断2张图像是否一致。一共采用了3种哈希算法,这里就不深入探讨彼此的优劣了。
import cv2
import numpy as np
import collections
from airtest.core.api import *
class UIConfidence:
@staticmethod
def flatten(x):
def iselement(e):
return not(isinstance(e, collections.Iterable) and not isinstance(e, str))
for el in x:
if(iselement(el)):
yield el
else:
yield from UIConfidence.flatten(el)
@staticmethod
def getAllHashValue(imgres):
return [UIConfidence.aHash(imgres), UIConfidence.dHash(imgres), UIConfidence.pHash(imgres)]
# 均值哈希算法
@staticmethod
def aHash(imgres):
img = cv2.imread(imgres)
# 缩放为8*8
img = cv2.resize(img, (8, 8), interpolation=cv2.INTER_CUBIC)
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# s为像素和初值为0,hash_str为hash值初值为''
s = 0
hash_str = ''
# 遍历累加求像素和
for i in range(8):
for j in range(8):
s = s + gray[i, j]
# 求平均灰度
avg = s / 64
# 灰度大于平均值为1相反为0生成图片的hash值
for i in range(8):
for j in range(8):
if gray[i, j] > avg:
hash_str = hash_str + '1'
else:
hash_str = hash_str + '0'
return hash_str
# 差值感知算法
@staticmethod
def dHash(imgres):
img = cv2.imread(imgres)
# 缩放8*9
img = cv2.resize(img, (9, 8),interpolation=cv2.INTER_CUBIC)
# 转换灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hash_str = ''
# 每行前一个像素大于后一个像素为1,相反为0,生成哈希
for i in range(8):
for j in range(8):
if gray[i, j] > gray[i, j + 1]:
hash_str = hash_str + '1'
else:
hash_str = hash_str + '0'
return hash_str
# 感知哈希算法(pHash)
@staticmethod
def pHash(imgres):
img = cv2.imread(imgres,0)
img = cv2.resize(img,(64,64), interpolation=cv2.INTER_CUBIC)
h,w=img.shape[:2]
vis0 = np.zeros((h,w),np.float32)
vis0[:h,:w] = img
vis1 = cv2.dct(cv2.dct(vis0))
vis1.resize(32,32)
img_list = list(UIConfidence.flatten(vis1.tolist()))
avg = sum(img_list)*1./len(img_list)
avg_list = ['0' if i<avg else '1' for i in img_list]
return ''.join(['%x' % int(''.join(avg_list[x:x+4]),2) for x in range(0,32*32,4)])
# Hash值对比
@staticmethod
def cmpHash(hash1, hash2):
n = 0
# hash长度不同则返回-1代表传参出错
if len(hash1)!=len(hash2):
return -1
# 遍历判断
n = sum([ch1 != ch2 for ch1, ch2 in zip(hash1, hash2)])
print(n)
return n
遗留
虽然引入了opencv这样的大佬,但实际运行的效果还是差强人意,在对比背景颜色接近的控件时,大概率识别为同一个控件。
如上图中桌面壁纸以及显示亮度均会被识别为与移动网络很相似(汉明距离小于2)。由此可见仅依赖简单的图像识别,似乎还不能接近完美的工作,后面我将继续探索如何更准确识别控件的方法 。