refactor(测试跟踪): 大批量导出测试用例优化

--story=1011416 --user=宋昌昌 【Bug转需求】【测试跟踪】功能用例-批量导出用例7000+测试跟踪服务挂了重启 https://www.tapd.cn/55049933/s/1361573
This commit is contained in:
song-cc-rock 2023-04-14 11:59:44 +08:00 committed by jianxing
parent 69f886b230
commit 9864e364f1
10 changed files with 179 additions and 35 deletions

View File

@ -250,11 +250,13 @@ export default {
xmind_export_tip: "Switch to Case List and export", xmind_export_tip: "Switch to Case List and export",
export_field_select_tips: "please select export fields", export_field_select_tips: "please select export fields",
export_to_excel: "Export excel", export_to_excel: "Export excel",
export_to_excel_tips: "support xls", export_to_excel_tips: "support xlsx",
export_to_excel_tips1: "support xls/xlsx", export_to_excel_tips1: "support xls/xlsx",
export_to_xmind: "Export xmind", export_to_xmind: "Export xmind",
export_to_xmind_tips: "support xmind", export_to_xmind_tips: "support xmind",
export_format: "Export Format", export_format: "Export Format",
export_to_zip: "Export zip",
export_to_zip_tips: "support zip",
}, },
case_desc: "Case Desc", case_desc: "Case Desc",
passing_rate: "Case Pass Rate", passing_rate: "Case Pass Rate",

View File

@ -222,10 +222,12 @@ export default {
xmind_export_tip: "请切换成用例列表导出!", xmind_export_tip: "请切换成用例列表导出!",
export_field_select_tips: "选择导出范围", export_field_select_tips: "选择导出范围",
export_to_excel: "导出Excel表格", export_to_excel: "导出Excel表格",
export_to_excel_tips: "支持xls文件", export_to_excel_tips: "支持xlsx文件",
export_to_excel_tips1: "支持xls/xlsx文件", export_to_excel_tips1: "支持xls/xlsx文件",
export_to_xmind: "导出思维导图", export_to_xmind: "导出思维导图",
export_to_xmind_tips: "支持xmind文件", export_to_xmind_tips: "支持xmind文件",
export_to_zip: "导出压缩包",
export_to_zip_tips: "支持zip文件",
}, },
case_desc: "用例描述", case_desc: "用例描述",
passing_rate: "用例通过率", passing_rate: "用例通过率",

View File

@ -221,10 +221,12 @@ export default {
xmind_export_tip: "請切換成用例列錶導出!", xmind_export_tip: "請切換成用例列錶導出!",
export_field_select_tips: "選擇導出範圍", export_field_select_tips: "選擇導出範圍",
export_to_excel: "導出Excel表格", export_to_excel: "導出Excel表格",
export_to_excel_tips: "支持xls文件", export_to_excel_tips: "支持xlsx文件",
export_to_excel_tips1: "支持xls/xlsx文件", export_to_excel_tips1: "支持xls/xlsx文件",
export_to_xmind: "導出思維導圖", export_to_xmind: "導出思維導圖",
export_to_xmind_tips: "支持xmind文件", export_to_xmind_tips: "支持xmind文件",
export_to_zip: "導出壓縮包",
export_to_zip_tips: "支持zip文件",
}, },
case_desc: "用例描述", case_desc: "用例描述",
passing_rate: "用例通過率", passing_rate: "用例通過率",

View File

@ -128,6 +128,33 @@ public class CompressUtils {
return zipFile; return zipFile;
} }
/**
* 将多个文件压缩至指定路径
*
* @param fileList 待压缩的文件列表
* @param zipFilePath 压缩文件路径
* @return 返回压缩好的文件
* @throws IOException
*/
public static File zipFilesToPath(String zipFilePath, List<File> fileList) throws IOException {
File zipFile = new File(zipFilePath);
// 文件输出流
FileOutputStream outputStream = getFileStream(zipFile);
// 压缩流
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
int size = fileList.size();
// 压缩列表中的文件
for (int i = 0; i < size; i++) {
File file = fileList.get(i);
zipFile(file, zipOutputStream);
}
// 关闭压缩流文件流
zipOutputStream.close();
outputStream.close();
return zipFile;
}
public static void deleteFile(String delPath) { public static void deleteFile(String delPath) {
try { try {
File file = new File(delPath); File file = new File(delPath);

View File

@ -375,8 +375,7 @@
</select> </select>
<select id="listByTestCaseIds" resultType="io.metersphere.dto.TestCaseDTO"> <select id="listByTestCaseIds" resultType="io.metersphere.dto.TestCaseDTO">
select test_case.*, api_test.name as apiName, load_test.name AS performName from test_case left join api_test on select test_case.* from test_case
test_case.test_id=api_test.id left join load_test on test_case.test_id=load_test.id
<where> <where>
<if test="!request.exportAll and request.ids != null and request.ids.size() > 0"> <if test="!request.exportAll and request.ids != null and request.ids.size() > 0">
and test_case.id in and test_case.id in
@ -394,6 +393,9 @@
test_case.`${order.name}` ${order.type} test_case.`${order.name}` ${order.type}
</foreach> </foreach>
</if> </if>
<if test="request.pageCount != 0">
limit ${request.pageStart}, ${request.pageCount}
</if>
</select> </select>
<select id="getMaxNumByProjectId" resultType="io.metersphere.base.domain.TestCase"> <select id="getMaxNumByProjectId" resultType="io.metersphere.base.domain.TestCase">

View File

@ -277,7 +277,7 @@ public class TestCaseController {
@RequiresPermissions(PermissionConstants.PROJECT_TRACK_CASE_READ_EXPORT) @RequiresPermissions(PermissionConstants.PROJECT_TRACK_CASE_READ_EXPORT)
@MsAuditLog(module = OperLogModule.TRACK_TEST_CASE, type = OperLogConstants.EXPORT, sourceId = "#request.id", title = "#request.name", project = "#request.projectId") @MsAuditLog(module = OperLogModule.TRACK_TEST_CASE, type = OperLogConstants.EXPORT, sourceId = "#request.id", title = "#request.name", project = "#request.projectId")
public void testCaseExport(HttpServletResponse response, @RequestBody TestCaseExportRequest request) { public void testCaseExport(HttpServletResponse response, @RequestBody TestCaseExportRequest request) {
testCaseService.testCaseExport(response, request); testCaseService.exportTestCaseZip(response, request);
} }
@PostMapping("/export/testcase/xmind") @PostMapping("/export/testcase/xmind")

View File

@ -2,6 +2,7 @@ package io.metersphere.listener;
import io.metersphere.commons.utils.LogUtil; import io.metersphere.commons.utils.LogUtil;
import io.metersphere.plan.service.TestPlanReportService; import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.service.TestCaseService;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -13,11 +14,16 @@ public class TrackAppStartListener implements ApplicationListener<ApplicationRea
@Resource @Resource
private TestPlanReportService testPlanReportService; private TestPlanReportService testPlanReportService;
@Resource
private TestCaseService testCaseService;
@Override @Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LogUtil.info("Start checking for incomplete reports"); LogUtil.info("Start checking for incomplete reports");
testPlanReportService.exceptionHandling(); testPlanReportService.exceptionHandling();
LogUtil.info("Completion Check Not Executed Completed Report"); LogUtil.info("Completion Check Not Executed Completed Report");
LogUtil.info("Start clean the tmp dir of jar classpath");
testCaseService.cleanUpTmpDirOfClassPath();
LogUtil.info("The tmp dir of jar classpath is cleared");
} }
} }

View File

@ -25,6 +25,15 @@ public class TestCaseBatchRequest extends TestCaseWithBLOBs {
*/ */
private Boolean exportAll = false; private Boolean exportAll = false;
/**
* v2.9 大批量导出分页参数
* pageStart: 偏移量
* pageCount: 数目
*/
private int pageStart = 0;
private int pageCount = 0;
@Getter @Getter
@Setter @Setter
public static class CustomFiledRequest { public static class CustomFiledRequest {

View File

@ -4,6 +4,7 @@ package io.metersphere.service;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.EasyExcelFactory; import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.enums.CellExtraTypeEnum; import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.github.pagehelper.Page; import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.*; import io.metersphere.base.domain.*;
@ -67,14 +68,12 @@ import org.apache.ibatis.session.SqlSessionFactory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.mybatis.spring.SqlSessionUtils; import org.mybatis.spring.SqlSessionUtils;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedInputStream; import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@ -207,6 +206,11 @@ public class TestCaseService {
private ThreadLocal<Integer> importCreateNum = new ThreadLocal<>(); private ThreadLocal<Integer> importCreateNum = new ThreadLocal<>();
// 导出CASE的最大值
private static final int EXPORT_CASE_MAX_COUNT = 1000;
private static final String EXPORT_CASE_TMP_DIR = "tmp";
private void setNode(TestCaseWithBLOBs testCase) { private void setNode(TestCaseWithBLOBs testCase) {
if (StringUtils.isEmpty(testCase.getNodeId()) || "default-module".equals(testCase.getNodeId())) { if (StringUtils.isEmpty(testCase.getNodeId()) || "default-module".equals(testCase.getNodeId())) {
TestCaseNodeExample example = new TestCaseNodeExample(); TestCaseNodeExample example = new TestCaseNodeExample();
@ -1502,26 +1506,110 @@ public class TestCaseService {
return list; return list;
} }
public void testCaseExport(HttpServletResponse response, TestCaseExportRequest request) { public void exportTestCaseZip(HttpServletResponse response, TestCaseExportRequest request) {
String projectId = request.getProjectId(); // zip, response stream
BufferedInputStream bis = null;
OutputStream os = null;
File tmpDir = null;
try {
tmpDir = new File(this.getClass().getClassLoader().getResource(StringUtils.EMPTY).getPath() +
EXPORT_CASE_TMP_DIR + File.separatorChar + EXPORT_CASE_TMP_DIR + "_" + UUID.randomUUID().toString());
// 生成tmp随机目录
FileUtils.deleteDir(tmpDir.getPath());
tmpDir.mkdirs();
// 生成EXCEL
List<File> batchExcels = generateCaseExportExcel(tmpDir.getPath(), request);
if (batchExcels.size() > 1) {
// EXCEL -> ZIP (EXCEL数目大于1)
File zipFile = CompressUtils.zipFilesToPath(tmpDir.getPath() + File.separatorChar + "caseExport.zip", batchExcels);
response.setContentType("application/octet-stream");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode("caseExport.zip", StandardCharsets.UTF_8.name()));
bis = new BufferedInputStream(new FileInputStream(zipFile.getPath()));
os = response.getOutputStream();
int len;
byte[] bytes = new byte[1024 * 2];
while ((len = bis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
} else {
// EXCEL (EXCEL数目等于1)
File singeFile = batchExcels.get(0);
response.setContentType("application/vnd.ms-excel");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode("caseExport.xlsx", StandardCharsets.UTF_8.name()));
bis = new BufferedInputStream(new FileInputStream(singeFile.getPath()));
os = response.getOutputStream();
int len;
byte[] bytes = new byte[1024 * 2];
while ((len = bis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
}
} catch (Exception e) {
LogUtil.error(e);
MSException.throwException("export case zip error");
} finally {
try {
if (bis != null) {
bis.close();
}
if (os != null) {
os.close();
}
FileUtils.deleteDir(tmpDir.getPath());
} catch (IOException e) {
e.printStackTrace();
}
}
}
private List<File> generateCaseExportExcel(String tmpZipPath, TestCaseExportRequest request) {
List<File> tmpExportExcelList = new ArrayList<>();
// 初始化ExcelHead
request.getCondition().setStatusIsNot(CommonConstants.TrashStatus); request.getCondition().setStatusIsNot(CommonConstants.TrashStatus);
TestCaseBatchRequest batchRequest = new TestCaseBatchRequest();
BeanUtils.copyBean(batchRequest, request);
List<TestCaseDTO> testCases = getExportData(batchRequest);
List<List<String>> headList = getTestcaseExportHeads(request); List<List<String>> headList = getTestcaseExportHeads(request);
boolean isUseCustomId = trackProjectService.useCustomNum(request.getProjectId());
// 设置导出参数
TestCaseBatchRequest initParam = new TestCaseBatchRequest();
BeanUtils.copyBean(initParam, request);
TestCaseBatchRequest batchRequest = setTestCaseExportParamIds(initParam);
// 1000条截取一次, 生成EXCEL
AtomicInteger i = new AtomicInteger(0);
SubListUtil.dealForSubList(batchRequest.getIds(), EXPORT_CASE_MAX_COUNT, (subIds) -> {
i.getAndIncrement();
batchRequest.setIds(subIds);
// 生成writeHandler
Map<Integer, Integer> rowMergeInfo = new HashMap<>();
FunctionCaseMergeWriteHandler writeHandler = new FunctionCaseMergeWriteHandler(rowMergeInfo, headList);
Map<String, List<String>> caseLevelAndStatusValueMap = trackTestCaseTemplateService.getCaseLevelAndStatusMapByProjectId(request.getProjectId());
FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(true, headList, caseLevelAndStatusValueMap);
List<TestCaseDTO> exportData = getExportData(batchRequest);
List<TestCaseExcelData> excelData = parseCaseData2ExcelData(exportData, rowMergeInfo, isUseCustomId, request.getOtherHeaders());
List<List<Object>> data = parseExcelData2List(headList, excelData);
File createFile = new File(tmpZipPath + File.separatorChar + "caseExport_" + i.get() + ".xlsx");
if (!createFile.exists()) {
try {
createFile.createNewFile();
} catch (IOException e) {
MSException.throwException(e);
}
}
//生成临时EXCEL
EasyExcel.write(createFile)
.head(Optional.ofNullable(headList).orElse(new ArrayList<>()))
.registerWriteHandler(handler)
.registerWriteHandler(writeHandler)
.excelType(ExcelTypeEnum.XLSX).sheet(Translator.get("test_case_import_template_sheet")).doWrite(data);
tmpExportExcelList.add(createFile);
});
return tmpExportExcelList;
}
Map<Integer, Integer> rowMergeInfo = new HashMap<>(); public void cleanUpTmpDirOfClassPath() {
FunctionCaseMergeWriteHandler writeHandler = new FunctionCaseMergeWriteHandler(rowMergeInfo, headList); File tmpDir = new File(this.getClass().getClassLoader().getResource(StringUtils.EMPTY).getPath() + EXPORT_CASE_TMP_DIR);
boolean isUseCustomId = trackProjectService.useCustomNum(projectId); if (tmpDir.exists()) {
FileUtils.deleteDir(tmpDir.getPath());
Map<String, List<String>> caseLevelAndStatusValueMap = trackTestCaseTemplateService.getCaseLevelAndStatusMapByProjectId(projectId); }
FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(true, headList, caseLevelAndStatusValueMap);
List<TestCaseExcelData> excelData = parseCaseData2ExcelData(testCases, rowMergeInfo, isUseCustomId, request.getOtherHeaders());
List<List<Object>> data = parseExcelData2List(headList, excelData);
new EasyExcelExporter(new TestCaseExcelDataFactory().getTestCaseExcelDataLocal().getClass())
.exportByCustomWriteHandler(response, headList, data, Translator.get("test_case_import_template_name"),
Translator.get("test_case_import_template_sheet"), handler, writeHandler);
} }
@NotNull @NotNull
@ -1678,16 +1766,22 @@ public class TestCaseService {
} }
public List<TestCaseDTO> getExportData(TestCaseBatchRequest request) { public List<TestCaseDTO> getExportData(TestCaseBatchRequest request) {
ServiceUtils.getSelectAllIds(request, request.getCondition(), return extTestCaseMapper.listByTestCaseIds(request);
(query) -> extTestCaseMapper.selectIds(query)); }
this.initRequest(request.getCondition(), true);
setDefaultOrder(request.getCondition()); private TestCaseBatchRequest setTestCaseExportParamIds(TestCaseBatchRequest param) {
Map<String, List<String>> filters = request.getCondition().getFilters(); boolean queryUi = DiscoveryUtil.hasService(MicroServiceName.UI_TEST);
param.getCondition().setQueryUi(queryUi);
this.initRequest(param.getCondition(), true);
setDefaultOrder(param.getCondition());
ServiceUtils.setBaseQueryRequestCustomMultipleFields(param.getCondition());
Map<String, List<String>> filters = param.getCondition().getFilters();
if (filters != null && !filters.containsKey("status")) { if (filters != null && !filters.containsKey("status")) {
filters.put("status", new ArrayList<>(0)); filters.put("status", new ArrayList<>(0));
} }
List<TestCaseDTO> testCaseList = extTestCaseMapper.listByTestCaseIds(request); ServiceUtils.getSelectAllIds(param, param.getCondition(),
return testCaseList; (query) -> extTestCaseMapper.selectIds(query));
return param;
} }
private List<TestCaseExcelData> parseCaseData2ExcelData(List<TestCaseDTO> testCaseList, Map<Integer, Integer> rowMergeInfo, private List<TestCaseExcelData> parseCaseData2ExcelData(List<TestCaseDTO> testCaseList, Map<Integer, Integer> rowMergeInfo,

View File

@ -1052,7 +1052,7 @@ export default {
fileNameSuffix = ".xmind"; fileNameSuffix = ".xmind";
} else { } else {
url = '/test/case/export/testcase' url = '/test/case/export/testcase'
fileNameSuffix = ".xlsx"; fileNameSuffix = this.selectCounts > 1000 ? ".zip" : ".xlsx";
} }
this.loading = true; this.loading = true;
store.isTestCaseExporting = true; store.isTestCaseExporting = true;