refactor: 优化功能案例导入

功能案例导入增加自定义ID的判断、增加忽略错误继续导入功能
This commit is contained in:
song-tianyang 2021-05-10 14:32:18 +08:00 committed by 刘瑞斌
parent 18492ec568
commit 16d60c3f39
12 changed files with 751 additions and 22 deletions

View File

@ -7,8 +7,11 @@ import io.metersphere.base.domain.ApiTestEnvironmentExample;
import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs;
import io.metersphere.base.mapper.ApiTestEnvironmentMapper;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.controller.request.EnvironmentRequest;
import io.metersphere.dto.BaseSystemConfigDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.service.SystemParameterService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -90,6 +93,18 @@ public class ApiTestEnvironmentService {
* @return
*/
public synchronized ApiTestEnvironmentWithBLOBs getMockEnvironmentByProjectId(String projectId, String protocal, String baseUrl) {
//创建的时候检查当前站点
SystemParameterService systemParameterService = CommonBeanFactory.getBean(SystemParameterService.class);
BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo();
if (baseSystemConfigDTO != null && StringUtils.isNotEmpty(baseSystemConfigDTO.getUrl())) {
baseUrl = baseSystemConfigDTO.getUrl();
if (baseUrl.startsWith("http:")) {
protocal = "http";
} else if (baseUrl.startsWith("https:")) {
protocal = "https";
}
}
String apiName = MockConfigStaticData.MOCK_EVN_NAME;
ApiTestEnvironmentWithBLOBs returnModel = null;
ApiTestEnvironmentExample example = new ApiTestEnvironmentExample();
@ -117,6 +132,21 @@ public class ApiTestEnvironmentService {
JSONArray conditions = httpObj.getJSONArray("conditions");
if (conditions.isEmpty()) {
needUpdate = true;
} else {
for (int i = 0; i < conditions.size(); i++) {
JSONObject obj = conditions.getJSONObject(i);
String socket = url;
if (socket.startsWith("http://")) {
socket = socket.substring(7);
} else if (socket.startsWith("https://")) {
socket = socket.substring(8);
}
if (!obj.containsKey("socket") || !StringUtils.equals(socket, String.valueOf(obj.get("socket")))) {
needUpdate = true;
} else if (!obj.containsKey("protocol") || !StringUtils.equals(protocal, String.valueOf(obj.get("protocol")))) {
needUpdate = true;
}
}
}
}
}
@ -228,4 +258,20 @@ public class ApiTestEnvironmentService {
return blobs;
}
public void checkMockEvnInfoByBaseUrl(String baseUrl) {
List<ApiTestEnvironmentWithBLOBs> allEvnList = this.selectByExampleWithBLOBs(null);
for (ApiTestEnvironmentWithBLOBs model : allEvnList) {
if (StringUtils.equals(model.getName(), MockConfigStaticData.MOCK_EVN_NAME)) {
String protocal = "";
if (baseUrl.startsWith("http:")) {
protocal = "http";
} else if (baseUrl.startsWith("https:")) {
protocal = "https";
}
model = this.checkMockEvnIsRightful(model, protocal, model.getProjectId(), model.getName(), baseUrl);
}
}
}
}

View File

@ -0,0 +1,87 @@
package io.metersphere.excel.handler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.alibaba.excel.write.style.row.AbstractRowHeightStyleStrategy;
import io.metersphere.i18n.Translator;
import org.apache.poi.ss.usermodel.Comment;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
/**
* @author song.tianyang
* @Date 2021/5/7 2:17 下午
* @Description
*/
public class FunctionCaseTemplateWriteHandler extends AbstractRowHeightStyleStrategy {
@Override
public void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer rowIndex, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
super.afterRowDispose(writeSheetHolder, writeTableHolder, row, relativeRowIndex, isHead);
if (isHead) {
Sheet sheet = writeSheetHolder.getSheet();
Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
// 在第一行 第3列创建一个批注
Comment comment1 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 2, 0, (short) 3, 1));
// 输入批注信息
comment1.setString(new XSSFRichTextString(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase")));
// 在第一行 第4列创建一个批注
Comment comment2 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 3, 0, (short) 3, 1));
// 输入批注信息
comment2.setString(new XSSFRichTextString(Translator.get("module_created_automatically")));
// 在第一行 第5列创建一个批注
Comment comment3 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 4, 0, (short) 3, 1));
// 输入批注信息
comment3.setString(new XSSFRichTextString(Translator.get("options") + "functional、performance、api"));
// 在第一行 第6列创建一个批注
Comment comment4 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 5, 0, (short) 3, 1));
// 输入批注信息
comment4.setString(new XSSFRichTextString(Translator.get("please_input_workspace_member")));
// 在第一行 第7列创建一个批注
Comment comment5 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 6, 0, (short) 3, 1));
// 输入批注信息
comment5.setString(new XSSFRichTextString(Translator.get("options") + "P0、P1、P2、P3"));
// 在第一行 第8列创建一个批注
Comment comment6 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 7, 0, (short) 3, 1));
// 输入批注信息
comment6.setString(new XSSFRichTextString(Translator.get("tag_tip_pattern")));
// 将批注添加到单元格对象中
sheet.getRow(0).getCell(1).setCellComment(comment1);
sheet.getRow(0).getCell(1).setCellComment(comment2);
sheet.getRow(0).getCell(1).setCellComment(comment3);
sheet.getRow(0).getCell(1).setCellComment(comment4);
sheet.getRow(0).getCell(1).setCellComment(comment5);
sheet.getRow(0).getCell(1).setCellComment(comment6);
}
}
@Override
protected void setHeadColumnHeight(Row row, int relativeRowIndex) {
}
@Override
protected void setContentColumnHeight(Row row, int relativeRowIndex) {
}
}

View File

@ -0,0 +1,338 @@
package io.metersphere.excel.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import io.metersphere.commons.constants.TestCaseConstants;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.excel.domain.ExcelErrData;
import io.metersphere.excel.domain.TestCaseExcelData;
import io.metersphere.excel.utils.ExcelValidateHelper;
import io.metersphere.i18n.Translator;
import io.metersphere.track.service.TestCaseService;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TestCaseDataIgnoreErrorListener extends EasyExcelListener<TestCaseExcelData> {
private TestCaseService testCaseService;
private String projectId;
protected List<TestCaseExcelData> updateList = new ArrayList<>(); //存储待更新用例的集合
protected boolean isUpdated = false; //判断是否更新过用例将会传给前端
Set<String> testCaseNames;
Set<String> userIds;
public boolean isUpdated() {
return isUpdated;
}
public TestCaseDataIgnoreErrorListener(Class clazz, String projectId, Set<String> testCaseNames, Set<String> userIds) {
this.clazz = clazz;
this.testCaseService = (TestCaseService) CommonBeanFactory.getBean("testCaseService");
this.projectId = projectId;
this.testCaseNames = testCaseNames;
this.userIds = userIds;
}
@Override
public String validate(TestCaseExcelData data, String errMsg) {
String nodePath = data.getNodePath();
StringBuilder stringBuilder = new StringBuilder(errMsg);
//校验所属模块"
if (nodePath != null) {
String[] nodes = nodePath.split("/");
//校验模块深度
if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
stringBuilder.append(Translator.get("test_case_node_level_tip") +
TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level") + "; ");
}
//模块名不能为空
for (int i = 0; i < nodes.length; i++) {
if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) {
stringBuilder.append(Translator.get("module_not_null") + "; ");
break;
}
}
//增加字数校验每一层不能超过100字
for (int i = 0; i < nodes.length; i++) {
String nodeStr = nodes[i];
if (StringUtils.isNotEmpty(nodeStr)) {
if (nodeStr.trim().length() > 100) {
stringBuilder.append(Translator.get("module") + Translator.get("test_track.length_less_than") + "100:" + nodeStr);
break;
}
}
}
}
//校验维护人
if (!userIds.contains(data.getMaintainer())) {
stringBuilder.append(Translator.get("user_not_exists") + "" + data.getMaintainer() + "; ");
}
/*
校验Excel中是否有ID
有的话校验ID是否已在当前项目中存在存在则更新用例
不存在则继续校验看是否重复不重复则新建用例
*/
if (null != data.getNum()) { //当前读取的数据有ID
if (null != testCaseService.checkIdExist(data.getNum(), projectId)) { //该ID在当前项目中存在
//如果前面所经过的校验都没报错
if (StringUtils.isEmpty(stringBuilder)) {
updateList.add(data); //将当前数据存入更新列表
stringBuilder.append("update_testcase"); //该信息用于在invoke方法中判断是否该更新用例
}
return stringBuilder.toString();
} else {
/*
该ID在当前数据库中不存在应当继续校验用例是否重复,
在下面的校验过程中num的值会被用于判断是否重复所以应当先设置为null
*/
data.setNum(null);
}
}
/*
校验用例
*/
if (testCaseNames.contains(data.getName())) {
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
BeanUtils.copyBean(testCase, data);
testCase.setProjectId(projectId);
String steps = getSteps(data);
testCase.setSteps(steps);
boolean dbExist = testCaseService.exist(testCase);
boolean excelExist = false;
if (dbExist) {
// db exist
stringBuilder.append(Translator.get("test_case_already_exists") + "" + data.getName() + "; ");
} else {
// @Data 重写了 equals hashCode 方法
excelExist = excelDataList.contains(data);
}
if (excelExist) {
// excel exist
stringBuilder.append(Translator.get("test_case_already_exists_excel") + "" + data.getName() + "; ");
} else {
excelDataList.add(data);
}
} else {
testCaseNames.add(data.getName());
excelDataList.add(data);
}
return stringBuilder.toString();
}
@Override
public void saveData() {
if (!(list.size() == 0)) {
Collections.reverse(list); //因为saveImportData里面是先分配最大的ID这个ID应该先发给list中最后的数据所以要reverse
List<TestCaseWithBLOBs> result = list.stream()
.map(item -> this.convert2TestCase(item))
.collect(Collectors.toList());
testCaseService.saveImportData(result, projectId);
this.isUpdated = true;
}
if (!(updateList.size() == 0)) {
List<TestCaseWithBLOBs> result2 = updateList.stream()
.map(item -> this.convert2TestCaseForUpdate(item))
.collect(Collectors.toList());
testCaseService.updateImportDataCarryId(result2, projectId);
this.isUpdated = true;
updateList.clear();
}
}
private TestCaseWithBLOBs convert2TestCase(TestCaseExcelData data) {
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
BeanUtils.copyBean(testCase, data);
testCase.setId(UUID.randomUUID().toString());
testCase.setProjectId(this.projectId);
testCase.setCreateTime(System.currentTimeMillis());
testCase.setUpdateTime(System.currentTimeMillis());
testCase.setCustomNum(data.getCustomNum());
String nodePath = data.getNodePath();
if (!nodePath.startsWith("/")) {
nodePath = "/" + nodePath;
}
if (nodePath.endsWith("/")) {
nodePath = nodePath.substring(0, nodePath.length() - 1);
}
testCase.setNodePath(nodePath);
//将标签设置为前端可解析的格式
String modifiedTags = modifyTagPattern(data);
testCase.setTags(modifiedTags);
if (StringUtils.isNotBlank(data.getStepModel())
&& StringUtils.equals(data.getStepModel(), TestCaseConstants.StepModel.TEXT.name())) {
testCase.setStepDescription(data.getStepDesc());
testCase.setExpectedResult(data.getStepResult());
} else {
String steps = getSteps(data);
testCase.setSteps(steps);
}
return testCase;
}
/**
* 将Excel中的数据对象转换为用于更新操作的用例数据对象
*
* @param data
* @return
*/
private TestCaseWithBLOBs convert2TestCaseForUpdate(TestCaseExcelData data) {
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
BeanUtils.copyBean(testCase, data);
testCase.setProjectId(this.projectId);
testCase.setUpdateTime(System.currentTimeMillis());
//调整nodePath格式
String nodePath = data.getNodePath();
if (!nodePath.startsWith("/")) {
nodePath = "/" + nodePath;
}
if (nodePath.endsWith("/")) {
nodePath = nodePath.substring(0, nodePath.length() - 1);
}
testCase.setNodePath(nodePath);
String steps = getSteps(data);
testCase.setSteps(steps);
//将标签设置为前端可解析的格式
String modifiedTags = modifyTagPattern(data);
testCase.setTags(modifiedTags);
return testCase;
}
/**
* 调整tags格式便于前端进行解析
* 例如对于标签1标签2将调整为:["标签1","标签2"]
*/
public String modifyTagPattern(TestCaseExcelData data) {
String tags = data.getTags();
try {
if (StringUtils.isNotBlank(tags)) {
JSONArray.parse(tags);
return tags;
}
return "[]";
} catch (Exception e) {
if (tags != null) {
Stream<String> stringStream = Arrays.stream(tags.split("[,;]")); //当标签值以中英文的逗号和分号分隔时才能正确解析
List<String> tagList = stringStream.map(tag -> tag = "\"" + tag + "\"")
.collect(Collectors.toList());
String modifiedTags = StringUtils.join(tagList, ",");
modifiedTags = "[" + modifiedTags + "]";
return modifiedTags;
} else {
return "[]";
}
}
}
public String getSteps(TestCaseExcelData data) {
JSONArray jsonArray = new JSONArray();
String[] stepDesc = new String[1];
String[] stepRes = new String[1];
if (data.getStepDesc() != null) {
stepDesc = data.getStepDesc().split("\r\n|\n");
} else {
stepDesc[0] = "";
}
if (data.getStepResult() != null) {
stepRes = data.getStepResult().split("\r\n|\n");
} else {
stepRes[0] = "";
}
String pattern = "(^\\d+)(\\.)?";
int index = stepDesc.length > stepRes.length ? stepDesc.length : stepRes.length;
for (int i = 0; i < index; i++) {
// 保持插入顺序判断用例是否有相同的steps
JSONObject step = new JSONObject(true);
step.put("num", i + 1);
Pattern descPattern = Pattern.compile(pattern);
Pattern resPattern = Pattern.compile(pattern);
if (i < stepDesc.length) {
Matcher descMatcher = descPattern.matcher(stepDesc[i]);
if (descMatcher.find()) {
step.put("desc", descMatcher.replaceAll(""));
} else {
step.put("desc", stepDesc[i]);
}
}
if (i < stepRes.length) {
Matcher resMatcher = resPattern.matcher(stepRes[i]);
if (resMatcher.find()) {
step.put("result", resMatcher.replaceAll(""));
} else {
step.put("result", stepRes[i]);
}
}
jsonArray.add(step);
}
return jsonArray.toJSONString();
}
@Override
public void invoke(TestCaseExcelData testCaseExcelData, AnalysisContext analysisContext) {
String errMsg;
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
String updateMsg = "update_testcase";
try {
//根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
errMsg = ExcelValidateHelper.validateEntity(testCaseExcelData);
//自定义校验规则
errMsg = validate(testCaseExcelData, errMsg);
} catch (NoSuchFieldException e) {
errMsg = Translator.get("parse_data_error");
LogUtil.error(e.getMessage(), e);
}
if (!StringUtils.isEmpty(errMsg)) {
//如果errMsg只有"update testcase"说明用例待更新
if (!errMsg.equals(updateMsg)) {
ExcelErrData excelErrData = new ExcelErrData(testCaseExcelData, rowIndex,
Translator.get("number") + " " + rowIndex + " " + Translator.get("row") + Translator.get("error")
+ "" + errMsg);
errList.add(excelErrData);
}
} else {
list.add(testCaseExcelData);
}
if (list.size() > BATCH_COUNT) {
saveData();
list.clear();
}
}
}

View File

@ -1,6 +1,7 @@
package io.metersphere.excel.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import io.metersphere.commons.utils.LogUtil;
@ -38,4 +39,19 @@ public class EasyExcelExporter {
}
}
public void exportByCustomWriteHandler(HttpServletResponse response, List data, String fileName, String sheetName, WriteHandler writeHandler) {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
try {
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".xlsx");
EasyExcel.write(response.getOutputStream(), this.clazz).registerWriteHandler(writeHandler).sheet(sheetName).doWrite(data);
} catch (UnsupportedEncodingException e) {
LogUtil.error(e.getMessage(), e);
throw new ExcelException("Utf-8 encoding is not supported");
} catch (IOException e) {
LogUtil.error(e.getMessage(), e);
throw new ExcelException("IO exception");
}
}
}

View File

@ -1,5 +1,6 @@
package io.metersphere.service;
import io.metersphere.api.service.ApiTestEnvironmentService;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.SystemHeaderMapper;
import io.metersphere.base.mapper.SystemParameterMapper;
@ -42,6 +43,8 @@ public class SystemParameterService {
private ExtSystemParameterMapper extSystemParameterMapper;
@Resource
private SystemHeaderMapper systemHeaderMapper;
@Resource
private ApiTestEnvironmentService apiTestEnvironmentService;
public String searchEmail() {
return extSystemParameterMapper.email();
@ -237,6 +240,7 @@ public class SystemParameterService {
public void saveBaseInfo(List<SystemParameter> parameters) {
SystemParameterExample example = new SystemParameterExample();
parameters.forEach(param -> {
// 去掉路径最后的 /
param.setParamValue(StringUtils.removeEnd(param.getParamValue(), "/"));
@ -247,6 +251,10 @@ public class SystemParameterService {
systemParameterMapper.insert(param);
}
example.clear();
if (StringUtils.equals(param.getParamKey(), "base.url")) {
apiTestEnvironmentService.checkMockEvnInfoByBaseUrl(param.getParamValue());
}
});
}

View File

@ -154,6 +154,13 @@ public class TestCaseController {
return testCaseService.testCaseImport(file, projectId, userId);
}
@PostMapping("/importIgnoreError/{projectId}/{userId}")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public ExcelResponse testCaseImportIgnoreError(MultipartFile file, @PathVariable String projectId, @PathVariable String userId) {
checkPermissionService.checkProjectOwner(projectId);
return testCaseService.testCaseImportIgnoreError(file, projectId, userId);
}
@GetMapping("/export/template")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public void testCaseTemplateExport(HttpServletResponse response) {

View File

@ -19,6 +19,8 @@ import io.metersphere.excel.domain.ExcelErrData;
import io.metersphere.excel.domain.ExcelResponse;
import io.metersphere.excel.domain.TestCaseExcelData;
import io.metersphere.excel.domain.TestCaseExcelDataFactory;
import io.metersphere.excel.handler.FunctionCaseTemplateWriteHandler;
import io.metersphere.excel.listener.TestCaseDataIgnoreErrorListener;
import io.metersphere.excel.listener.TestCaseDataListener;
import io.metersphere.excel.utils.EasyExcelExporter;
import io.metersphere.i18n.Translator;
@ -97,7 +99,8 @@ public class TestCaseService {
TestCaseFileMapper testCaseFileMapper;
@Resource
TestCaseTestMapper testCaseTestMapper;
private void setNode(TestCaseWithBLOBs testCase){
private void setNode(TestCaseWithBLOBs testCase) {
if (StringUtils.isEmpty(testCase.getNodeId()) || "default-module".equals(testCase.getNodeId())) {
TestCaseNodeExample example = new TestCaseNodeExample();
example.createCriteria().andProjectIdEqualTo(testCase.getProjectId()).andNameEqualTo("默认模块");
@ -224,7 +227,7 @@ public class TestCaseService {
String remark = tc.getRemark();
String prerequisite = tc.getPrerequisite();
if (StringUtils.equals(steps, caseSteps) && StringUtils.equals(remark, caseRemark) && StringUtils.equals(prerequisite, casePrerequisite)) {
//MSException.throwException(Translator.get("test_case_already_exists"));
//MSException.throwException(Translator.get("test_case_already_exists"));
return tc;
}
}
@ -487,7 +490,7 @@ public class TestCaseService {
testcase.setCustomNum(String.valueOf(number));
}
testcase.setReviewStatus(TestCaseReviewStatus.Prepare.name());
mapper.insert(testcase);
mapper.insert(testcase);
});
}
sqlSession.flushStatements();
@ -516,6 +519,7 @@ public class TestCaseService {
/**
* 把Excel中带ID的数据更新到数据库
* feat(测试跟踪):通过Excel导入导出时有ID字段可通过Excel导入来更新用例 (#1727)
*
* @param testCases
* @param projectId
*/
@ -555,8 +559,9 @@ public class TestCaseService {
public void testCaseTemplateExport(HttpServletResponse response) {
try {
EasyExcelExporter easyExcelExporter = new EasyExcelExporter(new TestCaseExcelDataFactory().getExcelDataByLocal());
easyExcelExporter.export(response, generateExportTemplate(),
Translator.get("test_case_import_template_name"), Translator.get("test_case_import_template_sheet"));
FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler();
easyExcelExporter.exportByCustomWriteHandler(response, generateExportTemplate(),
Translator.get("test_case_import_template_name"), Translator.get("test_case_import_template_sheet"), handler);
} catch (Exception e) {
MSException.throwException(e);
}
@ -619,16 +624,15 @@ public class TestCaseService {
}
list.add(new TestCaseExcelData());
TestCaseExcelData explain = new TestCaseExcelData();
explain.setName(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase"));
explain.setNodePath(Translator.get("module_created_automatically"));
explain.setType(Translator.get("options") + "functional、performance、api");
explain.setTags(Translator.get("tag_tip_pattern"));
// explain.setMethod(Translator.get("options") + "manual、auto");
explain.setPriority(Translator.get("options") + "P0、P1、P2、P3");
explain.setMaintainer(Translator.get("please_input_workspace_member"));
list.add(explain);
// TestCaseExcelData explain = new TestCaseExcelData();
// explain.setName(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase"));
// explain.setNodePath(Translator.get("module_created_automatically"));
// explain.setType(Translator.get("options") + "functional、performance、api");
// explain.setTags(Translator.get("tag_tip_pattern"));
//// explain.setMethod(Translator.get("options") + "manual、auto");
// explain.setPriority(Translator.get("options") + "P0、P1、P2、P3");
// explain.setMaintainer(Translator.get("please_input_workspace_member"));
// list.add(explain);
return list;
}
@ -1020,9 +1024,95 @@ public class TestCaseService {
/**
* 更新项目下用例的CustomNum值
*
* @param projectId 项目ID
*/
public void updateTestCaseCustomNumByProjectId(String projectId) {
extTestCaseMapper.updateTestCaseCustomNumByProjectId(projectId);
}
public ExcelResponse testCaseImportIgnoreError(MultipartFile multipartFile, String projectId, String userId) {
ExcelResponse excelResponse = new ExcelResponse();
boolean isUpdated = false; //判断是否更新了用例
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest();
queryTestCaseRequest.setProjectId(projectId);
List<TestCase> testCases = extTestCaseMapper.getTestCaseNames(queryTestCaseRequest);
Set<String> testCaseNames = testCases.stream()
.map(TestCase::getName)
.collect(Collectors.toSet());
List<ExcelErrData<TestCaseExcelData>> errList = null;
if (multipartFile == null) {
MSException.throwException(Translator.get("upload_fail"));
}
if (multipartFile.getOriginalFilename().endsWith(".xmind")) {
try {
XmindCaseParser xmindParser = new XmindCaseParser(this, userId, projectId, testCaseNames);
errList = xmindParser.parse(multipartFile);
if (CollectionUtils.isEmpty(xmindParser.getNodePaths())
&& CollectionUtils.isEmpty(xmindParser.getTestCase())
&& CollectionUtils.isEmpty(xmindParser.getUpdateTestCase())) {
if (errList == null) {
errList = new ArrayList<>();
}
ExcelErrData excelErrData = new ExcelErrData(null, 1, Translator.get("upload_fail") + "" + Translator.get("upload_content_is_null"));
errList.add(excelErrData);
excelResponse.setErrList(errList);
}
List<TestCaseWithBLOBs> continueCaseList = xmindParser.getContinueValidatedCase();
if (CollectionUtils.isNotEmpty(continueCaseList) || CollectionUtils.isNotEmpty(xmindParser.getUpdateTestCase())) {
if (CollectionUtils.isNotEmpty(xmindParser.getUpdateTestCase())) {
continueCaseList.removeAll(xmindParser.getUpdateTestCase());
this.updateImportData(xmindParser.getUpdateTestCase(), projectId);
}
List<String> nodePathList = xmindParser.getValidatedNodePath();
if (CollectionUtils.isNotEmpty(nodePathList)) {
testCaseNodeService.createNodes(nodePathList, projectId);
}
if (CollectionUtils.isNotEmpty(continueCaseList)) {
Collections.reverse(continueCaseList);
this.saveImportData(continueCaseList, projectId);
}
}
xmindParser.clear();
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(e.getMessage());
}
} else {
UserRoleExample userRoleExample = new UserRoleExample();
userRoleExample.createCriteria()
.andRoleIdIn(Arrays.asList(RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER))
.andSourceIdEqualTo(currentWorkspaceId);
Set<String> userIds = userRoleMapper.selectByExample(userRoleExample).stream().map(UserRole::getUserId).collect(Collectors.toSet());
try {
//根据本地语言环境选择用哪种数据对象进行存放读取的数据
Class clazz = new TestCaseExcelDataFactory().getExcelDataByLocal();
TestCaseDataIgnoreErrorListener easyExcelListener = new TestCaseDataIgnoreErrorListener(clazz, projectId, testCaseNames, userIds);
//读取excel数据
EasyExcelFactory.read(multipartFile.getInputStream(), clazz, easyExcelListener).sheet().doRead();
errList = easyExcelListener.getErrList();
isUpdated = easyExcelListener.isUpdated();
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(e.getMessage());
}
}
//如果包含错误信息就导出错误信息
if (!errList.isEmpty()) {
excelResponse.setSuccess(false);
excelResponse.setErrList(errList);
excelResponse.setIsUpdated(isUpdated);
} else {
excelResponse.setSuccess(true);
}
return excelResponse;
}
}

View File

@ -59,6 +59,10 @@ public class XmindCaseParser {
*/
private List<String> nodePaths;
private List<TestCaseWithBLOBs> continueValidatedCase;
private List<String> errorPath;
public XmindCaseParser(TestCaseService testCaseService, String userId, String projectId, Set<String> testCaseNames) {
this.testCaseService = testCaseService;
this.maintainer = userId;
@ -69,11 +73,14 @@ public class XmindCaseParser {
compartDatas = new ArrayList<>();
process = new DetailUtil();
nodePaths = new ArrayList<>();
continueValidatedCase = new ArrayList<>();
errorPath = new ArrayList<>();
}
private static final String TC_REGEX = "(?:tc:|tc|tc)";
private static final String PC_REGEX = "(?:pc:|pc|pc)";
private static final String RC_REGEX = "(?:rc:|rc|rc)";
private static final String ID_REGEX = "(?:id:|id|id)";
private static final String TAG_REGEX = "(?:tag:|tag|tag)";
public void clear() {
@ -126,6 +133,7 @@ public class XmindCaseParser {
* 验证用例的合规性
*/
private boolean validate(TestCaseWithBLOBs data) {
boolean validatePass = true;
String nodePath = data.getNodePath();
if (!nodePath.startsWith("/")) {
nodePath = "/" + nodePath;
@ -137,27 +145,41 @@ public class XmindCaseParser {
if (data.getName().length() > 200) {
validatePass = false;
process.add(Translator.get("test_case") + Translator.get("test_track.length_less_than") + "200", nodePath + data.getName());
}
if (!StringUtils.isEmpty(nodePath)) {
String[] nodes = nodePath.split("/");
if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
validatePass = false;
process.add(Translator.get("test_case_node_level_tip") +
TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level"), nodePath);
if (!errorPath.contains(nodePath)) {
errorPath.add(nodePath);
}
}
for (int i = 0; i < nodes.length; i++) {
if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) {
validatePass = false;
process.add(Translator.get("test_case") + Translator.get("module_not_null"), nodePath + data.getName());
if (!errorPath.contains(nodePath)) {
errorPath.add(nodePath);
}
break;
} else if (nodes[i].trim().length() > 100) {
validatePass = false;
process.add(Translator.get("module") + Translator.get("test_track.length_less_than") + "100 ", nodes[i].trim());
if (!errorPath.contains(nodePath)) {
errorPath.add(nodePath);
}
break;
}
}
}
if (StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) {
validatePass = false;
process.add(Translator.get("functional_method_tip"), nodePath + data.getName());
}
@ -176,9 +198,11 @@ public class XmindCaseParser {
// 用例等级和用例性质处理
if (!priorityList.contains(data.getPriority())) {
validatePass = false;
process.add(Translator.get("test_case_priority") + Translator.get("incorrect_format"), nodePath + data.getName());
}
if (data.getType() == null) {
validatePass = false;
process.add(Translator.get("test_case_type") + Translator.get("incorrect_format"), nodePath + data.getName());
}
@ -186,9 +210,13 @@ public class XmindCaseParser {
TestCaseExcelData compartData = new TestCaseExcelData();
BeanUtils.copyBean(compartData, data);
if (compartDatas.contains(compartData)) {
validatePass = false;
process.add(Translator.get("test_case_already_exists_excel"), nodePath + "/" + compartData.getName());
}
compartDatas.add(compartData);
if (validatePass) {
this.continueValidatedCase.add(data);
}
return true;
}
@ -305,6 +333,7 @@ public class XmindCaseParser {
List<Attached> steps = new LinkedList<>();
StringBuilder rc = new StringBuilder();
List<String> tags = new LinkedList<>();
StringBuilder customId = new StringBuilder();
if (attacheds != null && !attacheds.isEmpty()) {
attacheds.forEach(item -> {
if (isAvailable(item.getTitle(), PC_REGEX)) {
@ -314,12 +343,15 @@ public class XmindCaseParser {
rc.append("\n");
} else if (isAvailable(item.getTitle(), TAG_REGEX)) {
tags.add(replace(item.getTitle(), TAG_REGEX));
} else if (isAvailable(item.getTitle(), ID_REGEX)) {
customId.append(replace(item.getTitle(), ID_REGEX));
} else {
steps.add(item);
}
});
}
testCase.setRemark(rc.toString());
testCase.setCustomNum(customId.toString());
testCase.setTags(JSON.toJSONString(tags));
testCase.setSteps(this.getSteps(steps));
// 校验合规性
@ -364,8 +396,23 @@ public class XmindCaseParser {
//检查目录合规性
this.validate();
} catch (Exception ex) {
ex.printStackTrace();
return process.parse(ex.getMessage());
}
return process.parse();
}
public List<TestCaseWithBLOBs> getContinueValidatedCase() {
return this.continueValidatedCase;
}
public List<String> getValidatedNodePath() {
List<String> returnPathList = new ArrayList<>(nodePaths);
if (CollectionUtils.isNotEmpty(returnPathList)) {
if (CollectionUtils.isNotEmpty(errorPath)) {
returnPathList.removeAll(errorPath);
}
}
return returnPathList;
}
}

View File

@ -2,13 +2,13 @@
<el-dialog class="testcase-import" :title="$t('test_track.case.import.case_import')" :visible.sync="dialogVisible"
@close="close">
<el-tabs v-model="activeName" simple>
<el-tabs v-model="activeName" @tab-click="clickTabs" simple>
<el-tab-pane :label="$t('test_track.case.import.excel_title')" name="excelImport">
<el-row>
<el-link type="primary" class="download-template"
@click="downloadTemplate"
>{{$t('test_track.case.import.download_template')}}
>{{ $t('test_track.case.import.download_template') }}
</el-link>
</el-row>
<el-row>
@ -39,10 +39,20 @@
<el-row>
<ul>
<li v-for="errFile in errList" :key="errFile.rowNum">
{{errFile.errMsg}}
{{ errFile.errMsg }}
</li>
</ul>
</el-row>
<el-row style="text-align: right" v-if="showExcelImportContinueBtn">
<div style="margin-right: 20px;margin-bottom: 10px;">
<el-checkbox v-model="uploadIgnoreError">{{ $t('test_track.case.import.ignore_error') }}</el-checkbox>
</div>
<el-button type="primary" @click="uploadContinue(false)">{{ $t('test_track.case.import.continue_upload') }}
</el-button>
<el-button @click="close">{{ $t('commons.cancel') }}</el-button>
</el-row>
</el-tab-pane>
<!-- Xmind 导入 -->
<el-tab-pane :label="$t('test_track.case.import.xmind_title')" name="xmindImport" style="border: 0px">
@ -98,10 +108,18 @@
<el-row>
<ul>
<li v-for="errFile in xmindErrList" :key="errFile.rowNum">
{{errFile.errMsg}}
{{ errFile.errMsg }}
</li>
</ul>
</el-row>
<el-row style="text-align: right" v-if="showXmindImportContinueBtn">
<div style="margin-right: 20px;margin-bottom: 10px;">
<el-checkbox v-model="uploadXmindIgnoreError">{{ $t('test_track.case.import.ignore_error') }}</el-checkbox>
</div>
<el-button type="primary" @click="uploadContinue(true)">{{ $t('test_track.case.import.continue_upload') }}
</el-button>
<el-button @click="close">{{ $t('commons.cancel') }}</el-button>
</el-row>
</el-tab-pane>
</el-tabs>
@ -124,16 +142,34 @@
activeName: 'excelImport',
dialogVisible: false,
fileList: [],
lastXmindFile: null,
lastExcelFile: null,
errList: [],
xmindErrList: [],
isLoading: false,
isUpdated: false
isUpdated: false,
clickTabsName: "",
showExcelImportContinueBtn: false,
showXmindImportContinueBtn: false,
uploadIgnoreError: false,
uploadXmindIgnoreError: false,
}
},
created() {
this.showExcelImportContinueBtn = false;
this.showXmindImportContinueBtn = false;
},
activated() {
this.showExcelImportContinueBtn = false;
this.showXmindImportContinueBtn = false;
},
methods: {
handleExceed(files, fileList) {
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
},
clickTabs(tab, event) {
this.clickTabsName = tab.name;
},
uploadValidate(file) {
let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
if (suffix != 'xls' && suffix != 'xlsx') {
@ -179,11 +215,13 @@
removeGoBackListener(this.close);
this.dialogVisible = false;
this.fileList = [];
this.showExcelImportContinueBtn = false;
this.showXmindImportContinueBtn = false;
this.errList = [];
this.xmindErrList = [];
//excel
if (this.isUpdated === true){
if (this.isUpdated === true) {
this.$emit("refreshAll");
this.isUpdated = false;
}
@ -202,8 +240,10 @@
});
},
upload(file) {
this.isLoading = false;
this.lastExcelFile = file.file;
this.fileList.push(file.file);
this.isLoading = false;
let user = JSON.parse(localStorage.getItem(TokenKey));
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
@ -212,18 +252,57 @@
this.$success(this.$t('test_track.case.import.success'));
this.dialogVisible = false;
this.$emit("refreshAll");
this.lastXmindFile = null;
this.lastExcelFile = null;
this.showExcelImportContinueBtn = false;
this.showXmindImportContinueBtn = false;
} else {
this.errList = res.errList;
this.isUpdated = res.isUpdated;
this.showExcelImportContinueBtn = true;
}
this.fileList = [];
}, erro => {
this.fileList = [];
this.lastXmindFile = null;
this.lastExcelFile = null;
});
},
uploadContinue(isImportXmind) {
this.isLoading = false;
let user = JSON.parse(localStorage.getItem(TokenKey));
let file = null;
if (isImportXmind) {
this.uploadXmindIgnoreError = true;
file = this.lastXmindFile;
} else {
this.uploadIgnoreError = true;
file = this.lastExcelFile;
}
this.result = this.$fileUpload('/test/case/importIgnoreError/' + this.projectId + '/' + user.id, file, null, {}, response => {
let res = response.data;
this.$success(this.$t('test_track.case.import.success'));
this.dialogVisible = false;
this.$emit("refreshAll");
this.fileList = [];
this.lastXmindFile = null;
this.lastExcelFile = null;
this.showExcelImportContinueBtn = false;
this.showXmindImportContinueBtn = false;
this.uploadIgnoreError = false;
this.uploadXmindIgnoreError = false;
}, erro => {
this.fileList = [];
this.lastXmindFile = null;
this.lastExcelFile = null;
this.uploadIgnoreError = false;
this.uploadXmindIgnoreError = false;
});
},
uploadXmind(file) {
this.isLoading = false;
this.fileList.push(file.file);
this.lastXmindFile = file.file;
let user = JSON.parse(localStorage.getItem(TokenKey));
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
@ -232,8 +311,13 @@
this.$success(this.$t('test_track.case.import.success'));
this.dialogVisible = false;
this.$emit("refreshAll");
this.lastXmindFile = null;
this.lastExcelFile = null;
this.showExcelImportContinueBtn = false;
this.showXmindImportContinueBtn = false;
} else {
this.xmindErrList = res.errList;
this.showXmindImportContinueBtn = true;
}
this.fileList = [];
}, erro => {

View File

@ -1342,6 +1342,8 @@ export default {
xmind_title: "Xmind",
import_desc: "Import instructions",
import_file: "upload files",
ignore_error: "Ignore errors ",
continue_upload: "Upload continue",
},
export: {
export: "Export cases"

View File

@ -1347,6 +1347,8 @@ export default {
xmind_title: "思维导图",
import_desc: "导入说明",
import_file: "上传文件",
ignore_error: "忽略错误",
continue_upload: "继续上传",
},
export: {
export: "导出用例"

View File

@ -1347,6 +1347,8 @@ export default {
xmind_title: "思維導圖",
import_desc: "導入說明",
import_file: "上傳文件",
ignore_error: "忽略錯誤",
continue_upload: "繼續上傳",
},
export: {
export: "導出用例"