前言
- 目前在公司,总是在下班前夕或者下班后发送阶段性测试报告,需要统计redmine中的bug情况,往往这个时候,就是公司redmine最卡的时候(不知道啥情况,肯定不是多人使用的原因,内网不可能这点人都带不动),统计的数据也较多,有本阶段进行状态修改情况,有本个版本总体情况,还要计算总体的修复率等等。。。考虑到这种情况,python还有redmine现成的库,为何不去试试实现一个自动化的方式呢?这个也是我这段时间学习python知识的总结!
首先我去了解了一下这个库,真是智能,每个字段都是固定的,这时不用更待何时!
目标统计
这是我们公司之前的阶段性测试报告的模板
在这里插入图片描述
可以看到之前我们要统计多少数据了吧!我们在发送邮件的时候这个只是一个数据支撑,还要细细说明这次的测试情况等等,所以初步想的实现点:
- 本阶段缺陷测试情况
- 本版本测试情况
- 被指派人缺陷状态情况
- 本版本缺陷结构分布情况
实现步骤
将相关的依赖环境安装好
from redminelib import Redmine
我需要将我需要的bug数据筛出来,这里我就要将我们内网的环境和需要字段的参数剔出来,以下是我的配置文件
Mysql_dict={'host':'192.168.4.22','port':'5407','user':'postgres','passwd':'@#456','db':'report','previous_version_unclosed':'13'}
#host:测试环境数据库地址;port:数据库端口;user:数据库用户名;passwd:数据库密码;db:需要存储的数据库表;previous_version_unclosed:上一轮未修复bug数
Redmine_dict={'host':'http://192.168.4.211:3000/','username':'yuanmingyue','pw':'123456','project_name':'hhya','fixed_version_id':[460,462],'cf_26':'462'}
#host:redmine地址;username:用户名;pw:密码;project_name:项目名称;fixed_version_id:目标版本号;cf_26:验证版本号 注意:一般目标版本号和验证版本号一致
Plan_dict={'test_time':'2021-01-20','test_addr':'https://hhhhh.com','tester':'haha,enen','test_version':'第一阶段测试报告','redmine_addr':'http://192.168.4.211:3000/projects/hhya'}
#test_time:测试时间;test_addr:测试地址;tester:测试人员;test_version:测试阶段title;redmine_addr:测试项目redmine地址
current_txt={'update_time':'2020-09-22','branch':'release/V2.5.5/test','git_log': '128f5409cafa41b4748d0aefc74db3a673ef1','test_progress':['pc端——haha——1、导航不可用;2、搜索不可用。。。哈哈哈哈哈哈哈哈哈哈或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或或','H5端——enen——1、交互还存在较多问题。。。']}
# update_time:安装时间;branch:测试分支;git_log:测试commitId;test_progress:测试内容情况等信息,注意:这里是一个列表,内容为:负责内容——负责人员——测试情况,我们以前在邮件中写的情况,可以分点写,中间要用——分开
现在将我需要的bug整理出来,以便后面进行整理归纳,并实现可视化图形展现
config_mysql=properties.Mysql_dict
config_Redmine=properties.Redmine_dict
config_Plan=properties.Plan_dict
config_current=properties.current_txt
previous_version_unclosed=properties.Mysql_dict['previous_version_unclosed']
issues_all=[]
issues_unrestricted=[]
issues=[]
redmine = Redmine(config_Redmine['host'], username=config_Redmine['username'], password=config_Redmine['pw'])
project_name = redmine.project.get(config_Redmine['project_name'])
#所有的打开bug
for i in config_Redmine['fixed_version_id']:
issues_one = list(redmine.issue.filter(project_id=project_name.id, tracker_id=1, status_id='o', set_filter=1,fixed_version_id=i))
for bug in issues_one:
issues.append(bug)
#本次目标版本的所有的bug
issues_unrestricted = list(redmine.issue.filter(project_id=project_name.id, tracker_id=1, status_id='*',set_filter=1, fixed_version_id=config_Redmine['cf_26']))
#所有大版本bug情况
for i in config_Redmine['fixed_version_id']:
issues_one=list(redmine.issue.filter(project_id=project_name.id, tracker_id=1,status_id='*', set_filter=1,fixed_version_id=i))
for bug in issues_one:
issues_all.append(bug)
#验证版本固定
issues_onverify = list(redmine.issue.filter(project_id=project_name.id, tracker_id=1,status_id='*', set_filter=1, cf_26=config_Redmine['cf_26']))
缺陷对应人员的数量及缺陷结构分布数量
def current_day():
resolve_user_list,new_user_list,refused_user_list,re_activation_user_list,suspend_user_list,pone_user_list,all_user_list,march,module_list = [],[],[],[],[],[],[],[],[]
install_time=config_current['update_time']
branch=config_current['branch']
git_log=config_current['git_log']
test_progress=config_current['test_progress']
test_edition=[install_time,branch,git_log]
for line in test_progress:
line = line.split('——')
march.append(line)
# user
with open(user_filepath, 'r',encoding='UTF-8') as file_user_read:
user_lines = file_user_read.readlines()
for user in user_lines:
n = 0
j = 0
x=0
k=0
p=0
g=0
c=0
for issue in issues:
if issue.assigned_to.name + '\n' == user:
j +=1
if issue.status.name == "已解决":
n += 1
if issue.status.name == "新建":
x += 1
if issue.status.name == "已拒绝":
k += 1
if issue.status.name == "推后处理":
p += 1
if issue.status.name == "挂起":
g += 1
if issue.status.name == "重新激活":
c += 1
if n!=0:
resolve_user_list.append([user[:-1],n]) #已解决的人员分布
if x!=0:
new_user_list.append([user[:-1],x]) #新建的人员分布
if k!=0:
refused_user_list.append([user[:-1],k]) #已拒绝的人员分布
if c!=0:
re_activation_user_list.append([user[:-1],c]) #重新激活的人员分布
if g!=0:
suspend_user_list.append([user[:-1],g]) #挂起的人员分布
if p!=0:
pone_user_list.append([user[:-1],p]) #推后处理的人员分布
if j!=0:
all_user_list.append([user[:-1],j])
# module
with open(module_filepath, 'r', encoding='UTF-8') as file_module_read:
module_lines = file_module_read.readlines()
for module in module_lines:
n = 0
for issue in issues:
if issue.custom_fields._resources[0]['value'] + '\n' == module:
n += 1
module_list.append([module[:-1],n])
对应图标展示
def chart_barh(chart_value,chart_name,img_name):
x_value,y_value = [],[]
for line in chart_value:
if isinstance(line[1],int):
x_value.append(line[0])
y_value.append(line[1])
plt.bar(x_value, y_value, color='blue', label='num')
# plt.figure(figsize=(20, 10)) # 设置画布大小
plt.grid(axis="y", linestyle='-.') # 设置画布背景,横向,虚线
plt.title(chart_name) # 设置图标名称
plt.xticks(rotation=45)
for i, (_x, _y) in enumerate(zip(x_value, y_value)):
plt.text(_x, _y, y_value[i], color='black', ha='center', va='bottom', fontsize=10, ) # 将数值显示在图形上
plt.savefig(img_name+'.png') # 保存数据到本地
# plt.show()
plt.clf()
plt.close()
这里又不得不说以下图表展示
import matplotlib.pyplot as plt
在这里插入图片描述
在这里插入图片描述
这里不做具体说明哈,网上百度还是很多哈,这里只做拉通讲解!!
以下就是把它展示出来
filename.write('<div style="margin-top: 30px">7.本版本未关闭缺陷人员对应</div><div style="margin-top: 2px">'
'<img src="cid:image1" width="640px" height="400px"></div>' # 所有缺陷人员对应
'<div style="margin-top: 30px">8.未修复缺陷模块对应</div><div style="margin-top: 2px">'
'<img src="cid:image3" width="640px" height="400px"></div>') # 未修复缺陷模块对应
filename.write('</body></html>')
#折线图画值
y2_value = chart2_value[1]
l = [i for i in range(len(chart_value[0]))]
fig=plt.figure(figsize=(20, 10)) # 设置画布大小
ax1 = fig.add_subplot(111)
ax1.plot(l,y2_value , 'or-', label=u'本阶段缺陷状态')
ax1.set_ylim([0, a+a*0.2])
for i, (_x, _y) in enumerate(zip(l, y2_value)):
plt.text(_x, _y, y2_value[i], color='black', fontsize=25)
plt.legend(prop={'family':'SimHei','size':20})
plt.tick_params(labelsize=25)
plt.rcParams['font.sans-serif'] = ['simsun']
ax2 = ax1.twinx()
ax2.set_ylim([0, a+a*0.2])
plt.bar(l, y_value, alpha=0.3, color='blue', label=u'总缺陷状态')
plt.legend(prop={'family': 'SimHei', 'size': 20}, loc="upper left")
plt.title(chart_name,fontdict={'weight':'normal','size': 30}) # 设置图标名称
plt.tick_params(labelsize=25)
plt.xticks(l, x_value)
for i, (_x, _y) in enumerate(zip(l, y_value)):
plt.text(_x, _y, y_value[i], color='black', ha='center', va='bottom', fontsize=25) # 将数值显示在图形上
# plt.show()
plt.savefig(img_name + '.png')
plt.clf()
plt.close()
目标版本的所有bug严重等级情况
def analysis():
# 源类型:严重 grave 致命 deadly 紧急 urgent 已解决 resolved 超时 timeout 阻塞 stop
# 目前类型:新建:new;已解决:resolved;已拒绝:refused ;重新激活:re_activation;已关闭:closed;挂起:suspend;推后处理:pone
# 严重 grave 致命 deadly 一般 popularly 提示 prompt 建议 suggestibly
grave_list = [] #严重缺陷列表
prompt_num,grave_num, deadly_num, popularly_num,suggestibly_num = 0,0,0, 0, 0
grave_unclosed=0
for issue in issues_unrestricted:
#统计bug严重等级
if issue.custom_fields._resources[1]['value'] == "致命":
deadly_num += 1
if issue.custom_fields._resources[1]['value'] == "严重":
grave_num += 1
if issue.custom_fields._resources[1]['value'] == "一般":
popularly_num += 1
if issue.custom_fields._resources[1]['value'] == "提示":
prompt_num += 1
if issue.custom_fields._resources[1]['value'] == "建议":
suggestibly_num += 1
new_nowversion=prompt_num+grave_num+deadly_num+popularly_num+suggestibly_num
for issue in issues_all:
if issue.custom_fields._resources[1]['value'] == "严重":
if issue.status.name == '已关闭' or issue.status.name == '挂起':
continue
grave_unclosed+=1
grave_list.append([grave_unclosed, issue.status.name, issue.id, issue.subject])
analysis_list=[['未修复严重缺陷列表',grave_list],['严重',grave_num],['致命',deadly_num],['一般',popularly_num],['提示',prompt_num],['建议',suggestibly_num],['新建缺陷总数',0]]
analysis_list[6][1]=new_nowversion
图表同上,没有任何区别
本个大版本的bug情况和本阶段的bug情况
这里统计的数据我都放在数据库中存放一份,以备不时之需
def SandP_mysql():
summer = [['本阶段新建', '本阶段已解决', '本阶段已拒绝', '本阶段重新激活', '本阶段挂起', '本阶段推后处理', '本阶段关闭', '本阶段', '本阶段']]
daily_row = [['新建', '已解决', '已拒绝', '重新激活', '挂起', '推后处理', '已关闭','未修复','总缺陷']]
new_unrestricted,resolved_unrestricted,refused_unrestricted,re_activation_onverify,suspend_onverify,pone_onverify,closed_onverify=0, 0, 0, 0, 0, 0, 0
new_all, resolved_all, refused_all, re_activation_all, suspend_all, pone_all, closed_all = 0, 0, 0, 0, 0, 0, 0
fix_percent=[]
for issue in issues_all:
if issue.status.name == "新建": new_all += 1
if issue.status.name == "已解决": resolved_all += 1
if issue.status.name == "已拒绝": refused_all += 1
if issue.status.name == "重新激活": re_activation_all += 1
if issue.status.name == "挂起": suspend_all += 1
if issue.status.name == "推后处理": pone_all += 1
if issue.status.name == '已关闭': closed_all += 1
totle_unclosed = new_all + resolved_all + re_activation_all+refused_all+pone_all # 未修复缺陷数
totle_bug=new_all + resolved_all + re_activation_all+refused_all+suspend_all+pone_all+closed_all#总缺陷数
daily_row.append([new_all, resolved_all, refused_all, re_activation_all,suspend_all, pone_all,closed_all,totle_unclosed,totle_bug])
for issue in issues_unrestricted:
if issue.status.name == "新建": new_unrestricted += 1
if issue.status.name == "已解决": resolved_unrestricted += 1
if issue.status.name == "已拒绝": refused_unrestricted += 1
for issue in issues_onverify:
if issue.status.name == "重新激活": re_activation_onverify += 1
if issue.status.name == "挂起": suspend_onverify += 1
if issue.status.name == "推后处理": pone_onverify += 1
if issue.status.name == "已关闭": closed_onverify += 1
totle_unclosed_unrestricted = new_unrestricted + resolved_unrestricted+re_activation_onverify # 未修复缺陷数
totle_bug_unrestricted=new_unrestricted + resolved_unrestricted + re_activation_onverify+refused_unrestricted+suspend_onverify+pone_onverify+closed_onverify#总缺陷数
summer.append([new_unrestricted, resolved_unrestricted, refused_unrestricted,re_activation_onverify,suspend_onverify,pone_onverify,closed_onverify,totle_unclosed_unrestricted,totle_bug_unrestricted])
db =psycopg2.connect(host=config_mysql['host'], port=int(config_mysql['port']), user=config_mysql['user'], password=config_mysql['passwd'], dbname=config_mysql['db'])
cursor = db.cursor()
fix_data=int(closed_onverify)/(int(previous_version_unclosed) - int(pone_onverify) - int(refused_unrestricted))
fix_percent.append('{:.2f}%'.format(fix_data * 100))
sql_all_bug = "insert into nowversion_status(project,vrsion,new,resolved,refused,re_activation,suspend,pone,closed,totle_unclosed,totle_bug,runtime) " \
"VALUES ('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')" \
% (project_name,config_Redmine['cf_26'],new_unrestricted,resolved_unrestricted,refused_unrestricted,re_activation_onverify,suspend_onverify, pone_onverify, closed_onverify,totle_unclosed,totle_bug,datetime.now())
try:
cursor.execute(sql_all_bug)
db.commit()
print("ok")
except:
# 如果发生错误则回滚
db.rollback()
print("no")
db.close()
这里想到,本阶段和本个版本都需要统计相同的数据,不如做个图标,将这两种情况做个对比展示出来,由此就出现了一下代码
def chart_column(chart_value,chart2_value,chart_name,img_name):
#柱状图画值x,y值
x_value = chart_value[0]
y_value = chart_value[1]
#获取柱状图纵坐标的最大值
a=0
for i in y_value:
if i>a:
a=i
#折线图画值
y2_value = chart2_value[1]
l = [i for i in range(len(chart_value[0]))]
fig=plt.figure(figsize=(20, 10)) # 设置画布大小
ax1 = fig.add_subplot(111)
ax1.plot(l,y2_value , 'or-', label=u'本阶段缺陷状态')
ax1.set_ylim([0, a+a*0.2])
for i, (_x, _y) in enumerate(zip(l, y2_value)):
plt.text(_x, _y, y2_value[i], color='black', fontsize=25)
plt.legend(prop={'family':'SimHei','size':20})
plt.tick_params(labelsize=25)
plt.rcParams['font.sans-serif'] = ['simsun']
ax2 = ax1.twinx()
ax2.set_ylim([0, a+a*0.2])
plt.bar(l, y_value, alpha=0.3, color='blue', label=u'总缺陷状态')
plt.legend(prop={'family': 'SimHei', 'size': 20}, loc="upper left")
plt.title(chart_name,fontdict={'weight':'normal','size': 30}) # 设置图标名称
plt.tick_params(labelsize=25)
plt.xticks(l, x_value)
for i, (_x, _y) in enumerate(zip(l, y_value)):
plt.text(_x, _y, y_value[i], color='black', ha='center', va='bottom', fontsize=25) # 将数值显示在图形上
# plt.show()
plt.savefig(img_name + '.png')
plt.clf()
plt.close()
在这里插入图片描述
并且还做了以下的一些信息展示,让其他人更加了解本个项目
在这里插入图片描述
在这里插入图片描述
file_html = 'daily_report.html' # html
with open(file_html, mode='a',encoding='utf-8') as filename:
filename.write('<html><meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
'<body link="blue" vlink="purple">')
filename.write('<div style="margin-top: 30px">1.测试安排</div>'
'<table border="1" cellspacing="0" width="440">'
'<tr><td>时间安排</td><td>'+str(config_Plan['test_time'])+'</td></tr>'
'<tr><td>测试地址</td><td>'+str(config_Plan['test_addr'])+'</td></tr>'
'<tr><td>redmine项目地址</td><td>'+str(config_Plan['redmine_addr'])+'</td></tr>'
'<tr><td>人员安排</td><td>'+str(config_Plan['tester'])+'</td></tr>'
'<tr><td>测试阶段</td><td>'+str(str(project_name)+config_Plan['test_version'])+'</td></tr>'
'</table>')
filename.write('<div style="margin-top: 30px">2.测试版本</div>'
'<table border="1" cellspacing="0" width="740">'
'<tr><td>序号</td><td>安装日期</td><td>branch</td><td>commit ID</td></tr>'
'<tr><td>1</td><td>'+str(person_testrate[-1][0])+'</td><td>'+str(person_testrate[-1][1])+'</td>'
'<td>'+str(person_testrate[-1][2])+'</td></tr>'
'</table>')
发送邮件
接下来我就想啊,我阶段性测试报告内容做完了,那我还想实现自动化发送给指定的人,那不就更加自行话了吗!安排~~~
首先就是生成报告,然后配置邮件内容
import smtplib
from email.mime.image import MIMEImage
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from daily_report import *
run()
time = datetime.now().timetuple()
today = str(time.tm_year) + '-' + str(time.tm_mon) + '-' + str(time.tm_mday)
msg_from=input('请输入发送人邮箱:')
passwd=input('请输入发送人邮箱授权码:')
msg_to=input('请输入收件人邮箱,多人用英文逗号隔开:').split(',')
# 创建一个带附件的实例
message = MIMEMultipart()
message['From'] = Header('哈哈', 'utf-8') # 发起人名字,不填展示为空
message['To'] = Header('项目组成员', 'utf-8') # 接收人名字,不填展示为空
subject = '酒城停车第一阶段测试报告'+ today# 邮件标题
message['Subject'] = Header(subject, 'utf-8')
file_html="daily_report.html"
with open('daily_report.html','r',encoding='utf-8') as f:
content = f.read()
content = str(content)
mail_msg = content
message.attach(MIMEText(mail_msg, 'html', 'utf-8')) # 如果要发送html需要将plain改成html
fp1 = open('people_unresolve_chart.png', 'rb')
msgImage1 = MIMEImage(fp1.read())
fp1.close()
msgImage1.add_header('Content-ID', '<image1>')
message.attach(msgImage1)
这里需要注意的是,我发送的邮件是带图片的,所以 msgImage1.add_header('Content-ID', '<image1>')这一块得应用ID,其他图片同理
打包成exe应用程序
自动发送邮件我成功了啊,但是我想别的测试人员也使用我的自动化代码,又不需要安装依赖那些东西,搞成意见生成就更好了。
pyinstaller -F send_email.py
在这里插入图片描述
命令窗口运行以上命令,但是有个问题啊,我的配置文件,可能需要随时改,别人那不就经常找我打包,就想要不他们自己写了,把配置文件上传上去不就好了
import tkinter as tk
from tkinter import filedialog
import sys
'''打开选择文件夹对话框'''
root = tk.Tk()
root.withdraw()
print('请选择配置文件所在文件夹位置~~~~')
Folderpath = filedialog.askdirectory() #获得选择好的文件夹
print('请选择redmine中bug相关人员的信息文件~~~~')
user_filepath = filedialog.askopenfilename() #获得选择好的文件
print('请选择redmine中配置的结构分布的信息文件~~~~')
module_filepath = filedialog.askopenfilename() #获得选择好的文件
sys.path.append(Folderpath)
在这里插入图片描述
就此,我的自动发送阶段性测试报告就此完成,大家有其他更好的意见多多提出哈!
如果大家想看完整的脚本代码,移步至https://download.csdn.net/download/qq_42261165/14928868,还有现成的exe文件哈