├── res ├── build.jpg ├── email.jpg ├── report1.png ├── report2.png ├── report3.png ├── report4.png ├── structure.jpg ├── send_email.jpg ├── file_structure.png ├── build.xml └── jmeter-results-detail-report_21.xsl ├── testCase └── baidu │ ├── config_default.txt │ ├── email_default.txt │ └── build.xml ├── config.py ├── client ├── config.conf └── client.py ├── schedule.py ├── config.conf ├── LICENSE ├── beanshell ├── logger.py ├── sendEmail.py ├── README.md ├── server.py └── testing.py /res/build.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/build.jpg -------------------------------------------------------------------------------- /res/email.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/email.jpg -------------------------------------------------------------------------------- /testCase/baidu/config_default.txt: -------------------------------------------------------------------------------- 1 | ip = 127.0.0.1 // 测试环境IP 2 | port = 9999 // 服务端口 -------------------------------------------------------------------------------- /res/report1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report1.png -------------------------------------------------------------------------------- /res/report2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report2.png -------------------------------------------------------------------------------- /res/report3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report3.png -------------------------------------------------------------------------------- /res/report4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report4.png -------------------------------------------------------------------------------- /res/structure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/structure.jpg -------------------------------------------------------------------------------- /res/send_email.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/send_email.jpg -------------------------------------------------------------------------------- /res/file_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/file_structure.png -------------------------------------------------------------------------------- /testCase/baidu/email_default.txt: -------------------------------------------------------------------------------- 1 | { 2 | "subject": "百度接口自动化测试报告", 3 | "receiveName": "baidu_all", 4 | "receiveEmail": "aaa@baidu.com,bbb@baidu.com" 5 | } -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # Author: leeyoshinari 4 | import configparser 5 | 6 | 7 | class Config(object): 8 | """读取配置文件""" 9 | def __init__(self): 10 | self.cfg = configparser.ConfigParser() 11 | self.cfg.read('config.conf', encoding='utf-8') 12 | 13 | def getConfig(self, key): 14 | return self.cfg.get('default', key, fallback=None) 15 | 16 | def __del__(self): 17 | pass 18 | -------------------------------------------------------------------------------- /client/config.conf: -------------------------------------------------------------------------------- 1 | [default] 2 | # 服务端IP和端口 3 | IP = 127.0.0.1 4 | PORT = 12020 5 | 6 | # 日志级别 7 | log_level = INFO 8 | # 日志路径 9 | log_path = logs 10 | 11 | # 端口重启后,是否自动执行测试任务,0为不执行,1为执行 12 | is_start = 1 13 | # 待测系统名称,和测试用例路径下的文件夹名称保持一直,例如testCase路径下的baidu文件夹 14 | # 如有多个独立的待测系统,则用逗号隔开;例如:baidu,tencent。如没用待测系统,可配置为空 15 | # 用逗号隔开的系统名的顺序必须和监听端口的顺序保持一致 16 | server_name = baidu,tencent 17 | # 待测系统启用的端口,通过判断该端口的重启自动执行测试任务。如不需要监听端口,可配置为空 18 | # 如需监听多个端口,用逗号隔开;例如:8001,8002,8003 19 | # 监听端口的顺序和 server_name 的顺序保持一直 20 | # 如果监听的是多个端口,每个端口的重启会触发执行对应的测试任务 21 | server_port = 8001,8002 22 | # 周期性执行测试任务时间间隔,单位:秒。如不需要周期性执行测试任务,需注释该配置项 23 | interval = 3600 24 | # 每天(工作日)定时执行测试任务的时间。如不需要定时执行测试任务,需注释该配置项 25 | timing = 20:20 26 | -------------------------------------------------------------------------------- /schedule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # Author: leeyoshinari 4 | import queue 5 | from threading import Thread 6 | from testing import Testing 7 | 8 | 9 | class Scheduler(object): 10 | def __init__(self): 11 | self.testing = Testing() 12 | self.test_task = queue.Queue() # 创建队列 13 | 14 | t = Thread(target=self.worker) 15 | t.start() 16 | 17 | @property 18 | def task(self): 19 | return None 20 | 21 | @task.setter 22 | def task(self, value): 23 | self.test_task.put((self.testing.run, value)) 24 | 25 | def worker(self): 26 | """ 27 | 从队列中获取测试任务,并开始执行 28 | :return: 29 | """ 30 | while True: 31 | func, param = self.test_task.get() 32 | func(param) 33 | self.test_task.task_done() 34 | -------------------------------------------------------------------------------- /config.conf: -------------------------------------------------------------------------------- 1 | [default] 2 | # 开启服务的IP和端口 3 | host = 127.0.0.1 4 | port = 12020 5 | 6 | # 自动化测试用例执行时间间隔,单位:秒 7 | interval = 1800 8 | 9 | # Jmeter脚本测试用例存放路径 10 | case_path = /home/ATI_Jmeter/testCase 11 | # 测试报告保存路径 12 | report_path = /home/ATI_Jmeter/reports 13 | # 测试记录保存地址,保存开始测试时间,测试任务名称,执行用例数,成功率 14 | record_name = record.txt 15 | 16 | # 如果接口自动化脚本维护在git上,则每次执行测试任务前,自动拉取git上的脚本 17 | # 是否自动从git拉去脚本到本地,0为不用git,1为用git 18 | is_git = 0 19 | # git本地仓库路径 20 | git_path = /home/ATI_Jmeter 21 | 22 | # 是否发送邮件,0为总不发送,1为总是发送,2为仅失败发送,3为仅成功发送 23 | is_email = 0 24 | # SMTP 25 | smtp = smtp.qq.com 26 | # 发件人邮箱 27 | sender_email = aaa@qq.com 28 | # 发件人邮箱登录密码,在`sendEmail.py`文件中第74行设置 29 | # password = 123456 30 | # 发件人的名字 31 | sender_name = aaa 32 | # 33 | # 收件人名字及邮箱地址,对应的是每个系统的文件夹中的email_default.txt文件,该文件为默认文件,实际使用时会复制并重命名成email.txt 34 | # email_default.txt文件中包括邮件主题、收件人姓名和收件人地址; 35 | # 36 | 37 | # 日志级别 38 | log_level = INFO 39 | # 日志路径 40 | log_path = logs 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 leeyoshinari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /beanshell: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.util.*; 3 | import org.apache.jmeter.services.FileServer; 4 | 5 | public Map getEnvConf(String filePath) throws IOException { 6 | File file = new File(filePath); 7 | FileReader fileReader = new FileReader(file); 8 | InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8"); 9 | BufferedReader reader = new BufferedReader(isr); 10 | String txt = ""; 11 | int lines = 0; 12 | String content = ""; 13 | Map confMap = new HashMap(); 14 | while ((txt = reader.readLine()) != null) { 15 | txt = txt.trim(); 16 | String[] item = txt.split("="); 17 | String[] itemEnd = item[1].split("//"); 18 | confMap.put(item[0].trim(), itemEnd[0].trim()); 19 | } 20 | System.out.println(confMap); 21 | reader.close(); 22 | fileReader.close(); 23 | return confMap; 24 | } 25 | 26 | String baseDir=FileServer.getFileServer().getBaseDir(); 27 | String confFileName = "config.txt"; 28 | String filePath = baseDir + System.getProperty("file.separator") + confFileName; 29 | Map envConfMap = getEnvConf(filePath); 30 | for(String key : envConfMap.keySet()){ 31 | vars.put(key, envConfMap.get(key)); 32 | } 33 | -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # Author: leeyoshinari 4 | 5 | import os 6 | import time 7 | import traceback 8 | import logging.handlers 9 | from config import Config 10 | 11 | cfg = Config() 12 | LEVEL = cfg.getConfig('log_level') 13 | log_path = cfg.getConfig('log_path') 14 | 15 | if not os.path.exists(log_path): 16 | os.mkdir(log_path) 17 | 18 | log_level = { 19 | 'DEBUG': logging.DEBUG, 20 | 'INFO': logging.INFO, 21 | 'WARNING': logging.WARNING, 22 | 'ERROR': logging.ERROR, 23 | 'CRITICAL': logging.CRITICAL 24 | } 25 | 26 | logger = logging.getLogger() 27 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s[line:%(lineno)d] - %(message)s') 28 | logger.setLevel(level=log_level.get(LEVEL)) 29 | 30 | current_day = time.strftime('%Y-%m-%d') 31 | log_name = os.path.join(log_path, current_day + '.log') 32 | 33 | file_handler = logging.handlers.RotatingFileHandler(filename=log_name, maxBytes=10 * 1024 * 1024, backupCount=7) 34 | # file_handler = logging.StreamHandler() 35 | 36 | file_handler.setFormatter(formatter) 37 | logger.addHandler(file_handler) 38 | 39 | 40 | def handle_exception(errors=(Exception,), is_return=False, default_value=None): 41 | """ 42 | Handle exception, throw an exception, or return a value. 43 | :param errors: Exception type 44 | :param is_return: Whether to return 'default_value'. Default False, if exception, don't throw an exception, but return a value. 45 | :param default_value: If 'is_return' is True, return 'default_value'. 46 | :return: 'default_value' 47 | """ 48 | def decorator(func): 49 | def decorator1(*args, **kwargs): 50 | if is_return: 51 | try: 52 | return func(*args, **kwargs) 53 | except errors: 54 | logger.error(traceback.format_exc()) 55 | return default_value 56 | else: 57 | try: 58 | return func(*args, **kwargs) 59 | except errors: 60 | raise 61 | 62 | return decorator1 63 | return decorator 64 | -------------------------------------------------------------------------------- /res/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | generating test report 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /testCase/baidu/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | generating test report 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /sendEmail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # Author: leeyoshinari 4 | import json 5 | import smtplib 6 | import shutil 7 | from email.mime.text import MIMEText 8 | from email.mime.multipart import MIMEMultipart 9 | from email.header import Header 10 | from logger import logger, cfg 11 | 12 | 13 | def sendMsg(html, paths, failure_num=1, is_path=True, is_send=True): 14 | """ 15 | 发送邮件 16 | :param html: 邮件正文用到的html文件路径,或者html 17 | :param paths: 18 | :param failure_num: 失败的用例数 19 | :param is_path: bool,True表示html是一个路径,False表示html是html 20 | :param is_send: bool,是否发邮件,仅用于第一次发送失败后,再次发送 21 | :return: 22 | """ 23 | flag = 0 24 | is_email = int(cfg.getConfig('is_email')) 25 | if is_email: 26 | if is_email == 1: 27 | flag = 1 28 | if is_email == 2: 29 | if failure_num > 0: 30 | flag = 1 31 | else: 32 | logger.info('所有用例执行成功,不发送邮件,已跳过。') 33 | if is_email == 3: 34 | if failure_num == 0: 35 | flag = 1 36 | else: 37 | logger.info('有执行失败的用例,不发送邮件,已跳过。') 38 | else: 39 | logger.info('设置为不自动发送邮件,已跳过。') 40 | 41 | if flag: 42 | try: 43 | if paths["method"] == "GET": 44 | shutil.copy(paths["email_file"], paths["new_email_file"]) 45 | else: 46 | if paths["post_data"].get('email'): 47 | replace_email(paths["email_file"], paths["post_data"]['email'], paths["new_email_file"]) 48 | 49 | email_dict = json.load(open(paths["new_email_file"], 'r', encoding='utf-8')) 50 | subject = email_dict['subject'] 51 | send_to = email_dict['receiveEmail'] 52 | receive_name = email_dict['receiveName'] 53 | logger.info('开始发送邮件,收件人{}'.format(send_to)) 54 | message = MIMEMultipart() 55 | message['From'] = Header(cfg.getConfig('sender_name')) # 发件人名字 56 | message['To'] = Header(receive_name) # 收件人名字 57 | message['Subject'] = Header(subject, 'utf-8') # 邮件主题 58 | 59 | if is_path: 60 | with open(html, 'r', encoding='utf-8') as f: 61 | fail_case = f.read() 62 | else: 63 | fail_case = html 64 | 65 | email_text = MIMEText(fail_case, 'html', 'utf-8') 66 | message.attach(email_text) # 添加邮件正文 67 | 68 | try: 69 | server = smtplib.SMTP_SSL(cfg.getConfig('smtp'), 465) 70 | except Exception as err: 71 | logger.error(err) 72 | server = smtplib.SMTP(cfg.getConfig('smtp'), 25) 73 | 74 | server.login(cfg.getConfig('sender_email'), '123456') # 登陆邮箱 75 | server.sendmail(cfg.getConfig('sender_email'), send_to.split(','), message.as_string()) # 发送邮件 76 | server.quit() 77 | del fail_case, email_text, message, server 78 | logger.info('邮件发送成功') 79 | except Exception as err: 80 | logger.error(err) 81 | if is_send: 82 | sendMsg(html, paths, is_path=is_path, is_send=False) # 发送失败后,重发一次 83 | 84 | 85 | def replace_email(src, new_dict, dst): 86 | old_dict = json.load(open(src, 'r', encoding='utf-8')) 87 | for k, v in new_dict.items(): 88 | old_dict.update({k: v}) 89 | 90 | with open(dst, 'w', encoding='utf-8') as f: 91 | f.write(json.dumps(old_dict)) 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ATI_Jmeter ![](https://visitor-badge.glitch.me/badge?page_id=leeyoshinari) 2 | ### 介绍 3 | 本项目是一整套使用Jmeter+Ant+Python完成接口自动化测试的解决方案;特别是多系统的测试任务执行,要比网上的教程方便的多。
4 | 5 | 整体架构图:
6 | ![structure](https://github.com/leeyoshinari/ATI_Jmeter/blob/master/res/structure.jpg) 7 | 8 | 功能:
9 | 1. 使用Jmeter维护接口测试用例; 10 | 2. 使用Ant执行测试任务,并生成测试报告; 11 | 3. 使用Python完成邮件发送及任务调度; 12 | 4. 通过get请求触发任务执行,调度方式灵活; 13 | 5. 具有定时功能,可周期性或者定时执行测试任务; 14 | 6. 通过监控端口,当服务重启后,可自动执行测试任务; 15 | 7. 支持自动从git拉取最新版本; 16 | 17 | 实现:
18 | 1. 使用Ant执行Jmeter脚本,并生成测试报告; 19 | 2. 考虑到邮件正文内容可读性,定制化修改测试报告模板; 20 | 3. 使用正则表达式提取测试报告中的信息,重新组合成邮件正文; 21 | 4. 通过get请求触发测试任务的执行; 22 | 5. 通过多线程和队列的方式执行测试任务; 23 | 6. 使用aiohttp框架启动后台服务,将测试报告加入到静态资源中,可通过链接访问; 24 | 7. 每次执行测试任务前,自动从git拉取最新版本;如git pull时需要登录,需要提前配置免登录; 25 | 26 | 生成的测试报告:
27 | 1. Ant生成的测试报告,长这个样子 28 | ![长这个样子](https://github.com/leeyoshinari/ATI_Jmeter/blob/master/res/email.jpg) 29 | 2. 邮件收到的测试报告,长这个样子 30 | ![长这个样子](https://github.com/leeyoshinari/ATI_Jmeter/blob/master/res/send_email.jpg) 31 | 32 | ### 部署 33 | 1、Jmeter和Ant部署参考网上教程,主要介绍测试报告模板修改和build.xml文件
34 | > 测试报告模板是在jmeter自带模板的基础上修改的,主要修改详见`res`文件夹中的`report`截图,说明如下,其他小的修改这里不赘述,可使用文档比较工具自行比较查看,也可[在线文档比较](http://www.jq22.com/textDifference) ;
35 | >> (1) 截图中标注的修改1和修改5,因为默认模板带有2个png的静态文件,生成测试报告时必须带上这2个静态文件,否则测试报告页面不好看,因此,需要去掉这2个静态文件;
36 | >> (2) 截图中标注的修改2,测试报告一般重点关注测试失败的用例,因此,需要把测试失败的用例展示在前面;
37 | >> (3) 截图中标注的修改3,把标题改成中文,因为测试报告会发给较多的人;如果你在外企工作,可以改成英文,但是相对应的脚本中的正则表达式也需要修改;
38 | >> (4) 截图中标注的修改4,添加了一个空的`span`标签,用于添加自定义的内容,提高发送的测试报告邮件的可读性;
39 | >> (5) 其他未标注出来的修改点,主要是默认模板没有我想看到的数据,把一些没有展示的数据展示出来,把一些“没用的”数据隐藏起来,以及一些样式的修改;
40 | 41 | > build.xml文件如下,具体配置已详细说明。强调:为了方便测试报告统一管理,也为了能够自动发送邮件,所有系统的build.xml中的测试报告路径必须是同一个文件夹
42 | ![build文件](https://github.com/leeyoshinari/ATI_Jmeter/blob/master/res/build.jpg) 43 | 44 | 2、克隆repository
45 | ```git clone https://github.com/leeyoshinari/ATI_Jmeter.git```
46 | 47 | 3、用例编写
48 | 在编写JMeter用例时,需要读取配置文件config_default.txt中的变量,然后将变量传递到JMeter中。
49 | 读取变量的方法是在beanshell中写Java脚本完成的,具体Java脚本详见`beanshell`中的脚本。 50 | 51 | 4、测试用例放置
52 | > (1)所有测试用例放在一个统一的文件夹中,例如`testCase`文件夹;
53 | > (2)针对不同系统的不同测试用例,可单独再放入一个文件夹中管理,例如:百度的测试用例放在`baidu`中、百度的BVT测试用例放在`baidu_bvt`中、腾讯的测试用例放在`tencent`中;
54 | > (3)每个系统的测试用例文件夹中,都需要放一个配置好的`build.xml`文件;注意:所有系统的测试报告路径必须是同一个文件夹;
55 | > (4)测试用例文件夹具体结构如下:
56 | > ![文件夹结构](https://github.com/leeyoshinari/ATI_Jmeter/blob/master/res/file_structure.png) 57 | 58 | 强烈建议文件夹及文件名称使用英文
59 | 为什么要按照上面的要求放置测试用例?这样放置方便执行测试任务,通过get或post请求`http://ip:port/run?systemName=baidu`就可以执行百度的测试用例。
60 | get请求,jmeter参数和邮件信息为默认值,post请求传的参数可以覆盖默认值。
61 | post请求传参示例:`{"params": {"ip": "127.0.0.1", "port": "9998"}, "email": {"subject": "腾讯接口自动化测试报告", "receiveName": "tencent_all", "receiveEmail": "aaa@baidu.com"}}` 62 | 63 | 5、修改配置文件config.conf
64 | > (1)线程池大小,建议设置1就够了;如确实调度较多测试用例的执行,可酌情增加;
65 | > (2)测试用例路径和测试报告路径,建议使用绝对路径;其中测试报告路径应和`build.xml`文件中的路径保持一致;
66 | > (3)如接口自动化脚本维护在git上,可配置git本地仓库路径,每次执行任务前,自动从git上拉取最新版本,默认拉取主分支;前提是已经clone到本地了;
67 | > (4)邮件发送配置,请确认SMTP服务配置正确;邮箱登录密码配置,请在`sendEmail.py`文件中第70行设置,如果密码不想让其他人看到,请将该py文件进行编译,或者直接将这个repository打包,具体打包方法,请往下看;
68 | 69 | 6、运行
70 | > Linux:
71 | > ```nohup python3 server.py &```
72 | > Windows
73 | > ```python server.py```
74 | 75 | 7、打包
76 | 经过前5步,如果该repository可以启动,且执行测试任务成功,则可以进行打包,使用pyinstaller进行打包。
77 | pyinstaller安装自行查找教程,须确保安装正确,否则打包会报错,下面直接进行打包: 78 | > (1)进入ATI_Jmeter文件夹,执行命令:
79 | > ```shell 80 | > pyinstaller -F server.py -p schedule.py -p logger.py -p config.py -p sendEmail.py -p testing.py --hidden-import logger --hidden-import schedule --hidden-import config --hidden-import sendEmail --hidden-import testing 81 | > ``` 82 | > (2)打包完成后,在当前路径下会生成dist文件夹,进入dist文件夹即可找到可执行文件server;
83 | > (3)将配置文件config.conf拷贝到dist文件夹下,并修改配置文件;
84 | > (4)如需要部署在其他服务器上,可将dist整个文件夹拷贝到其他服务器,启动server
85 | > ```nohup ./server &``` 86 | 87 | 8、CI/CD,以Jenkins为例,在Jenkins构建后操作中增加一个get请求,请求的url为`http://IP:PORT/run?systemName=系统名称`,此处系统名称应和testCase用例文件夹中的对应的系统名称保持一致。 88 | 89 | 9、如果你所在的项目还没有用到CI/CD,或者项目本身有较多配置项,每次手动更改配置重启项目后,也想自动执行测试任务;亦或是你不想配置CI/CD,则需要执行客户端;
90 | 进入client文件夹,将脚本和配置文件拷贝到项目所在的服务器上,运行即可,也可以按照步骤6的方式进行打包。
91 | 92 | 修改配置文件config.conf:
93 | > (1)系统名称必须和测试用例文件夹中的名称保持一致,例如可配置成`baidu`;如需测试多个系统,名字用英文逗号`,`隔开;
94 | > (2)系统端口号即系统占用的端口号;如需监控多个系统的端口,端口用英文逗号`,`隔开;
95 | 注意:如测试多个系统,系统名称的排序和系统端口的排序必须保持一致 96 | 97 | ### 注意 98 | 1、如需部署client,部署的服务器必须支持`netstat`命令,以便根据端口号查进程号;仅支持Linux系统;
99 | 2、已经测试的版本:Jmeter-5.2.1、Ant-1.10.7
100 | 3、另外还有一个项目,使用Python编写的接口自动化测试框架,用Excel维护测试用例,感兴趣的话[可以点我](https://github.com/leeyoshinari/ATI) 。 101 | 102 | ### Requirements 103 | 1. aiohttp>=3.6.2 104 | 2. GitPython>=3.1.2 105 | 3. requests 106 | 4. Python 3.7+ 107 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Author: leeyoshinari 4 | import os 5 | import glob 6 | import json 7 | import asyncio 8 | from aiohttp import web 9 | from schedule import Scheduler 10 | from sendEmail import sendMsg 11 | from logger import logger, cfg 12 | 13 | 14 | schedule = Scheduler() 15 | report_path = cfg.getConfig('report_path') 16 | IP = cfg.getConfig('host') 17 | PORT = cfg.getConfig('port') 18 | if not os.path.exists(report_path): 19 | os.mkdir(report_path) 20 | 21 | 22 | async def get_list(request): 23 | """ 24 | 获取当前测试用例目录下的系统名,及执行测试任务需要的get请求的url 25 | """ 26 | case_path = cfg.getConfig('case_path') 27 | max_num = 0 28 | for i in os.listdir(case_path): 29 | if max_num < len(i): 30 | max_num = len(i) 31 | dirs = [d+' '*(max_num-len(d))+'\t\t'+'http://'+IP+':'+PORT+'/run?systemName='+d for d in os.listdir(case_path) if os.path.isdir(os.path.join(case_path, d))] 32 | return web.Response(body='\n'.join(dirs)) 33 | 34 | 35 | async def run(request): 36 | """ 37 | run接口,把测试任务放入队列中 38 | :param request: 39 | :return: 40 | """ 41 | if request.method == 'GET': 42 | paths = {"method": "GET"} 43 | system_name = request.query.get('systemName') # 待执行测试的系统根路径 44 | case_path = os.path.join(cfg.getConfig('case_path'), system_name) # 测试用例路径 45 | paths.update({"system_name": system_name}) 46 | paths.update({"case_path": case_path}) # 测试用例路径 47 | paths.update({"record_path": os.path.join(case_path, cfg.getConfig('record_name'))}) # 测试结果记录路径 48 | paths.update({"build_file": os.path.join(cfg.getConfig('case_path'), system_name, 'build.xml')}) # build.xml路径 49 | paths.update({"email_file": os.path.join(case_path, 'email_default.txt')}) # 邮件默认配置文件 50 | paths.update({"config_file": os.path.join(case_path, 'config_default.txt')}) # jmx执行的配置文件对应的默认配置文件 51 | paths.update({"new_email_file": os.path.join(case_path, 'email.txt')}) # 邮件配置文件 52 | paths.update({"new_config_file": os.path.join(case_path, 'config.txt')}) # jmx执行的配置文件 53 | if os.path.exists(case_path): 54 | if not os.path.exists(paths["record_path"]): 55 | f = open(paths["record_path"], 'a') 56 | f.close() 57 | 58 | if os.path.exists(paths["build_file"]): 59 | schedule.task = paths 60 | return web.Response( 61 | body=json.dumps({'code': 1, 'message': '操作成功,测试任务正在准备执行', 'data': None}, ensure_ascii=False)) 62 | else: 63 | return web.Response(body=json.dumps({'code': 0, 'message': 'build.xml文件不存在,测试任务执行失败', 'data': None}, 64 | ensure_ascii=False)) 65 | else: 66 | return web.Response(body=json.dumps({ 67 | 'code': 0, 'message': '未找到与系统名称对应的脚本,请确认系统名称是否正确,脚本是否存在!', 'data': None}, ensure_ascii=False)) 68 | else: 69 | post_data = await request.json() 70 | paths = {"method": "POST"} 71 | system_name = request.query.get('systemName') # 待执行测试的系统根路径 72 | case_path = os.path.join(cfg.getConfig('case_path'), system_name) # 测试用例路径 73 | paths.update({"system_name": system_name}) 74 | paths.update({"case_path": case_path}) # 测试用例路径 75 | paths.update({"record_path": os.path.join(case_path, cfg.getConfig('record_name'))}) # 测试结果记录路径 76 | paths.update({"build_file": os.path.join(cfg.getConfig('case_path'), system_name, 'build.xml')}) # build.xml路径 77 | paths.update({"email_file": os.path.join(case_path, 'email_default.txt')}) # 邮件默认配置文件 78 | paths.update({"config_file": os.path.join(case_path, 'config_default.txt')}) # jmx执行的配置文件对应的默认配置文件 79 | paths.update({"new_email_file": os.path.join(case_path, 'email.txt')}) # 邮件配置文件 80 | paths.update({"new_config_file": os.path.join(case_path, 'config.txt')}) # jmx执行的配置文件 81 | paths.update({"post_data": post_data}) 82 | 83 | if os.path.exists(case_path): 84 | if not os.path.exists(paths["record_path"]): 85 | f = open(paths["record_path"], 'a') 86 | f.close() 87 | 88 | if os.path.exists(paths["build_file"]): 89 | schedule.task = paths 90 | return web.Response( 91 | body=json.dumps({'code': 1, 'message': '操作成功,测试任务正在准备执行', 'data': None}, ensure_ascii=False)) 92 | else: 93 | return web.Response(body=json.dumps({'code': 0, 'message': 'build.xml文件不存在,测试任务执行失败', 'data': None}, 94 | ensure_ascii=False)) 95 | else: 96 | return web.Response(body=json.dumps({ 97 | 'code': 0, 'message': '未找到与系统名称对应的脚本,请确认系统名称是否正确,脚本是否存在!', 'data': None}, ensure_ascii=False)) 98 | 99 | async def sendEmail(request): 100 | """ 101 | get请求,用于发送邮件,用于客户端异常时发送邮件提醒 102 | :param request: 103 | :return: 104 | """ 105 | name = request.match_info['name'] 106 | port = request.match_info['port'] 107 | ind = request.match_info['ind'] 108 | IP = request.match_info['IP'] 109 | email_file = glob.glob(os.path.join(cfg.getConfig('case_path'), name, 'email_*.txt')) 110 | if len(email_file) == 0: 111 | return web.Response(body=json.dumps({'code': 0, 'message': '没有设置收件人邮箱地址的txt文件,测试任务执行失败', 'data': None}, ensure_ascii=False)) 112 | elif len(email_file) > 1: 113 | return web.Response(body=json.dumps({'code': 0, 'message': '应该只有一个收件人邮箱地址的txt文件,但是找到了多个,测试任务执行失败', 'data': None}, ensure_ascii=False)) 114 | 115 | if int(ind) == 1: 116 | msg = f'{IP} 服务器上的 {port} 端口已经停了,无法执行 {name} 的接口自动化测试,请及时检查端口状态' 117 | else: 118 | msg = f'{IP} 服务器上的 {name} 接口自动化测试执行异常,请检查测试用例,或手动执行get请求 http://{IP}:{PORT}/run?systemName={name} ' 119 | html = f'' \ 120 | f'

异常提醒:{msg}!

' \ 121 | f'

此邮件自动发出,请勿回复。

' 122 | try: 123 | sendMsg(html, email_file[0], is_path=False) 124 | return web.Response(body=json.dumps({'code': 1, 'message': '邮件提醒发送成功!', 'data': None}, ensure_ascii=False)) 125 | except Exception as err: 126 | return web.Response(body=json.dumps({'code': 0, 'message': err, 'data': None}, ensure_ascii=False)) 127 | 128 | 129 | async def main(): 130 | app = web.Application() 131 | app.router.add_static('/testReport/', path=report_path) 132 | 133 | app.router.add_route('GET', '/', get_list) 134 | app.router.add_route('*', '/run', run) 135 | # app.router.add_route('GET', '/sendEmail/{name}/{port}/{ind}/{IP}', sendEmail) 136 | 137 | app_runner = web.AppRunner(app) 138 | await app_runner.setup() 139 | site = web.TCPSite(app_runner, IP, int(PORT)) 140 | await site.start() 141 | 142 | 143 | loop = asyncio.get_event_loop() 144 | loop.run_until_complete(main()) 145 | loop.run_forever() 146 | -------------------------------------------------------------------------------- /testing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Author: leeyoshinari 4 | import os 5 | import re 6 | import time 7 | import shutil 8 | from threading import Thread 9 | from git import Repo 10 | from sendEmail import sendMsg 11 | from logger import logger, cfg, handle_exception 12 | 13 | 14 | class Testing(object): 15 | def __init__(self): 16 | self.interval = int(cfg.getConfig('interval')) 17 | self.tasking = [] 18 | 19 | t = Thread(target=self.lookup) 20 | t.start() 21 | 22 | def run(self, paths): 23 | """ 24 | 执行测试任务 25 | :param paths: 字典 26 | :return: 27 | """ 28 | try: 29 | if int(cfg.getConfig('is_git')): 30 | logger.info('准备从git上拉取最新版本') 31 | repo = Repo(cfg.getConfig('git_path')) 32 | remote = repo.remote() 33 | remote.pull() 34 | logger.info('从git上拉取版本成功') 35 | except Exception as err: 36 | logger.error(err) 37 | 38 | build_path = paths["build_file"] 39 | logger.info(f'开始执行测试任务{build_path}') 40 | 41 | try: 42 | if paths["method"] == "GET": 43 | shutil.copy(paths["config_file"], paths["new_config_file"]) # 复制,用于jmx执行 44 | else: 45 | if paths["post_data"].get('params'): 46 | replace_config(paths["config_file"], paths["post_data"]['params'], paths["new_config_file"]) 47 | 48 | report_name = f'{paths["system_name"]}-{int(time.time() * 1000)}' 49 | _ = os.popen('nohup ant -f {} -DReportName={} &'.format(build_path, report_name)) # 执行测试 50 | paths.update({"file_name": report_name}) 51 | paths.update({"start_time": time.time()}) 52 | self.tasking.append(paths) 53 | time.sleep(3) 54 | except Exception as err: 55 | logger.error(err) 56 | 57 | @handle_exception(is_return=True, default_value=False) 58 | def post_deal(self, paths): 59 | msg = self.parse_html(paths["file_name"] + '.html', paths["case_path"]) # 重组html 60 | 61 | sendMsg(msg['fail_case'], paths, failure_num=msg['failure_num']) # 发送邮件 62 | 63 | string = f"{paths['start_time']},{paths['build_file']},{msg['total_num']},{msg['failure_num']}\n" 64 | logger.info(f'写测试记录到本地, {string}') 65 | with open(paths["record_path"], 'a', encoding='utf-8') as f: 66 | f.write(string) 67 | 68 | logger.info('测试完成') 69 | return True 70 | 71 | @handle_exception() 72 | def parse_html(self, file_name, case_path): 73 | """ 74 | 提取自动生成的测试报告中的一些信息,重组测试报告用于邮件发送 75 | :param case_path: 测试用例路径 76 | :param file_name: 测试报告名称 77 | :return: 78 | """ 79 | all_case = os.path.join(cfg.getConfig('report_path'), file_name) # 完整的测试报告路径 80 | fail_case = os.path.join(cfg.getConfig('report_path'), 'send_' + file_name) # 处理好用于邮件发送的测试报告路径 81 | logger.info('开始处理html测试报告{}'.format(all_case)) 82 | with open(all_case, 'r', encoding='utf-8') as f: 83 | htmls = f.readlines() 84 | 85 | html = '' 86 | for line in htmls: 87 | html += line.strip() 88 | 89 | # 提取用例总数,成功率数据 90 | case_num = re.findall('响应时间最大值.*(\d+)(\d+)' 91 | '\d{1,3}.\d+%', html)[0] 92 | total_num = [int(case_num[0])] 93 | failure_num = [int(case_num[1])] 94 | 95 | # 提取出概览和失败用例,用于邮件发送 96 | # res = re.findall('(.*?)

所有用例', html)[0] 97 | res = html.split('

所有用例')[0] 98 | url = 'http://{}:{}/testReport/{}'.format(cfg.getConfig('host'), cfg.getConfig('port'), file_name) 99 | logger.info(f'详细测试报告跳转链接为 {url}') 100 | # 添加完整测试报告路径跳转链接 101 | jump_url = f'如需查看详细测试结果,请点我' 102 | 103 | # 添加历史数据 104 | with open(os.path.join(case_path, cfg.getConfig('record_name')), 'r', encoding='utf-8') as f: 105 | history = f.readlines() 106 | for line in history: 107 | datas = line.split(',') 108 | total_num.append(int(datas[-2])) 109 | failure_num.append(int(datas[-1])) 110 | ratio = 100 - round(100 * sum(failure_num) / sum(total_num), 2) 111 | history = f'

历史数据概览

' \ 115 | f'
累计执行次数' \ 113 | f'累计执行用例数累计执行失败用例数执行成功率
{len(total_num)}{sum(total_num)}{sum(failure_num)}{ratio}%
' 116 | 117 | res1 = re.sub('(.*?)', jump_url+history, res) 118 | # 添加尾巴 119 | res = res1 + '

此邮件自动发出,请勿回复。

' 120 | # 写到本地 121 | with open(fail_case, 'w', encoding='utf-8') as f: 122 | f.writelines(res) 123 | 124 | logger.info('html测试报告处理完成') 125 | del htmls, html, res, res1, history 126 | return {'all_case': all_case, 'fail_case': fail_case, 'total_num': total_num[0], 'failure_num': failure_num[0]} 127 | 128 | def lookup(self): 129 | while True: 130 | time.sleep(2) 131 | n = len(self.tasking) 132 | logger.info(f'当前正在执行的任务数为{n}') 133 | inds = [] 134 | for i in range(n): 135 | html_path = os.path.join(cfg.getConfig('report_path'), self.tasking[i]["file_name"]) + '.html' 136 | logger.info(html_path) 137 | if not os.path.exists(html_path): 138 | if time.time() - self.tasking[i]["start_time"] < self.interval: 139 | continue 140 | else: 141 | logger.error(f'测试任务执行超时') 142 | inds.append(i) 143 | html = f'' \ 144 | f'

异常提醒:{self.tasking[i]["build_file"]} 测试任务执行超时,请检查!

' \ 145 | f'

此邮件自动发出,请勿回复。

' 146 | try: 147 | sendMsg(html, self.tasking[i], is_path=False) 148 | except Exception as err: 149 | logger.error(err) 150 | else: 151 | time.sleep(1) 152 | logger.info('测试任务执行完成') 153 | flag = self.post_deal(self.tasking[i]) 154 | if flag: 155 | inds.append(i) 156 | 157 | for j in range(len(inds) - 1, -1, -1): 158 | self.tasking.pop(inds[j]) 159 | 160 | 161 | def replace_config(src, new_dict, dst): 162 | old_dict = {} 163 | with open(src, 'r', encoding='utf-8') as f: 164 | lines = f.readlines() 165 | 166 | for line in lines: 167 | k_v = line.split('//')[0].split('=') 168 | old_dict.update({k_v[0].strip(): k_v[1].strip()}) 169 | 170 | for k, v in new_dict.items(): 171 | old_dict.update({k: v}) 172 | 173 | lines = [f'{k}={v}\n' for k, v in old_dict.items()] 174 | with open(dst, 'w', encoding='utf-8') as f: 175 | f.writelines(lines) 176 | -------------------------------------------------------------------------------- /client/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Author: leeyoshinari 4 | import os 5 | import time 6 | import json 7 | import traceback 8 | import configparser 9 | import logging.handlers 10 | import requests 11 | 12 | 13 | class Config(object): 14 | def __init__(self): 15 | self.cfg = configparser.ConfigParser() 16 | self.cfg.read('config.conf', encoding='utf-8') 17 | 18 | def getConfig(self, key): 19 | return self.cfg.get('default', key, fallback=None) 20 | 21 | 22 | cfg = Config() 23 | LEVEL = cfg.getConfig('log_level') 24 | log_path = cfg.getConfig('log_path') 25 | 26 | if not os.path.exists(log_path): 27 | os.mkdir(log_path) 28 | 29 | log_level = { 30 | 'DEBUG': logging.DEBUG, 31 | 'INFO': logging.INFO, 32 | 'WARNING': logging.WARNING, 33 | 'ERROR': logging.ERROR, 34 | 'CRITICAL': logging.CRITICAL 35 | } 36 | 37 | logger = logging.getLogger() 38 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s[line:%(lineno)d] - %(message)s') 39 | logger.setLevel(level=log_level.get(LEVEL)) 40 | current_day = time.strftime('%Y-%m-%d') 41 | log_name = os.path.join(log_path, current_day + '.log') 42 | 43 | # 日志输出到文件中 44 | file_handler = logging.handlers.RotatingFileHandler(filename=log_name, maxBytes=10 * 1024 * 1024, backupCount=7) 45 | # 日志输出到控制台 46 | # file_handler = logging.StreamHandler() 47 | file_handler.setFormatter(formatter) 48 | logger.addHandler(file_handler) 49 | 50 | 51 | def handle_exception(errors=(Exception,), is_return=False, default_value=None): 52 | """ 53 | Handle exception, throw an exception, or return a value. 54 | :param errors: Exception type 55 | :param is_return: Whether to return 'default_value'. Default False, if exception, don't throw an exception, but return a value. 56 | :param default_value: If 'is_return' is True, return 'default_value'. 57 | :return: 'default_value' 58 | """ 59 | def decorator(func): 60 | def decorator1(*args, **kwargs): 61 | if is_return: 62 | try: 63 | return func(*args, **kwargs) 64 | except errors: 65 | logger.error(traceback.format_exc()) 66 | return default_value 67 | else: 68 | try: 69 | return func(*args, **kwargs) 70 | except errors: 71 | raise 72 | 73 | return decorator1 74 | 75 | return decorator 76 | 77 | 78 | @handle_exception(is_return=True, default_value='127.0.0.1') 79 | def get_ip(): 80 | """ 81 | 获取当前服务器IP地址 82 | :return: IP 83 | """ 84 | result = os.popen("hostname -I |awk '{print $1}'").readlines() 85 | logger.debug(result) 86 | if result: 87 | IP = result[0].strip() 88 | else: 89 | logger.warning('未获取到服务器IP地址') 90 | IP = '127.0.0.1' 91 | 92 | return IP 93 | 94 | 95 | @handle_exception(is_return=True, default_value='') 96 | def port_to_pid(port): 97 | """ 98 | 根据端口号查询进程号 99 | :param port: 端口号 100 | :return: 进程号 101 | """ 102 | result = os.popen(f'netstat -nlp|grep {port} |tr -s " "').readlines() 103 | flag = f':{port}' 104 | res = [line.strip() for line in result if flag in line] 105 | logger.debug(res[0]) 106 | p = res[0].split(' ') 107 | pp = p[3].split(':')[-1] 108 | if str(port) == pp: 109 | pid = p[p.index('LISTEN') + 1].split('/')[0] 110 | else: 111 | pid = '' 112 | 113 | return pid 114 | 115 | 116 | @handle_exception(is_return=True) 117 | def put_queue(system_name): 118 | """ 119 | get请求,触发任务执行,将测试任务放入队列中,如果无测试任务执行,则会立即执行,否则会排队 120 | :param system_name: 系统名 121 | :return: 122 | """ 123 | url = f"http://{cfg.getConfig('IP')}:{cfg.getConfig('PORT')}/run/{system_name}" 124 | res = requests.get(url=url) 125 | if res.status_code == 200: 126 | data = json.loads(res.text) 127 | if data['code'] == 1: 128 | logger.info(data['message']) 129 | else: 130 | logger.error(data['message']) 131 | send_email(system_name, '12020', ind=2) 132 | else: 133 | logger.error(f'请求异常-{system_name},状态码为:{res.status_code}') 134 | send_email(system_name, '12020', ind=2) 135 | 136 | 137 | @handle_exception(is_return=True) 138 | def send_email(name, port, ind=1): 139 | """ 140 | 端口停止,发送邮件提醒 141 | :param name: 系统名 142 | :param port: 系统端口 143 | :param ind: 标志,错误类型 144 | :return: 145 | """ 146 | IP = get_ip() 147 | url = f"http://{cfg.getConfig('IP')}:{cfg.getConfig('PORT')}/sendEmail/{name}/{port}/{ind}/{IP}" 148 | res = requests.get(url=url) 149 | if res.status_code == 200: 150 | data = json.loads(res.text) 151 | if data['code'] == 1: 152 | logger.info(data['message']) 153 | else: 154 | logger.error(data['message']) 155 | else: 156 | logger.error(f'请求异常-{name},状态码为:{res.status_code}') 157 | 158 | 159 | def main(): 160 | names = cfg.getConfig('server_name') 161 | ports = cfg.getConfig('server_port') 162 | interval = cfg.getConfig('interval') 163 | timing = cfg.getConfig('timing') 164 | is_start = cfg.getConfig('is_start') 165 | 166 | port = ports.split(',') 167 | name = names.split(',') 168 | 169 | if len(port) == len(name): 170 | error_times = [0] * len(port) 171 | PID = [0] * len(port) 172 | if interval: # 如果周期性执行 173 | interval = int(interval) 174 | start_time = [time.time()] * len(port) # 初始化开始时间 175 | while True: 176 | for i in range(len(port)): 177 | pid = port_to_pid(port[i]) 178 | if time.time() - start_time[i] > interval: # 如果满足时间间隔 179 | if pid: 180 | logger.info(f'{name[i]}环境已开始执行') 181 | put_queue(name[i]) 182 | else: 183 | logger.error(f'{name[i]}环境对应的端口{port[i]}已经停止') 184 | send_email(name[i], port[i]) 185 | continue 186 | start_time[i] = time.time() 187 | 188 | if is_start: 189 | if pid: 190 | if pid != PID[i]: # 如果服务重启,则立即执行 191 | time.sleep(10) 192 | PID[i] = pid 193 | logger.info(f'{name[i]}环境已开始执行') 194 | put_queue(name[i]) 195 | start_time[i] = time.time() # 重置周期性执行开始时间 196 | error_times[i] = 0 197 | else: 198 | error_times[i] = error_times[i] + 1 199 | logger.error(f'{name[i]}环境对应的端口{port[i]}已经停止') 200 | if error_times[i] == 2: 201 | send_email(name[i], port[i]) 202 | 203 | time.sleep(29) 204 | elif timing: # 如果定时执行 205 | set_hour = int(timing.split(':')[0]) 206 | set_minute = int(timing.split(':')[1]) 207 | while True: 208 | current_hour = int(time.strftime('%H')) 209 | if current_hour - set_hour == 0: 210 | current_minute = int(time.strftime('%M')) 211 | if current_minute - set_minute == 0: # 如果满足时、分 212 | week = int(time.strftime('%w')) 213 | if week < 6: # 工作日运行,非工作日不运行 214 | for i in range(len(port)): 215 | pid = port_to_pid(port[i]) 216 | if pid: 217 | logger.info(f'{name[i]}环境已开始执行') 218 | put_queue(name[i]) 219 | else: 220 | logger.error(f'{name[i]}环境对应的端口{port[i]}已经停止') 221 | send_email(name[i], port[i]) 222 | 223 | if is_start: 224 | for i in range(len(port)): 225 | pid = port_to_pid(port[i]) 226 | if pid: 227 | if pid != PID[i]: # 如果服务重启,则立即执行 228 | time.sleep(10) 229 | PID[i] = pid 230 | logger.info(f'{name[i]}环境已开始执行') 231 | put_queue(name[i]) 232 | error_times[i] = 0 233 | else: 234 | error_times[i] = error_times[i] + 1 235 | logger.error(f'{name[i]}环境对应的端口{port[i]}已经停止') 236 | if error_times[i] == 2: 237 | send_email(name[i], port[i]) 238 | 239 | time.sleep(29) 240 | 241 | else: 242 | for i in range(len(port)): 243 | pid = port_to_pid(port[i]) 244 | if pid: 245 | logger.info(f'{name[i]}环境已开始执行') 246 | put_queue(name[i]) 247 | else: 248 | logger.error(f'{name[i]}环境对应的端口{port[i]}已经停止') 249 | send_email(name[i], port[i]) 250 | else: 251 | raise Exception('系统名称和系统对应的端口配置异常') 252 | 253 | 254 | if __name__ == '__main__': 255 | main() 256 | -------------------------------------------------------------------------------- /res/jmeter-results-detail-report_21.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | <xsl:value-of select="$titleReport" /> 39 | 99 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
133 | 134 | 135 | 136 | 137 |
138 | 139 | 140 | 141 |
142 | 143 | 144 |

145 | 146 | 147 | 148 | 149 | 150 |
报告时间:
151 |
152 |
153 | 154 | 155 |

测试结果概览

156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | Failure 185 | 186 | 187 | 190 | 193 | 198 | 203 | 208 | 213 | 214 |
用例总数失败用例数成功率平均响应时间响应时间最小值响应时间最大值
188 | 189 | 191 | 192 | 194 | 195 | 196 | 197 | 199 | 200 | 201 | 202 | 204 | 205 | 206 | 207 | 209 | 210 | 211 | 212 |
215 | 216 |
217 | 218 | 219 | 220 | 221 | 222 |

失败用例详情

223 | 224 | 225 | 226 | 227 | 228 | 229 |

230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 |
ResponseFailure MessageResponse Data
-
253 |
254 | 255 |
256 |
257 |
258 | 259 | 260 |

所有用例

261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | Failure 294 | 295 | 296 | 306 | 309 | 312 | 317 | 322 | 332 | 338 | 339 | 340 | 341 | page_details_ 342 | 368 | 369 | 370 | 371 |
Case IDSamplesFailuresAverage Time
297 | 298 | # 299 | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308 | 310 | 311 | 318 | 319 | 320 | 321 | 333 | 334 | javascript:change('page_details_') 335 | page_details__image展开 336 | 337 |
343 |
344 | Details for Page "" 345 | 346 | 347 | 348 | 349 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 |
MethodIterationSuccess
/
366 |
367 |
372 |
373 | 374 | 375 | 376 | 377 | NaN 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | NaN 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 |
415 | --------------------------------------------------------------------------------