├── Performance.py ├── Poco使用手册(持续编写中).docx ├── README.md ├── TestCase ├── TC_101.py ├── TC_102.py ├── TC_103.py ├── TC_104.py └── __init__.py ├── __init__.py ├── config.ini ├── core ├── MultiAdb.py ├── RunTestCase.py ├── __init__.py ├── index.py └── tmp.txt ├── start.py ├── template ├── __init__.py ├── app.css ├── app.js ├── header.html ├── highcharts.js ├── performance.html ├── 框架.gif └── 赞赏码.png └── tools ├── Config.py ├── Email.py ├── Excel.py ├── File.py ├── Init_MiniCap.py ├── Json.py ├── ScreenOFF.py ├── Screencap.py ├── TimeOut.py └── __init__.py /Performance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | from DreamMultiDevices.start import * 5 | from DreamMultiDevices.core.MultiAdb import MultiAdb as Madb 6 | import time 7 | import threading 8 | import multiprocessing 9 | import traceback 10 | from DreamMultiDevices.tools.Excel import * 11 | from DreamMultiDevices.tools.Json import * 12 | from DreamMultiDevices.tools.Screencap import * 13 | from multiprocessing import Process,Value 14 | import json 15 | from collections import deque 16 | 17 | ''' 18 | 性能数据进程,首先根据storage_by_excel参数创建excel或json文件,再定期塞数据进去,最后统计各项的最大最小平均值。 19 | ''' 20 | def enter_performance(madb,flag,start,storage_by_excel=True,adb_log=True): 21 | print("设备{}进入enter_performance方法".format(madb.get_mdevice())) 22 | wb="" 23 | jsonfilepath="" 24 | if adb_log: 25 | logfile=madb.create_adb_log(time.localtime()) 26 | if storage_by_excel: 27 | #创表 28 | filepath, sheet, wb = create_log_excel(time.localtime(), madb.get_nickname()) 29 | #塞数据 30 | collect_data(madb,flag,storage_by_excel,sheet=sheet) 31 | #计算各平均值最大值最小值等并塞数据 32 | avglist,maxlist,minlist=calculate(sheet) 33 | record_to_excel(sheet,avglist,color=(230, 230 ,250)) 34 | record_to_excel(sheet,maxlist,color=(193, 255, 193)) 35 | record_to_excel(sheet,minlist,color=(240, 255 ,240)) 36 | wb.save() 37 | else: 38 | #创建json文件 39 | jsonfilepath = create_log_json(time.localtime(),madb.get_nickname()) 40 | print("创建json文件成功:{}".format(jsonfilepath)) 41 | collect_data(madb,flag,storage_by_excel,jsonfilepath=jsonfilepath) 42 | calculate_by_json(jsonfilepath) 43 | nowtime = time.strftime("%H%M%S", start) 44 | reportpath = os.path.join(os.getcwd(), "Report") 45 | filename = reportpath + "\\" + madb.get_nickname() + "_" + str(nowtime) + ".html" 46 | print("要操作的文件名为:", filename) 47 | if storage_by_excel: 48 | reportPlusPath = EditReport(filename,storage_by_excel,avglist, maxlist, minlist,wb=wb) 49 | else: 50 | reportPlusPath = EditReport(filename,storage_by_excel, jsonfilepath=jsonfilepath) 51 | if adb_log: 52 | f=open(logfile,"w") 53 | f.close() 54 | print("设备{}生成报告:{}完毕".format(madb.get_mdevice(), reportPlusPath)) 55 | 56 | 57 | #接受设备madb类对象、excel的sheet对象、共享内存flag、默认延时一小时 58 | def collect_data(madb,flag,storage_by_excel,sheet="",jsonfilepath="",timeout=60): 59 | 60 | starttime=time.time() 61 | dequelist = deque([]) 62 | n=0 63 | totalcpu,maxcpu=madb.get_totalcpu() 64 | SurfaceViewFlag=madb.get_isSurfaceView() 65 | try: 66 | while True: 67 | #当执行一小时或flag为1时,跳出。 68 | # Performance.py可以单独执行,检查apk的性能,此时要把下面的flag.value注掉。因为这个是用于进程通信的,单独执行性能时没有必要。 69 | n+=1 70 | #为了确保截取统计数据不出错,至少打印3行 71 | if (time.time() - starttime > timeout) or (flag.value==1 and n>3): 72 | break 73 | total=allocated= used=free=totalcpu= allocatedcpu="" 74 | 75 | #开启n个线程,每个线程去调用Madb类里的方法,获取adb的性能数据 76 | get_allocated_memory = MyThread(madb.get_allocated_memory,args=()) 77 | get_memory_info = MyThread(madb.get_memoryinfo,args=()) 78 | get_total_cpu = MyThread(madb.get_totalcpu,args=() ) 79 | get_allocated_cpu = MyThread(madb.get_allocated_cpu,args=() ) 80 | get_png=MyThread(GetScreen,args=(time.time(), madb.get_mdevice(), "performance")) 81 | #为了避免重复场景不渲染导致的fps统计为0,fps取过去一秒内的最大值(约8次)。 82 | Threadlist=[] 83 | for i in range(8): 84 | get_fps = MyThread(madb.get_fps, args=(SurfaceViewFlag,)) 85 | Threadlist.append(get_fps) 86 | #批量执行 87 | get_allocated_memory.start() 88 | get_memory_info.start() 89 | get_total_cpu.start() 90 | get_allocated_cpu.start() 91 | get_png.start() 92 | 93 | for p in Threadlist: 94 | p.start() 95 | fpstmp = p.get_result() 96 | if len(dequelist) < 9 : 97 | dequelist.append(fpstmp) 98 | else: 99 | dequelist.popleft() 100 | dequelist.append(fpstmp) 101 | if "N/a" in dequelist: 102 | fps="N/a" 103 | else: 104 | fps=max(dequelist) 105 | #批量获得结果 106 | allocated=get_allocated_memory.get_result() 107 | total,free,used=get_memory_info.get_result() 108 | totalcpu,unused_maxcpu=get_total_cpu.get_result() 109 | allocatedcpu=get_allocated_cpu.get_result() 110 | png=get_png.get_result() 111 | #批量回收线程 112 | get_allocated_memory.join() 113 | get_memory_info.join() 114 | get_total_cpu.join() 115 | get_allocated_cpu.join() 116 | get_png.join() 117 | for p in Threadlist: 118 | p.join() 119 | #将性能数据填充到一个数组里,塞进excel 120 | nowtime = time.localtime() 121 | inputtime = str(time.strftime("%H:%M:%S", nowtime)) 122 | if storage_by_excel: 123 | if allocatedcpu=="N/a": 124 | list = ["'" + inputtime, total, "N/a", used, free,"'"+format(totalcpu / maxcpu, "0.2f") + "%","N/a", fps] 125 | else: 126 | list = ["'" + inputtime, total, allocated, used, free, "'"+format(totalcpu / maxcpu,"0.2f")+"%", "'"+format(float(allocatedcpu)/maxcpu,"0.2f")+"%", fps] 127 | record_to_excel(sheet,list,png=png) 128 | # 将性能数据填充到一个数组里,塞进json 129 | else: 130 | if fps=="N/a": 131 | fps=0 132 | if allocatedcpu == "N/a": 133 | list = [inputtime, total, allocated, used, free, float(format(float(totalcpu)/maxcpu,".2f")),0, fps, png] 134 | else: 135 | list =[inputtime, total, allocated, used, free, float(format(float(totalcpu)/maxcpu,".2f")), float(format(float(allocatedcpu)/maxcpu,"0.2f")), fps,png] 136 | record_to_json(jsonfilepath,list) 137 | 138 | except Exception as e: 139 | print(madb.get_mdevice()+ traceback.format_exc()) 140 | 141 | 142 | #线程类,用来获取线程函数的返回值 143 | class MyThread(threading.Thread): 144 | def __init__(self, func, args=()): 145 | super(MyThread, self).__init__() 146 | self.func = func 147 | self.args = args 148 | 149 | def run(self): 150 | self.result = self.func(*self.args) 151 | 152 | def get_result(self): 153 | threading.Thread.join(self) # 等待线程执行完毕 154 | try: 155 | return self.result 156 | except Exception as e: 157 | print( traceback.format_exc()) 158 | return None 159 | 160 | '''nowjsonfile 161 | 小T写的。编辑由BR生成的html文件,将功能与性能整合成一个html。 162 | ''' 163 | def EditReport(origin_html_path,storage_by_excelavglist,avglist="",maxlist="",minlist="",wb="",jsonfilepath=""): 164 | #取项目的绝对路径 165 | rootPath = os.path.abspath(os.path.dirname(inspect.getfile(inspect.currentframe())) + os.path.sep + ".") 166 | templatePath= os.path.join(rootPath, "template") 167 | # 读取报告文件 168 | f = open(origin_html_path, "r+", encoding="UTF-8") 169 | fr = f.read() 170 | f.close() 171 | 172 | # 拼接CSS样式 173 | fr_prev, fr_next = GetHtmlContent(fr, "", True, 1) 174 | css = open(templatePath+"\\app.css", "r+", encoding='UTF-8') 175 | css_str = css.read() 176 | css.close() 177 | fr = fr_prev + "\n" + css_str + "\n" + fr_next 178 | 179 | # 拼接头部按钮 180 | fr_prev, fr_next = GetHtmlContent(fr, "", True, 1) 199 | highchartspath=templatePath+"\\highcharts.js" 200 | highcharts_str="" 201 | js = open(templatePath+"\\app.js", "r+", encoding='UTF-8') 202 | js_str = js.read() 203 | js.close() 204 | fr = fr_prev + "\n" + highcharts_str+"\n"+js_str + "\n" + fr_next 205 | Time_series=TotalMemory=AllocatedMemory=UsedMemory=FreeMemory=TotalCPU=AllocatedCPU=FPS=PNG="" 206 | Max_AllocatedMemory=Min_AllocatedMemory=Avg_AllocatedMemory=Max_AllocatedCPU=Min_AllocatedCPU=Avg_AllocatedCPU=Max_FPS=Min_FPS=Avg_FPS=0 207 | data_count="" 208 | if storage_by_excelavglist: 209 | # 嵌入性能测试结果到excel 210 | sheet = wb.sheets("Sheet1") 211 | Time_series=get_json(sheet,"Time") 212 | TotalMemory=get_json(sheet,"TotalMemory(MB)") 213 | AllocatedMemory=get_json(sheet,"AllocatedMemory(MB)") 214 | UsedMemory=get_json(sheet,"UsedMemory(MB)") 215 | FreeMemory=get_json(sheet,"FreeMemory(MB)") 216 | TotalCPU=get_json(sheet,"TotalCPU") 217 | AllocatedCPU=get_json(sheet,"AllocatedCPU") 218 | FPS=get_json(sheet,"FPS") 219 | FPSlist=json.loads(FPS) 220 | FPSlist=FPSlist["FPS"] 221 | 222 | PNG=get_json(sheet,"PNGAddress") 223 | Max_AllocatedMemory=maxlist[2] 224 | Min_AllocatedMemory=minlist[2] 225 | Avg_AllocatedMemory=avglist[2] 226 | Max_AllocatedCPU=maxlist[6] 227 | Min_AllocatedCPU=minlist[6] 228 | Avg_AllocatedCPU=avglist[6] 229 | Max_FPS=maxlist[7] 230 | Min_FPS=minlist[7] 231 | Avg_FPS=avglist[7] 232 | data_count = {"Max_AllocatedMemory": [Max_AllocatedMemory], "Min_AllocatedMemory": [Min_AllocatedMemory], 233 | "Avg_AllocatedMemory": [Avg_AllocatedMemory], "Max_AllocatedCPU": [Max_AllocatedCPU], 234 | "Min_AllocatedCPU": [Min_AllocatedCPU], "Avg_AllocatedCPU": [Avg_AllocatedCPU], 235 | "Max_FPS": [Max_FPS], 236 | "Min_FPS": [Min_FPS], "Avg_FPS": [Avg_FPS]} 237 | data_count = "\n" + "var data_count=" + json.dumps(data_count) 238 | # 嵌入性能测试结果到json 239 | else: 240 | jsonfilepath=(jsonfilepath) 241 | jsondata = open(jsonfilepath, "r+", encoding='UTF-8') 242 | jsondata = json.load(jsondata) 243 | Time_series=json.dumps({"Time":jsondata["Time_series"]}) 244 | TotalMemory=json.dumps({"TotalMemory(MB)":jsondata["TotalMemory"]}) 245 | AllocatedMemory=json.dumps({"AllocatedMemory(MB)":jsondata["AllocatedMemory"]}) 246 | UsedMemory=json.dumps({"UsedMemory(MB)":jsondata["UsedMemory"]}) 247 | FreeMemory=json.dumps({"FreeMemory(MB)":jsondata["FreeMemory"]}) 248 | TotalCPU=json.dumps({"TotalCPU":jsondata["TotalCPU"]}) 249 | AllocatedCPU=json.dumps({"AllocatedCPU":jsondata["AllocatedCPU"]}) 250 | FPS=json.dumps({"FPS":jsondata["FPS"]}) 251 | PNG=json.dumps({"PNGAddress":jsondata["PNGAddress"]}) 252 | data_count=json.dumps(jsondata["data_count"]) 253 | data_count=data_count[1:-1] 254 | data_count = "\n" + "var data_count=" + data_count 255 | #data_series和data_count会被嵌入到html里,作为highcharts的数据源。 256 | data_series = Time_series + "\n" + "var TotalMemory=" + TotalMemory + "\n" + "var AllocatedMemory=" + AllocatedMemory + "\n" + "var UsedMemory=" + UsedMemory + "\n" + "var FreeMemory=" \ 257 | + FreeMemory + "\n" + "var TotalCPU=" + TotalCPU + "\n" + "var AllocatedCPU=" + AllocatedCPU + "\n" + "var FPS=" + FPS + "\n" + "var PNG=" + PNG + "\n" 258 | fr_prev, fr_next = GetHtmlContent(fr, "// tag data", False, 1) 259 | fr = fr_prev + data_series + "\n" + data_count + "\n" + fr_next 260 | 261 | 262 | # 写入文件 263 | newPath = origin_html_path.replace(".html", "_PLUS.html") 264 | f = open( newPath, "w", encoding="UTF-8") 265 | f.write(fr) 266 | f.close() 267 | 268 | return newPath 269 | 270 | # 小T写的。获取需要插入性能图表的节点,reverse参数决定了从左数还是从右数,然后将html拆成2分,方便填标签。很有趣的思路。 271 | def GetHtmlContent(content, tag, reverse=False, round_num=1): 272 | fr_r_index = "" 273 | if reverse: 274 | fr_r_index = content.rfind(tag) 275 | else: 276 | fr_r_index = content.find(tag) 277 | for i in range(1, round_num): 278 | if reverse: 279 | fr_r_index = content.rfind(tag, 0, fr_r_index) 280 | else: 281 | fr_r_index = content.find(tag, fr_r_index + 1) 282 | fr_prev = content[0:fr_r_index] 283 | fr_next = content[fr_r_index:len(content)] 284 | return fr_prev, fr_next 285 | 286 | #调试代码,单独执行的话,flag默认为1。 287 | if __name__ == "__main__": 288 | devicesList = Madb().getdevices() 289 | print("最终的devicesList=",devicesList) 290 | 291 | start=time.localtime() 292 | ''' 293 | madb = Madb(devicesList[0]) 294 | flag = Value('i', 0) 295 | enter_performance (madb, flag, start,) 296 | ''' 297 | print("启动进程池") 298 | flag = Value('i', 0) 299 | Processlist=[] 300 | for i in range(len(devicesList)): 301 | madb = Madb(devicesList[i]) 302 | if madb.get_androidversion()<5: 303 | print("设备{}的安卓版本低于5,不支持。".format(madb.get_mdevice())) 304 | break 305 | print("{}开始进行性能测试".format(madb.get_mdevice())) 306 | # 根据设备列表去循环创建进程,对每个进程调用下面的enter_processing方法。 307 | p = Process(target=enter_performance, args=(madb, flag, start,)) 308 | Processlist.append(p) 309 | for p in Processlist: 310 | p.start() 311 | for p in Processlist: 312 | p.join() 313 | 314 | 315 | print("性能测试结束") -------------------------------------------------------------------------------- /Poco使用手册(持续编写中).docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saint228/DreamMultiDevices/c6b8ec96586f2957856bfa442411bb6ec2ccf0f6/Poco使用手册(持续编写中).docx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DreamMultiDevices 2 | 基于Python/Airtest/Unittest的自动化多设备测试 3 | 4 | 欢迎加入QQ群:739857090 一起讨论。 5 | 6 | 效果动画: 7 | ![img](https://github.com/saint228/DreamMultiDevices/blob/master/template/%E6%A1%86%E6%9E%B6.gif) 8 | 9 | 10 | 11 | 1.本框架由无声 and Treize编写,落落 and 人生如梦 测试。 12 | 须事先安装如下环境:python3.6以上、airtest、pocoui、BeautifulReport、unittest、xlwings(如果你想使用Excel储存性能数据的话)。 13 | 安装方法 14 | 15 | pip install DreamMultiDevices 16 | 17 | ******************************************************************************************************** 18 | 19 | 首先,非常重要的一点。先确认你电脑里的adb环境!!! 20 | 本框架使用airtest的adb作为默认adb,目前该adb采用40版本,电脑里其他应用如果使用adb且非该版本,会产生冲突。 21 | 可以复制\Lib\site-packages\airtest\core\android\static\adb 目录下的所有文件,替换掉本地的其他adb版本。 22 | 23 | 其次,python的安装目录不要有空格,类似 C:\Program Files\python 这样的目录因为中间有空格,在执行adb命令时会报错。如果不愿意重装python,可以手动修改框架代码里的各个ADB变量,为其指定合理的路径。 24 | 25 | ******************************************************************************************************** 26 | 27 | 28 | 第一次运行前需要手动修改config.ini,里面的包名和包路径是必填项,也支持在运行过程中通过set方式修改。 29 | 30 | 31 | madb.set_packagename("")#填待测apk的包名 32 | madb.set_packagepath("")#填待测apk在硬盘上的绝对路径 33 | madb.set_TestCasePath("")#填本地测试用例目录的绝对路径 34 | 35 | 程序入口 36 | 37 | from DreamMultiDevices.start import * 38 | 39 | if __name__ == "__main__": 40 | start() 41 | 42 | 2./config.ini。整个项目的配置文件。 43 | 44 | [config] 45 | packName填写待测试包名,str; 46 | activityname 填写测试包的入口类名,留空的话,Madb也会在需要时自动计算,可能会多花一点时间。 47 | deviceslist填写测试设备名,以逗号分隔,(PS:如不想使用配置,留空即可,则读取当前所有已连接的设备)元组; 48 | apkpath填写待测试应用的安装地址,须使用绝对路径,str; 49 | testcase填写期望测试的用例id,须是一个int元组,RunTestCase函数会比对testcase元组与TestCase目录下的TC_*.py文件,在元组内的用例才会被测试; 50 | needclickinstall和needclickstartapp 填写True或False,设置是否需要安装点击或运行点击,不为True则不执行; 51 | timeout_of_per_action填写全局sleep时间,int; 52 | timeout_of_startapp填写安装app等待时间,int(已废弃); 53 | iteration填写权限脚本循环次数,int。 54 | 55 | skip_pushapk2devices填写“是否跳过pushapk2devices函数”的标志,1为True 0为False,以下同。 56 | auto_delete_package填写“是否在安装包过程中自动删除设备里的旧包”的标志。 57 | auto_install_package填写“是否需要重新安装新包”的标志。 58 | skip_check_of_install填写“是否要跳过安装过程中的自动输入权限检查部分”的标志。 59 | skip_check_of_startapp填写“是否要跳过安装过程中的自动输入权限检查部分”的标志。 60 | skip_performance填写“是否要跳过性能监控部分”的标志。 61 | storage_by_excel填写“是否使用excel存储性能数据”的标志,填1为使用excel,填0为使用json。 62 | 63 | [TestCaseforDevice] 64 | 按设备配置执行用例,不填则默认全部 65 | 66 | [Email] 67 | mail_host 邮件host地址 68 | mail_user 邮件账户名 69 | mail_pass 邮件密码 70 | sender 邮件发件人 71 | receivers 邮件收件人 72 | 73 | 3./start.py。可以使用pycharm运行,也可以被其他方法调用运行。 74 | 75 | 4./core/index index是整个框架的索引,负责根据devices分发进程,让每个设备各自去运行enterprocess()函数。该函数里进行设备的初始化,在确保初始化成功、安装包成功的情况下,启动待测试apk并调用RunTestCase函数,进行测试用例的分发。 76 | 当needPerformance为True时,还会同步运行enter_performance()函数,对设备进行性能监控并同步记录在excel文件里。 77 | 78 | 5./core/MultiADB Madb类,集成了各个与device有关的方法。 79 | 80 | 6./tools/PushApk2Devices 负责安装apk到设备上,会先判断待测包是否已存在,存在则删除并重装,重装时会自动调用inputThread进行安装权限的点击。这里的代码需要用户自行完成,具体写法请参考inputThread里已经提供的示范代码。 81 | 82 | 7.MultiAdb.py里的StartApp函数 。StartApp负责启动apk,然后会进行应用开启权限的点击,此处代码也需要用户自行完成。 83 | 84 | 8./core/RunTestCase。RunTestCase是运行测试用例的分发函数,读取之前配置表上的testcase元组并与TestCase目录下的文件进行比对,一致的则列入测试范围。 85 | 86 | 9./TestCase目录。本目录下放置所有的待测试用例。用例须以TC_开头,用例采用标准的unittest格式。每条用例的执行结果会是一个suite对象,并在全部执行完以后,聚合到RunTestCase的report对象上。可以通过set_TestCasePath("")方法重置。 87 | 88 | 10./TestCast/TC_******.py 单个用例的执行文件,由用户自行编写,最后须符合unittest格式。特别要说明一点,BeautifulReport的默认截图方法是异常时触发语法糖截图。使用时略有不便,我新增了GetScreen()函数,可以在任意需要时实时截图,优先采用MiniCap方式截图。 89 | 90 | 11./Report/Html报告。RunTestCase使用BeautifulReport库进行报告输出。会在调用文件所在的目录生成一个Report目录,输出内容在Report目录下,以设备名和时间命名,相关截图则存储在Report/Screen目录下。 91 | 92 | 12.新增了Performance.py,用以处理adbdump抓取的性能数据,同时在tools目录下新增了Excel.py。用来处理表格。限于adb的效率,大概4、5秒能抓一次,抓取时会同步截图。 93 | 划重点:性能测试不支持模拟器,所有的手机模拟器都是x86架构,而99%的安卓手机都是arm架构,adb在不同的架构下抓取dump的返回值不同,所以我写的adb抓性能的代码在模拟器上运行会出错。这不是bug,也不会修。(2019/8/13实现) 94 | 95 | 13.完成性能测试后,会在/Report目录下重新生成xxx_PLUS.html的报告,是在BeautifulReport基础上拼接了性能部分的页面显示。 96 | 97 | 14.@Timeout.timeout()装饰器是用来限制每个用例执行时间上限的,因为有时候碰到用例执行被block,占用大量时间导致脚本运行不畅。挂了这个装饰器以后,可以限定每个用例的最长时间,超时会抛异常并开始执行下个用例。 98 | 99 | 15.config.ini里新加了2个字段:adb_log以及keywords。用来控制是否监听adblog以及监听的过滤字段。 100 | 101 | ------------------------------------------- 102 | 微信打赏 103 | 104 | 以前我一直对打赏这种行为不屑一顾,但真正得收到社区成员千翻百计找到我的打赏码给我打赏的时候还是很开心,感觉工作得到大家的认可,真的很开心。我也有时候会打赏别人,让激动的心情有了发泄的出口。 请不要打赏太多,知道了你们的心意就好了。我将会用收到的money通通拿来去楼下自动售货机买饮料。^_^ 105 | ![image](template/赞赏码.png) 106 | -------------------------------------------------------------------------------- /TestCase/TC_101.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import unittest 5 | from DreamMultiDevices.tools import Screencap 6 | from airtest.core.api import * 7 | from poco.drivers.unity3d import UnityPoco 8 | from tools.TimeOut import Timeout 9 | 10 | _print = print 11 | def print(*args, **kwargs): 12 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 13 | 14 | def Main(devices): 15 | print("{}进入unittest".format(devices)) 16 | class TC101(unittest.TestCase): 17 | u'''测试用例101的集合''' 18 | 19 | @classmethod 20 | def setUpClass(self): 21 | u''' 这里放需要在所有用例执行前执行的部分''' 22 | pass 23 | 24 | def setUp(self): 25 | u'''这里放需要在每条用例前执行的部分''' 26 | print("我是setUp,在每条用例之前执行") 27 | 28 | @Timeout.timeout(30) 29 | def test_01_of_101(self): 30 | u'''用例test_01_of_101的操作步骤''' 31 | # 每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 32 | poco = UnityPoco() 33 | print("我是TC101的test_01_of_101方法") 34 | t = 1 35 | self.assertEquals(1, t) 36 | 37 | def test_02_of_101(self): 38 | u'''用例test_02_of_101的操作步骤''' 39 | # 每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 40 | poco = UnityPoco() 41 | time.sleep(5) 42 | print("我是TC102的test_02_of_101方法") 43 | Screencap.GetScreen(time.time(), devices, "test_02_of_101的描述") 44 | t = 1 45 | self.assertEquals(2, t) 46 | 47 | 48 | def tearDown(self): 49 | u'''这里放需要在每条用例后执行的部分''' 50 | print("我是tearDown,在每条用例之后执行") 51 | 52 | @classmethod 53 | def tearDownClass(self): 54 | u'''这里放需要在所有用例后执行的部分''' 55 | pass 56 | 57 | srcSuite = unittest.makeSuite(TC101) 58 | return srcSuite -------------------------------------------------------------------------------- /TestCase/TC_102.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import unittest 5 | from DreamMultiDevices.tools import Screencap 6 | from airtest.core.api import * 7 | from poco.drivers.unity3d import UnityPoco 8 | from tools.TimeOut import Timeout 9 | 10 | _print = print 11 | def print(*args, **kwargs): 12 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 13 | 14 | def Main(devices): 15 | class TC102(unittest.TestCase): 16 | u'''测试用例102的集合''' 17 | 18 | @classmethod 19 | def setUpClass(self): 20 | u''' 这里放需要在所有用例执行前执行的部分''' 21 | pass 22 | 23 | def setUp(self): 24 | u'''这里放需要在每条用例前执行的部分''' 25 | print("我是setUp,在每条用例之前执行") 26 | 27 | @Timeout.timeout(30) 28 | def test_01_of_102(self): 29 | u'''用例test_01_of_102的操作步骤''' 30 | # 每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 31 | print("我是TC102的test_01_of_102方法") 32 | poco = UnityPoco() 33 | time.sleep(20) 34 | t = 1 35 | self.assertEquals(1, t) 36 | 37 | def test_02_of_102(self): 38 | u'''用例test_02_of_102的操作步骤''' 39 | #每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 40 | print("我是TC102的test_02_of_102方法") 41 | poco = UnityPoco() 42 | Screencap.GetScreen(time.time(), devices, "test_02_of_102的描述") 43 | t = 1 44 | self.assertEquals(2, t) 45 | 46 | 47 | 48 | def tearDown(self): 49 | u'''这里放需要在每条用例后执行的部分''' 50 | print("我是tearDown,在每条用例之后执行") 51 | 52 | @classmethod 53 | def tearDownClass(self): 54 | u'''这里放需要在所有用例后执行的部分''' 55 | pass 56 | 57 | srcSuite = unittest.makeSuite(TC102) 58 | return srcSuite -------------------------------------------------------------------------------- /TestCase/TC_103.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import unittest 5 | from DreamMultiDevices.tools import Screencap 6 | from airtest.core.api import * 7 | from poco.drivers.unity3d import UnityPoco 8 | from tools.TimeOut import Timeout 9 | 10 | _print = print 11 | def print(*args, **kwargs): 12 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 13 | 14 | def Main(devices): 15 | class TC103(unittest.TestCase): 16 | u'''测试用例102的集合''' 17 | 18 | @classmethod 19 | def setUpClass(self): 20 | u''' 这里放需要在所有用例执行前执行的部分''' 21 | pass 22 | 23 | def setUp(self): 24 | u'''这里放需要在每条用例前执行的部分''' 25 | print("我是setUp,在每条用例之前执行") 26 | 27 | @Timeout.timeout(30) 28 | def test_01_of_103(self): 29 | u'''用例test_01_of_103的操作步骤''' 30 | # 每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 31 | print("我是TC102的test_01_of_103方法") 32 | poco = UnityPoco() 33 | t = 1 34 | self.assertEquals(1, t) 35 | 36 | def test_02_of_103(self): 37 | u'''用例test_02_of_103的操作步骤''' 38 | #每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 39 | print("我是TC102的test_02_of_103方法") 40 | poco = UnityPoco() 41 | Screencap.GetScreen(time.time(), devices, "test_02_of_103的描述") 42 | t = 1 43 | self.assertEquals(2, t) 44 | 45 | 46 | 47 | def tearDown(self): 48 | u'''这里放需要在每条用例后执行的部分''' 49 | print("我是tearDown,在每条用例之后执行") 50 | 51 | @classmethod 52 | def tearDownClass(self): 53 | u'''这里放需要在所有用例后执行的部分''' 54 | pass 55 | 56 | srcSuite = unittest.makeSuite(TC103) 57 | return srcSuite -------------------------------------------------------------------------------- /TestCase/TC_104.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import unittest 5 | from DreamMultiDevices.tools import Screencap 6 | from airtest.core.api import * 7 | from poco.drivers.unity3d import UnityPoco 8 | from tools.TimeOut import Timeout 9 | 10 | _print = print 11 | def print(*args, **kwargs): 12 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 13 | 14 | def Main(devices): 15 | class TC104(unittest.TestCase): 16 | u'''测试用例102的集合''' 17 | 18 | @classmethod 19 | def setUpClass(self): 20 | u''' 这里放需要在所有用例执行前执行的部分''' 21 | pass 22 | 23 | def setUp(self): 24 | u'''这里放需要在每条用例前执行的部分''' 25 | print("我是setUp,在每条用例之前执行") 26 | 27 | @Timeout.timeout(30) 28 | def test_01_of_104(self): 29 | u'''用例test_01_of_104的操作步骤''' 30 | # 每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 31 | print("我是TC102的test_01_of_104方法") 32 | poco = UnityPoco() 33 | t = 1 34 | self.assertEquals(1, t) 35 | 36 | def test_02_of_104(self): 37 | u'''用例test_02_of_104的操作步骤''' 38 | #每个函数里分别实例poco,否则容易出现pocoserver无限重启的情况 39 | print("我是TC102的test_02_of_104方法") 40 | poco = UnityPoco() 41 | Screencap.GetScreen(time.time(), devices, "test_02_of_104的描述") 42 | t = 1 43 | self.assertEquals(2, t) 44 | 45 | 46 | 47 | def tearDown(self): 48 | u'''这里放需要在每条用例后执行的部分''' 49 | print("我是tearDown,在每条用例之后执行") 50 | 51 | @classmethod 52 | def tearDownClass(self): 53 | u'''这里放需要在所有用例后执行的部分''' 54 | pass 55 | 56 | srcSuite = unittest.makeSuite(TC104) 57 | return srcSuite -------------------------------------------------------------------------------- /TestCase/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import os,inspect 5 | 6 | #自动引入当前文件夹下所有py文件 7 | from DreamMultiDevices.tools import File 8 | 9 | Path = os.path.abspath(os.path.dirname(inspect.getfile(inspect.currentframe())) + os.path.sep + ".") 10 | pyList = File.GetPyList(Path) 11 | 12 | __all__ = pyList 13 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saint228/DreamMultiDevices/c6b8ec96586f2957856bfa442411bb6ec2ccf0f6/__init__.py -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | [config] 3 | packname =com.toutiao.kaililong.xjsj 4 | #packname =com.galasports.nba 5 | #packname =com.huawei.android.thememanager 6 | activityname = 7 | deviceslist = 8 | apkpath =E:\TestSVN\RO3D\apk\tt_xjsj_124382_042602_sy.apk 9 | testcasepath = 10 | testcase = 100,101,102,103,104 11 | timeout_of_per_action = 1 12 | timeout_of_startapp = 30 13 | iteration = 20 14 | 15 | 16 | #0=False 1=True 17 | skip_pushapk2devices= 1 18 | auto_delete_package=0 19 | auto_install_package=0 20 | skip_check_of_install = 1 21 | skip_check_of_startapp = 1 22 | skip_performance = 0 23 | #select "1" to use excel to save data and select "0" to use json 24 | storage_by_excel=1 25 | adb_log=1 26 | keywords=Unity 27 | screenoff =1 28 | isSurfaceView=1 29 | 30 | [TestCaseforDevice] 31 | 62001 = 32 | 62025 = 101,102,104 33 | 34 | [Email] 35 | mail_host="smtp.XXX.com" 36 | mail_user="XXXX" 37 | mail_pass="XXXXXX" 38 | sender = 'sender@qq.com' 39 | receivers = ['xx@qq.com'] 40 | 41 | -------------------------------------------------------------------------------- /core/MultiAdb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import os,inspect 5 | import sys 6 | import threading 7 | import queue 8 | from DreamMultiDevices.core import RunTestCase 9 | from DreamMultiDevices.tools import Config 10 | from airtest.core.api import * 11 | from airtest.core.error import * 12 | from poco.drivers.android.uiautomation import AndroidUiautomationPoco 13 | from airtest.core.android.adb import ADB 14 | import subprocess 15 | from airtest.utils.apkparser import APK 16 | 17 | _print = print 18 | def print(*args, **kwargs): 19 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 20 | 21 | adb = ADB().adb_path 22 | #同文件内用queue进行线程通信 23 | q = queue.Queue() 24 | 25 | ''' 26 | MultiAdb类封装了所有与设备有关的方法。 27 | 大部分方法都单独写了注释。 28 | 29 | ''' 30 | 31 | class MultiAdb: 32 | 33 | def __init__(self,mdevice=""): 34 | #获取当前文件的上层路径 35 | self._parentPath=os.path.abspath(os.path.dirname(inspect.getfile(inspect.currentframe())) + os.path.sep + ".") 36 | #获取当前项目的根路径 37 | self._rootPath=os.path.abspath(os.path.dirname(self._parentPath) + os.path.sep + ".") 38 | self._configPath=self._rootPath+"\config.ini" 39 | self._devicesList = Config.getValue(self._configPath, "deviceslist", ) 40 | self._packagePath = Config.getValue(self._configPath, "apkpath")[0] 41 | self._packageName = Config.getValue(self._configPath, "packname")[0] 42 | self._activityName = Config.getValue(self._configPath, "activityname")[0] 43 | self._skip_pushapk2devices=Config.getValue(self._configPath, "skip_pushapk2devices")[0] 44 | self._auto_delete_package=Config.getValue(self._configPath,"auto_delete_package")[0] 45 | self._auto_install_package=Config.getValue(self._configPath,"auto_install_package")[0] 46 | self._skip_check_of_install = Config.getValue(self._configPath, "skip_check_of_install")[0] 47 | self._skip_check_of_startapp = Config.getValue(self._configPath, "skip_check_of_startapp")[0] 48 | self._skip_performance=Config.getValue(self._configPath,"skip_performance")[0] 49 | self._storage_by_excel=Config.getValue(self._configPath,"storage_by_excel")[0] 50 | self._adb_log=Config.getValue(self._configPath,"adb_log")[0] 51 | self._keywords=Config.getValue(self._configPath,"keywords")[0] 52 | self._screenoff=Config.getValue(self._configPath,"screenoff")[0] 53 | self._startTime=time.time() 54 | self._timeout_of_per_action=int(Config.getValue(self._configPath, "timeout_of_per_action")[0]) 55 | self._timeout_of_startapp=int(Config.getValue(self._configPath, "timeout_of_startapp")[0]) 56 | self._mdevice=mdevice 57 | # 处理模拟器端口用的冒号 58 | if ":" in self._mdevice: 59 | self._nickName = self._mdevice.split(":")[1] 60 | else: 61 | self._nickName=self._mdevice 62 | self._iteration=int(Config.getValue(self._configPath, "iteration")[0]) 63 | self._allTestcase=Config.getValue(self._configPath, "testcase") 64 | try: 65 | self._testcaseForSelfDevice =Config.getTestCase(self._configPath, self._nickName) 66 | if self._testcaseForSelfDevice[0]=="": 67 | self._testcaseForSelfDevice = self._allTestcase 68 | except Exception: 69 | self._testcaseForSelfDevice=self._allTestcase 70 | self._testCasePath=Config.getValue(self._configPath, "testcasepath")[0] 71 | if self._testCasePath=="": 72 | self._testCasePath=os.path.join(self._rootPath, "TestCase") 73 | 74 | if self._activityName=="": 75 | self._activityName=APK(self.get_apkpath()).activities[0] 76 | self._isSurfaceView=Config.getValue(self._configPath,"isSurfaceView")[0] 77 | 78 | #获取设备列表 79 | def get_devicesList(self): 80 | return self._devicesList 81 | #获取apk的本地路径 82 | def get_apkpath(self): 83 | return self._packagePath 84 | #获取包名 85 | def get_packagename(self): 86 | return self._packageName 87 | #获取Activity类名 88 | def get_activityname(self): 89 | return self._activityName 90 | 91 | #获取是否跳过安装apk步骤的flag 92 | def get_skip_pushapk2devices(self): 93 | return self._skip_pushapk2devices 94 | 95 | #获取是否需要在安装应用时点击二次确认框的flag 96 | def get_skip_check_of_install(self): 97 | return self._skip_check_of_install 98 | 99 | 100 | #获取是否需要在打开应用时点击二次确认框的flag 101 | def get_skip_check_of_startapp(self): 102 | return self._skip_check_of_startapp 103 | 104 | #获取当前设备id 105 | def get_mdevice(self): 106 | return self._mdevice 107 | 108 | #获取当前设备id的昵称,主要是为了防范模拟器和远程设备带来的冒号问题。windows的文件命名规范里不允许有冒号。 109 | def get_nickname(self): 110 | return self._nickName 111 | 112 | #获取启动app的延时时间 113 | def get_timeout_of_startapp(self): 114 | return self._timeout_of_startapp 115 | 116 | #获取每步操作的延时时间 117 | def get_timeout_of_per_action(self): 118 | return self._timeout_of_per_action 119 | 120 | #获取运行循环点击处理脚本的循环次数 121 | def get_iteration(self): 122 | return self._iteration 123 | 124 | #获取所有的用例名称列表 125 | def get_alltestcase(self): 126 | return self._allTestcase 127 | 128 | #获取针对特定设备的用例列表 129 | def get_testcaseforselfdevice(self): 130 | return self._testcaseForSelfDevice 131 | 132 | #获取测试用例路径,不填是默认根目录TestCase 133 | def get_TestCasePath(self): 134 | return self._testCasePath 135 | 136 | #获取项目的根目录绝对路径 137 | def get_rootPath(self): 138 | return self._rootPath 139 | 140 | #获取是否要自动删除包的开关 141 | def auto_delete_package(self): 142 | return self._auto_delete_package 143 | 144 | def auto_install_package(self): 145 | return self._auto_install_package 146 | #获取是否需要性能测试的开关 147 | def get_skip_performance(self): 148 | return self._skip_performance 149 | 150 | #获取是否需要用excel来存储性能数据 151 | def get_storage_by_excel(self): 152 | return self._storage_by_excel 153 | 154 | #获取是否需要记录adblog 155 | def get_adb_log(self): 156 | return self._adb_log 157 | 158 | #获取是否需要在测试结束以后灭屏 159 | def get_screenoff(self): 160 | return self._screenoff 161 | 162 | def get_isSurfaceView(self): 163 | return self._isSurfaceView 164 | 165 | #修改当前设备的方法 166 | def set_mdevice(self,device): 167 | self._mdevice=device 168 | 169 | #写回包名、包路径、测试用例路径等等到配置文件 170 | 171 | def set_packagename(self,packagename): 172 | configPath=self._configPath 173 | Config.setValue(configPath,"packname",packagename) 174 | 175 | def set_packagepath(self, packagepath): 176 | configPath = self._configPath 177 | Config.setValue(configPath, "apkpath", packagepath) 178 | 179 | def set_TestCasePath(self,TestCasepath): 180 | configPath=self._configPath 181 | Config.setValue(configPath,"testcasepath",TestCasepath) 182 | 183 | # 本方法用于读取实时的设备连接 184 | def getdevices(self): 185 | deviceslist=[] 186 | for devices in os.popen(adb + " devices"): 187 | if "\t" in devices: 188 | if devices.find("emulator")<0: 189 | if devices.split("\t")[1] == "device\n": 190 | deviceslist.append(devices.split("\t")[0]) 191 | print("设备{}被添加到deviceslist中".format(devices)) 192 | return deviceslist 193 | 194 | #启动APP的方法,核心是airtest的start_app函数,后面的一大堆if else 是用来根据设备进行点击操作的。需要用户自行完成。 195 | def StartApp(self): 196 | devices=self.get_mdevice() 197 | skip_check_of_startapp=self.get_skip_check_of_startapp() 198 | skip_check_of_startapp = True if skip_check_of_startapp == "1" else False 199 | print("{}进入StartAPP函数".format(devices)) 200 | start_app(self.get_packagename()) 201 | if not skip_check_of_startapp: 202 | print("设备{},skip_check_of_startapp为{},开始初始化pocoui,处理应用权限".format(devices,skip_check_of_startapp)) 203 | # 获取andorid的poco代理对象,准备进行开启应用权限(例如申请文件存储、定位等权限)点击操作 204 | pocoAndroid = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False) 205 | n=self.get_iteration() 206 | 207 | #以下代码写得极丑陋,以后有空再重构,期望是参数化。 208 | if devices == "127.0.0.1:62001": 209 | # 这里是针对不同机型进行不同控件的选取,需要用户根据自己的实际机型实际控件进行修改 210 | count = 0 211 | while not pocoAndroid("android.view.View").exists(): 212 | print("{}开启应用的权限点击,循环第{}次".format(devices,count)) 213 | if count >= n: 214 | break 215 | if pocoAndroid("com.android.packageinstaller:id/permission_allow_button").exists(): 216 | pocoAndroid("com.android.packageinstaller:id/permission_allow_button").click() 217 | else: 218 | time.sleep(self.get_timeout_of_per_action()) 219 | count += 1 220 | elif devices == "127.0.0.1:62025": 221 | count = 0 222 | while not pocoAndroid("android.view.View").exists(): 223 | print("{}开启应用的权限点击,循环第{}次".format(devices,count)) 224 | if count >= 3: 225 | break 226 | if pocoAndroid("android:id/button1").exists(): 227 | pocoAndroid("android:id/button1").click() 228 | else: 229 | time.sleep(3) 230 | count += 1 231 | else: 232 | print("设备{},skip_check_of_startapp{},不做开启权限点击操作".format(devices,skip_check_of_startapp)) 233 | return None 234 | 235 | #推送apk到设备上的函数,读配置决定要不要进行权限点击操作。 236 | def PushApk2Devices(self): 237 | skip_pushapk2devices=self.get_skip_pushapk2devices() 238 | skip_pushapk2devices = True if skip_pushapk2devices == "1" else False 239 | if skip_pushapk2devices: 240 | return "Skip" 241 | device=self.get_mdevice() 242 | skip_check_of_install=self.get_skip_check_of_install() 243 | skip_check_of_install = True if skip_check_of_install == "1" else False 244 | #启动一个线程,执行AppInstall函数 245 | try: 246 | installThread = threading.Thread(target=self.AppInstall, args=()) 247 | installThread.start() 248 | if not skip_check_of_install: 249 | #如果配置上skip_check_of_install为True,则再开一个线程,执行安装权限点击操作 250 | print("设备{},skip_check_of_install为{},开始进行安装点击权限操作".format(device,skip_check_of_install)) 251 | inputThread = threading.Thread(target=self.InputEvent, args=()) 252 | inputThread.start() 253 | inputThread.join() 254 | else: 255 | print("设备{},skip_check_of_install为{},不进行安装点击权限操作".format(device,skip_check_of_install)) 256 | installThread.join() 257 | #从queue里获取线程函数的返回值 258 | result = q.get() 259 | if result=="Install Success": 260 | return "Success" 261 | else: 262 | return "Fail" 263 | except Exception as e: 264 | print(e) 265 | pass 266 | 267 | #安装应用的方法,先判断应用包是否已安装,如已安装则卸载,然后按配置路径去重新安装。 268 | def AppInstall(self): 269 | devices=self.get_mdevice() 270 | apkpath=self.get_apkpath() 271 | package=self.get_packagename() 272 | print("设备{}开始进行自动安装".format(devices)) 273 | auto_delete_package=self.auto_delete_package() 274 | auto_delete_package = True if auto_delete_package == "1" else False 275 | auto_install_package=self.auto_install_package() 276 | auto_install_package = True if auto_install_package == "1" else False 277 | try: 278 | if auto_delete_package: 279 | if self.isinstalled(): 280 | uninstallcommand = adb + " -s " + str(devices) + " uninstall " + package 281 | print("正在{}上卸载{},卸载命令为:{}".format(devices, package, uninstallcommand)) 282 | os.popen(uninstallcommand) 283 | #time.sleep(self.get_timeout_of_startapp()) 284 | installcommand = adb + " -s " + str(devices) + " install -r " + apkpath 285 | print("正在{}上安装{},安装命令为:{}".format(devices, package, installcommand)) 286 | if auto_install_package: 287 | os.system(installcommand) 288 | if self.isinstalled(): 289 | print("{}上安装成功,退出AppInstall线程".format(devices)) 290 | #将线程函数的返回值放入queue 291 | q.put("Install Success") 292 | return True 293 | else: 294 | print("{}上安装未成功".format(devices)) 295 | q.put("Install Fail") 296 | return False 297 | except Exception as e: 298 | print("{}上安装异常".format(devices)) 299 | print(e) 300 | q.put("Install Fail") 301 | 302 | 303 | def InputEvent(self): 304 | devices=self.get_mdevice() 305 | print("设备{}开始进行自动处理权限".format(devices)) 306 | # 获取andorid的poco代理对象,准备进行开启安装权限(例如各个品牌的自定义系统普遍要求的二次安装确认、vivo/oppo特别要求的输入手机账号密码等)的点击操作。 307 | pocoAndroid = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False) 308 | # 这里是针对不同机型进行不同控件的选取,需要用户根据自己的实际机型实际控件进行修改 309 | n = self.get_iteration() 310 | #先实现功能,以后有空参数化函数 311 | if devices == "172.16.6.82:7425": 312 | count = 0 313 | # 找n次或找到对象以后跳出,否则等5秒重试。 314 | while True: 315 | print("{}安装应用的权限点击,循环第{}次".format(devices,count)) 316 | if count >= n: 317 | print("{}退出InputEvent线程".format(devices)) 318 | break 319 | if pocoAndroid("com.coloros.safecenter:id/et_login_passwd_edit").exists(): 320 | pocoAndroid("com.coloros.safecenter:id/et_login_passwd_edit").set_text("123456") 321 | time.sleep(2) 322 | if pocoAndroid("android.widget.FrameLayout").offspring("android:id/buttonPanel").offspring("android:id/button1").exists(): 323 | pocoAndroid("android.widget.FrameLayout").offspring("android:id/buttonPanel").offspring( 324 | "android:id/button1").click() 325 | break 326 | else: 327 | time.sleep(5) 328 | count += 1 329 | elif devices == "127.0.0.1:62025": 330 | count = 0 331 | while True: 332 | print("{}安装应用的权限点击,循环第{}次".format(devices,count)) 333 | if count >= n: 334 | break 335 | if pocoAndroid("com.android.packageinstaller:id/continue_button").exists(): 336 | pocoAndroid("com.android.packageinstaller:id/continue_button").click() 337 | else: 338 | time.sleep(5) 339 | count += 1 340 | 341 | #判断给定设备里是否已经安装了指定apk 342 | def isinstalled(self): 343 | devices=self.get_mdevice() 344 | package=self.get_packagename() 345 | command=adb + " -s {} shell pm list package".format(devices) 346 | commandresult=os.popen(command) 347 | print("设备{}进入isinstalled方法,package={}".format(devices,package)) 348 | for pkg in commandresult: 349 | #print(pkg) 350 | if "package:" + package in pkg: 351 | print("在{}上发现已安装{}".format(devices,package)) 352 | return True 353 | print("在{}上没找到包{}".format(devices,package)) 354 | return False 355 | 356 | #判断给定设备的安卓版本号 357 | def get_androidversion(self): 358 | command=adb+" -s {} shell getprop ro.build.version.release".format(self.get_mdevice()) 359 | version=os.popen(command).read().split(".")[0] 360 | version=int(version) 361 | return version 362 | 363 | #判断给定设备运行指定apk时的内存占用 364 | def get_allocated_memory(self): 365 | command=adb + " -s {} shell dumpsys meminfo {}".format(self.get_mdevice(),self.get_packagename()) 366 | #print(command) 367 | memory=os.popen(command) 368 | list=[] 369 | for line in memory: 370 | line=line.strip() 371 | list=line.split(' ') 372 | if list[0]=="TOTAL": 373 | while '' in list: 374 | list.remove('') 375 | allocated_memory=format(int(list[1])/1024,".2f") 376 | q.put(allocated_memory) 377 | return allocated_memory 378 | q.put("N/a") 379 | return "N/a" 380 | 381 | #判断给定设备运行时的内存总占用 382 | def get_totalmemory(self): 383 | command = adb + " -s {} shell dumpsys meminfo ".format(self.get_mdevice()) 384 | #print(command) 385 | memory=os.popen(command) 386 | TotalRAM=0 387 | for line in memory: 388 | line=line.strip() 389 | list = line.split(":") 390 | if list[0]=="Total RAM": 391 | if self.get_androidversion()<7: 392 | TotalRAM = format(int(list[1].split(" ")[1])/1024,".2f") 393 | elif self.get_androidversion()>6: 394 | TotalRAM = format(int(list[1].split("K")[0].replace(",",""))/1024,".2f") 395 | break 396 | q.put(TotalRAM) 397 | return TotalRAM 398 | 399 | #判断给定设备运行时的空闲内存 400 | def get_freememory(self): 401 | command = adb + " -s {} shell dumpsys meminfo ".format(self.get_mdevice()) 402 | #print(command) 403 | memory = os.popen(command) 404 | FreeRAM=0 405 | for line in memory: 406 | line = line.strip() 407 | list = line.split(":") 408 | if list[0]=="Free RAM": 409 | if self.get_androidversion()<7: 410 | FreeRAM = format(int(list[1].split(" ")[1])/1024,".2f") 411 | elif self.get_androidversion()>6: 412 | FreeRAM = format(int(list[1].split("K")[0].replace(",",""))/1024,".2f") 413 | break 414 | q.put(FreeRAM) 415 | return FreeRAM 416 | 417 | #判断给定设备运行时的总使用内存 418 | def get_usedmemory(self): 419 | command = adb + " -s {} shell dumpsys meminfo ".format(self.get_mdevice()) 420 | #print(command) 421 | memory = os.popen(command) 422 | UsedRAM=0 423 | for line in memory: 424 | line = line.strip() 425 | list = line.split(":") 426 | if list[0]=="Used RAM": 427 | if self.get_androidversion()<7: 428 | UsedRAM = format(int(list[1].split(" ")[1])/1024,".2f") 429 | elif self.get_androidversion()>6: 430 | UsedRAM = format(int(list[1].split("K")[0].replace(",",""))/1024,".2f") 431 | break 432 | q.put(UsedRAM) 433 | return UsedRAM 434 | 435 | #判断给定设备运行时的Total/Free/Used内存,一次dump,加快获取速度 436 | def get_memoryinfo(self): 437 | command = adb + " -s {} shell dumpsys meminfo ".format(self.get_mdevice()) 438 | #print(command) 439 | memory = os.popen(command) 440 | androidversion=self.get_androidversion() 441 | for line in memory: 442 | line = line.strip() 443 | list = line.split(":") 444 | if list[0]=="Total RAM": 445 | if androidversion<7: 446 | TotalRAM = format(int(list[1].split(" ")[1])/1024,".2f") 447 | elif androidversion>6: 448 | TotalRAM = format(int(list[1].split("K")[0].replace(",",""))/1024,".2f") 449 | elif list[0]=="Free RAM": 450 | if androidversion<7: 451 | FreeRAM = format(int(list[1].split(" ")[1])/1024,".2f") 452 | elif androidversion > 6: 453 | FreeRAM = format(int(list[1].split("K")[0].replace(",",""))/1024,".2f") 454 | elif list[0] == "Used RAM": 455 | if androidversion<7: 456 | UsedRAM = format(int(list[1].split(" ")[1]) / 1024, ".2f") 457 | elif androidversion > 6: 458 | UsedRAM = format(int(list[1].split("K")[0].replace(",", "")) / 1024, ".2f") 459 | q.put(TotalRAM,FreeRAM,UsedRAM) 460 | return TotalRAM, FreeRAM,UsedRAM 461 | 462 | #判断给定设备运行时的总CPU占用。部分手机在安卓8以后多内核会分别显示CPU占用,这里统一除以内核数。 463 | def get_totalcpu(self): 464 | command = adb + " -s {} shell top -n 1 ".format(self.get_mdevice()) 465 | print(command) 466 | commandresult =os.popen(command) 467 | cputotal=0 468 | andversion=self.get_androidversion() 469 | maxcpu=1 470 | #ABI判断设备的内核型号,对一般安卓手机来说,基本都是ARM,但对模拟器来说,内核一般是X86。可以用这个值来判断真机或模拟器。 471 | ABIcommand = adb + " -s {} shell getprop ro.product.cpu.abi".format(self.get_mdevice()) 472 | ABI = os.popen(ABIcommand).read().strip() 473 | #逐行分析adbdump,根据不同的版本解析返回结果集,获得相应的cpu数据。 474 | for line in commandresult: 475 | list=line.strip().split(" ") 476 | while '' in list: 477 | list.remove('') 478 | if len(list)>8 : 479 | if andversion <7 and ABI != "x86": 480 | if ("%" in list[2]and list[2]!="CPU%"): 481 | cpu=int(list[2][:-1]) 482 | if cpu!=0: 483 | cputotal=cputotal+cpu 484 | else: 485 | break 486 | if andversion < 7 and ABI == "x86": 487 | if "%cpu" in list[0]: 488 | maxcpu = list[0] 489 | idlecpu = list[4] 490 | cputotal = int(maxcpu.split("%")[0]) - int(idlecpu.split("%")[0]) 491 | maxcpu = int(int(maxcpu.split("%")[0]) / 100) 492 | elif andversion ==7 and ABI != "x86": 493 | if ("%" in list[4] and list[4] != "CPU%"): 494 | cpu = int(list[4][:-1]) 495 | if cpu != 0: 496 | cputotal = cputotal + cpu 497 | else: 498 | break 499 | elif andversion == 7 and ABI == "x86": 500 | if "%cpu" in list[0]: 501 | maxcpu = list[0] 502 | idlecpu = list[4] 503 | cputotal = int(maxcpu.split("%")[0]) - int(idlecpu.split("%")[0]) 504 | maxcpu = int(int(maxcpu.split("%")[0]) / 100) 505 | elif andversion >7 and ABI != "x86": 506 | if "%cpu" in list[0]: 507 | maxcpu = list[0] 508 | idlecpu= list[4] 509 | cputotal=int(maxcpu.split("%")[0])-int(idlecpu.split("%")[0]) 510 | maxcpu=int(int(maxcpu.split("%")[0])/100) 511 | #由于cputotal在安卓7以下的adbdump里,无法通过Total-Idle获得,只能通过各个进程的CPU占用率累加,故有可能因四舍五入导致总和超过100%。当这种情况发生,则手动将cputotal置为100%。 512 | if cputotal/maxcpu>100: 513 | cputotal=100 514 | q.put(cputotal,maxcpu) 515 | return cputotal,maxcpu 516 | 517 | #判断给定设备运行时的总使用CPU 518 | def get_allocated_cpu(self): 519 | start=time.time() 520 | ABIcommand = adb + " -s {} shell getprop ro.product.cpu.abi".format(self.get_mdevice()) 521 | ABI = os.popen(ABIcommand).read().strip() 522 | #包名过长时,包名会在adbdump里被折叠显示,所以需要提前将包名压缩,取其前11位基本可以保证不被压缩也不被混淆 523 | packagename=self.get_packagename()[0:11] 524 | command = adb + " -s {} shell top -n 1 |findstr {} ".format(self.get_mdevice(),packagename) 525 | #print(command) 526 | subresult= os.popen(command).read() 527 | version=self.get_androidversion() 528 | if subresult == "" : 529 | q.put("N/a") 530 | return "N/a" 531 | else: 532 | cpuresult = subresult.split(" ") 533 | #去空白项 534 | while '' in cpuresult: 535 | cpuresult.remove('') 536 | #print(self.get_mdevice(),"cpuresult=",cpuresult) 537 | cpu="" 538 | if version<7 and ABI!="x86": 539 | cpu = cpuresult[2].split("%")[0] 540 | if version < 7 and ABI== "x86": 541 | cpu = cpuresult[8] 542 | elif version ==7 and ABI!="x86": 543 | cpu=cpuresult[4].split("%")[0] 544 | elif version ==7 and ABI=="x86": 545 | cpu=cpuresult[8] 546 | elif version>7: 547 | cpu = cpuresult[8] 548 | q.put(cpu) 549 | return cpu 550 | 551 | def get_fps(self,SurfaceView): 552 | if SurfaceView=="1": 553 | fps=self.get_fps_SurfaceView() 554 | elif SurfaceView=="0": 555 | fps= self.get_fps_gfxinfo() 556 | return fps 557 | 558 | def get_fps_gfxinfo(self): 559 | device = self.get_mdevice() 560 | package = self.get_packagename() 561 | command = adb+ " -s {} shell dumpsys gfxinfo {}".format(device,package) 562 | print(command) 563 | results = os.popen(command) 564 | stamp_time=0 565 | frames=0 566 | fps="N/a" 567 | for line in results: 568 | #if "Draw" and "Prepare" and "Process" and "Execute" in line: 569 | list = line.strip().split("\t") 570 | if len(list)!=4 or "Draw" in line: 571 | continue 572 | else: 573 | stamp_time+=float(list[0])+float(list[1])+float(list[2])+float(list[3]) 574 | frames+=1 575 | if len(line)==0: 576 | break 577 | try: 578 | fps=round(stamp_time/(frames),1) 579 | print("使用GfxInfo方式收集到有效fps数据") 580 | except: 581 | print("使用GfxInfo方式未收集到有效fps数据") 582 | 583 | return fps 584 | 585 | 586 | #算法提取自 https://github.com/ChromiumWebApps/chromium/tree/master/build/android/pylib 587 | def get_fps_SurfaceView(self): 588 | device=self.get_mdevice() 589 | package=self.get_packagename() 590 | activity=self.get_activityname() 591 | androidversion=self.get_androidversion() 592 | command="" 593 | if androidversion<7: 594 | command=adb+ " -s {} shell dumpsys SurfaceFlinger --latency 'SurfaceView'".format(device) 595 | elif androidversion==7: 596 | command=adb+ " -s {} shell \"dumpsys SurfaceFlinger --latency 'SurfaceView - {}/{}'\"".format(device,package,activity) 597 | elif androidversion>7: 598 | command = adb + " -s {} shell \"dumpsys SurfaceFlinger --latency 'SurfaceView - {}/{}#0'\"".format(device, package, activity) 599 | #command= adb + " -s {} shell \"dumpsys SurfaceFlinger --latency\"".format(device) 600 | print(command) 601 | results=os.popen(command) 602 | if not results: 603 | print("nothing") 604 | return (None, None) 605 | #print(device,results.read()) 606 | timestamps = [] 607 | #定义纳秒 608 | nanoseconds_per_second = 1e9 609 | #定义刷新间隔 610 | refresh_period = 16666666 / nanoseconds_per_second 611 | #定义挂起时间戳 612 | pending_fence_timestamp = (1 << 63) - 1 613 | #遍历结果集 614 | for line in results: 615 | #去空格并分列 616 | line = line.strip() 617 | list = line.split("\t") 618 | #剔除非数据列 619 | if len(list) != 3: 620 | continue 621 | #取中间一列数据 622 | timestamp = float(list[1]) 623 | # 当时间戳等于挂起时间戳时,舍弃 624 | if timestamp == pending_fence_timestamp: 625 | continue 626 | timestamp /= nanoseconds_per_second 627 | #安卓7的adbdump提供255行数据,127行0以及128行真实数据,所以需要将0行剔除 628 | if timestamp!=0: 629 | timestamps.append(timestamp) 630 | #获得总帧数 631 | frame_count = len(timestamps) 632 | #获取帧列表总长、规范化帧列表总长 633 | frame_lengths, normalized_frame_lengths = self.GetNormalizedDeltas(timestamps, refresh_period, 0.5) 634 | if len(frame_lengths) < frame_count - 1: 635 | print('Skipping frame lengths that are too short.') 636 | frame_count = len(frame_lengths) + 1 637 | #数据不足时,返回None 638 | if not refresh_period or not len(timestamps) >= 3 or len(frame_lengths) == 0: 639 | print("使用SurfaceView方式未收集到有效fps数据") 640 | return "N/a" 641 | #总秒数为时间戳序列最后一位减第一位 642 | seconds = timestamps[-1] - timestamps[0] 643 | fps = round((frame_count - 1) / seconds,1) 644 | #这部分计算掉帧率。思路是先将序列化过的帧列表重新序列化,由于min_normalized_delta此时为None,故直接求出frame_lengths数组中各个元素的差值保存到数组deltas中。 645 | #length_changes, normalized_changes = self.GetNormalizedDeltas(frame_lengths, refresh_period) 646 | #求出normalized_changes数组中比0大的数,这部分就是掉帧。 647 | #jankiness = [max(0, round(change)) for change in normalized_changes] 648 | #pause_threshold = 20 649 | #normalized_changes数组中大于0小于20的总和记为jank_count。这块算法是看明白了,但思路get不到。。。 650 | #jank_count = sum(1 for change in jankiness if change > 0 and change < pause_threshold) 651 | return fps 652 | 653 | #将时间戳序列分2列并相减,得到时间差的序列。 654 | #时间差序列中,除刷新间隔大于0.5的时间差重新序列化 655 | def GetNormalizedDeltas(self,data, refresh_period, min_normalized_delta=None): 656 | deltas = [t2 - t1 for t1, t2 in zip(data, data[1:])] 657 | if min_normalized_delta != None: 658 | deltas = filter(lambda d: d / refresh_period >= min_normalized_delta, 659 | deltas) 660 | 661 | return (list(deltas), [delta / refresh_period for delta in deltas]) 662 | 663 | def create_adb_log(self,nowtime): 664 | reportpath = os.path.join(os.getcwd(), "Report") 665 | logpath=os.path.join(reportpath, "Data") 666 | nowtime=time.strftime("%m%d%H%M", nowtime) 667 | filename=logpath+"\\"+self.get_nickname()+"_"+nowtime+".txt" 668 | print("filename=",filename) 669 | keywords=self._keywords 670 | if keywords!="": 671 | command = adb + " -s {} logcat |findstr ".format(self.get_mdevice())+keywords+"> "+filename 672 | else: 673 | command = adb + " -s {} logcat ".format(self.get_mdevice()) + "> " + filename 674 | print("command=",command) 675 | os.popen(command) 676 | return filename 677 | 678 | def check_device(self): 679 | ABIcommand = adb + " -s {} shell getprop ro.product.cpu.abi".format(self.get_mdevice()) 680 | ABI = os.popen(ABIcommand).read().strip() 681 | versioncommand=adb+" -s {} shell getprop ro.build.version.release ".format(self.get_mdevice()) 682 | version=os.popen(versioncommand).read().strip() 683 | devicenamecommand = adb + " -s {} shell getprop ro.product.model".format(self.get_mdevice()) 684 | devicename=os.popen(devicenamecommand).read().strip() 685 | batterycommand=adb+ " -s {} shell dumpsys battery".format(self.get_mdevice()) 686 | battery=os.popen(batterycommand) 687 | for line in battery: 688 | if "level:" in line: 689 | battery=line.split(":")[1].strip() 690 | break 691 | wmsizecommand = adb + " -s {} shell wm size".format(self.get_mdevice()) 692 | size = os.popen(wmsizecommand).read().strip() 693 | size = size.split(":")[1].strip() 694 | DPIcommand= adb+ " -s {} shell wm density".format(self.get_mdevice()) 695 | dpi = os.popen(DPIcommand).read().strip() 696 | if "Override density" in dpi: 697 | dpi= dpi.split(":")[2].strip() 698 | else: 699 | dpi = dpi.split(":")[1].strip() 700 | android_id_command= adb + " -s {} shell settings get secure android_id ".format(self.get_mdevice()) 701 | android_id=os.popen(android_id_command).read().strip() 702 | mac_address_command = adb + " -s {} shell cat /sys/class/net/wlan0/address".format(self.get_mdevice()) 703 | mac_address=os.popen(mac_address_command).read().strip() 704 | if "Permission denied" in mac_address: 705 | mac_address="Permission denied" 706 | typecommand= adb + " -s {} shell getprop ro.product.model".format(self.get_mdevice()) 707 | typename=os.popen(typecommand).read().strip() 708 | brandcommand= adb + " -s {} shell getprop ro.product.brand".format(self.get_mdevice()) 709 | brand=os.popen(brandcommand).read().strip() 710 | namecommand=adb + " -s {} shell getprop ro.product.name".format(self.get_mdevice()) 711 | name=os.popen(namecommand).read().strip() 712 | core_command = adb + " -s {} shell cat /sys/devices/system/cpu/present".format(self.get_mdevice()) 713 | core_num=os.popen(core_command).read().strip()[2] 714 | core_num=int(core_num)+1 715 | device=self.get_mdevice() 716 | package=self.get_packagename() 717 | activity=self.get_activityname() 718 | androidversion=self.get_androidversion() 719 | isSurfaceView=False 720 | isGfxInfo=False 721 | SurfaceView_command="" 722 | if androidversion<7: 723 | SurfaceView_command=adb+ " -s {} shell dumpsys SurfaceFlinger --latency 'SurfaceView'".format(device) 724 | elif androidversion==7: 725 | SurfaceView_command=adb+ " -s {} shell \"dumpsys SurfaceFlinger --latency 'SurfaceView - {}/{}'\"".format(device,package,activity) 726 | elif androidversion>7: 727 | SurfaceView_command = adb + " -s {} shell \"dumpsys SurfaceFlinger --latency 'SurfaceView - {}/{}#0'\"".format(device, package, activity) 728 | print(SurfaceView_command) 729 | results=os.popen(SurfaceView_command) 730 | for line in results: 731 | print("surface",line) 732 | if line =="16666666": 733 | continue 734 | elif len(line)>10: 735 | isSurfaceView=True 736 | break 737 | 738 | GfxInfo_command=adb + " -s {} shell dumpsys gfxinfo {}".format(device,package) 739 | results = os.popen(GfxInfo_command) 740 | for line in results: 741 | #print("gfx",line) 742 | if "Draw" and "Prepare" and "Process" and "Execute" in line: 743 | isGfxInfo=True 744 | break 745 | deviceinfo={"ABI":ABI,"VERSION":version,"DEVICENAME":devicename,"BATTERY":battery,"VMSIZE":size,"DPI":dpi,"ANDROID_ID":android_id,"MAC_ADDRESS":mac_address,"TYPE":typename,"BRAND":brand,"NAME":name,"CORE_NUM":core_num,"isSurfaceView":isSurfaceView,"isGfxInfo":isGfxInfo} 746 | return deviceinfo 747 | 748 | if __name__=="__main__": 749 | ''' 750 | #android 9 751 | madb1=MultiAdb("172.16.6.82:7413") 752 | print("total1=",madb1.get_totalcpu()) 753 | #android 8 754 | madb2=MultiAdb("172.16.6.82:7437") 755 | print("total2=",madb2.get_totalcpu()) 756 | #android 7 757 | madb3=MultiAdb("172.16.6.82:7409") 758 | print("total3=",madb3.get_totalcpu()) 759 | #android 6 760 | madb4=MultiAdb("172.16.6.82:7441") 761 | print("total4=",madb4.get_totalcpu()) 762 | ''' 763 | devicesList = MultiAdb().getdevices() 764 | print("最终的devicesList=",devicesList) 765 | for device in devicesList: 766 | madb=MultiAdb(device) 767 | #print(madb.check_device()) 768 | print(madb.get_memoryinfo()) 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | -------------------------------------------------------------------------------- /core/RunTestCase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | #import os 4 | #import time 5 | import unittest 6 | from BeautifulReport import BeautifulReport 7 | from airtest.core.api import * 8 | from DreamMultiDevices.tools import File 9 | from DreamMultiDevices.TestCase import * 10 | _print = print 11 | def print(*args, **kwargs): 12 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 13 | 14 | #运行Testcase的主函数 15 | def RunTestCase(madb,start): 16 | devices=madb.get_mdevice() 17 | print("进入{}的RunTestCase".format(devices)) 18 | # 获取路径 19 | package = madb.get_packagename() 20 | TestCasePath = madb.get_TestCasePath() 21 | #print("TestCasePath=",TestCasePath) 22 | if not os.path.exists(TestCasePath): 23 | print("测试用例需放到‘TestCase’文件目录下") 24 | reportpath = os.path.join(os.getcwd(), "Report") 25 | #读取ini文件,获得期望测试的用例列表 26 | TestList=madb.get_testcaseforselfdevice() 27 | print("{}的待测用例为:{}".format(madb.get_mdevice(),TestList)) 28 | # 通过GetPyList方法,取得目录里可测试的用例列表 29 | scriptList = File.GetPyList(TestCasePath) 30 | suite = unittest.TestSuite() 31 | for i in range(len(TestList)): 32 | fileName = "TC_" + TestList[i] 33 | #print("fileName=",fileName) 34 | if fileName in scriptList: 35 | #在整个命名空间里遍历所有名称为"TC_xx.py"形式的文件,默认这些文件都是unittest测试文件,然后调用其Main函数。 36 | result = globals()[fileName].Main(devices) 37 | suite.addTests(result) 38 | #聚合报告到BR 39 | unittestReport = BeautifulReport(suite) 40 | nowtime=time.strftime("%H%M%S",start) 41 | #unittestReport.report(filename=madb.get_nickdevice()+"_"+str(nowtime),description=package, report_dir=reportpath,rundevice=madb.get_mdevice()) 42 | unittestReport.report(filename=madb.get_nickname()+"_"+str(nowtime),description=package, report_dir=reportpath) 43 | stop_app(package) 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saint228/DreamMultiDevices/c6b8ec96586f2957856bfa442411bb6ec2ccf0f6/core/__init__.py -------------------------------------------------------------------------------- /core/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import time 5 | from multiprocessing import Process,Value 6 | from DreamMultiDevices.core.MultiAdb import MultiAdb as Madb 7 | from airtest.core.error import * 8 | from poco.exceptions import * 9 | from airtest.core.api import * 10 | from DreamMultiDevices.core import RunTestCase 11 | from DreamMultiDevices.tools.Email import * 12 | from DreamMultiDevices.tools.ScreenOFF import * 13 | import traceback 14 | from DreamMultiDevices.Performance import * 15 | 16 | index_print = print 17 | def print(*args, **kwargs): 18 | index_print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 19 | 20 | ''' 21 | 整个框架的主程序,根据配置表读取设备,并逐一为其分配功能测试进程和性能测试进程。 22 | 由于每个设备需要调用2个进程配合工作,所以使用Value进行进程间通信。Value会传递一个int值,默认为0,在功能进程结束时通知性能进程随之结束。 23 | ''' 24 | 25 | def main(): 26 | #默认去config.ini里读取期望参与测试的设备,若为空,则选择当前连接的所有状态为“device”的设备 27 | devicesList = Madb().get_devicesList() 28 | if devicesList[0] == "": 29 | devicesList = Madb().getdevices() 30 | print("最终的devicesList=",devicesList) 31 | if Madb().get_apkpath()=="" or Madb().get_packagename()=="": 32 | print("配置文件填写不全,packagename和apkpath是必填项") 33 | devicesList=None 34 | #读取是否需要同步性能测试的配置。 35 | skip_performance=Madb().get_skip_performance() 36 | skip_performance=True if skip_performance=="1" else False 37 | is_storaged_by_excel=Madb().get_storage_by_excel() 38 | is_storaged_by_excel=True if is_storaged_by_excel=="1" else False 39 | adb_log = Madb().get_adb_log() 40 | adb_log=True if adb_log=="1" else False 41 | reportpath = os.path.join(os.getcwd(), "Report") 42 | # 没有Report目录时自动创建 43 | if not os.path.exists(reportpath): 44 | os.mkdir(reportpath) 45 | os.mkdir(reportpath + "/Screen") 46 | os.mkdir(reportpath + "/Data") 47 | print(reportpath) 48 | print("测试开始") 49 | if devicesList: 50 | try: 51 | print("启动进程池") 52 | list=[] 53 | # 根据设备列表去循环创建进程,对每个进程调用下面的enter_processing/enter_enter_performance方法。 54 | for i in range(len(devicesList)): 55 | #start会被传递到2个进程函数里,作为区分最终产物html和excel的标志 56 | start=time.localtime() 57 | madb=Madb(devicesList[i]) 58 | if madb.get_androidversion()<5: 59 | print("设备{}的安卓版本低于5,不支持。".format(madb.get_mdevice())) 60 | continue 61 | else: 62 | #进程通信变量flag,默认为0,完成测试时修改为1。 63 | flag = Value('i', 0) 64 | fpsflag= Value('i',0) 65 | if not skip_performance: 66 | p1 = Process(target=enter_performance, args=(madb,flag,start,is_storaged_by_excel,adb_log)) 67 | list.append(p1) 68 | p2=Process(target=enter_processing, args=(i,madb,flag,start,)) 69 | list.append(p2) 70 | for p in list: 71 | p.start() 72 | for p in list: 73 | p.join() 74 | print("进程回收完毕") 75 | print("测试结束") 76 | screenoff=Madb().get_screenoff() 77 | screenoff = True if screenoff == "1" else False 78 | if screenoff: 79 | for i in devicesList: 80 | setScreenOFF(i) 81 | print("设备{}已灭屏".format(i)) 82 | except AirtestError as ae: 83 | print("Airtest发生错误" + traceback.format_exc()) 84 | except PocoException as pe: 85 | print("Poco发生错误" + traceback.format_exc()) 86 | except Exception as e: 87 | print("发生未知错误" + traceback.format_exc()) 88 | else: 89 | print("未找到设备,测试结束") 90 | mailtext="自动化测试完毕" 91 | ''' 92 | try: 93 | sendemail(mailtext) 94 | except Exception as e: 95 | print("邮件发送失败"+ traceback.format_exc()) 96 | ''' 97 | ''' 98 | 功能进程模块 99 | 首先调用airtest库的方法进行设备连接并初始化,然后读取配表,进行应用的安装、启动、权限点击等操作。同步的操作会分由线程来完成。 100 | 确定启动应用成功以后,调用分配测试用例的RunTestCase函数。 101 | 在用例执行完毕以后,将Value置为1。 102 | ''' 103 | 104 | def enter_processing(processNo,madb,flag,start): 105 | devices = madb.get_mdevice() 106 | print("进入{}进程,devicename={}".format(processNo,devices)) 107 | isconnect="" 108 | try: 109 | #调用airtest的各个方法连接设备 110 | connect_device("Android:///" + devices) 111 | time.sleep(madb.get_timeout_of_per_action()) 112 | auto_setup(__file__) 113 | isconnect="Pass" 114 | print("设备{}连接成功".format(devices)) 115 | installflag="" 116 | startflag="" 117 | if isconnect == "Pass": 118 | try: 119 | print("设备{}开始安装apk".format(devices)) 120 | #尝试推送apk到设备上 121 | installResult = madb.PushApk2Devices() 122 | if installResult == "Success": 123 | print("{}确定安装成功".format(devices)) 124 | installflag = "Success" 125 | if installResult == "Skip": 126 | print("{}设备跳过PushAPK2Device步骤".format(devices)) 127 | installflag="Success" 128 | except Exception as e: 129 | print("{}安装失败,installResult={}".format(devices, installResult)+ traceback.format_exc()) 130 | if installflag=="Success": 131 | try: 132 | #尝试启动应用 133 | madb.StartApp() 134 | startflag = "Success" 135 | except Exception as e: 136 | print("运行失败"+traceback.format_exc()) 137 | time.sleep(madb.get_timeout_of_per_action()) 138 | #应用启动成功则开始运行用例 139 | if (startflag=="Success"): 140 | RunTestCase.RunTestCase(madb, start) 141 | print("{}完成测试".format(devices)) 142 | else: 143 | print("{}未运行测试。".format(devices)) 144 | else: 145 | print("设备{}连接失败".format(devices)) 146 | except Exception as e: 147 | print( "连接设备{}失败".format(devices)+ traceback.format_exc()) 148 | #无论结果如何,将flag置为1,通知Performance停止记录。 149 | flag.value = 1 150 | 151 | 152 | if __name__ == "__main__": 153 | screenoff = Madb().get_screenoff() 154 | screenoff = True if screenoff == "1" else False 155 | devicesList = Madb().getdevices() 156 | if screenoff: 157 | for i in devicesList: 158 | setScreenOFF(i) 159 | print("设备{}已灭屏".format(i)) 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /core/tmp.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saint228/DreamMultiDevices/c6b8ec96586f2957856bfa442411bb6ec2ccf0f6/core/tmp.txt -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys,os 3 | from DreamMultiDevices.core import index 4 | __author__ = "无声" 5 | 6 | def start(): 7 | index.main() 8 | 9 | #从根目录启动,确保相对路径调用正常 10 | if __name__ == '__main__': 11 | os.popen("adb start-server") 12 | start() 13 | 14 | -------------------------------------------------------------------------------- /template/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saint228/DreamMultiDevices/c6b8ec96586f2957856bfa442411bb6ec2ccf0f6/template/__init__.py -------------------------------------------------------------------------------- /template/app.css: -------------------------------------------------------------------------------- 1 | .reportBtn { 2 | color:#7AD7BE; 3 | font-size:18px; 4 | font-weight:600; 5 | } 6 | .disableReport { 7 | color:#70AD47!important; 8 | } 9 | .Screen { 10 | text-align:center; 11 | } 12 | .Screen img { 13 | max-width:100%; 14 | max-height:420px; 15 | } -------------------------------------------------------------------------------- /template/app.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 功能报告 4 |        5 | 性能报告 6 |
7 |
-------------------------------------------------------------------------------- /template/performance.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/框架.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saint228/DreamMultiDevices/c6b8ec96586f2957856bfa442411bb6ec2ccf0f6/template/框架.gif -------------------------------------------------------------------------------- /template/赞赏码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saint228/DreamMultiDevices/c6b8ec96586f2957856bfa442411bb6ec2ccf0f6/template/赞赏码.png -------------------------------------------------------------------------------- /tools/Config.py: -------------------------------------------------------------------------------- 1 |  # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | import configparser 4 | import os 5 | con = configparser.ConfigParser() 6 | 7 | #解析config文件并将其结果转成一个list,对单个的value,到时候可以用[0]来取到。 8 | def getValue(path,key): 9 | con.read(path) 10 | result = con.get("config",key) 11 | list=result.split(",") 12 | return list 13 | 14 | #基本同上,读取TestCaseforDevice 节点下的键值 15 | def getTestCase(path,device=""): 16 | if device!="": 17 | con.read(path) 18 | result = con.get("TestCaseforDevice",device) 19 | list=result.split(",") 20 | return list 21 | else: 22 | return [] 23 | 24 | def getEmail(path,key): 25 | con.read(path) 26 | result = con.get("Email", key) 27 | return result 28 | 29 | #重新写回配置文件 30 | def setValue(configpath,key,value): 31 | if key!="" and value!="": 32 | con.set("config",key,value) 33 | con.write(open(configpath, "w")) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tools/Email.py: -------------------------------------------------------------------------------- 1 | __author__ = "无声" 2 | 3 | # !/usr/bin/python 4 | # -*- coding: UTF-8 -*- 5 | 6 | import smtplib,os,inspect 7 | from email.mime.text import MIMEText 8 | from email.header import Header 9 | from DreamMultiDevices.tools import Config 10 | 11 | def sendemail(message): 12 | # 第三方 SMTP 服务 13 | configPath=os.path.abspath(os.path.dirname(os.path.abspath(os.path.dirname(inspect.getfile(inspect.currentframe())) + os.path.sep + ".")) + os.path.sep + ".")+"\config.ini" 14 | mail_host= Config.getEmail(configPath, "mail_host") 15 | mail_user= Config.getEmail(configPath, "mail_user") 16 | mail_pass= Config.getEmail(configPath, "mail_pass") 17 | sender = Config.getEmail(configPath, "sender") 18 | receivers = Config.getEmail(configPath, "receivers") 19 | print(mail_host,mail_user,mail_pass,sender,receivers) 20 | smtpObj = smtplib.SMTP() 21 | smtpObj.connect(mail_host, 25) # 25 为 SMTP 端口号 22 | smtpObj.login(mail_user, mail_pass) 23 | smtpObj.sendmail(sender, receivers, message.as_string()) 24 | print("邮件发送成功") 25 | 26 | -------------------------------------------------------------------------------- /tools/Excel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | from airtest.core.android.adb import ADB 4 | import xlwings as xw 5 | import os 6 | import time 7 | import json 8 | import traceback 9 | adb = ADB().adb_path 10 | 11 | reportpath = os.path.join(os.getcwd(), "Report") 12 | datapath=os.path.join(reportpath, "Data") 13 | 14 | #创建一个log_excel用以记录性能数据 15 | def create_log_excel(nowtime,device): 16 | create_time=time.strftime("%m%d%H%M", nowtime) 17 | exclefile = datapath+"\\"+create_time+ "_"+ device + "_log.xlsx" 18 | app = xw.App(visible=True, add_book=False) 19 | wb = app.books.add() 20 | sheet = wb.sheets.active 21 | sheet.range('A1').value = ["Time","TotalMemory(MB)", "AllocatedMemory(MB)","UsedMemory(MB)","FreeMemory(MB)","TotalCPU","AllocatedCPU","FPS","","PNG","PNGAddress"] 22 | sheet.range('A1:K1').color=205, 197, 191 23 | if os.path.exists(exclefile): 24 | raise Exception( "FileHasExisted") 25 | wb.save(exclefile) 26 | print("创建Excel文件:{}".format(exclefile)) 27 | return exclefile,sheet,wb 28 | 29 | #计算一个sheet里已存在的所有数据,然后返回该sheet里的各项的平均、最大、最小值。 30 | def calculate(sheet): 31 | #获取excel里的所有已被编辑数据 32 | rng = sheet.range('A1').expand() 33 | #获取行号 34 | nrow = rng.last_cell.row 35 | #获得对应数据列的数据 36 | AllocatedMemory=sheet.range("C2:C{}".format(nrow)).value 37 | sum_UsedMemory=sheet.range("D2:D{}".format(nrow)).value 38 | sum_FreeMemory=sheet.range("E2:E{}".format(nrow)).value 39 | sum_TotalCPU=sheet.range("F2:F{}".format(nrow)).value 40 | AllocatedCPU=sheet.range("G2:G{}".format(nrow)).value 41 | FPS=sheet.range("H2:H{}".format(nrow)).value 42 | #sum_TotalCPU=[] 43 | #为了统计最大最小平均值,需要剔除掉空值的N/a。 44 | while "N/a" in AllocatedMemory: 45 | AllocatedMemory.remove("N/a") 46 | while "N/a" in AllocatedCPU: 47 | AllocatedCPU.remove("N/a") 48 | for i in range(len(AllocatedCPU)): 49 | AllocatedCPU[i]=float(AllocatedCPU[i][:-1]) 50 | while "N/a" in FPS: 51 | FPS.remove("N/a") 52 | for i in range(len(sum_TotalCPU)): 53 | sum_TotalCPU[i]=float(sum_TotalCPU[i][:-1]) 54 | avg_am,max_am,min_am=getcount(AllocatedMemory) 55 | avg_um,max_um,min_um=getcount(sum_UsedMemory) 56 | avg_fm,max_fm,min_fm=getcount(sum_FreeMemory) 57 | avg_tc,max_tc,min_tc=getcount(sum_TotalCPU) 58 | avg_ac,max_ac,min_ac=getcount(AllocatedCPU) 59 | avg_fps,max_fps,min_fps=getcount(FPS) 60 | #CPU的数据需要处理下百分号 61 | if avg_tc=="N/a": 62 | pass 63 | else: 64 | avg_tc = str(format(avg_tc, ".2f")) + "%" 65 | max_tc = str(format(max_tc, ".2f")) + "%" 66 | min_tc = str(format(min_tc, ".2f")) + "%" 67 | if avg_ac=="N/a": 68 | pass 69 | else: 70 | avg_ac = str(format(avg_ac ,".2f")) + "%" 71 | max_ac = str(format(max_ac ,".2f")) + "%" 72 | min_ac = str(format(min_ac ,".2f")) + "%" 73 | #填充excel表格的list。每个字段对应excle的一列。 74 | avglist = ["平均值","",avg_am,avg_um,avg_fm,avg_tc,avg_ac,avg_fps] 75 | maxlist = ["最大值:","",max_am,max_um,max_fm,max_tc,max_ac,max_fps] 76 | minlist = ["最小值:","",min_am,min_um,min_fm,min_tc,min_ac,min_fps] 77 | return avglist,maxlist,minlist 78 | 79 | #统计一个list的平均、最大、最小值 80 | def getcount(list): 81 | sum = avg = max = min = 0 82 | flag = 0 83 | try: 84 | for Na in list: 85 | flag = flag + 1 86 | if flag == 1: 87 | sum = float(Na) 88 | max = float(Na) 89 | min = float(Na) 90 | else: 91 | sum = sum + float(Na) 92 | if float(Na) > max: 93 | max= float(Na) 94 | elif float(Na) < min: 95 | min = float(Na) 96 | except Exception as e: 97 | print(traceback.format_exc()) 98 | if sum == 0: 99 | avg = "N/a" 100 | max = "N/a" 101 | min = "N/a" 102 | else: 103 | avg = float(format(sum / flag,".2f")) 104 | return avg,max,min 105 | 106 | #读取传过来的list和excel,将list写入excel的下一行 107 | def record_to_excel(sheet,list,**kwargs): 108 | rng = sheet.range('A1').expand() 109 | nrow = rng.last_cell.row 110 | currentcell="A"+str(nrow+1) 111 | #记录截图的链接和实际地址 112 | currentcellpng="J"+str(nrow+1) 113 | currentcellpngvalue="K"+str(nrow+1) 114 | currentcellrange=currentcell+":"+"H"+str(nrow+1) 115 | sheet.range(currentcell).value =list 116 | #分行换色 117 | if nrow % 2 == 0: 118 | sheet.range(currentcellrange).color = 173, 216, 230 119 | else: 120 | sheet.range(currentcellrange).color = 221, 245, 250 121 | #单独处理颜色参数以及截图参数 122 | for key, value in kwargs.items(): 123 | if key=="color": 124 | sheet.range(currentcellrange).color=value 125 | if key == "png": 126 | sheet.range(currentcellpng).add_hyperlink(value,"截图","提示:点击打开截图") 127 | sheet.range(currentcellpngvalue).value=value 128 | sheet.autofit() 129 | 130 | #在excel里查找指定键名的列,将该列所有数值(不算最后3行统计行)返回成一个serieslist 131 | def get_series(sheet,Key): 132 | rng = sheet.range('A1').expand() 133 | nrow = rng.last_cell.row-3 134 | rng2=sheet.range('A1:K1') 135 | serieslist = [] 136 | for key in rng2: 137 | if key.value==Key: 138 | cum=key.address 139 | cum=cum.split("$")[1] 140 | tmp=cum+"2:"+cum+str(nrow) 141 | serieslist=sheet.range(tmp).value 142 | break 143 | if Key=="TotalCPU" or Key=="AllocatedCPU" : 144 | for i in range(len(serieslist)): 145 | if serieslist[i] == "N/a": 146 | serieslist[i] = 0 147 | else: 148 | stringnum=serieslist[i][:-1] 149 | serieslist[i] = float(stringnum) 150 | if Key=="AllocatedMemory(MB)" or Key=="FPS": 151 | for i in range(len(serieslist)): 152 | if serieslist[i]=="N/a": 153 | serieslist[i]=0 154 | return serieslist 155 | 156 | #在序列表里查询指定键值对,转成json返回 157 | def get_json(sheet,Key): 158 | series = get_series(sheet, Key) 159 | series_json=json.dumps({Key:series}) 160 | return series_json 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /tools/File.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | import os 4 | 5 | #从一个目录里获取所有的文件名并返回一个列表,剔除其中的__init__.py和__pycache__。 6 | def GetPyList(filePath): 7 | dirList = os.listdir(filePath) 8 | pyList = [] 9 | for i in range(len(dirList)): 10 | fileName = dirList[i].split(".") 11 | if dirList[i] != "__init__.py" and dirList[i] != "__pycache__": 12 | if fileName[1].lower() == "py": 13 | pyList.append(fileName[0]) 14 | return pyList 15 | 16 | -------------------------------------------------------------------------------- /tools/Init_MiniCap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | 5 | import os 6 | import inspect 7 | import time 8 | from airtest.core.android.adb import ADB 9 | import traceback 10 | from DreamMultiDevices.core.MultiAdb import MultiAdb as Madb 11 | from DreamMultiDevices.tools.Screencap import * 12 | 13 | _print = print 14 | def print(*args, **kwargs): 15 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 16 | 17 | adb = ADB().adb_path 18 | 19 | #用来给设备初始化MiniCap的,介绍见 https://blog.csdn.net/saint_228/article/details/92142914 20 | def ini_MiniCap(devices): 21 | try: 22 | parent_path = os.path.abspath(os.path.dirname(inspect.getfile(inspect.currentframe())) + os.path.sep + ".") 23 | root_path = os.path.abspath(os.path.dirname(parent_path) + os.path.sep + ".") 24 | print("项目目录为{}".format(root_path)) 25 | ABIcommand=adb+" -s {} shell getprop ro.product.cpu.abi".format(devices) 26 | ABI=os.popen(ABIcommand).read().strip() 27 | print("ABI为{}".format(ABI)) 28 | AndroidVersion = os.popen(adb + " -s {} shell getprop ro.build.version.sdk".format(devices)).read().strip() 29 | airtest_minicap_path=os.path.abspath(os.path.dirname(root_path) + os.path.sep + ".")+"\\airtest\\core\\android\\static\\stf_libs" 30 | airtest_minicapso_path= os.path.abspath(os.path.dirname(root_path) + os.path.sep + ".")+"\\airtest\\core\\android\\static\\stf_libs\\minicap-shared\\aosp\\libs\\"+"android-{}\\{}\\minicap.so".format(AndroidVersion,ABI) 31 | push_minicap=adb + " -s {} push {}/{}/minicap".format(devices,airtest_minicap_path,ABI) +" /data/local/tmp/" 32 | push_minicapso = adb + " -s {} push {}".format(devices, airtest_minicapso_path) + " /data/local/tmp/" 33 | print("推送minicap和minicap.so") 34 | os.popen(push_minicap) 35 | os.popen(push_minicapso) 36 | chmod=adb+ " -s {} shell chmod 777 /data/local/tmp/*".format(devices) 37 | print("赋予权限成功") 38 | os.popen(chmod) 39 | wm_size_command=adb+" -s {} shell wm size".format(devices) 40 | vm_size=os.popen(wm_size_command).read() 41 | vm_size=vm_size.split(":")[1].strip() 42 | print("屏幕分辨率为{}".format(vm_size)) 43 | start_minicap=adb + " -s {} shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P {}@{}/0 -t".format(devices,vm_size,vm_size) 44 | result=os.popen(start_minicap).read() 45 | print(result) 46 | print("设备{}上已经成功安装并开启了MiniCap。".format(devices)) 47 | except Exception as e: 48 | print( e,traceback.format_exc()) 49 | 50 | if __name__=="__main__": 51 | devicesList = Madb().get_devicesList() 52 | if devicesList[0] == "": 53 | devicesList = Madb().getdevices() 54 | print("最终的devicesList=", devicesList) 55 | for device in devicesList: 56 | ini_MiniCap(device) 57 | #GetScreenbyMiniCap(time.time(),device,"测试") 58 | -------------------------------------------------------------------------------- /tools/Json.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import xlwings as xw 5 | import os 6 | import time 7 | import json 8 | import numpy as np 9 | 10 | ''' 11 | 生成一个json文件用来存储每次性能测试的数据 12 | ''' 13 | 14 | reportpath = os.path.join(os.getcwd(), "Report") 15 | datapath=os.path.join(reportpath, "Data") 16 | 17 | def create_log_json(nowtime,device): 18 | create_time = time.strftime("%m%d%H%M", nowtime) 19 | jsonfile =datapath+"\\"+ create_time + "_" + device + "_log.json" 20 | if os.path.exists(jsonfile): 21 | raise Exception( "FileHasExisted") 22 | f = open(jsonfile, "w") 23 | resultData = { 24 | "Time_series": [], 25 | "TotalMemory": [], 26 | "AllocatedMemory": [], 27 | "UsedMemory": [], 28 | "FreeMemory": [], 29 | "TotalCPU": [], 30 | "AllocatedCPU": [], 31 | "FPS": [], 32 | "PNGAddress": [], 33 | "data_count": [], 34 | } 35 | f.write(json.dumps(resultData)) 36 | f.close() 37 | return jsonfile 38 | ''' 39 | 由于highcharts绘图需要浮点数,在记录到json前,先强制将所有的性能数据格式化一下。 40 | ''' 41 | def record_to_json(jsonfilepath,list): 42 | for i in range(len(list)): 43 | if list[i] =="N/a": 44 | list[i] = "0" 45 | list[1]=float(list[1]) 46 | list[2]=float(list[2]) 47 | list[3]=float(list[3]) 48 | list[4]=float(list[4]) 49 | list[5]=float(list[5]) 50 | list[6]=float(list[6]) 51 | list[7]=float(list[7]) 52 | f = open(jsonfilepath, "r+") 53 | strdata=f.read() 54 | #将文件输入点定位到文件头,每次用最新数据覆盖老数据。 55 | f.seek(0) 56 | dictdata=json.loads(strdata) 57 | dictdata["Time_series"].append(list[0]) 58 | dictdata["TotalMemory"].append(list[1]) 59 | dictdata["AllocatedMemory"].append(list[2]) 60 | dictdata["UsedMemory"].append(list[3]) 61 | dictdata["FreeMemory"].append(list[4]) 62 | dictdata["TotalCPU"].append(list[5]) 63 | dictdata["AllocatedCPU"].append(list[6]) 64 | dictdata["FPS"].append(list[7]) 65 | dictdata["PNGAddress"].append(list[8]) 66 | strdata=json.dumps(dictdata) 67 | f.write(strdata) 68 | f.close() 69 | 70 | ''' 71 | 类似excel的统计函数,计算各个字段的最大、最小、平均值。然后写回文件。 72 | ''' 73 | def calculate_by_json(jsonfile): 74 | f = open(jsonfile, "r+") 75 | strdata=f.read() 76 | f.seek(0) 77 | dictdata=json.loads(strdata) 78 | memorylist=list(dictdata["AllocatedMemory"]) 79 | cpulist=list(dictdata["AllocatedCPU"]) 80 | fpslist=list(dictdata["FPS"]) 81 | while 0 in memorylist: 82 | memorylist.remove(0) 83 | while 0 in cpulist: 84 | cpulist.remove(0) 85 | while 0 in fpslist: 86 | fpslist.remove(0) 87 | Max_AllocatedMemory=max(memorylist) 88 | Min_AllocatedMemory=min(memorylist) 89 | Avg_AllocatedMemory=format(np.average(memorylist),".2f") 90 | Max_AllocatedCPU=max(cpulist) 91 | Min_AllocatedCPU=min(cpulist) 92 | Avg_AllocatedCPU=format(np.average(cpulist),".2f") 93 | Max_FPS=Min_FPS=Avg_FPS="N/a" 94 | #防止对某些应用或某些机型,因取不到fps导致max函数报错因而中断流程的问题。 95 | if len(fpslist)!=0: 96 | Max_FPS=max(fpslist) 97 | Min_FPS=min(fpslist) 98 | Avg_FPS=format(np.average(fpslist),".2f") 99 | dictdata["data_count"].append({"Max_AllocatedMemory": [Max_AllocatedMemory], "Min_AllocatedMemory": [Min_AllocatedMemory], "Avg_AllocatedMemory": [Avg_AllocatedMemory], "Max_AllocatedCPU": [str(Max_AllocatedCPU)+"%"], "Min_AllocatedCPU": [str(Min_AllocatedCPU)+"%"], "Avg_AllocatedCPU": [str(Avg_AllocatedCPU)+"%"], "Max_FPS": [Max_FPS], "Min_FPS": [Min_FPS], "Avg_FPS": [Avg_FPS]}) 100 | strdata=json.dumps(dictdata) 101 | print("strdata=",strdata) 102 | f.write(strdata) 103 | f.close() 104 | 105 | -------------------------------------------------------------------------------- /tools/ScreenOFF.py: -------------------------------------------------------------------------------- 1 | __author__ = "无声" 2 | 3 | # !/usr/bin/python 4 | # -*- coding: UTF-8 -*- 5 | 6 | import sys,os,time 7 | 8 | from airtest.core.android.adb import ADB 9 | adb = ADB().adb_path 10 | 11 | def setScreenOFF(device): 12 | platform = sys.platform 13 | command1 = command2 = "" 14 | # setScreenON(device) 15 | if platform == "win32": 16 | command1 = "adb -s {} shell dumpsys window policy|findstr mScreenOnFully".format(device) 17 | else: 18 | command1 = "adb -s {} shell dumpsys window policy|grep mScreenOnFully".format(device) 19 | print(command1) 20 | result = os.popen(command1) 21 | line = result.read() 22 | if "mScreenOnEarly=false" in line: 23 | pass 24 | else: 25 | command2 = "adb -s {} shell input keyevent 26".format(device) 26 | os.popen(command2) 27 | off = True 28 | n = 0 29 | while off or n < 10: 30 | result = os.popen(command1) 31 | line = result.read() 32 | if "mScreenOnEarly=true" in line: 33 | os.popen(command2) 34 | time.sleep(2) 35 | else: 36 | break 37 | n += 1 38 | print(device, "has been ScreenOFF") 39 | -------------------------------------------------------------------------------- /tools/Screencap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import traceback 5 | import os,inspect 6 | import time 7 | from PIL import Image 8 | from airtest.core.android.adb import ADB 9 | 10 | ''' 11 | _print = print 12 | def print(*args, **kwargs): 13 | _print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), *args, **kwargs) 14 | ''' 15 | adb = ADB().adb_path 16 | reportpath = os.path.join(os.getcwd(), "Report") 17 | screenpath = os.path.join(reportpath, "Screen") 18 | 19 | def GetScreen(starttime,devices,action): 20 | ABIcommand = adb + " -s {} shell getprop ro.product.cpu.abi".format(devices) 21 | ABI = os.popen(ABIcommand).read().strip() 22 | if ABI=="x86": 23 | png = GetScreenbyADBCap(starttime, devices, action) 24 | else: 25 | try: 26 | png=GetScreenbyADBCap (starttime,devices,action) 27 | except: 28 | png=GetScreenbyMiniCap(starttime,devices,action) 29 | return png 30 | 31 | #用ADBCAP的方法截图 32 | def GetScreenbyADBCap(starttime,devices,action): 33 | #先给昵称赋值,防止生成图片的命名格式问题。 34 | 35 | if ":" in devices: 36 | nickname = devices.split(":")[1] 37 | else: 38 | nickname=devices 39 | print("screenpath=",screenpath) 40 | png = screenpath +"\\"+ time.strftime('%Y%m%d_%H%M%S',time.localtime(starttime))+ nickname+ "_" + action+ ".png" 41 | print("png=",png) 42 | os.system(adb + " -s " + devices + " shell screencap -p /sdcard/screencap.png") 43 | time.sleep(1) 44 | fp = open(png, "a+", encoding="utf-8") 45 | fp.close() 46 | os.system(adb + " -s " + devices + " pull /sdcard/screencap.png " + png) 47 | time.sleep(0.5) 48 | #ADB截图过大,需要压缩,默认压缩比为0.2,全屏。 49 | compressImage(png) 50 | print("") 51 | return png 52 | 53 | #用MiniCap的方法截图,使用前需要确保手机上已经安装MiniCap和MiniCap.so。一般用过STF和airtestide的手机会自动安装,若未安装,则可以执行Init_MiniCap.py,手动安装。 54 | def GetScreenbyMiniCap(starttime,devices,action,ssFlipped=False): 55 | # 先给昵称赋值,防止生成图片的命名格式问题。 56 | if ":" in devices: 57 | nickname = devices.split(":")[1] 58 | else: 59 | nickname=devices 60 | #创建图片 61 | png = screenpath + "\\" + time.strftime("%Y%m%d_%H%M%S_", time.localtime(starttime)) + nickname + "_" + action + ".png" 62 | #获取设备分辨率 63 | wmsizecommand = adb + " -s {} shell wm size".format(devices) 64 | size = os.popen(wmsizecommand).read() 65 | size = size.split(":")[1].strip() 66 | if ssFlipped: 67 | slist = size.split("x") 68 | size = slist[1] + "x" + slist[0] 69 | #将设备号和分辨率填入minicap的命令,获得截图。 70 | screen=adb + " -s {} shell \" LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P {}@{}/0 -s > /sdcard/screencap.png\"".format(devices,size, size) 71 | print(screen) 72 | os.popen(screen) 73 | time.sleep(0.5) 74 | os.system(adb + " -s " + devices + " pull /sdcard/screencap.png " + png) 75 | print("") 76 | print("返回的png为",png) 77 | return png 78 | 79 | # 图片压缩批处理,cr为压缩比,其他参数为屏幕截取范围 80 | def compressImage(path,cr=0.2,left=0,right=1,top=0,buttom=1): 81 | # 打开原图片压缩 82 | sImg =Image.open(path) 83 | w, h = sImg.size# 获取屏幕绝对尺寸 84 | box=(int(w*left),int(h*top),int(w*right),int(h*buttom)) 85 | sImg=sImg.crop(box) 86 | time.sleep(0.1) 87 | # 设置压缩尺寸和选项 88 | dImg = sImg.resize((int(w*cr), int(h*cr)), Image.ANTIALIAS) 89 | time.sleep(0.1) 90 | # 压缩图片路径名称 91 | dImg.save(path) # save这个函数后面可以加压缩编码选项JPEG之类的 92 | 93 | 94 | 95 | if __name__=="__main__": 96 | #GetScreen(time.time(),"172.16.6.82:7437","test") 97 | GetScreenbyMiniCap(time.time(),"172.16.6.82:7597","test") 98 | 99 | 100 | -------------------------------------------------------------------------------- /tools/TimeOut.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "无声" 3 | 4 | import threading 5 | import sys 6 | 7 | 8 | class KThread(threading.Thread): 9 | 10 | def __init__(self, *args, **kwargs): 11 | threading.Thread.__init__(self, *args, **kwargs) 12 | self.killed = False 13 | self.exception=None 14 | 15 | def start(self): 16 | self.__run_backup = self.run 17 | self.run = self.__run 18 | threading.Thread.start(self) 19 | 20 | def __run(self): 21 | sys.settrace(self.globaltrace) 22 | self.__run_backup() 23 | self.run = self.__run_backup 24 | 25 | def globaltrace(self, frame, why, arg): 26 | if why == 'call': 27 | return self.localtrace 28 | else: 29 | return None 30 | 31 | def localtrace(self, frame, why, arg): 32 | if self.killed: 33 | if why == 'line': 34 | raise SystemExit() 35 | return self.localtrace 36 | 37 | def kill(self): 38 | self.killed = True 39 | 40 | 41 | class Timeout(Exception): 42 | 43 | def timeout(seconds): 44 | 45 | def timeout_decorator(func): 46 | 47 | def _new_func(oldfunc, result, oldfunc_args, oldfunc_kwargs): 48 | result.append(oldfunc(*oldfunc_args, **oldfunc_kwargs)) 49 | 50 | def _(*args, **kwargs): 51 | result = [] 52 | new_kwargs = { 53 | 'oldfunc': func, 54 | 'result': result, 55 | 'oldfunc_args': args, 56 | 'oldfunc_kwargs': kwargs 57 | } 58 | thd = KThread(target=_new_func, kwargs=new_kwargs) 59 | thd.start() 60 | thd.join(seconds) 61 | alive = thd.isAlive() 62 | thd.kill() 63 | if alive: 64 | raise Timeout(u'function run too long, timeout %d seconds.' % seconds) 65 | elif thd.exception is not None: 66 | raise thd.exception 67 | return result[0] 68 | 69 | _.__name__ = func.__name__ 70 | _.__doc__ = func.__doc__ 71 | return _ 72 | 73 | return timeout_decorator -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #构造文件 --------------------------------------------------------------------------------