一、背景

最开始工作是在一家公司做手机测试,什么的都不懂,adb 也不知道,反正只要点点点就行了,然后跑 monkey 也是用的 monkey 图形化界面工具。当时觉得写这工具的人好厉害啊。
然后在另外一家公司做手机测试,发现也有这种 monkey 图形化界面工具。
后来进了新公司,闲来无事也写一个玩玩。
工具没有给公司的同事使用,个人认为不利于成长。

二、实现

环境:win7 + python:3.4
框架:tkinter
打包工具:pyinstaller(把 py 文件打包成 exe 可执行文件)

三、效果图


四、代码

没有使用 MVC 模式,逻辑代码和页面没有分离,没有进行参数化配置修改。再加上一些 bug😂

# coding:utf-8
from tkinter import *
from datetime import datetime
import subprocess
import time
import threading
import os


class MonkeyTest:
    def __init__(self):
        self.root = Tk()
        self.root.title('0easy')
        self.line = 1  # 用于统计monkey日志的行数
        self.flagWhile = 1  # 循环判断标志位

    def createLogin(self):
        self.frameLogin = Frame()
        self.frameLogin.pack()
        rowTitle, rowUsername, rowPassword, rowLogin, rowTip = 1, 2, 3, 4, 5
        padxTitle, padyTitle = 150, 20
        padxLabel, padyLabel = 10, 10
        padxEntry = 20
        # 标题
        labelTitle = Label(self.frameLogin, text='Monkey测试工具', font=('微软雅黑', 20))
        labelTitle.grid(row=rowTitle, column=0, columnspan=2, padx=padxTitle, pady=padyTitle)
        # 账号
        labelUsername = Label(self.frameLogin, text='账号:')
        labelUsername.grid(row=rowUsername, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.entryUsername = Entry(self.frameLogin)
        self.entryUsername.grid(row=rowUsername, column=1, sticky=W, padx=padxEntry)
        # 密码
        labelPassword = Label(self.frameLogin, text='密码:')
        labelPassword.grid(row=rowPassword, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.entryPassword = Entry(self.frameLogin, show='*')
        self.entryPassword.grid(row=rowPassword, column=1, sticky=W, padx=padxEntry)
        # 登录
        btnLogin = Button(self.frameLogin, text='  登录  ', command=self.login)
        btnLogin.grid(row=rowLogin, column=0, sticky=E, padx=10)
        # 退出
        btnQuit = Button(self.frameLogin, text='  退出  ', command=self.loginOut)
        btnQuit.grid(row=rowLogin, column=1, padx=10)
        # 提示语
        self.labelReminder = Label(self.frameLogin, text='')
        self.labelReminder.grid(row=rowTip, column=0, columnspan=2, sticky=W + E)
        self.root.mainloop()

    def login(self):
        username = self.entryUsername.get().strip()
        password = self.entryPassword.get().strip()
        if username == 'monkey' and password == '123456':
            self.labelReminder['text'] = '登录成功'
            self.frameLogin.destroy()   # 销毁登录页面
            self.createRun()    # 创建monkey运行界面
        else:
            self.entryPassword.delete(0, END)   # 清空密码框
            self.labelReminder['text'] = '账号或者密码错误'
            self.labelReminder['fg'] = 'red'

    def loginOut(self):
        self.root.destroy()

    def createRun(self):
        self.frame = Frame()
        self.frame.pack()
        rowTitle, rowDevice, rowEvent, rowSeed, rowIsCrash = 1, 2, 3, 4, 5
        rowIsANR, rowPackage, rowRemind, rowExecute, rowInfo = 6, 7, 8, 9, 10
        padxTitle, padyTitle = 10, 20
        padxLabel, padyLabel = 10, 10
        padxEntry, padyEntry = 20, 20
        padxButton, padyButton = 10, 20
        padxListbox, padyListbox = 10, 10
        # keywords = ['// CRASH:', 'ANR in ']
        # 标题
        labelTitle = Label(self.frame, text='Monkey测试工具', font=('微软雅黑', 20))
        labelTitle.grid(row=rowTitle, columnspan=2, padx=padxTitle, pady=padyTitle)
        # 设备ID
        btnDevice = Button(self.frame, text='获取设备ID', command=self.getDeivceID)
        btnDevice.grid(row=rowDevice, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.entryDevice = Entry(self.frame)
        self.entryDevice.grid(row=rowDevice, column=1, sticky=W, padx=padxEntry)
        # 事件次数
        labelEvent = Label(self.frame, text='事件次数:')
        labelEvent.grid(row=rowEvent, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.entryEvent = Entry(self.frame)
        self.entryEvent.grid(row=rowEvent, column=1, sticky=W, padx=padxEntry)
        # seed值
        labelSeed = Label(self.frame, text='seed值:')
        labelSeed.grid(row=rowSeed, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.entrySeed = Entry(self.frame)
        self.entrySeed.grid(row=rowSeed, column=1, sticky=W, padx=padxEntry)
        # crash
        labelCrash = Label(self.frame, text='出现crash是否继续:')
        labelCrash.grid(row=rowIsCrash, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.vCrash = IntVar()
        btnCrash = Checkbutton(self.frame, variable=self.vCrash)
        btnCrash.select()  # 默认勾选
        btnCrash.grid(row=rowIsCrash, column=1, sticky=W, padx=padxButton)
        # ANR
        labelANR = Label(self.frame, text='出现ANR是否继续:')
        labelANR.grid(row=rowIsANR, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.vANR = IntVar()
        btnANR = Checkbutton(self.frame, variable=self.vANR)
        btnANR.select()  # 默认勾选
        btnANR.grid(row=rowIsANR, column=1, sticky=W, padx=padxButton)
        # 包名
        labelPkg = Label(self.frame, text='请输入包名:')
        labelPkg.grid(row=rowPackage, column=0, sticky=E, padx=padxLabel, pady=padyLabel)
        self.entryPkg = Entry(self.frame)
        self.entryPkg.grid(row=rowPackage, column=1, sticky=W, padx=padxEntry)
        self.entryPkg.insert(0, 'com.android.settings') # 默认settings包
        # 提示信息
        self.labelRemind = Label(self.frame, fg='red', font=('微软雅黑'))
        self.labelRemind.grid(row=rowRemind, column=0, columnspan=2, sticky=E + W, padx=padxLabel, pady=padyLabel)
        # 执行
        self.btnExecute = Button(self.frame, text='  执行  ', command=self.execute)
        self.btnExecute.grid(row=rowExecute, column=0, sticky=E, padx=padxButton)
        # 停止
        btnStop = Button(self.frame, text='  停止  ', command=self.stop)
        btnStop.grid(row=rowExecute, column=1, sticky=W, padx=padxButton)
        # 展示信息
        self.listInfo = Listbox(self.frame, width=70, height=10)
        self.listInfo.grid(row=rowInfo, column=0, columnspan=2, padx=padxListbox, pady=padyListbox)
        self.root.mainloop()

    def getDeivceID(self):
        '''
        获取设备id
        :return:
        '''
        cmd = 'adb devices >c:/devices.log'
        # cmd2 = 'adb devices'
        # proc = subprocess.Popen(cmd2, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
        # result = proc.communicate()[0].split(b'\n')[1].split(b'\t')[0]
        os.system(cmd)
        with open('c:/devices.log','r') as f:
            result = f.readlines()[1].split('\t')[0]
        self.entryDevice.delete(0, END) # 清空设备id编辑框
        self.entryDevice.insert(0, result)  # 填写设备id

    def execute(self):
        # 设备id不为空,运行脚本时其实没有限定设备id,此项主要用来检测是否能运行adb
        if len(self.entryDevice.get().strip()) == 0:
            self.labelRemind['text'] = '请输入设备ID'
            return
        # 事件次数为数字
        self.event = self.entryEvent.get().strip()
        if not self.event.isdigit():
            self.labelRemind['text'] = '请输入正确的次数'
            return
        # seed值为数字
        self.seed = self.entrySeed.get().strip()
        if not self.seed.isdigit():
            self.labelRemind['text'] = '请输入正确的seed'
            return
        # 是否忽略crash
        self.crash = '--ignore-crashes'
        if self.vCrash.get() == 0:
            self.crash = ''
        # 是否忽略ANR
        self.anr = '--ignore-timeouts'
        if self.vANR.get() == 0:
            self.anr = ''
        # 包名不为空
        self.pkg = self.entryPkg.get().strip()
        if len(self.pkg) == 0:
            self.labelRemind['text'] = '需要填写测试的包名'
            return
        # 按钮置灰
        self.btnExecute['state'] = 'disabled'
        lTime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        self.listInfo.insert(END, lTime + '  Monkey测试开始。。。')
        # 重置提示语
        self.labelRemind['text'] = ''
        # 重置行数
        self.line = 1
        # 重置循环标志位
        self.flagWhile = 1
        # 运行monkey语句
        t1 = threading.Thread(target=self.executeCmd)
        t1.start()
        # 显示monkey日志
        t2 = threading.Thread(target=self.startReadLog)
        t2.start()

    def executeCmd(self):
        cmd = 'adb shell monkey -p ' + self.pkg + ' ' + self.crash + ' ' + self.anr + ' --monitor-native-crashes --throttle 1000 -s ' + self.seed + ' -v -v -v ' + self.event + ' >c:/monkey.log'
        os.system(cmd)

    def startReadLog(self):
        '''
        5秒读一次monkey的log,
        第一次等5秒是让adb命令先运行,防止先读log而又没有monkey.log文件,导致异常
        :return:
        '''
        while self.flagWhile > 0:   # 标志位初始值大于0,进入循环
            time.sleep(5)
            self.readLog()

    def stop(self):
        self.btnExecute['state'] = 'active' # 激活执行按钮
        self.flagWhile = -1 # 标志位小于0,跳出循环

    def readLog(self):
        count = 1
        f = open('c:/monkey.log', 'r', encoding='utf8')
        for i in f.readlines():
            count = count + 1   # 记录每次读取log的行数,每读一行,计数+1
            # count > line 防止重复insret
            if count > self.line and ('// CRASH:' in i or 'ANR in ' in i):
                lTime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                self.listInfo.insert(END, lTime + '  ' + i)
            # 存在'// Monkey finished',就判断monkey结束
            elif '// Monkey finished' in i:
                lTime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                self.listInfo.insert(END, lTime + '  Monkey测试结束!')
                self.stop()
        # print(self.line, count)
        self.line = count


m = MonkeyTest()
m.createLogin()

五、打包

1.先安装一个 pywin32.exe 文件

下载地址:https://sourceforge.net/projects/pywin32/files/pywin32/Build%20220/
自己下载对应的 exe 文件版本

2.在 cmd 命令窗口输入:pip install pyinstaller

安装成功后,会在对应的 python/script 目录下生成一个 pyinstaller.exe 文件

3.重新启动 cmd 命令窗口输入:pyinstaller -F c:/monkey.py


--noconsole 去掉 dos 窗口
-i c:/logo.ico 加上 logo

六、使用

1.首先配置好 adb 环境,打开工具,登录账号:monkey,密码:123456,登录,代码的 login() 方法可以修改账号密码
2.获取设备 ID,输入对应的信息,然后点击【运行】
3.日志保存在 c:/monkey.log

七、问题

1.为什么我打包的文件运行一直有 dos 黑框?
打包命令后面加上:--noconsole 或者百度其他方法。
尽管用了--noconsole,在点击【获取设备 ID】和【执行】的时候还是弹 dos 窗口。
目前还没找到解决办法,有解决的办法的同学请在下面留言告诉我,感谢。

2.为什么执行 adb 命令用 os.popen 或者 os.system,不用 subprocess.popen?
打包时去掉了 dos 窗口,用 subprocess.poppen 执行无效。

3.为什么匿名?
使用社区的时间比较少,有时候遇到一些问题,不会及时回复,会显得比较高冷。匿名了,你就不会知道是谁这么高冷了。


↙↙↙阅读原文可查看相关链接,并与作者交流