script/Testlink转换工具/ExcelToXml3/ExcelToXml.py

412 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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导入测试案例')