当前位置: 首页 > news >正文

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多脚本、多设备批处理运行测试用例自动生成测试报告

一&#xff1a;主要内容 框架功能、框架架构及测试报告效果 airtest安装、环境搭建 框架搭建、框架运行说明 框架源码 二&#xff1a;框架功能及测试报告效果 1. 框架功能&#xff1a; 该框架笔者用来作为公司的项目的前端自动化&#xff0c;支持pc和app&#xff0c;本文…...

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

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

Vue3 + TypeScript 组件和文件命名规范及 setup 导入顺序规范

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

netty之处理连接源码分析

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

Dockerfile文件编写

1、打nginx原始包 登录后复制 ROM nginxENV LANG zh_CN.UTF-8 ENV LC_ALL zh_CN.UTF-8 ENV TZ Asia/Singapore# 设置时区&#xff0c;同样保持在一层 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项目&#xff0c;采用Pycharm或者命令行创建皆可。此处&#xff0c;以命令行方式作为演示&#xff0c;项目名为django_vue。 django-admin startproject djang…...

楼梯区域分割系统:Web效果惊艳

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

Day10加一

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

UTF-8简介

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

基于Openwrt系统架构,实现应用与驱动的实例。

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

SQL进阶技巧:如何利用三次指数平滑模型预测商品零售额?

目录 0 问题背景 1 数据准备 2 问题解决 2.1 模型构建 (1)符号规定 (2)基本假设...

HTB:Cicada[WriteUP]

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

小张求职记四

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

适用于 c++ 的 wxWidgets框架源码编译SDK-windows篇

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

flink 内存配置(二):设置TaskManager内存

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

【C++ 算法进阶】算法提升八

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

阿里云实时数据仓库HologresFlink

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

生成式语言模型的文本生成评价指标(从传统的基于统计到现在的基于语义)

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

【网安案例学习】暴力破解攻击(Brute Force Attack)

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

时间序列预测(十八)——实现配置管理和扩展命令行参数解析器

如图&#xff0c;这是一个main,py文件&#xff0c;在此代码中&#xff0c;最开始定义了许多模型参数&#xff0c;为了使项目更加灵活和可扩展&#xff0c;便于根据不同的需求调整参数和配置&#xff0c;可以根据实际需要扩展参数和配置项。 下面是如何实现配置管理和扩展命令行…...

Vue问题汇总解决

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

Spark学习

Spark简介 1.Spark是什么 首先spark是一个计算引擎&#xff0c;而不是存储工具&#xff0c;计算引擎有很多&#xff1a; 第一代&#xff1a;MapReduce廉价机器实现分布式大数据处理 第二代&#xff1a;Tez基于MR优化了DAG&#xff0c;性能比MR快一些 第三代&#xff1a;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&#xff08;数据库优先&#xff09; 含义&#xff1a;这种模式是先创建数据库架构&#xff0c;包括表、视图、存储过程等数据库对象。然后通过实体框架&#xff08;Entity Framework&#xff09;等工具&#xff0c;根据已有的数据…...

w012基于springboot的社区团购系统设计

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…...