最近在测试安卓的时候,经常会用到Monkey,Monkey作为安卓的基础工具,必须要到命令行去敲敲敲,做起来非常非常麻烦,于是我就想能不能利用学会的Python知识直接开发一个带有界面的安卓测试工具。
整个实现流程并不困难,一个界面填写数据,提交到后台,然后Python调用os模块,执行shell命令,然后就可以实现GUI端的Monkey了。
前段时间一直都在看web端的知识,我就想用web写一个界面,然后后台用Django执行,就可以很简单的完成这个工具了。
前端就是用Bootstrap来实现了,对我来说,写了这么久得博客,这块东西比较容易了,代码就不放了,bootstrap套套就出来了,界面图如下:
本来上面还有一个结束Monkey按钮的,被我拿掉了,拿掉的原因后面会说。
现在该有的东西都有了,后台实现起来也不难,获取前端给的参数,然后执行命令就行了,第一版用的是POST提交表单的方式执行,我发现了这么些问题:
用POST提交是最简单的了,我也最熟悉,不过存在上面的问题,总是很操蛋,于是我就改为使用Ajax异步提交数据,这样就不会导致执行Monkey的时候整个页面卡死的情况了。
页面没有修改,改了一个方法,jQuery的方法如下:
$(document).ready(function () {
$("#sub").click(function () {
$.post("/monkey/",
{
"package_name": $("#package_name").val(),
"event_count": $("#event_count").val(),
"log_path": $("#log_path").val(),
"log_level": $("#log_level").val(),
"delay": $("#delay").val(),
"sys_event": $("#sys_event").val(),
"touch_event": $("#touch_event").val(),
"zoom_event": $("#zoom_event").val(),
"finger_event": $("#finger_event").val(),
"ball_event": $("#ball_event").val(),
"nav_event": $("#nav_event").val(),
"switch_event": $("#switch_event").val()
}
)
})
});
使用这个方法实现,是解决了一些问题,执行Monkey的时候页面也不会卡了,也可以正常的获取Monkey进程,不过依然存在这么些问题:
第一个问题倒是还好,占用两个结束两次就行了,倒也还能接受,第二个问题就无法接受了,NND不能直观的开始和结束,我写这个工具就没有什么意义了,我专门试了直接调用python执行停止命令是正常的。代码如下:
$(document).ready(function () {
$("#stop_monkey").click(function () {
$.post("/monkey/stop_monkey/",
{
"monkey_id": $("#monkey_id").val()
})
})
});
@csrf_exempt
def stop_monkey(request):
data = {
"msg": "Monkey已经停止"
}
if request.method == 'POST':
command = 'adb shell kill {}'.format(request.POST['monkey_id'])
cm.run_monkey(command)
return JsonResponse(data, safe=False)
return JsonResponse(data, safe=False)
若读者知道,请发邮件告诉我到底是为什么这里不会执行,我丢了print在这上面是可以正常打印的。
web端的实验失败了,效果并没有达到我的预期,只能换一种方案了,用桌面的GUI端来实现这个方案试试,然而GUI端又有很多选择,PYQT据说是最好的,但是在我电脑数据抹掉之前我有装过这个,虽然最后装成功了,但是整个装的过程太操蛋了,有点心理阴影,wxpython也是一个不错的选择,不过还要装第三方包,有点折腾,最终我选择了最基本的Tkinter来处理GUI。那么问题来了,Tkinter没用过,要重新去熟悉这个包。
《Tkinter介绍文档》。。。麻蛋又是全英文的,果然英语还是必须要学好,我大概看了两小时,把一些主要的点扫了一遍,就开始写了,代码丑也是没办法的,时间少,没办法去慢慢想设计模式。
写完的界面是这样的:
整个GUI的代码有100多行,有点长,布局是用Grid写的,所有的代码我就不贴了,我把按钮的方法贴一下:
connect_text = Button(master, text='获取设备号',
command=lambda: cm.set_text(device_name, ad.get_devices()))
up_pknm_conf = Button(master, text='修改包名',
command=lambda: cm.update_conf(status, 'package_name', cm.get_text(package_name)))
up_log_path_conf = Button(master, text='修改日志地址',
command=lambda: cm.update_conf(status, 'log_path', cm.get_text(log_path)))
up_log_level_conf = Button(master, text='修改日志等级',
command=lambda: cm.update_conf(status, 'log_level', cm.get_text(log_level)))
up_count_conf = Button(master, text='修改测试数量',
command=lambda: cm.update_conf(status, 'count', cm.get_text(count)))
up_delay_conf = Button(master, text='修改延时',
command=lambda: cm.update_conf(status, 'delay', cm.get_text(delay)))
up_touch_conf = Button(master, text='修改触摸事件',
command=lambda: cm.update_conf(status, 'touch', cm.get_text(touch)))
up_motion_conf = Button(master, text='修改手势事件',
command=lambda: cm.update_conf(status, 'motion', cm.get_text(motion)))
up_pinch_conf = Button(master, text='修改缩放事件',
command=lambda: cm.update_conf(status, 'pinch', cm.get_text(pinch)))
up_trackball_conf = Button(master, text='修改轨迹球事件',
command=lambda: cm.update_conf(status, 'trackball', cm.get_text(trackball)))
up_screen_conf = Button(master, text='修改屏幕事件',
command=lambda: cm.update_conf(status, 'screen', cm.get_text(screen)))
up_nav_conf = Button(master, text='修改导航事件',
command=lambda: cm.update_conf(status, 'nav', cm.get_text(nav)))
up_major_conf = Button(master, text='修改主要事件',
command=lambda: cm.update_conf(status, 'major', cm.get_text(major)))
up_system_conf = Button(master, text='修改系统事件',
command=lambda: cm.update_conf(status, 'system', cm.get_text(system)))
up_app_conf = Button(master, text='修改切屏事件',
command=lambda: cm.update_conf(status, 'app', cm.get_text(app)))
up_keyboard_conf = Button(master, text='修改键盘事件',
command=lambda: cm.update_conf(status, 'keyboard', cm.get_text(keyboard)))
up_anyevents_conf = Button(master, text='修改其他事件',
command=lambda: cm.update_conf(status, 'anyevents', cm.get_text(anyevents)))
cat_monkey_pid = Button(master, text='显示Monkey进程',
command=lambda: cm.set_text(monkey_pid, ad.get_monkey_id()))
start_monkey = Button(master, text='开始Monkey',
command=lambda: mk.merge_command(cm.get_text(log_path), *cm.collect(*ENTRYLIST)))
stop_monkey = Button(master, text='结束Monkey',
command=lambda: ad.stop_monkey(status))
get_monkey = Button(master, text='获取Monkey',
command=lambda: cm.set_text(status, mk.get_monkey(cm.get_text(log_path), *cm.collect(*ENTRYLIST))))
值得一提的是,在Button组件中的command执行函数是不能带参数的,否则就会报错,不知道Tkinter的作者是怎么想的,不带参数的函数能有几个啊。。。。。
我用了一个折中的方法,用lambda函数来处理这一块,这样就可以加上参数了。
其他方法我使用了三个函数,common来处理普通的方法,adb专门处理操作shell的方法,而monkey专门处理和Monkey有关的方法,这样设计以后工具就很容易拓展了,可以加上性能监控的方法等高级功能。
简单的贴一下方法。
class Adb:
def __init__(self):
self.mk = monkey.Monkey()
self.cm = common.Common()
def get_devices(self):
""" 获取设备名称 :return:设备名称 """
a = os.popen('adb devices')
devices = a.readlines()
spl = devices[1].find(' ')
devices_name = devices[1][:spl]
if devices_name == '':
return "请确认设备是否连接"
else:
return devices_name
def get_monkey_id(self):
""" 获取monkey进程ID :return:monkey进程id """
if self.get_devices():
a = os.popen('adb shell ps | grep monkey')
try:
monkey_id = a.read().split(' ')[5]
print "进程为{} 的Monkey已停止".format(monkey_id)
except Exception:
monkey_id = ''
return monkey_id
else:
print "设备未连接"
def stop_monkey(self, entry):
""" 停止monkey :param monkey_id:monkey的进程号 :return:None """
monkey_id = self.get_monkey_id()
if monkey_id != '请确认你的设备是否连接':
os.system('adb shell kill {}'.format(monkey_id))
self.cm.set_text(entry, "进程为{} 的Monkey已停止".format(monkey_id))
else:
print "设备未连接"
def merge_command(self, path, *args):
""" 组合命令,Monkey使用 :param path:日志地址 :param args:Monkey命令中的其他参数 :return:None """
member = ' '.join(args)
command = 'adb shell monkey {} > {}'.format(member, path)
self.run(command)
def get_monkey(self, path, *args):
""" 获取Monkey命令 :param path: 日志地址 :param args: Monkey命令中的其他参数 :return: """
if self.check_total(*args):
member = ' '.join(args)
command = 'adb shell monkey {} > {}'.format(member, path)
return command
else:
return '事件百分数大于100%,请修正后再获取'
def check_total(self, *args):
""" 检查事件百分比是否合规,大于100则返回False :param args:传入的事件列表 :return:True """
rst = []
all_list = self.deal_list(*args)
for x in all_list:
x = x.split(' ')[1]
rst.append(int(x))
num = sum(rst)
if num > 100:
return False
else:
return True
def deal_list(self, *args):
""" 处理列表数据,返回只有事件的列表 :param args:传入的列表数据 :return:list """
rst = []
for x in range(1, 12):
rst.append(args[x])
print rst
return rst
用GUI端来处理也会存在这么个问题,点击开始的时候回出现整个GUI卡住的情况,必须要等Monkey执行完毕才能正常操作其他东西,因此我想了一个折中的方案,加一个状态显示的文本框,加一个生成Monkey命令的按钮,这样生成命令然后在GUI上就可以使用其他命令了,加上多进程或者多线程应该能解决这个问题,但是现在我对多线程多进程几乎是0了解,等了解了之后再来完善这部分的功能。
就实现了这么一点点功能,代码量差不多也要300行,不得不说真是一个操蛋的工程。
最后,我仍然没有解决这个操蛋的问题,等研究完多线程之后再来重新改善这段代码。
最后的最后,虽然功能还不齐全,还是放上Github,怎么说也是一个可以试想功能的工具吧。
最近博客上线了,读者可以关注我的博客。
博客地址:www.wengyb.com