# -*- 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 import sys reload(sys) sys.setdefaultencoding('utf-8') #解决写入文件时候编码错误 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(u'不支持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 == u'中': return '2' elif zh_str == u'低': return '1' elif zh_str == u'高': return '3' # 都不存在返回原字符 return zh_str def __get_exec_type(self, zh_str): ''' 执行方式中文转对应数字,不存在返回原字符 Args: zh_str: 待转换字符 ''' if zh_str == u'手工': return '1' elif zh_str == u'自动的': 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)) else: # 合并的单元格 module_name = sheet.cell_value(rlow, clow) # 空模块名默认是单例 if module_name.strip() == '': continue if module_dic.has_key(module_name): print(u'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.has_key(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'
', text, re.M) return doc def create_tree(self): ''' 根据TestLink格式要求构造字典树 Returns: 成功True, 失败False ''' if not self.__get_modules(0): print(u'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(u'ERROR: 创建字典树中,读取标题失败') # sheet页中的模块 for mk, mv in v.items(): # print('\t模块[{}]'.format(mk)) # 模块统计 self.__result[1] = self.__result[1] + 1 testcase = sorted(mv['testcase'].items(), key = lambda x:x[0]) for title, content in testcase: # 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 ''' tree = sorted(self.__xml_tree.items(), key = lambda x:x[0]) for k, v in tree: # 创建根节点 dom = XmlDom() root = None # 如果是单例模式 if v.has_key('single_case'): root = dom.create_root('testcases') else: root = dom.create_root('testsuite') # k-sheet页名 root.setAttribute('name', k) # 遍历模块 order = 1 module_v = sorted(v.items(), key = lambda x:x[0]) for mk, mv in module_v: # 如果不是单例模式,则创建模块节点 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 case_title = sorted(mv['testcase'].items(), key = lambda x:x[0]) for title, cases in case_title: # 标题案例节点 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.has_key('single_case'): file_name = u'Example_Testsuite_Default_%s_Testcases.xml' % k else: file_name = u'Example_Testsuite_Default_%s_Testsuite.xml' % k path = os.path.join(file_path.decode('gb2312'), 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 create_text(self, name, text=''): """ 创建普通Text数据节点 Args: name: 节点名 text: 写入节点的值 Returns: 节点对象 """ node = self.dom.createElement(name) cdata_text = self.dom.createTextNode(text) node.appendChild(cdata_text) return node def write(self, file_name): ''' 写入xml文件 Args: file_name: 写入的文件名 ''' # 先写入 with open(file_name, 'w') 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'>(\s+)<', r']]><', doc, 0, re.M) return doc if __name__ == "__main__": print(u'Excel转XML助手,用于TestLink导入测试案例')