Skip to content

Commit

Permalink
现有Word用例转Metersphere导入格式
Browse files Browse the repository at this point in the history
  • Loading branch information
olivia2046 committed Sep 6, 2024
1 parent 8927250 commit 8e116b0
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 1 deletion.
Binary file added existing_word_tc.docx
Binary file not shown.
2 changes: 1 addition & 1 deletion populate_excel_sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
sheet_name_prefix="测试用例执行结果"
#实际结果在模板中的位置
actual_result_loc = 'A7'
target_wb_path = root_dir + os.sep + "dummy_target.xlsx"
target_wb_path = root_dir + os.sep + "excel_multi_sheets_target.xlsx"
# 因需要带格式复制sheet, 且copy_worksheet只能在同一个workbook内复制sheet,因此先复制模板文件到目标文件,再在目标文件内操作
try:
copyfile(tmpl_wb_path, target_wb_path)
Expand Down
301 changes: 301 additions & 0 deletions tc_word_to_excel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# -*- coding: utf-8 -*-
'''
@author: olivia.dou
Created on: 2023/6/24 8:34
desc:
使用步骤
1. 将现有Word用例去除其他部分(如封面页、文档描述等),仅保留用例章节,单独保存一份文档(docx格式)
2. 修改tc_fields_loc中各字段对应值在Word用例表格中的位置(行,列),以0起始
3. 修改root_dir,source_path,dest_wb_path
4. tc_steps_prefix和expected_result_prefix(去除前缀后为需要提取的内容)
5. 运行脚本
6. 如果用例中有截图,将embeded_images_path对应路径下生成的图片批量上传到metersphere服务器/app/metersphere/data/image/markdown路径下(需要root权限)
7. 将Excel文件导入Metersphere平台
'''

import os,openpyxl,docx2txt,shutil
import traceback
from xml.dom import minidom
from docx import Document
from docx.table import Table
from docx.text.paragraph import Paragraph
from openpyxl.drawing.image import Image
from msphere_lib import generate_random_string

# 指定测试用例中各字段值的位置(行,列), 索引从0开始
tc_fields_loc = {"ID":(1,1),"用例名称":(2,1),"所属模块":None,'前置条件':None,'备注':(3,1),'步骤描述':(4,0),'预期结果':(5,0),
'编辑模式':None,'标签':None,'责任人':(0,1),'用例等级':None,'用例状态':None,'用例类型':None,'附加字段':(6,0)}
tc_steps_prefix = "预置条件和测试步骤:"
expected_result_prefix = "预期结果: \n"

current_file_path = os.path.abspath(__file__)
root_dir = os.path.dirname(current_file_path)
source_path = root_dir + os.sep + "existing_word_tc.docx"
source_doc = Document(source_path)
dest_wb_path = root_dir + os.sep +'dummy_output.xlsx' #目标用例workbook
dest_wb = openpyxl.Workbook()
dest_wb.save(dest_wb_path) # 同名覆盖,否则创建
#dest_sheet = dest_wb.create_sheet(dest_sheet_name)
dest_sheet = dest_wb.active
dest_wb.close()

#写入标题:
head = ['ID','用例名称','所属模块','前置条件','备注','步骤描述','预期结果','编辑模式','标签','责任人','用例等级','用例状态','用例类型','附加字段']
dest_sheet.append(head)

full_path_titles=[]
titles = []


def is_directory_empty(folder_path):
if not os.path.isdir(folder_path):
raise ValueError("Invalid folder path.")

if len(os.listdir(folder_path)) == 0:
return True
else:
return False


def clear_directory(folder_path):
if not os.path.isdir(folder_path):
raise ValueError("Invalid folder path.")

for filename in os.listdir(folder_path):
file_path = os.path.join(folder_path, filename)
if os.path.isfile(file_path):
os.remove(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)

# extract text and write images in Temporary Image directory
embeded_images_path = root_dir + os.sep + 'source_images/word/media'
if not os.path.exists(embeded_images_path):
os.makedirs(embeded_images_path)
#如果文件夹非空,则清空文件夹
if not is_directory_empty(embeded_images_path):
clear_directory(embeded_images_path )
docx2txt.process(source_path,embeded_images_path)

def get_case_module_path(full_path_titles, titles):
"""
:param full_path_titles: 完整路径的标题列表
:param titles: 当前标题列表
:return: 用例所在模块
"""
try:
#在prev_titles的基础上用当前标题列表按照层级替换
title_level = titles[0][0] #从titles的第一个level开始比对
for i,title in enumerate(full_path_titles):
if title[0]==title_level and title_level in [level for (level,title) in titles]:
full_path_titles[i]=titles[[level for (level,title) in titles].index(title_level)]
title_level+=1
elif title[0]==title_level: #titles路径短
for index in range(i,len(full_path_titles)):
del full_path_titles[index]

if title_level in [level for (level,title) in titles]: #titles里有后续level的标题
index = [level for (level,title) in titles].index(title_level)
full_path_titles.extend(titles[index:])

return full_path_titles
except Exception as e:
traceback.print_exc(e)


def iter_block_items(parent):
# https://github.com/python-openxml/python-docx/issues/40
from docx.document import Document
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.table import _Cell, Table
from docx.text.paragraph import Paragraph
"""
Yield each paragraph and table child within *parent*, in document order.
Each returned value is an instance of either Table or Paragraph. *parent*
would most commonly be a reference to a main Document object, but
also works for a _Cell object, which itself can contain paragraphs and tables.
"""
if isinstance(parent, Document):
parent_elm = parent.element.body
elif isinstance(parent, _Cell):
parent_elm = parent._tc
else:
raise ValueError("something's not right")

# print('parent_elm: '+str(type(parent_elm)))
for child in parent_elm.iterchildren():
if isinstance(child, CT_P):
yield Paragraph(child, parent)
elif isinstance(child, CT_Tbl):
yield Table(child, parent) # No recursion, return tables as tables
# table = Table(child, parent) # Use recursion to return tables as paragraphs
# for row in table.rows:
# for cell in row.cells:
# yield from iter_block_items(cell)

def convert_content_to_list(content, prefix):
"""处理测试步骤、预期结果内容,按换行符拆分成列表
:param content: 获取到的测试步骤单元格全部内容
:param prefix: 需去除的前缀内容
:return: 处理完的列表
"""
content = content.replace(prefix, "").strip(' ')
content_list = content.split('\n')
return content_list


def insert_actual_result(source_doc, source_cell,target_sheet, target_row_index,target_col_index):
"""
:param source_cell:
:param target_row_index:
:param target_col_index:
:return:
"""

for paragraph in source_cell.paragraphs:
for run in paragraph.runs:
#if run._r.xml.startswith('<w:drawing'):
if '<w:drawing' in run._r.xml:
# Parse the xml code for the blip
xml_parser = minidom.parseString(run._r.xml)

items = xml_parser.getElementsByTagName('a:blip')

if items:
for i,item in enumerate(items):
image_path = item.getAttribute('r:embed')
# image_path = item._element.find('.//a:blip', run._r.nsmap).get(
# '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
image_filename = source_doc.part.related_parts[image_path].partname.replace("/word/media/","")
print(image_filename)

# # 将图片插入到Excel表格单元格
# img = Image(embeded_images_path + image_filename)
# img.width = 100
# img.height = 100
# target_sheet.column_dimensions[
# target_sheet.cell(row=target_row_index, column=target_col_index).column_letter].width = img.width / 6
# target_sheet.row_dimensions[target_row_index].height = img.height
# #target_sheet.cell(row=target_row_index, column=target_col_index).value = None
# img.anchor=get_column_letter(target_col_index) + str(target_row_index)
# target_sheet.add_image(img)

# 插入图片无法导入Metersphere,需插入图片路径
# 生成随机字符串,重命名文件(避免服务器上文件重名覆盖)
new_name = generate_random_string(8) + ".png"
os.chdir(embeded_images_path)
os.rename(image_filename,new_name)
target_sheet.cell(row=target_row_index, column=target_col_index).value += \
"![image.png](/resource/md/get?fileName=" + new_name + ')'

else:
target_sheet.cell(row=target_row_index, column=target_col_index).value += run.text


blocks = iter_block_items(source_doc)
last_case_row_index = 1
try:
for block in blocks:
#print(block.text if isinstance(block, Paragraph) else '<table>')
if isinstance(block, Paragraph):
print(block.text)
if block.style.name is not None and block.style.name.startswith('Heading'):
level = int(block.style.name[-1]) # 获取标题级别
title = block.text # 获取标题文本
if level in [lev for (lev,tit) in titles]: # titles里已有该级别的标题
index = [lev for (lev,tit) in titles].index(level) # 获取该级别标题的索引位置
titles[index] = (level,title) # 将该级别的标题改为当前标题
len_titles = len(titles)
i=index+1
while i<len(titles): # 当前级别后还有元素
del titles[i] # 删除后面的元素
else:
titles.append((level, title))
#print(titles)
elif isinstance(block, Table):
# for row in block.rows:
# row_data = []
# for cell in row.cells:
# for paragraph in cell.paragraphs:
# row_data.append(paragraph.text)
# print("\t".join(row_data))
table = block
# tables.append(table)
# 获取用例对应的模块完整路径
full_path_titles = get_case_module_path(full_path_titles, titles)
# 将标题处理成模块
module = "/" + "/".join([title for i,title in full_path_titles])
print(module)
# 读取用例信息
tc_steps = convert_content_to_list(table.cell(*tc_fields_loc['步骤描述']).text, tc_steps_prefix)
expected_results =convert_content_to_list(table.cell(*tc_fields_loc['预期结果']).text, expected_result_prefix)
n_rows = max(len(tc_steps),len(expected_results))
tc_data = []

for i in range(min(len(tc_steps),len(expected_results))):
tc_data.append({})
tc_data[i]['tc_step'] = tc_steps[i]
tc_data[i]['expected_result'] = expected_results[i]

for i in range(min(len(tc_steps),len(expected_results)),n_rows): #两者长度不等
tc_data.append({})
if len(tc_steps)<len(expected_results):
tc_data[i]['tc_step']=''
tc_data[i]['expected_result'] = expected_results[i]
else:
tc_data[i]['tc_step'] = tc_steps[i]
tc_data[i]['expected_result'] = ''


tc_id = table.cell(*tc_fields_loc['ID']).text
tc_name = table.cell(*tc_fields_loc['用例名称']).text
module = module
precondition = '' #视情况而定,合肥项目模板中前置条件和测试步骤混在一起无法区分,因此放弃填充前置条件,统一作为测试步骤处理
memo= table.cell(*tc_fields_loc['备注']).text
edit_mode = 'STEP'
status = '已完成'
owner = table.cell(*tc_fields_loc['责任人']).text
priority = 'P2'
case_type = '正常用例'
label = ''
additional_field = ''

for i in range(n_rows):
if i==0:
dest_sheet.append([tc_id,tc_name,module,precondition,memo,tc_data[i]['tc_step'],
tc_data[i]['expected_result'],edit_mode,label,owner,priority,status,case_type,additional_field])
#将实际结果插入附加字段
source_cell = table.cell(*tc_fields_loc['附加字段'])
#print(source_cell._tc.xml)
insert_actual_result(source_doc,source_cell,dest_sheet,last_case_row_index+1,head.index('附加字段')+1)
else:
dest_sheet.append(['', '', '', '', '', tc_data[i]['tc_step'], tc_data[i]['expected_result'],
'', '', '', '', '', '', ''])


# 合并单元格
if n_rows>1:
for col_index in [head.index('ID'),head.index('用例名称'),head.index('所属模块'),head.index('前置条件'),
head.index('备注'),head.index('编辑模式'),head.index('标签'),head.index('责任人'),head.index('用例等级'),
head.index('用例状态'),head.index('用例类型'),head.index('附加字段')]:
dest_sheet.merge_cells(start_row=last_case_row_index + 1, end_row=last_case_row_index+n_rows, start_column=col_index+1,
end_column=col_index+1)
last_case_row_index = last_case_row_index+n_rows


dest_wb.save(dest_wb_path)

except Exception as e:
traceback.print_exc()



0 comments on commit 8e116b0

Please sign in to comment.