airtest+poco多脚本、多设备批处理运行测试用例自动生成测试报告
一:主要内容
框架功能、框架架构及测试报告效果
airtest安装、环境搭建
框架搭建、框架运行说明
框架源码
二:框架功能及测试报告效果
1. 框架功能:
该框架笔者用来作为公司的项目的前端自动化,支持pc和app,本文的air脚本是针对app的,关于pc的脚本会专门在写一篇文章说明,该框架功能如下:
支持在安卓多台设备中批量运行所有后缀为air的测试脚本(因为ios的连接需要macOS,我是windows机所以暂时只连了安卓端的ios未做测试)
支持指定某个用例或某几个用例在某台设备或某几台设备中进行运行
支持控制测试用例执行顺序,默认会将登录用例排在第一,退出用例排在最后执行,如果想要自定义其他顺序,可以在run.py文件中修改sort_cases函数方法即可
支持多脚本多设备运行完成后,生成一份汇总的测试报告,且点击汇总测试报告中具体的某一个用例,还能查看该用例详细的airtest报告
2. 框架架构说明
3. 测试报告效果:
给大家看一下多设备、多脚本的测试报告效果:
点击详情效果:
三:airtest安装、环境搭建
1.python环境安装
这里不再赘述,安装并配置好环境变量后,执行python -V查看是否安装成功
2.airtestIDE安装
airtest安装很简单,安装airtestIDE,从官网下载:http://airtest.netease.com/
下载后解压缩到本地,我的本地位置为:G:\AirtestIDE_2020-01-21_py3_win64\AirtestIDE_2020-01-21_py3_win64\AirtestIDE.exe,双击exe文件即为启动airtestIDE工具即可
3.包安装
需要安装如下包:
pip install airtest
pip install pocoui
如果执行不能安装成功,则可以使用如下命令:
pip install -i http://pypi.douban.com/simple --trusted-host pypi.douban.com airtest
pip install -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com pocoui
如果想用airtest编写selenium即pc自动化脚本,则还需要安装如下包:
pip install seleniumpip install pynputpip install airtest_selenium
关于这一步的安装也就是 pip install airtest_selenium,也可以从airtest安装目录下拷贝该文件夹到python目录下
我的python目录为:G:\python3.6.5;
我的airtest安装目录为:G:\AirtestIDE_2020-01-21_py3_win64\AirtestIDE_2020-01-21_py3_win64,该路径下有个airtest_selenium文件夹;
可以拷贝airtest目录下的airtest_selenium文件夹到python目录下。
如果想用airtest编写selenium即pc自动化脚本,除了安装上面的包,因为airtest-selenium自动化因为需要打开浏览器,所以我们还需要配置谷歌浏览器路径和下载匹配的谷歌驱动文件
airtest设置谷歌启动路径:airtestIDE界面-点击选项-点击设置-点击chrome path-选择谷歌安装路径一直到chrome.exe文件
下载匹配的谷歌驱动文件:
可以使用该网站下载:https://npm.taobao.org/mirrors/chromedriver
下载后替换掉airtest根目录我的路径是G:\AirtestIDE_2020-01-21_py3_win64\AirtestIDE_2020-01-21_py3_win64下的chromedriver.exe文件即可
4.框架版本说明
该框架使用版本如下:
python 3.6.5airtest 1.1.3pocoui 1.0.79pynput 1.6.8airtestIDE 1.2.3
四:框架搭建、框架运行说明
1.框架搭建
该框架搭建很简单,就是一个python工程:
该工程根目录下开始时有一个result空文件夹、一个report_tpl.html模板文件、run.py启动脚本、docs文件夹是我自己放的一些项目描述文档可有可无,.air文件是自己通过airtestIDE编写的项目的自动化脚本
2.框架脚本文件说明
run.py #启动文件,python run.py即可
report_tpl.html #测试报告模板文件
report.html #自动生成的测试报告文件,会将汇总的执行结果的json数据即下面的summary数据格式与report_tpl.html结合,生成测试报告
result #文件夹,用于存放每个测试用例的执行json结果数据格式为下面的results数据格式
xxx.air #测试用例,所有以.air文件名称结尾的文件夹都是测试用例
xxx.air/log #每个测试用例的日志文件,以设备号区分,每个设备号下存放一份测试结果日志文件log.html #每个测试用例在每个设备中运行的具体效果,即测试报告中点击具体测试用例右侧弹出的页面详情效果log.txt #每个测试用例在每个设备中运行的json结果数据
3.框架运行编写建议
执行命令时可以用python run.py运行整个框架
但是写脚本或者调试脚本时,用airtestIDE来操作,即从airtestIDE中新建编辑.air脚本保存到该框架的根目录下,调试通过后再用run.py进行批量脚本、批量设备去执行。
这样就比较清晰
五:框架源码
1.run.py
1 # -*- encoding=utf-8 -*-2 # Run Airtest in parallel on multi-device3 import os4 import traceback5 import subprocess6 import webbrowser7 import time8 import json9 import shutil10 from airtest.core.android.adb import ADB11 from jinja2 import Environment, FileSystemLoader12 13 14 def run(devices, airs):15 """"16 run_all17 18 19 """20 try:21 data_r=[]22 global time_s23 time_s = time.time()24 for air in airs:25 results = load_jdon_data(air)26 tasks = run_on_multi_device(devices, air, results)27 for task in tasks:28 status = task['process'].wait()29 results['tests'][task['dev']] = run_one_report(task['air'], task['dev'])30 results['tests'][task['dev']]['status'] = status31 name = air.split(".")[0]32 json.dump(results, open(get_path("result")+os.sep+name+'_data.json', "w"), indent=4)33 data_r.append(results)34 run_summary(data_r)35 except Exception as e:36 traceback.print_exc()37 38 39 def run_on_multi_device(devices, air, results):40 """41 在多台设备上运行airtest脚本42 Run airtest on multi-device43 """44 tasks = []45 for dev in devices:46 log_dir = get_path("log",dev,air)47 #命令行执行:airtest run openOrder.air --device Android://127.0.0.1:5037/b7f0c036 --log F:\airtest_code\good_store_project\log\openOrder48 cmd = [49 "airtest",50 "run",51 air,52 "--device",53 "Android:///" + dev,54 "--log",55 log_dir56 ]57 try:58 tasks.append({59 'process': subprocess.Popen(cmd, cwd=os.getcwd()),60 'dev': dev,61 'air': air62 })63 except Exception as e:64 traceback.print_exc()65 return tasks66 67 #点击每个用例的详情页面68 def run_one_report(air, dev):69 """"70 生成一个脚本的测试报告71 Build one test report for one air script72 """73 try:74 log_dir = get_path("log",dev, air)75 log = os.path.join(log_dir, 'log.txt')76 if os.path.isfile(log):77 #命令行执行:airtest report F:\airtest_code\good_store_project\openOrder.air --log_root F:\airtest_code\good_store_project\log\openOrder --outfile F:\airtest_code\good_store_project\log\openOrder\openOrder.html --lang zh78 #如果是selenium,则最后要加上selenium插件79 #airtest report F:\airtest_code\good_store_project\openOrder.air --log_root F:\airtest_code\good_store_project\log\openOrder --outfile F:\airtest_code\good_store_project\log\openOrder\openOrder.html --lang zh --plugins airtest_selenium.report80 cmd = [81 "airtest",82 "report",83 air,84 "--log_root",85 log_dir,86 "--outfile",87 os.path.join(log_dir, 'log.html'),88 "--lang",89 "zh"90 ]91 ret = subprocess.call(cmd, shell=True, cwd=os.getcwd())92 return {93 'status': ret,94 'path': os.path.join(log_dir, 'log.html')95 }96 else:97 print("Report build Failed. File not found in dir %s" % log)98 except Exception as e:99 traceback.print_exc()
100 return {'status': -1, 'device': dev, 'path': ''}
101
102
103 def run_summary(data):
104 """"
105 生成汇总的测试报告
106 Build sumary test report
107 """
108 try:
109 for i in data:
110 c = get_json_value_by_key(i,"status")
111
112 summary = {
113 'time': "%.3f" % (time.time() - time_s),
114 'success': c.count(0),
115 'count': len(c)
116 }
117 summary['start_all'] = time.strftime("%Y-%m-%d %H:%M:%S",
118 time.localtime(time_s))
119 summary["result"] = data
120 print("summary++++++++++",summary)
121
122 env = Environment(loader=FileSystemLoader(os.getcwd()),
123 trim_blocks=True)
124 html = env.get_template('report_tpl.html').render(data=summary)
125 with open("report.html", "w", encoding="utf-8") as f:
126 f.write(html)
127 webbrowser.open("report.html")
128 except Exception as e:
129 traceback.print_exc()
130
131
132 def load_jdon_data(air):
133 """"
134 加载进度
135 返回一个空的进度数据
136 """
137 clear_log_dir(air)
138 return {
139 'start': time.time(),
140 'script': air,
141 'tests': {}
142
143 }
144
145 def clear_log_dir(air):
146 """"
147 清理log文件夹 openCard.air/log
148 Remove folder openCard.air/log
149 """
150 log = os.path.join(os.getcwd(), air, 'log')
151 if os.path.exists(log):
152 shutil.rmtree(log)
153
154 #获取key为status的值
155 def get_json_value_by_key(in_json, target_key, results=[]):
156 for key,value in in_json.items(): # 循环获取key,value
157 if key == target_key:
158 results.append(value)
159 if isinstance(value, dict):
160 get_json_value_by_key(value,target_key)
161 return results
162
163 #获取路径
164 def get_path(content,device=None,air="openCard.air"):
165 root_path = os.getcwd()
166 path = os.getcwd()
167 if content=="result":
168 #返回测试报告路径
169 path = os.path.join(root_path,"result")
170 elif content == "log":
171 log_dir = os.path.join(root_path,air, 'log', device.replace(".", "_").replace(':', '_'))
172 #如果没有日志路径则创建一个
173 if not os.path.exists(log_dir):
174 os.makedirs(log_dir)
175 #返回日志路径
176 path = log_dir
177 elif content == "cases":
178 #返回测试用例路径
179 path = os.path.join(root_path,air)
180 else:
181 #返回根目录
182 path = root_path
183 return path
184
185 #获取路径下所有air的测试用例文件
186 def get_cases(path):
187 cases=[]
188 for name in os.listdir(get_path(path)): # 遍历当前路径下的文件夹和文件名称
189 if name.endswith(".air"):
190 cases.append(name)
191 return cases
192
193 def sort_cases(cases,loginAir,outAir):
194 #清除列表中的登录、退出登录,然后将其分别添加到列表的第一位和最后一位
195 cases.remove(loginAir)
196 cases.remove(outAir)
197 cases.insert(0, loginAir)
198 cases.insert(len(airs), outAir)
199 return cases
200
201
202 if __name__ == '__main__':
203
204 """
205 初始化数据
206 Init variables here
207 """
208 #获取所有已连接的设备列表
209 devices = [tmp[0] for tmp in ADB().devices()]
210 #设置指定设备执行测试用例
211 # devices = ["BTY4C16705003852","b7f0c036"]
212 #获取所有测试用例
213 airs = get_cases("root")
214 #将登录用例排在最前面执行,退出用例排在最后面执行
215 sort_airs = sort_cases(airs,"loginPro.air","loginOutPro.air")
216 #获取指定用例,按顺序执行
217 # sort_airs = ["openCardPro.air","openOrderPro.air","quickMoneyPro.air"]
218 """
219 执行脚本
220 excute scripts
221 """
222 # 运行所有脚本
223 run(devices, sort_airs)
2.report_tpl.html
1 <!DOCTYPE html>2 <html>3 <head>4 <meta http-equiv="X-UA-Compatible" content="IE=edge">5 <link rel="shortcut icon" type="image/png" href="http://airtest.netease.com/static/img/icon/favicon.ico">6 <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>7 <meta name="viewport" content="width=device-width, initial-scale=1">8 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 9 <title>Airtest 多设备并行测试结果汇总</title>10 </head>11 <style type="text/css">12 *{13 margin: 0;14 padding: 0;15 }16 body{17 background: #eeeeee18 }19 .container {20 width: 75%;21 min-width: 800px;22 margin: auto23 }24 body.zh .en{25 display: none;26 }27 body.en .zh{28 display: none;29 }30 h1{31 margin-top: 50px;32 text-align: center;33 }34 .center{35 text-align: center;36 margin-top: 15px;37 margin-bottom: 30px;38 font-size: 14px;39 position: relative;40 }41 .btn{42 border: solid 1px #c0c0c0;43 padding: 5px 20px;44 border-radius: 3px;45 background: white;46 cursor: context-menu;47 }48 .btn.lang:hover {49 background: #5cb85c26;50 border-color: #0a790a;51 }52 .btn.lang {53 position: absolute;54 top: 0;55 }56 .head {57 margin: 20px 0 30px 0;58 }59 .head, .table{60 background: white;61 border-radius: 5px;62 box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);63 padding: 30px 20px;64 65 }66 .head .progress{67 background: #dddddd;68 color: white;69 border-radius: 5px;70 text-align: center;71 margin-top: 12px;72 }73 .head .progress-bar-success{74 width: 0;75 transition: all 0.5s ease;76 background: #5cb85c;77 border-radius: 5px;78 }79 .table-title {80 text-align: center;81 margin-bottom: 20px;82 font-size: 18px;83 font-weight: bold;84 position: relative;85 }86 .table-row{87 border: solid 1px #e5e5e5;88 margin-top: -1px;89 cursor: context-menu;90 }91 .table-row:hover, .table-row.active{92 background: beige;93 }94 .table-head{95 background: aliceblue;96 }97 .table-head:hover{98 background: aliceblue;99 }
100 .table-head .table-col{
101 padding-top: 10px;
102 padding-bottom: 10px;
103 font-weight: bold;
104 text-align: center;
105 }
106 .table-col{
107 display: inline-block;
108 width: 200px;
109 line-height: 30px;
110 padding: 5px 10px;
111 border-left: solid 1px #e5e5e5;
112 margin-top: -1px;
113 margin-right: -5px;
114 }
115 .table-col.short{
116 width: 100px;
117 text-align: center;
118 }
119 .table-col.mid{
120 width: 200px;
121 text-align: center;
122 }
123 .table-col:first-child{
124 border: none;
125 }
126 .table-col.long{
127 width: calc(100% - 700px);
128 }
129 .table-col.success{
130 color: green;
131 }
132 .table-col.failed{
133 color: red;
134 }
135 .detail{
136 text-align: center;
137 font-size: 14px;
138 color: gray;
139 }
140 .iframe{
141 position: fixed;
142 top: 0;
143 right: -100%;
144 width: 70%;
145 min-width: 800px;
146 height: 100%;
147 box-shadow: 0 5px 10px grey;
148 transition: right 0.5s ease;
149 background: white;
150 max-width: 1100px;
151 }
152 .iframe-tools{
153 position: absolute;
154 top: 23px;
155 left: -34px;
156 background: white;
157 box-shadow: -2px 2px 5px grey;
158 border-radius: 7px;
159 }
160 .iframe-tools .close, .iframe-tools .open{
161 width: 32px;
162 height: 50px;
163 color: gray;
164 cursor: context-menu;
165 display: block;
166 }
167 .iframe.show{
168 right: 0;
169 }
170 iframe{
171 width: 100%;
172 height: calc(100% - 70px);
173 border: none;
174 }
175 .iframe-head {
176 height: 60px;
177 line-height: 70px;
178 text-align: center;
179 border-bottom: solid 1px #ddd;
180 box-shadow: 2px 0 6px #999;
181 margin-bottom: 10px;
182 }
183 ::-webkit-scrollbar {
184 width: 10px;
185 height: 10px;
186 background-color: rgba(0,0,0,.34);
187 }
188 ::-webkit-scrollbar-thumb {
189 background-color: #8b8b8b;
190 border-radius: 10px;
191 }
192 ::-webkit-scrollbar-track {
193 background-color: #f5f5f5;
194 -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.22);
195 }
196 .iframe .close {
197 background: url('data:img/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgEAYAAAAj6qa3AAAABGdBTUEAALGPC/xhBQAAAAFzUkdC AK9OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dE AAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAA6JJREFUaN7tmc9LG0EUx99sE+xB/wBJ vQhCPLTojAr+Af6iKJSSVhD0EhNC1HgWxIt/gBA0MaQXqQoV2gb/Ag+Krs4KHlSMN38EFRGkIupm Xw/reEgIu9lsNi34vQTcie99P/NmZt8E4FWvKkpt6bZ0W/rdu0rnYVdektmBNEETNPHlS3Ytu5Zd Oz6mC3SBLgwMVNo4o4wy2tmp1qv1av3REVthK2xldNS2ANRN3dT99SvVqEa1pyfGGGMMkcpUprKq VgqEME436SbdvL8XebEAC7CAppkFQYyMwwM8wMP376SVtJJWlyt3HMYwhrFsFg7hEA6HhpRBZVAZ XFwst3GcwzmcS6VImIRJ+O3b/IHAgCFCB3RARyTCfdzHfdGoIQCxlkSpQwxiEKuqMkoMt3Ebt1UV kpCE5MCAElACSuDHD7uMN280bzRvdHcTiUhE+vWroPHcvJ4nCBuxERvfv9+t2a3ZrTk4EM/z9gC5 QW6QG05PyR7ZI3uitJ+ejAK9VEgTNEHT0pJdS0PMeLHGRQWQEAmR0OhorvGXvI3+T8tYy1jL2OfP uI7ruL68rP/V7TZL3urSMF3qBYwDBw48HOacc85jsULDDQE4DcIp40UDKDcIp41bBmA3CDJDZsjM 1ZXTxksG8JIHZZRRnw93cAd3lpYKHZd5IMSpIYMMsqqaNh6EIAQ1DeqgDuoCAd7De3jPt29W8y8Z gJDVijAtm2Y8V2/syu9863zrfOvgwOPz+Dy+/X04gRM4+fTpOYz1OGUybjsA20GU2biQ6WaoWGnV WrVW/ecPzuIszmazxX4f/ehHv6ZhBCMYub0tV5627QFCdIJO0ImuLuiDPuj7/dv05lYIRJl7DdsA 2G3cKRAlA7DapIjjTJR60cenTU2X5T1AzLjVJgXmYR7mR0akcWlcGu/v1x8633QVXQGWS91gV69U 02UaQLmM58ppEIYAnDJeKRCFr8QqZNxpEAWvxNRr9Vq9TqeLblLiEId4MMgVrnAlmbRqPI9riU0X 3uEd3n34kHszlPdqehY9i55Fb29re2t7a3svL4mXeIn340d9Rkl+xYgZX4VVWB0Z0Y0nEnYZF8pk MplMZn/fc+O58dyYf8UmLuIirqkppV1pV9p//sx7bhSYpmiKpoaHyQW5IBfxuH58SZJT7+qFZLg0 QhCC0OQk93M/909PlxxQgBC/B+j38KGQU4aNQOj5PD7qn5OTZQuoB/B6K238f8nrVf+6/gLOvYPg ZwC/JwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMy0wNlQyMDozMTo1NCswODowMMqAOUgAAAAl dEVYdGRhdGU6bW9kaWZ5ADIwMTktMDMtMDZUMjA6MzE6NTQrMDg6MDC73YH0AAAASHRFWHRzdmc6 YmFzZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9jOHk0dXZzNXd0 Zy9DbG9zZS5zdmfc199nAAAAAElFTkSuQmCC') no-repeat;
198 background-size: 20px;
199 background-position: center;
200 border-bottom: solid 2px #e5e5e5;
201 }
202 .iframe .open {
203 background: url("data:img/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgEAYAAAAj6qa3AAAABGdBTUEAALGPC/xhBQAAAAFzUkdC AK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dE AAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAABS5JREFUaN7tmGtIU28cx3+/s0tFGyEW SLfJ0F4Yk3YttNUY5prBoMsha1EUQoUZESW9KIXKYWiZQhR7UfYiRgmZkBaGLCuTaucsR6RiVxLp It1cLJjn/P4vxurf3785by3Rz5uxZ8/vec73s+fsec4Appjir0Qn08l0so0bDdcN1w3X1erxmoeJ d9DBwGqsxup586iYiqnY69WWa8u15SrVpBHwKwsXMipGxahu3TKyRtbIJiVNMgEAUAqlUJqaKr4Q X4gvGhuN3cZuY3di4uQR8AsajaAUlIKyoSEzMTMxM1GpnGQCANCKVrSaTCF3yB1y37yZXpZell42 c+akEfBDhAtd6MrIkG6Xbpdur61NqUqpSqmaNm3CCdDe197X3k9J0ev0Or2OZUkggYSsrJhF2NCG tlWrZvXO6p3V6/GspJW0kqTSoeqG7DDWGBIMCYaEzExSk5rUTicchINw0G6HAiiAguRkQEBAAKzE SqwcwQT1UA/1a9cGpUFpUFpdHWncujXyKooDxI1XUJZlWZaVSF6GXoZehjZvph20g3YcOgQlUAIl aWl/QjYAANRADdS43ZyaU3PqXbsijUTjJkC3RbdFt8VsBic4wXn2LB7Gw3h48eI/FngQyEY2slVU 8C7exbv27x8zAZF7Viaj1bSaVp84gb3Yi7379gEHHHA4bitsdBw7xnEcx3FFRSO+QI1Go9FoEhLk drldbq+rgyZogiazOd7RYoVUpCJVYeGwd4Eld5bcWXJnzhy5XC6Xy5ubJ1rwKLgMl+Gy5OSYBaRd SbuSdkWhYD4xn5hP9fWRVo0m3kFGxsWLXCFXyBUWFMQsYPrS6UunLz1zBo/iUTxqNMY7wrBZA2tg TW2twqfwKXx5eZFGURzyHGDYa9hr2Lt+Pa2jdbQuup9ONBobv8z+MvvL7E2bOOSQw/7+6CeSwUpM XaYuU9f8+aIgCqJw4wY8gAfwYMaMeEcZHl6v7JrsmuyawxHIDmQHsr9//2+PQVdAv7xf3i8vKcEq rMKqhIRRX4se9KAnimyPb95AGZRBWVcX8cQT//o1etCDnmCQLtAFuhAKQRIkQZJGg3a0oz0nJ+Z5 jsNxON7aGuoL9YX6HA5uAbeAWxAKDdZ9gIDIc/aiRUKP0CP0OJ3DzUnN1EzNT55AMRRDcUMDetGL 3qamcGo4NZza0hJwB9wB97dvYAUrWP9ngHRIh3QA/SX9Jf2l6IFlaAF0m27Tbb9fUAgKQZGT8xSf 4lMMBoeqGyBAPC+eF88XFWEd1mGdRPL78nCYrtJVuurxkItc5Dp3zq/wK/yK1tYBXXnggR+uzhgw gxnM7e2iX/SLfputbUXbirYVnz/HWv5DgDZDm6HNmDs3cqLLzY0IGKwsug0eOMCreBWv6ugYh2i/ h4CAnj8XH4oPxYdZWY9PPz79+PSHD8Md5ocAJp/JZ/Jzc2E37Ibd//rm8yEf8oNB2kk7aWdeHh/m w3z48uU/Hjiau5zKqby7W7AIFsGSldWGbdiGPT0jHe/nOeAUnIJTP+95ukt36W5np8iKrMiaTHEP XkEVVPHuHXOPucfciwZ/9Wq04zLRH73IW50usrQ6OiTbJNsk2ywWv9Kv9Cvb2+MWvJIqqfLjR9gA G2BDdrbviO+I70hn51iNLxUcgkNwWCy4HJfj8mfPoAVaoMVqfVTzqOZRzdu38QoOJ+EknPz6lelj +pg+u9333vfe9z4QGPN59KX6Un2p2x15rP3bzvbM+P9lp9uj26PbY7WOfqQpppiI/AOjmiKrfUvK NAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMy0wNlQyMDozMTo1NCswODowMMqAOUgAAAAldEVY dGRhdGU6bW9kaWZ5ADIwMTktMDMtMDZUMjA6MzE6NTQrMDg6MDC73YH0AAAASHRFWHRzdmc6YmFz ZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9jOHk0dXZzNXd0Zy9z aGFyZS5zdmftz7m3AAAAAElFTkSuQmCC") no-repeat;
204 background-size: 20px;
205 background-position: center;
206 }
207 select{height: 28px; line-height: auto; vertical-align: middle; height: 22px\9; padding: 3px 0\9; box-sizing:content-box; font-size: 13px;}
208 :root select{padding: 0; height: 28px;}
209 </style>
210 <body class="zh">
211 <div class="container-fluid" >
212 <div class="container">
213 <div class="main">
214 <div class="material">
215 <h1>汇总报告</h1>
216 <div class="center">
217 <div class="btn lang">Switch to English version</div>
218 <div class="time zh">开始时间:{{data['start_all']}},耗时 <b>{{data['time']}}</b> 秒</div>
219 <div class="time en">Started at:{{data['start_all']}},cost <b>{{data['time']}}</b> s</div>
220 </div>
221 <div class="head">
222 <header class="zh"><span class="rate"></span>成功率:</span> {{data["success"]}}/{{data["count"]}}</header>
223 <header class="en"><span class="rate">Success rate:</span> {{data["success"]}}/{{data["count"]}}</header>
224 <div>
225 <div class="progress">
226 <div class="progress-bar progress-bar-success" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width: {{data['success'] *100 / data['count']}}%">
227 <span class="">{{'%0.2f' % (data["success"] *100 / data["count"])}}%</span>
228 </div>
229 </div>
230 </div>
231 </div>
232 <select name="" id="exit" style="width: 100px;">
233 <option class="zh" value="all">全部</option>
234 <option class="en" value="all">all</option>
235 <option class="en" value="成功">success</option>
236 <option class="zh" value="成功">成功</option>
237 <option class="en" value="失败">failed</option>
238 <option class="zh" value="失败">失败</option>
239 </select>
240 <div class="table" >
241 <div class="table-title">
242 <span class="running_detail zh">用例列表</span>
243 <span class="running_detail en">Detail</span>
244 </div>
245 <div class="table-content" id="tab">
246 <div class="table-row table-head">
247 <div class="table-col short zh">序号</div>
248 <div class="table-col short zh">状态</div>
249 <div class="table-col mid zh">用例</div>
250 <div class="table-col long zh">设备</div>
251 <div class="table-col short en">id</div>
252 <div class="table-col short en">result</div>
253 <div class="table-col mid en">case</div>
254 <div class="table-col long en">device</div>
255 <div class="table-col ">--</div>
256 </div>
257 {% set ns = namespace(found=0) %}
258 {% for dat in data['result'] %}
259 {% for dev, item in dat['tests'].items() %}
260 <div class="table-row" path="{{item['path']}}" >
261 {% set ns.found = ns.found + 1 %}
262 <div class="table-col short">{{ns.found}}</div>
263 <div class="table-col short zh {{'success' if item['status']==0 else 'failed'}}">{{"成功" if item['status']==0 else "失败"}}</div>
264 <div class="table-col short en {{'success' if item['status']==0 else 'failed'}}">{{"success" if item['status']==0 else "failed"}}</div>
265 <div class="table-col mid">{{dat['script']}}</div>
266 <div class="table-col long">{{dev}}</div>
267
268 <div class="table-col detail zh">点击可查看详情</div>
269 <div class="table-col detail en">click to see detail</div>
270 </div>
271 {% endfor %}
272 {% endfor %}
273 </div>
274 </div>
275 </div>
276 </div>
277
278 <div class="iframe">
279 <div class="iframe-head"></div>
280 <iframe src='.'></iframe>
281 <div class="iframe-tools">
282 <div class="close"></div>
283 <a class="open" href='.' target='_blank'></a>
284 </div>
285 </div>
286 </div>
287 </body>
288 <script type="text/javascript">
289 var Lang = 'zh' // or en
290 var rows = document.querySelectorAll('.table-row')
291 var iframe = document.querySelector('.iframe')
292 var iframeHead = document.querySelector('.iframe-head')
293 var open = document.querySelector('.open')
294 var close = document.querySelector('.iframe .close')
295 var langBtn = document.querySelector('.lang')
296 var body = document.body
297 var prevActiveRow = null
298 function init() {
299 for(i=0; i<rows.length; i++){
300 addEvent(rows[i], 'click', function(e){
301 path = this.getAttribute('path')
302 console.log(this)
303 if(path) {
304 showIframe(this)
305 }
306 })
307 }
308 addEvent(close, 'click', function(e){
309 iframe.className='iframe'
310 })
311 addEvent(langBtn, 'click', function(e){
312 if(Lang == 'zh'){
313 Lang = 'en';
314 this.innerText = '切换到中文版'
315 } else {
316 Lang = 'zh'
317 this.innerText = "Switch to English version"
318 }
319 document.body.className = Lang
320 if (iframe.className.indexOf('show')>=0) {
321 showIframe(prevActiveRow)
322 }
323 })
324 document.body.className = Lang
325 }
326 function showIframe(obj){
327 var num = obj.querySelector('.table-col.short').innerText
328 var device = obj.querySelector('.table-col.long').innerText
329 if(Lang =='en') {
330 num = ordinal_suffix_of(num)
331 iframeHead.innerHTML = "Test report running in the " + num + ' device "' + device + '"'
332 open.setAttribute('title', 'open in a new tab')
333 close.setAttribute('title', 'close')
334 }
335 else {
336 iframeHead.innerHTML = "第 " + num + " 台设备 【" + device + "】 的测试报告"
337 open.setAttribute('title', '在新标签页打开')
338 close.setAttribute('title', '关闭')
339 }
340 iframe.querySelector('iframe').setAttribute('src', path)
341 open.setAttribute('href', path)
342 iframe.className='iframe show'
343 if(prevActiveRow){
344 prevActiveRow.className = "table-row"
345 }
346 obj.className = 'table-row active'
347 prevActiveRow = obj
348 }
349 function ordinal_suffix_of(i) {
350 i = Number(i)
351 var j = i % 10,
352 k = i % 100;
353 if (j == 1 && k != 11) {
354 return i + "st";
355 }
356 if (j == 2 && k != 12) {
357 return i + "nd";
358 }
359 if (j == 3 && k != 13) {
360 return i + "rd";
361 }
362 return i + "th";
363 }
364 function addEvent(obj,type,handle) {
365 try{// Chrome、FireFox、Opera、Safari、IE9.0 and above
366 obj.addEventListener(type,handle);
367 }catch(e){
368 try{// IE8.0 and below
369 obj.attachEvent('on'+ type,handle);
370 }catch(e){// Browser in earlier vesion
371 obj['on'+ type]= handle;
372 }
373 }
374 }
375 init()
376 $(document).ready(function(){
377 $('#exit').change(function(){ // 下拉框绑定change事件
378 var exit_code = $(this).children('option:selected').val(); // 获取下拉框选中值
379 $('#tab .table-row').each(function() {
380 var self = $(this).children().eq(1).text(); // 获取每行第二列的值
381 if(exit_code=='all'){ // 选中all时,数据全部显示
382 $(this).show();
383 }else{ // 选中其他的值时,进一步判断
384 if(self!=exit_code){ // 列中的值和选中值不一致
385 $(this).hide(); // 该行不显示
386 $('#tab .table-head').show()
387 }else{
388 $(this).show();
389 }
390 }
391 });
392 })
393 })
394 </script>
395 </html>
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】
相关文章:

airtest+poco多脚本、多设备批处理运行测试用例自动生成测试报告
一:主要内容 框架功能、框架架构及测试报告效果 airtest安装、环境搭建 框架搭建、框架运行说明 框架源码 二:框架功能及测试报告效果 1. 框架功能: 该框架笔者用来作为公司的项目的前端自动化,支持pc和app,本文…...

Prometheus套装部署到K8S+Dashboard部署详解
1、添加helm源并更新 helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update2、创建namespace kubectl create namespace monitoring 3、安装Prometheus监控套装 helm install prometheus prometheus-community/prome…...

python使用pymysql
为了封装这个数据库操作为一个通用方法,我们可以创建一个函数,该函数接受数据库连接参数(如主机名、用户名、密码、数据库名)、SQL语句以及必要的参数(用于参数化查询)。下面是一个简单的封装示例ÿ…...

Vue3 + TypeScript 组件和文件命名规范及 setup 导入顺序规范
前言 在 Vue3 项目中,合理的文件命名规范和导入顺序不仅有助于提高代码的可读性,还能增强团队协作的效率。特别是在使用 TypeScript 和 Composition API 的项目中,清晰的组件和文件结构尤为重要。本文将详细介绍 Vue3 TypeScript 项目中的组…...

netty之处理连接源码分析
写在前面 在这篇文章看了netty服务是如何启动的,服务启动成功后,也就相当于是迎宾工作都已经准备好了,那么当客人来了怎么招待客人呢?也就是本文要看的处理连接的工作。 1:正文 先启动源码example模块的echoserver&a…...

Dockerfile文件编写
1、打nginx原始包 登录后复制 ROM nginxENV LANG zh_CN.UTF-8 ENV LC_ALL zh_CN.UTF-8 ENV TZ Asia/Singapore# 设置时区,同样保持在一层 RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \echo "${TZ}" > /etc/timezoneRUN apt-get …...

Oracle SQL 使用 ROWNUM 分页查询速度太慢的问题及解决方案!
在使用 Oracle 数据库进行数据查询时,分页查询是一种常见的需求。传统上,开发者常常使用 ROWNUM 来实现分页功能。 然而,当数据量较大时,使用 ROWNUM 进行分页查询可能会导致性能问题。本文将深入探讨这一问题的原因,并提供多种解决方案,以提高分页查询的性能。 一、RO…...

Django3 + Vue.js 前后端分离书籍添加项目Web开发实战
文章目录 Django3后端项目创建切换数据库创建Django实战项目App新建Vue.js前端项目 Django3后端项目创建 创建Django项目,采用Pycharm或者命令行创建皆可。此处,以命令行方式作为演示,项目名为django_vue。 django-admin startproject djang…...

楼梯区域分割系统:Web效果惊艳
楼梯区域分割系统源码&数据集分享 [yolov8-seg-FocalModulation&yolov8-seg-GFPN等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Global Al l…...

Day10加一
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。 最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 你可以假设除了整数 0 之外,这个整数不会以零开头。 class Solution {public int[] plusOne(int[] di…...

UTF-8简介
UTF-8 UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用一至四个字节对Unicode字符集中的所有有效编码点进行编码,属于Unicode标准的一部分,最初由肯汤普逊…...

基于Openwrt系统架构,实现应用与驱动的实例。
一、在openwrt系统架构,编写helloworld的应用程序。 第一步先创建目录,项目代码要放在 openwrt根目下的 package 目录中,这里源码写在了 hellworld 的 src 目录下,因为外层还有需要编写的文件。 mkdir -p ~/openwrt/package/hel…...

SQL进阶技巧:如何利用三次指数平滑模型预测商品零售额?
目录 0 问题背景 1 数据准备 2 问题解决 2.1 模型构建 (1)符号规定 (2)基本假设...

HTB:Cicada[WriteUP]
目录 连接至HTB服务器并启动靶机 使用nmap对靶机进行开放端口扫描 使用nmap对靶机开放端口进行脚本、服务信息扫描 首先尝试空密码连接靶机SMB服务 由于不知道账户名,这里我们使用crackmapexec对smb服务进行用户爆破 通过该账户连接至靶机SMB服务器提取敏感信…...

小张求职记四
学校食堂的装修富丽堂皇,像个金碧辉煌的宫殿,可实际上却充斥着廉价的塑料制品和刺鼻的消毒水味。这金玉其外败絮其中的景象,与食堂承包商的“精明算计”如出一辙。 小张和小丽应约来到了一个档口下,“红烧肉”,之前就是…...

适用于 c++ 的 wxWidgets框架源码编译SDK-windows篇
本文章记录了下载wxWidgets源码在windows 11上使用visual Studio 2022编译的全过程,讲的不详细请给我留言,让我知道错误并改进。 本教程是入门级。有更深入的交流可以留言给我。 如今互联网流行现在大家都忘记了这块桌面的开发,我认为桌面应用还是有用武之地,是WEB无法替代…...

flink 内存配置(二):设置TaskManager内存
TaskManager在Flink中运行用户代码。根据需要配置内存使用,可以极大地减少Flink的资源占用,提高作业的稳定性。 注意下面的讲解适用于TaskManager 1.10之后的版本。与JobManager进程的内存模型相比,TaskManager内存组件具有类似但更复杂的结构…...

【C++ 算法进阶】算法提升八
复杂计算 (括号问题相关递归套路 重要) 题目 给定一个字符串str str表示一个公式 公式里面可能有整数 - * / 符号以及左右括号 返回最终计算的结果 题目分析 本题的难点主要在于可能会有很多的括号 而我们直接模拟现实中的算法的话code会难写 要考虑…...

阿里云实时数据仓库HologresFlink
1. 实时数仓Hologres特点 专注实时场景:数据实时写入、实时更新,写入即可见,与Flink原生集成,支持高吞吐、低延时、有模型的实时数仓开发,满足业务洞察实时性需求。 亚秒级交互式分析:支持海量数据亚秒级交…...

生成式语言模型的文本生成评价指标(从传统的基于统计到现在的基于语义)
文本生成评价指标 以 BLEU 为代表的基于统计的文本评价指标基于 BERT 等预训练模型的文本评价指标 1.以 BLEU 为代表的基于统计的文本评价指标 1.BLEU(Bilingual Evaluation Understudy, 双语评估辅助工具) 所有评价指标的鼻祖,核心思想是比较 候选译文 和 参考…...

【网安案例学习】暴力破解攻击(Brute Force Attack)
### 案例与影响 暴力破解攻击在历史上曾导致多次重大安全事件,特别是在用户数据泄露和账户被盗的案例中。随着计算能力的提升和密码管理技术的进步,暴力破解的威胁虽然有所减弱,但仍需警惕,特别是在面对高价值目标时。 【故事一…...

时间序列预测(十八)——实现配置管理和扩展命令行参数解析器
如图,这是一个main,py文件,在此代码中,最开始定义了许多模型参数,为了使项目更加灵活和可扩展,便于根据不同的需求调整参数和配置,可以根据实际需要扩展参数和配置项。 下面是如何实现配置管理和扩展命令行…...

Vue问题汇总解决
作者:fyupeng 技术专栏:☞ https://github.com/fyupeng 项目地址:☞ https://github.com/fyupeng/distributed-blog-system-api 留给读者 我们经常在使用Vue开发遇到一些棘手的问题,解决后通常要进行总结,避免下次重复…...

Spark学习
Spark简介 1.Spark是什么 首先spark是一个计算引擎,而不是存储工具,计算引擎有很多: 第一代:MapReduce廉价机器实现分布式大数据处理 第二代:Tez基于MR优化了DAG,性能比MR快一些 第三代:Spark…...

一些小细节代码笔记汇总
Python cv2抓取摄像头图片保存到本地 import cv2 import datetime, ossavePath "E:/Image/"if not os.path.exists(savePath):os.makedirs(savePath)cap cv2.VideoCapture(0) capture Falseif not cap.isOpened():print("无法打开摄像头")exit()while…...

L4.【LeetCode笔记】链表题的VS平台调试代码
不用调用87.【C语言】数据结构之链表的头插和尾插文章提到的头插函数 记下这个模板代码,可用于在Visual Studio上调试出问题的测试用例 如创建链表[1,2,3,4,5] #include <stdilb.h> // Definition for singly-linked list.struct ListNode {int val;struct ListNode *…...

JavaCV 之高斯滤波:图像降噪与细节保留的魔法
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s…...

VsCode显示空格
ctrl shift p选择Preferences: Open User Settings (JSON) 加上"editor.renderWhitespace": "all" {"cmake.configureOnOpen": true,"files.encoding": "gb2312","editor.fontVariations": false,"edito…...

.Net C# 基于EFCore的DBFirst和CodeFirst
DBFirst和CodeFirst 1 概念介绍 1.1 DBFirst(数据库优先) 含义:这种模式是先创建数据库架构,包括表、视图、存储过程等数据库对象。然后通过实体框架(Entity Framework)等工具,根据已有的数据…...

w012基于springboot的社区团购系统设计
🙊作者简介:拥有多年开发工作经验,分享技术代码帮助学生学习,独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。🌹赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板ÿ…...