游戏测试 【测试工具】apk 安装器 2.0

特尔斯特 · 2022年01月05日 · 最后由 觞释 回复于 2022年01月06日 · 4427 次阅读

场景:
1.随着游戏的上线经常会面临安装不同渠道包到不同模拟器或手机上的操作,每次手动操作感觉很麻烦。
2.在手机上测试有 BUG 的时候开发期望能能把完整日志拉出来排查 BUG,每次都 adb 手动操纵效率很低。
基于这两点把之前做的东西升级了一下,让它能给不同设备安装不同 apk,同时还可以直接拉出完整日志

界面:

代码:
DeviceManager.py 用来处理整个界面逻辑
InstallItem.py 用来处理每个 device 的安装、销毁、拉日志等逻辑

# -*- coding: utf-8 -*-
import os
import tkinter as tk
from tkinter import *  # 导入 Tkinter 库
import multiprocessing
import threading
from InstallItem import InstallItem


class DeviceManager():

    def __init__(self):
        try:
            cmd = 'adb connect 127.0.0.1:7555'  # 木木模拟器比较常用,所以写在程序里
            os.system(cmd)
        except EXCEPTION as e:
            print(e)

        self.root = Tk()
        self.root.geometry('1050x300')
        self.root.title("DeviceManager")
        self.log_path=StringVar()
        self.log_path.set("D:\\DeviceLog")
        self.apk_path = StringVar()
        self.apk_path.set("D:\\")
        #连接模拟器
        self.init_port =IntVar()

        #nox模拟器  62001  第二个开始 62025+index
        self.init_port.set(62001)
        self.increase_num = IntVar()
        self.increase_num.set(1)



        self.apk_name = StringVar()
        self.apk_name.set("请输入apk完整包名")
        self.select_device_list =[]
        self.device_list = self.get_device_list()
        self.cb_list =[]
        self.pool = multiprocessing.Pool(processes=6)
        self.button_list=[]
        self.button_pull_log_list=[]
        self.install_item_list=[]


    def get_log_path(self):#得到log存放路径
        return self.log_path.get()

    def get_apk_path(self):#得到apk更目录路径
        return self.apk_path.get()


    def draw_log_path(self):
        label_log=tk.Label(self.root, text="日志保存路径:")
        label_log.grid(row=0,column=1,sticky='w')

        entry_log_path = Entry(self.root, width=50, textvariable=self.log_path)
        entry_log_path.grid(row=0,column=2,sticky='w')

        button_open_log = Button(self.root, text="打开", command=lambda: self.open_file(entry_log_path.get()))
        button_open_log.grid(row=0 ,column=4)

        #刷新adb
        button_refresh = Button(self.root,text="刷新adb",bg="LightSteelBlue",command=lambda:self.refresh_adb())
        button_refresh.grid(row=0,column=7)
        #关闭adb
        button_close_adb = Button(self.root,text="关闭adb",command=self.close_adb)
        button_close_adb.grid(row=0,column=8)
        #连接模拟器
        button_connect_device = Button(self.root, text="连接模拟器", command=self.start_connect_device_thread)
        button_connect_device.grid(row=1, column=12)

    def draw_device_connect(self):
        label_device_start_port = tk.Label(self.root,text="起始端口:")
        label_device_start_port.grid(row=1,column=7,sticky='w')

        entry_device_start_port = Entry(self.root, width=5, textvariable=self.init_port)
        entry_device_start_port.grid(row=1,column=8,sticky='w')

        label_device_num = tk.Label(self.root, text="连接数量:")#模拟器多开时想要连接的数量
        label_device_num.grid(row=1, column=9, sticky='w')

        entry_device_num = Entry(self.root, width=5, textvariable=self.increase_num)
        entry_device_num.grid(row=1, column=10, sticky='w')



    def close_adb(self):
        print("关闭 adb")
        try:
            cmd = 'adb kill-server'
            os.system(cmd)
        except EXCEPTION as e:
            print(e)


    def refresh_adb(self):
        print("刷新adb")
        try:
            cmd = 'adb devices'
            os.system(cmd)
        except EXCEPTION as e:
            print(e)

        old_device_list =self.device_list
        new_device_list =self.get_device_list()
        #old-new 的差集用old_new表示
        old_new  = set(old_device_list)-set(new_device_list)
        print("old-new:",list(old_new))
        for item in self.install_item_list:
            if item.device in list(old_new):
                item.destroy()

        #new-old 的差集用new_old表示
        new_old = set(new_device_list)-set(old_device_list)
        start_draw_index =  len(set(old_device_list)&set(new_device_list))
        self.draw_installer_item(list(new_old),start_draw_index)
        self.device_list=new_device_list

    def connect_device(self,index):
        try:
            port =self.init_port.get()+index
            print(port)
            cmd = 'adb connect 127.0.0.1:'+str(port)
            print(cmd)
            os.system(cmd)
        except:
            print("连接第%d个模拟器有问题" % index)

    def start_connect_device_thread(self):
        print(self.increase_num.get())
        for i in range(self.increase_num.get()):
            t= threading.Thread(target=self.connect_device,args=(i,))
            t.start()


    def draw_apk_path(self):
        label_apk_path = tk.Label(self.root, text="APK根目录:")
        label_apk_path.grid(row=1, column=1, sticky='w')

        entry_apk_path = Entry(self.root, width=50, textvariable=self.apk_path)
        entry_apk_path.grid(row=1, column=2, sticky='w')

        button_open_apkPath = Button(self.root, text="打开", command=lambda : self.open_file(entry_apk_path.get()))
        button_open_apkPath.grid(row=1, column=4)




    def draw_installer_item(self,device_list,old_device_num=0):
        for index,device in enumerate(device_list):
            start_row=index+old_device_num+2
            install_item=InstallItem(self.root,device,start_row,self)
            self.install_item_list.append(install_item)
            self.root.grid_columnconfigure((0,3,5), minsize=20)



    def mainloop(self):
        self.draw_log_path()
        self.draw_device_connect()
        self.draw_apk_path()
        self.draw_installer_item(self.device_list)
        self.root.mainloop()

    def get_device_list(self):#得到设备列表
        os.system("adb devices")
        res = os.popen("adb devices").readlines()
        device_list = [sub.split('\t')[0] for sub in res[1:-1]]
       # device_list = [sub for sub in res[1:-1]]
        return device_list

    def open_file(self,path):#打开文件
        os.system("explorer "+path)




if __name__=='__main__':
    multiprocessing.freeze_support()#不加这句打包成exe后会出现死循环
    apkInstaller= DeviceManager()
    apkInstaller.mainloop()

# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import *  # 导入 Tkinter 库
import os
import time
import threading


class InstallItem:

    def __init__(self,root,device,index,DeviceManager):
        self.DeviceManager = DeviceManager
        self.device  = device
        self.entry_default = StringVar()
        self.entry_default.set("请输入apk完整包名")

        self.root=root
        self.lab_device = tk.Label(self.root, text=device)
        self.lab_device.grid(row=index,column=1,stick='w')

        self.entry_apk = Entry(self.root, width=65, textvariable=self.entry_default)
        self.entry_apk.grid(row=index,column=2,stick='w')

        self.button_install = Button(self.root, text="安装",command=self.thread_install_apk)
        self.button_install.grid(row=index,column=4,stick='w')

        self.button_pull_log = Button(self.root,text="拉日志",command=self.pull_log)
        self.button_pull_log.grid(row=index,column=6,stick='w')

    def destroy(self):
        self.button_install.destroy()
        self.button_pull_log.destroy()
        self.entry_apk.destroy()
        self.lab_device.destroy()

    def pull_log(self):
        print("device",self.device)
        if "emulator" in self.device or "127.0.0.1" in self.device:#如果是模拟器
            device_log_path = "sdcard/Android/data/*****/files/Logs"
        else:# 如果是手机
            device_log_path = "/storage/emulated/0/Android/data/******/files/Logs"
        if ':' in self.device:
            device_name = self.device.replace(':', '-')  # 这里用来处理模拟器多开,冒号在路径名中无法使用,所以替换一下
        else:
            device_name = self.device
        computer_copy_path = self.DeviceManager.get_log_path() +'\\' + device_name+"\\" + self.set_file_name()  # 本地的路径,存在指定的文件夹再加上设备名作为区分
        print(computer_copy_path)
        if not os.path.exists(computer_copy_path):
            os.makedirs(computer_copy_path)

        cmd = 'adb -s {0} pull {1} {2}'.format(self.device, device_log_path, computer_copy_path)
        # print(cmd)
        os.system(cmd)


    def set_file_name(self):#设置文件名
         now=time.strftime("%Y-%m-%d-%H-%M-%S",time.localtime(time.time()))
         return now

    def install_apk(self):
        # 安装游戏
        print(self.entry_apk.get())
        cmd = "adb -s " + self.device + " install -r " + self.DeviceManager.get_apk_path() + "\\" + self.entry_apk.get()
        print("installing ", cmd)
        os.system(cmd)

    def thread_install_apk(self):#用多线程来安装,不然点击安装后会卡住主线程,无法实现多apk同时安装
        t = threading.Thread(target=self.install_apk, )
        t.start()

** 一些细节 **
【刷新 adb】

当接入新的设备或者拔掉旧的设备时需要进行刷新操作,如果直接执行 adb devices 命令来进行操作的话,就会把已经正在执行安装操作的条目刷新成空白,所以这里的刷新逻辑是这样:
1.取刷新前的设备列表为 old_device_list ,取执行 adb devices 后得到的列表为 new_device_list
2.用 old_new 表示 old-new 的差集,这个差集代表已经去掉的设备
3.用 new_old 表示 new_old 的差集,这个差集代表新增的设备
4.先销毁已经去掉的设备再增加新增的设备到界面上
5.新增设备的 index 从 old_device_list 和 new_device_list 的交集数量往上加

def refresh_adb(self):
    print("刷新adb")
    try:
        cmd = 'adb devices'
        os.system(cmd)
    except EXCEPTION as e:
        print(e)

    old_device_list =self.device_list
    new_device_list =self.get_device_list()
    #old-new 的差集用old_new表示
    old_new  = set(old_device_list)-set(new_device_list)
    print("old-new:",list(old_new))
    for item in self.install_item_list:
        if item.device in list(old_new):
            item.destroy()

    #new-old 的差集用new_old表示
    new_old = set(new_device_list)-set(old_device_list)
    start_draw_index =  len(set(old_device_list)&set(new_device_list))
    self.draw_installer_item(list(new_old),start_draw_index)
    self.device_list=new_device_list

【连接模拟器】

如果数量=1 ,就直接连接这个模拟器
如果数量>1 ,以所填的端口为基础依次往上加
比如 nox 模拟器多开时,第二个端口是 62025 ,第三个是 62026 ,如果要连接这两个只需要起始端口填 62025,连接数量填 2 即可。
用多线程操作避免阻塞。

def connect_device(self,index):
      try:
          port =self.init_port.get()+index
          print(port)
          cmd = 'adb connect 127.0.0.1:'+str(port)
          print(cmd)
          os.system(cmd)
      except:
          print("连接第%d个模拟器有问题" % index)

  def start_connect_device_thread(self):
      print(self.increase_num.get())
      for i in range(self.increase_num.get()):
          t= threading.Thread(target=self.connect_device,args=(i,))
          t.start()

【拉日志】


拉出的日志按设备名分不同文件夹,再按操作时间分子文件夹

总结
多线程安装明显没有手动安装快,但是比较方便,如果不是很急我可以挂着这边安装另一边同时做其他事情。
拉日志的功能明显比自己用 adb 命令操作便捷了很多,帮我节省了很多时间。

共收到 4 条回复 时间 点赞

弄得不错,收藏备用~

nice~ 下午尝试了下,批量安装还挺好用。但是目前拉日志功能是从 android 本地的安装的 app 下面拉?不是实时抓取 logcat 日志

觞释 回复

开发的打印日志都在这个文件里,相当于 unity 的 console 日志,他们改 Bug 需要这个,你也可以根据你们项目的需要改成 logcat 日志

特尔斯特 回复

好的 👍 👍 ,我已经改成我们项目(测试 apk)日志保存的路径了。还是弄得挺好的,感谢大佬分享!
当前使用还遇到两个小问题:

  1. 拉取指定 sdcard/Android/data/.../files/ 目录下很多日志(总共大于 1 百 M)的情况下,工具会卡死;(这里我本地再加了一个逻辑,如果遇到卡死的设备,先清掉 device_log_path 下的文件再拉取)
  2. 如果先运行工具(脚本),再启动模拟器,刷新 adb 模拟器刷不出来(这个很好解决,在 refresh_adb 函数里加下连模拟器的 cmd 就 OK 了)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册