412 lines
15 KiB
Python
412 lines
15 KiB
Python
# -*- coding:utf-8 -*-
|
||
# 日期:2019-8-14
|
||
# 作者:Wang WenJie
|
||
# 版本:1.2.2
|
||
# 说明:Excel转Xml,用于TestLink导入测试案例
|
||
#*******************************************
|
||
# 修改说明:
|
||
# 2019-9-10:修复CDATA类型文件转换后无法被读取
|
||
# 修改换行标记
|
||
# 2019-10-22:修复模块中间出现空白行解析不全
|
||
#*******************************************
|
||
|
||
import xlrd
|
||
import xml.dom.minidom as minidom
|
||
import re
|
||
import os
|
||
|
||
|
||
class ExcelToXml(object):
|
||
"""
|
||
Excel转Xml,用于TestLink导入测试案例
|
||
|
||
构造字典树,遍历写入xml文件
|
||
"""
|
||
|
||
def __init__(self, file_name):
|
||
try:
|
||
self.__xl = xlrd.open_workbook(file_name, formatting_info = True)
|
||
except NotImplementedError:
|
||
raise ValueError('不支持xlsx格式文件')
|
||
self.__xml_tree = {} # 全局字典树
|
||
self.__result = [0, 0, 0] #sheet页,模块,测试案例
|
||
_, self.__filename = os.path.split(file_name)
|
||
|
||
def __get_importance(self, zh_str):
|
||
'''
|
||
重要性中文转对应数字,不存在返回原字符
|
||
|
||
Args:
|
||
zh_str: 待转换字符
|
||
'''
|
||
if zh_str == '中':
|
||
return '2'
|
||
elif zh_str == '低':
|
||
return '1'
|
||
elif zh_str == '高':
|
||
return '3'
|
||
# 都不存在返回原字符
|
||
return zh_str
|
||
|
||
def __get_exec_type(self, zh_str):
|
||
'''
|
||
执行方式中文转对应数字,不存在返回原字符
|
||
|
||
Args:
|
||
zh_str: 待转换字符
|
||
'''
|
||
if zh_str == '手工':
|
||
return '1'
|
||
elif zh_str == '自动的':
|
||
return '2'
|
||
# 都不存在返回原字符
|
||
return zh_str
|
||
|
||
def __get_modules(self, col):
|
||
'''
|
||
从各个sheet页中的模块名开始遍历Excel中的内容,构造字典树
|
||
|
||
Args:
|
||
col: 模块所在列
|
||
|
||
Returns:
|
||
成功True, 失败False
|
||
'''
|
||
# 获取所有sheet页名称
|
||
sheet_names = self.__xl.sheet_names()
|
||
for sheet_name in sheet_names:
|
||
sheet = self.__xl.sheet_by_name(sheet_name)
|
||
nrows = sheet.nrows #总行数
|
||
# 没有数据直接忽略
|
||
if nrows < 2:
|
||
continue
|
||
tmp_index = (0, 1) # 第一行为标题
|
||
module_dic = {}
|
||
non_merged = [] # 未合并的行数
|
||
rest_merged = [] # 中间空白行引起的剩余模块
|
||
length = 1 # 记录遍历到多少行
|
||
for rlow, rhigh, clow, chigh in sheet.merged_cells:
|
||
# 不是第一列模块,忽略
|
||
if chigh != (col + 1):
|
||
continue
|
||
# 第一行没有标题
|
||
if rlow == 0:
|
||
tmp_index = (0, 0)
|
||
# 如果有单独一行的模块,或者没有合并单元格的模块
|
||
if rlow > tmp_index[1]:
|
||
non_merged.append((tmp_index[1], rlow))
|
||
elif rlow == tmp_index[1]: # 合并的单元格
|
||
module_name = sheet.cell_value(rlow, clow)
|
||
# 空模块名默认是单例
|
||
if module_name.strip() == '':
|
||
continue
|
||
if module_dic.__contains__(module_name):
|
||
print('ERROR:[{}-{}]合并单元格中有重复模块名[{}]'.format(self.__filename, sheet_name, module_name))
|
||
return False
|
||
module_dic[module_name] = {'coord': (rlow, rhigh)}
|
||
length = rhigh
|
||
tmp_index = (rlow, rhigh)
|
||
# 尾部剩余的没有合并单元格的添加进去
|
||
if length != nrows:
|
||
non_merged.append((length, nrows))
|
||
for i in range(0, len(non_merged) - 1):
|
||
_, rlow = non_merged[i]
|
||
_, rhigh = non_merged[i+1]
|
||
rest_merged.append((rlow, rhigh))
|
||
# 尾部还是单行的,或者没有合并的
|
||
start = 0
|
||
end = 1
|
||
last_name = ''
|
||
for rlow, rhigh in rest_merged:
|
||
module_name = sheet.cell_value(rlow, col)
|
||
# 如果该单元格有模块名
|
||
if module_name.strip() != '':
|
||
if module_dic.__contains__(module_name):
|
||
# 解决读取BUG,会把Excel多个合并单元格全部读取出来
|
||
low, high = module_dic.get(module_name)['coord']
|
||
if rlow != low and rhigh != high:
|
||
print('ERROR:[{}-{}]有重复模块名[{}]'.format(self.__filename, sheet_name, module_name))
|
||
return False
|
||
else:
|
||
continue
|
||
# 记录模块行数坐标
|
||
module_dic[module_name] = {'coord': (rlow, rhigh)}
|
||
start = rlow
|
||
end = rhigh
|
||
last_name = module_name
|
||
else: # 空白行由于步骤引起的,属于上一个模块
|
||
if last_name.strip() == '':
|
||
continue
|
||
module_dic[last_name] = {'coord': (start, end+1)}
|
||
end = end + 1
|
||
# 单例模式
|
||
if last_name == '' and not module_dic:
|
||
module_dic['single_case'] = {'coord': (1, nrows)}
|
||
self.__xml_tree[sheet_name] = module_dic
|
||
return True
|
||
|
||
def __get_step(self, sheet_name, rlow, rhigh, col_action, col_result, execution_type):
|
||
'''
|
||
获取标题对应的步骤和结果
|
||
返回:step列表
|
||
'''
|
||
steps = []
|
||
step_num = 1
|
||
# sheet页实例
|
||
sheet = self.__xl.sheet_by_name(sheet_name)
|
||
for row in range(rlow, rhigh):
|
||
# 动作
|
||
action = sheet.cell_value(row, col_action)
|
||
# 结果
|
||
result = sheet.cell_value(row, col_result)
|
||
# 构造字典,键值和节点名称对应
|
||
data = {
|
||
'step_number': str(step_num),
|
||
'actions': self.__auto_break(action),
|
||
'expectedresults': self.__auto_break(result),
|
||
'execution_type': execution_type
|
||
}
|
||
step_num = step_num + 1
|
||
steps.append(data)
|
||
return steps
|
||
|
||
def __get_title(self, sheet_name, col):
|
||
'''
|
||
获取sheet中模块对应的所有测试案例标题及起止行数
|
||
|
||
Args:
|
||
sheet_name: sheet页名称
|
||
col: 标题所在列数,从0计数
|
||
|
||
Returns:
|
||
成功True, 失败False
|
||
'''
|
||
sheet = self.__xl.sheet_by_name(sheet_name)
|
||
for k, v in self.__xml_tree[sheet_name].items():
|
||
title_dic = {} # 标题字典
|
||
start = 0 # 记录起始行
|
||
end = 0 # 记录空白的终止行
|
||
curr_title = ''# 当前标题
|
||
for row in range(v['coord'][0], v['coord'][1]):
|
||
title = sheet.cell_value(row, col)
|
||
if title.strip() != '': # 如果是标题行
|
||
title_dic[title] = {'coord': (row, row + 1)}
|
||
start = row
|
||
end = row + 1
|
||
curr_title = title
|
||
else: # 如果是步骤引发的空格
|
||
end = end + 1
|
||
title_dic[curr_title] = {'coord': (start, end)}
|
||
self.__xml_tree[sheet_name][k]['testcase'] = title_dic
|
||
return True
|
||
|
||
def __auto_break(self, text):
|
||
'''
|
||
实现步骤和结果自动换行
|
||
|
||
Args:
|
||
text: 待换行的字符床
|
||
'''
|
||
doc = re.sub(r'\n', r'<br />', text, re.M)
|
||
return doc
|
||
|
||
def create_tree(self):
|
||
'''
|
||
根据TestLink格式要求构造字典树
|
||
|
||
Returns:
|
||
成功True, 失败False
|
||
'''
|
||
if not self.__get_modules(0):
|
||
print('ERROR: 创建字典树中,读取模块失败')
|
||
return False
|
||
# sheet页模块
|
||
for k, v in self.__xml_tree.items():
|
||
# print('一级模块{}:'.format(k))
|
||
# sheet数量统计
|
||
self.__result[0] = self.__result[0] + 1
|
||
sheet = self.__xl.sheet_by_name(k)
|
||
if not self.__get_title(k, 1):
|
||
print('ERROR: 创建字典树中,读取标题失败')
|
||
# sheet页中的模块
|
||
for mk, mv in v.items():
|
||
# print('\t模块[{}]'.format(mk))
|
||
# 模块统计
|
||
self.__result[1] = self.__result[1] + 1
|
||
for title, content in mv['testcase'].items():
|
||
# print('\t\t标题[{}]'.format(title))
|
||
# 测试案例统计
|
||
self.__result[2] = self.__result[2] + 1
|
||
row = content['coord'][0] # 标题所在行
|
||
summary = sheet.cell_value(row, 4) # 摘要
|
||
precond = sheet.cell_value(row, 5) # 前提
|
||
exe_type = self.__get_exec_type(sheet.cell_value(row, 3)) # 执行方式
|
||
importance = self.__get_importance(sheet.cell_value(row, 2)) # 重要性
|
||
# 步骤
|
||
steps = self.__get_step(k, content['coord'][0], content['coord'][1], 6, 7, exe_type)
|
||
case = {
|
||
'node_order': str(self.__result[2]),
|
||
'version': '1',
|
||
'summary': self.__auto_break(summary),
|
||
'preconditions': self.__auto_break(precond),
|
||
'execution_type': exe_type,
|
||
'importance': importance,
|
||
'status': '1',
|
||
'is_open': '1',
|
||
'active': '1',
|
||
'steps': steps
|
||
}
|
||
self.__xml_tree[k][mk]['testcase'][title]['case'] = case
|
||
return True
|
||
|
||
def get_tree(self):
|
||
'''
|
||
返回构造好的字典树
|
||
'''
|
||
return self.__xml_tree
|
||
|
||
def write_xml(self, file_path):
|
||
'''
|
||
把字典树导出为xml格式
|
||
|
||
Args:
|
||
file_path: xml文件保存路径
|
||
|
||
Returns:
|
||
成功True, 失败False
|
||
'''
|
||
for k, v in self.__xml_tree.items():
|
||
# 创建根节点
|
||
dom = XmlDom()
|
||
root = None
|
||
# 如果是单例模式
|
||
if v.__contains__('single_case'):
|
||
root = dom.create_root('testcases')
|
||
else:
|
||
root = dom.create_root('testsuite')
|
||
# k-sheet页名
|
||
root.setAttribute('name', k)
|
||
# 遍历模块
|
||
order = 1
|
||
for mk, mv in v.items():
|
||
# 如果不是单例模式,则创建模块节点
|
||
module = None
|
||
if mk != 'single_case':
|
||
module = dom.create_node('testsuite')
|
||
# mk-模块名
|
||
module.setAttribute('name', mk)
|
||
root.appendChild(module)
|
||
module_order = dom.create_cdata('node_order', str(order))
|
||
module.appendChild(module_order)
|
||
order = order + 1
|
||
else: # 单例模式模块节点用根节点覆盖
|
||
module = root
|
||
for title, cases in mv['testcase'].items():
|
||
# 标题案例节点
|
||
testcase = dom.create_node('testcase')
|
||
testcase.setAttribute('name', title)
|
||
module.appendChild(testcase)
|
||
for node, text in cases['case'].items():
|
||
# 非step节点直接写入
|
||
if node != 'steps':
|
||
tmp = dom.create_cdata(node, text)
|
||
testcase.appendChild(tmp)
|
||
else:
|
||
steps = dom.create_node(node)
|
||
testcase.appendChild(steps)
|
||
# steps是一个列表
|
||
for step_list in text:
|
||
step = dom.create_node('step')
|
||
steps.appendChild(step)
|
||
# 创建steps中的单个step
|
||
for node, text in step_list.items():
|
||
tmp = dom.create_cdata(node, text)
|
||
step.appendChild(tmp)
|
||
# 创建输出文件名
|
||
file_name = ''
|
||
if v.__contains__('single_case'):
|
||
file_name = r'Example_Testsuite_Default_{}_Testcases.xml'.format(k)
|
||
else:
|
||
file_name = r'Example_Testsuite_Default_{}_Testsuite.xml'.format(k)
|
||
path = os.path.join(file_path, file_name)
|
||
# 写入文件
|
||
dom.write(path)
|
||
return True
|
||
|
||
def get_result(self):
|
||
'''
|
||
返回转换sheet、模块和测试案例统计
|
||
'''
|
||
return self.__result
|
||
|
||
|
||
class XmlDom(object):
|
||
"""
|
||
创建xml文件方法,使用minidom方式创建
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.dom = None
|
||
|
||
def create_root(self, name):
|
||
'''
|
||
创建根节点
|
||
|
||
Args:
|
||
name: 根节点名称
|
||
'''
|
||
self.dom = minidom.getDOMImplementation().createDocument(None, name, None)
|
||
root = self.dom.documentElement
|
||
return root
|
||
|
||
def create_node(self, name):
|
||
'''
|
||
创建一个节点
|
||
|
||
Args:
|
||
name: 节点名称
|
||
'''
|
||
node = self.dom.createElement(name)
|
||
return node
|
||
|
||
def create_cdata(self, name, text=''):
|
||
"""
|
||
创建CDATA数据节点
|
||
|
||
Args:
|
||
name: 节点名
|
||
text: 写入节点的值
|
||
|
||
Returns:
|
||
节点对象
|
||
"""
|
||
node = self.dom.createElement(name)
|
||
cdata_text = self.dom.createCDATASection(text)
|
||
node.appendChild(cdata_text)
|
||
return node
|
||
|
||
def write(self, file_name):
|
||
'''
|
||
写入xml文件
|
||
|
||
Args:
|
||
file_name: 写入的文件名
|
||
'''
|
||
# 先写入
|
||
with open(file_name, 'w', encoding='utf-8') as f:
|
||
xml_str = self.dom.toprettyxml()
|
||
pretty = self.__pretty_xml(xml_str)
|
||
f.write(pretty)
|
||
# 释放
|
||
self.dom.unlink()
|
||
|
||
def __pretty_xml(self, xml_doc):
|
||
'''
|
||
格式化xml中的CDATA
|
||
'''
|
||
doc = re.sub(r'>\n<\!\[', r'><![', xml_doc, 0, re.M)
|
||
doc = re.sub(r'\]\]>(\s+)<', r']]><', doc, 0, re.M)
|
||
return doc
|
||
|
||
if __name__ == "__main__":
|
||
print('Excel转XML助手,用于TestLink导入测试案例') |