refactor(测试跟踪): 同一个测试计划禁止重复执行

【【测试计划】执行中的计划禁止再次执行,防止重复执行堆积任务过多】https://www.tapd.cn/55049933/prong/tasks/view/1155049933001012434

Signed-off-by: fit2-zhao <yong.zhao@fit2cloud.com>
This commit is contained in:
fit2-zhao 2023-07-04 18:02:24 +08:00 committed by f2c-ci-robot[bot]
parent e229eb3ae4
commit ba167cee86
15 changed files with 151 additions and 546 deletions

View File

@ -1,8 +1,5 @@
package io.metersphere.api.exec.queue; package io.metersphere.api.exec.queue;
import io.metersphere.api.jmeter.JMeterService;
import io.metersphere.api.jmeter.JMeterThreadUtils;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.dto.JmeterRunRequestDTO; import io.metersphere.dto.JmeterRunRequestDTO;
import io.metersphere.utils.LoggerUtil; import io.metersphere.utils.LoggerUtil;
@ -19,10 +16,6 @@ public class ExecTask implements Runnable {
@Override @Override
public void run() { public void run() {
CommonBeanFactory.getBean(JMeterService.class).addQueue(request); LoggerUtil.info("任务执行超时", request.getReportId());
Object res = PoolExecBlockingQueueUtil.take(request.getReportId());
if (res == null && !JMeterThreadUtils.isRunning(request.getReportId(), request.getTestId())) {
LoggerUtil.info("任务执行超时", request.getReportId());
}
} }
} }

View File

@ -10,16 +10,15 @@ import io.metersphere.api.jmeter.utils.ServerConfig;
import io.metersphere.api.jmeter.utils.SmoothWeighted; import io.metersphere.api.jmeter.utils.SmoothWeighted;
import io.metersphere.base.domain.TestResource; import io.metersphere.base.domain.TestResource;
import io.metersphere.commons.config.KafkaConfig; import io.metersphere.commons.config.KafkaConfig;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.ExtendedParameter; import io.metersphere.commons.constants.ExtendedParameter;
import io.metersphere.commons.exception.MSException; import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.*; import io.metersphere.commons.utils.ApiFileUtil;
import io.metersphere.commons.utils.GenerateHashTreeUtil;
import io.metersphere.commons.utils.HashTreeUtil;
import io.metersphere.commons.utils.JSON;
import io.metersphere.config.JmeterProperties; import io.metersphere.config.JmeterProperties;
import io.metersphere.constants.BackendListenerConstants;
import io.metersphere.constants.RunModeConstants;
import io.metersphere.dto.*; import io.metersphere.dto.*;
import io.metersphere.engine.Engine; import io.metersphere.engine.Engine;
import io.metersphere.jmeter.JMeterBase;
import io.metersphere.service.ApiPoolDebugService; import io.metersphere.service.ApiPoolDebugService;
import io.metersphere.service.PluginService; import io.metersphere.service.PluginService;
import io.metersphere.service.RedisTemplateService; import io.metersphere.service.RedisTemplateService;
@ -29,13 +28,8 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.threads.ThreadGroup;
import org.apache.jmeter.util.JMeterUtils; import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -88,65 +82,6 @@ public class JMeterService {
} }
} }
/**
* 添加调试监听
*/
private void addDebugListener(JmeterRunRequestDTO request) {
MsDebugListener resultCollector = new MsDebugListener();
resultCollector.setName(request.getReportId());
resultCollector.setProperty(TestElement.TEST_CLASS, MsDebugListener.class.getName());
resultCollector.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ViewResultsFullVisualizer"));
resultCollector.setEnabled(true);
resultCollector.setRunMode(request.getRunMode());
resultCollector.setFakeErrorMap(request.getFakeErrorMap());
// 添加DEBUG标示
HashTree test = ArrayUtils.isNotEmpty(request.getHashTree().getArray())
? request.getHashTree().getTree(request.getHashTree().getArray()[0]) : null;
if (test != null && ArrayUtils.isNotEmpty(test.getArray())
&& test.getArray()[0] instanceof ThreadGroup) {
ThreadGroup group = (ThreadGroup) test.getArray()[0];
group.setProperty(BackendListenerConstants.MS_DEBUG.name(), true);
}
request.getHashTree().add(request.getHashTree().getArray()[0], resultCollector);
}
private void runLocal(JmeterRunRequestDTO request) {
init();
// 接口用例集成报告/测试计划报告日志记录
if (StringUtils.isNotEmpty(request.getTestPlanReportId())
&& StringUtils.equals(request.getReportType(), RunModeConstants.SET_REPORT.toString())) {
FixedCapacityUtil.put(request.getTestPlanReportId(), new StringBuffer());
} else {
// 报告日志记录
FixedCapacityUtil.put(request.getReportId(), new StringBuffer());
}
LoggerUtil.debug("监听MessageCache.tasks当前容量" + FixedCapacityUtil.size());
if (request.isDebug() && !StringUtils.equalsAny(request.getRunMode(), ApiRunMode.DEFINITION.name())) {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加同步接收结果 Listener");
JMeterBase.addBackendListener(request, request.getHashTree(), MsApiBackendListener.class.getCanonicalName());
}
if (MapUtils.isNotEmpty(request.getExtendedParameters())
&& request.getExtendedParameters().containsKey(ExtendedParameter.SYNC_STATUS)
&& (Boolean) request.getExtendedParameters().get(ExtendedParameter.SYNC_STATUS)) {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加Debug Listener");
addDebugListener(request);
}
if (request.isDebug()) {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加Debug Listener");
addDebugListener(request);
} else {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加同步接收结果 Listener");
JMeterBase.addBackendListener(request, request.getHashTree(), MsApiBackendListener.class.getCanonicalName());
}
LoggerUtil.info("资源:[" + request.getTestId() + "] 加入JMETER中开始执行", request.getReportId());
ApiLocalRunner runner = new ApiLocalRunner(request.getHashTree());
runner.run(request.getReportId());
}
private void fileProcessing(JmeterRunRequestDTO request) { private void fileProcessing(JmeterRunRequestDTO request) {
ElementUtil.coverArguments(request.getHashTree()); ElementUtil.coverArguments(request.getHashTree());
//解析HashTree里的文件信息 //解析HashTree里的文件信息
@ -251,10 +186,6 @@ public class JMeterService {
} }
} }
public void addQueue(JmeterRunRequestDTO request) {
this.runLocal(request);
}
public boolean getRunningQueue(String poolId, String reportId) { public boolean getRunningQueue(String poolId, String reportId) {
try { try {
List<TestResource> resources = GenerateHashTreeUtil.setPoolResource(poolId); List<TestResource> resources = GenerateHashTreeUtil.setPoolResource(poolId);

View File

@ -1,174 +0,0 @@
package io.metersphere.api.jmeter;
import com.fasterxml.jackson.core.type.TypeReference;
import io.metersphere.api.exec.queue.PoolExecBlockingQueueUtil;
import io.metersphere.utils.ReportStatusUtil;
import io.metersphere.commons.constants.CommonConstants;
import io.metersphere.commons.utils.*;
import io.metersphere.vo.ResultVO;
import io.metersphere.constants.BackendListenerConstants;
import io.metersphere.constants.RunModeConstants;
import io.metersphere.dto.MsRegexDTO;
import io.metersphere.dto.ResultDTO;
import io.metersphere.jmeter.JMeterBase;
import io.metersphere.service.ApiExecutionQueueService;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.service.TestResultService;
import io.metersphere.utils.LoggerUtil;
import io.metersphere.utils.RetryResultUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient;
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class MsApiBackendListener extends AbstractBackendListenerClient implements Serializable {
private ApiExecutionQueueService apiExecutionQueueService;
private TestResultService testResultService;
private List<SampleResult> queues;
private ResultDTO dto;
// 当前场景报告/用例结果状态
private ResultVO resultVO;
private RedisTemplateService redisTemplateService;
/**
* 参数初始化方法
*/
@Override
public void setupTest(BackendListenerContext context) throws Exception {
LoggerUtil.info("初始化监听");
queues = new LinkedList<>();
this.setParam(context);
if (apiExecutionQueueService == null) {
apiExecutionQueueService = CommonBeanFactory.getBean(ApiExecutionQueueService.class);
}
if (testResultService == null) {
testResultService = CommonBeanFactory.getBean(TestResultService.class);
}
if (redisTemplateService == null) {
redisTemplateService = CommonBeanFactory.getBean(RedisTemplateService.class);
}
resultVO = new ResultVO();
super.setupTest(context);
}
@Override
public void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context) {
LoggerUtil.info("接收到JMETER执行数据【" + sampleResults.size() + "", dto.getReportId());
if (dto.isRetryEnable()) {
queues.addAll(sampleResults);
} else {
if (!StringUtils.equals(dto.getReportType(), RunModeConstants.SET_REPORT.toString())) {
dto.setConsole(FixedCapacityUtil.getJmeterLogger(getReportId(), false));
}
sampleResults = RetryResultUtil.clearLoops(sampleResults);
JMeterBase.resultFormatting(sampleResults, dto);
testResultService.saveResults(dto);
resultVO = ReportStatusUtil.getStatus(dto, resultVO);
dto.getArbitraryData().put(CommonConstants.LOCAL_STATUS_KEY, resultVO);
sampleResults.clear();
}
}
@Override
public void teardownTest(BackendListenerContext context) {
try {
LoggerUtil.info("进入TEST-END处理报告" + dto.getRunMode(), dto.getReportId());
super.teardownTest(context);
// 获取执行日志
if (!StringUtils.equals(dto.getReportType(), RunModeConstants.SET_REPORT.toString())) {
dto.setConsole(FixedCapacityUtil.getJmeterLogger(getReportId(), true));
}
if (dto.isRetryEnable()) {
LoggerUtil.info("重试结果处理开始", dto.getReportId());
// 清理过程步骤
queues = RetryResultUtil.clearLoops(queues);
JMeterBase.resultFormatting(queues, dto);
LoggerUtil.info("合并重试结果集", dto.getReportId());
RetryResultUtil.mergeRetryResults(dto.getRequestResults());
LoggerUtil.info("执行结果入库存储", dto.getReportId());
testResultService.saveResults(dto);
resultVO = ReportStatusUtil.getStatus(dto, resultVO);
dto.getArbitraryData().put(CommonConstants.LOCAL_STATUS_KEY, resultVO);
LoggerUtil.info("重试结果处理结束", dto.getReportId());
}
// 全局并发队列
PoolExecBlockingQueueUtil.offer(dto.getReportId());
// 整体执行结束更新资源状态
testResultService.testEnded(dto);
if (StringUtils.isNotEmpty(dto.getQueueId())) {
LoggerUtil.info("串行进入下一个执行点", dto.getReportId());
apiExecutionQueueService.queueNext(dto);
}
// 更新测试计划报告
LoggerUtil.info("Check Processing Test Plan report status" + dto.getQueueId() + "" + dto.getTestId());
apiExecutionQueueService.checkTestPlanCaseTestEnd(dto.getTestId(), dto.getRunMode(), dto.getTestPlanReportId());
LoggerUtil.info("TEST-END处理结果集完成", dto.getReportId());
JvmUtil.memoryInfo();
} catch (Exception e) {
LoggerUtil.error("结果集处理异常", dto.getReportId(), e);
} finally {
queues.clear();
redisTemplateService.delFilePath(dto.getReportId());
FileUtils.deleteBodyFiles(dto.getReportId());
if (FileServer.getFileServer() != null) {
LoggerUtil.info("进入监听开始关闭CSV", dto.getReportId());
FileServer.getFileServer().closeCsv(dto.getReportId());
}
ApiLocalRunner.clearCache(dto.getReportId());
}
}
/**
* 初始化参数
*
* @param context
*/
private void setParam(BackendListenerContext context) {
dto = new ResultDTO();
dto.setTestId(context.getParameter(BackendListenerConstants.TEST_ID.name()));
dto.setRunMode(context.getParameter(BackendListenerConstants.RUN_MODE.name()));
dto.setReportId(context.getParameter(BackendListenerConstants.REPORT_ID.name()));
dto.setReportType(context.getParameter(BackendListenerConstants.REPORT_TYPE.name()));
dto.setTestPlanReportId(context.getParameter(BackendListenerConstants.MS_TEST_PLAN_REPORT_ID.name()));
if (context.getParameter(BackendListenerConstants.RETRY_ENABLE.name()) != null) {
dto.setRetryEnable(Boolean.parseBoolean(context.getParameter(BackendListenerConstants.RETRY_ENABLE.name())));
}
dto.setQueueId(context.getParameter(BackendListenerConstants.QUEUE_ID.name()));
dto.setRunType(context.getParameter(BackendListenerConstants.RUN_TYPE.name()));
if (dto.getArbitraryData() == null) {
dto.setArbitraryData(new LinkedHashMap<>());
}
String ept = context.getParameter(BackendListenerConstants.EPT.name());
if (StringUtils.isNotEmpty(ept)) {
dto.setExtendedParameters(JSON.parseObject(context.getParameter(BackendListenerConstants.EPT.name()), Map.class));
}
if (StringUtils.isNotBlank(context.getParameter(BackendListenerConstants.FAKE_ERROR.name()))) {
Map<String, List<MsRegexDTO>> fakeErrorMap = JSON.parseObject(
context.getParameter(BackendListenerConstants.FAKE_ERROR.name()),
new TypeReference<Map<String, List<MsRegexDTO>>>() {});
dto.setFakeErrorMap(fakeErrorMap);
}
}
private String getReportId() {
String reportId = dto.getReportId();
if (StringUtils.isNotEmpty(dto.getTestPlanReportId())
&& !FixedCapacityUtil.containsKey(dto.getTestPlanReportId())
&& StringUtils.equals(dto.getReportType(), RunModeConstants.SET_REPORT.toString())) {
reportId = dto.getTestPlanReportId();
}
return reportId;
}
}

View File

@ -49,8 +49,6 @@ public class TestResultService {
@Resource @Resource
private ApiEnvironmentRunningParamService apiEnvironmentRunningParamService; private ApiEnvironmentRunningParamService apiEnvironmentRunningParamService;
@Resource @Resource
private RedisTemplateService redisTemplateService;
@Resource
private ApiScenarioExecutionInfoService scenarioExecutionInfoService; private ApiScenarioExecutionInfoService scenarioExecutionInfoService;
@Resource @Resource
private ApiScenarioReportStructureService apiScenarioReportStructureService; private ApiScenarioReportStructureService apiScenarioReportStructureService;
@ -80,54 +78,12 @@ public class TestResultService {
this.add(ApiRunMode.JENKINS_SCENARIO_PLAN.name()); this.add(ApiRunMode.JENKINS_SCENARIO_PLAN.name());
}}; }};
// 接口测试 用例/接口
private static final List<String> caseRunModes = new ArrayList<>() {{
this.add(ApiRunMode.DEFINITION.name());
this.add(ApiRunMode.JENKINS.name());
this.add(ApiRunMode.API_PLAN.name());
}};
// 测试计划 用例/接口
private static final List<String> planCaseRunModes = new ArrayList<>() {{
this.add(ApiRunMode.SCHEDULE_API_PLAN.name());
this.add(ApiRunMode.JENKINS_API_PLAN.name());
this.add(ApiRunMode.MANUAL_PLAN.name());
}};
private static final List<String> apiRunModes = new ArrayList<>() {{ private static final List<String> apiRunModes = new ArrayList<>() {{
this.add(ApiRunMode.DEFINITION.name()); this.add(ApiRunMode.DEFINITION.name());
this.add(ApiRunMode.API_PLAN.name()); this.add(ApiRunMode.API_PLAN.name());
this.add(ApiRunMode.SCHEDULE_API_PLAN.name()); this.add(ApiRunMode.SCHEDULE_API_PLAN.name());
}}; }};
/**
* 执行结果存储
*
* @param dto 执行结果
*/
public void saveResults(ResultDTO dto) {
// 处理环境
List<String> environmentList = new LinkedList<>();
if (dto.getArbitraryData() != null && dto.getArbitraryData().containsKey("ENV")) {
environmentList = (List<String>) dto.getArbitraryData().get("ENV");
}
//处理环境参数
if (CollectionUtils.isNotEmpty(environmentList)) {
apiEnvironmentRunningParamService.parseEnvironment(environmentList);
}
// 测试计划用例触发结果处理
if (planCaseRunModes.contains(dto.getRunMode())) {
apiDefinitionExecResultService.saveApiResultByScheduleTask(dto);
} else if (caseRunModes.contains(dto.getRunMode())) {
// 手动触发/批量触发 用例结果处理
apiDefinitionExecResultService.saveApiResult(dto);
} else if (scenarioRunModes.contains(dto.getRunMode())) {
// 场景报告结果处理
apiScenarioReportService.saveResult(dto);
}
}
/** /**
* 批量存储来自NODE/K8s的执行结果 * 批量存储来自NODE/K8s的执行结果
*/ */

View File

@ -5,15 +5,12 @@ import io.metersphere.api.dto.RequestResultExpandDTO;
import io.metersphere.base.domain.*; import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*; import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtApiDefinitionExecResultMapper; import io.metersphere.base.mapper.ext.ExtApiDefinitionExecResultMapper;
import io.metersphere.base.mapper.ext.ExtApiTestCaseMapper;
import io.metersphere.base.mapper.plan.TestPlanApiCaseMapper; import io.metersphere.base.mapper.plan.TestPlanApiCaseMapper;
import io.metersphere.base.mapper.plan.ext.ExtTestPlanApiCaseMapper;
import io.metersphere.commons.constants.ApiRunMode; import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.CommonConstants; import io.metersphere.commons.constants.CommonConstants;
import io.metersphere.commons.constants.NoticeConstants; import io.metersphere.commons.constants.NoticeConstants;
import io.metersphere.commons.constants.TriggerMode; import io.metersphere.commons.constants.TriggerMode;
import io.metersphere.commons.enums.ApiReportStatus; import io.metersphere.commons.enums.ApiReportStatus;
import io.metersphere.commons.enums.ExecutionExecuteTypeEnum;
import io.metersphere.commons.utils.*; import io.metersphere.commons.utils.*;
import io.metersphere.dto.PlanReportCaseDTO; import io.metersphere.dto.PlanReportCaseDTO;
import io.metersphere.dto.RequestResult; import io.metersphere.dto.RequestResult;
@ -49,8 +46,6 @@ public class ApiDefinitionExecResultService {
@Resource @Resource
private ApiTestCaseMapper apiTestCaseMapper; private ApiTestCaseMapper apiTestCaseMapper;
@Resource @Resource
private ApiDefinitionMapper apiDefinitionMapper;
@Resource
private NoticeSendService noticeSendService; private NoticeSendService noticeSendService;
@Resource @Resource
private UserMapper userMapper; private UserMapper userMapper;
@ -61,14 +56,6 @@ public class ApiDefinitionExecResultService {
@Resource @Resource
private ApiExecutionInfoService apiExecutionInfoService; private ApiExecutionInfoService apiExecutionInfoService;
@Resource @Resource
private TestPlanApiCaseMapper testPlanApiCaseMapper;
@Resource
private ApiCaseExecutionInfoService apiCaseExecutionInfoService;
@Resource
private ExtApiTestCaseMapper extApiTestCaseMapper;
@Resource
private ExtTestPlanApiCaseMapper extTestPlanApiCaseMapper;
@Resource
private RedisTemplateService redisTemplateService; private RedisTemplateService redisTemplateService;
/** /**
@ -89,76 +76,52 @@ public class ApiDefinitionExecResultService {
} }
} }
public void saveApiResult(ResultDTO dto) {
LoggerUtil.info("接收到API/CASE执行结果【 " + dto.getRequestResults().size() + " 】条");
this.mergeRetryResults(dto);
for (RequestResult item : dto.getRequestResults()) {
if (item.getResponseResult() != null && item.getResponseResult().getResponseTime() <= 0) {
item.getResponseResult().setResponseTime((item.getEndTime() - item.getStartTime()));
}
if (!StringUtils.startsWithAny(item.getName(), "PRE_PROCESSOR_ENV_", "POST_PROCESSOR_ENV_")) {
ApiDefinitionExecResult result = this.editResult(item, dto.getReportId(), dto.getConsole(), dto.getRunMode(), dto.getTestId(), null);
if (result != null) {
result.setResourceId(dto.getTestId());
apiExecutionInfoService.insertExecutionInfo(result);
User user = getUser(dto, result);
//如果是测试计划用例更新接口用例的上次执行结果
TestPlanApiCase testPlanApiCase = testPlanApiCaseMapper.selectByPrimaryKey(dto.getTestId());
if (testPlanApiCase != null) {
ApiTestCaseWithBLOBs apiTestCase = apiTestCaseMapper.selectByPrimaryKey(testPlanApiCase.getApiCaseId());
if (apiTestCase != null) {
apiTestCase.setLastResultId(dto.getReportId());
apiTestCaseMapper.updateByPrimaryKeySelective(apiTestCase);
}
redisTemplateService.unlock(dto.getTestId(), dto.getReportId());
}
// 发送通知
LoggerUtil.info("执行结果【 " + result.getName() + " 】入库存储完成");
sendNotice(result, user);
}
}
}
}
public void batchSaveApiResult(List<ResultDTO> resultDTOS) { public void batchSaveApiResult(List<ResultDTO> resultDTOS) {
if (CollectionUtils.isEmpty(resultDTOS)) { if (CollectionUtils.isEmpty(resultDTOS)) {
LoggerUtil.info("未接收到处理结果 ");
return; return;
} }
LoggerUtil.info("接收到API/CASE执行结果【 " + resultDTOS.size() + "");
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
ApiDefinitionExecResultMapper definitionExecResultMapper = sqlSession.getMapper(ApiDefinitionExecResultMapper.class); ApiDefinitionExecResultMapper batchExecResultMapper = sqlSession.getMapper(ApiDefinitionExecResultMapper.class);
ApiTestCaseMapper batchApiTestCaseMapper = sqlSession.getMapper(ApiTestCaseMapper.class); ApiTestCaseMapper batchApiTestCaseMapper = sqlSession.getMapper(ApiTestCaseMapper.class);
TestPlanApiCaseMapper planApiCaseMapper = sqlSession.getMapper(TestPlanApiCaseMapper.class); TestPlanApiCaseMapper planApiCaseMapper = sqlSession.getMapper(TestPlanApiCaseMapper.class);
for (ResultDTO dto : resultDTOS) { for (ResultDTO dto : resultDTOS) {
this.mergeRetryResults(dto); this.mergeRetryResults(dto);
LoggerUtil.info("开始存储报告结果[ " + dto.getRequestResults().size() + " ]", dto.getReportId()); LoggerUtil.info("开始存储报告结果[ " + dto.getRequestResults().size() + " ]", dto.getReportId());
if (CollectionUtils.isNotEmpty(dto.getRequestResults())) { if (CollectionUtils.isEmpty(dto.getRequestResults())) {
for (RequestResult item : dto.getRequestResults()) { LoggerUtil.info("未解析到执行结果", dto.getReportId());
if (!StringUtils.startsWithAny(item.getName(), "PRE_PROCESSOR_ENV_", "POST_PROCESSOR_ENV_")) { continue;
ApiDefinitionExecResult result = this.editResult(item, dto.getReportId(), dto.getConsole(), dto.getRunMode(), dto.getTestId(), definitionExecResultMapper); }
if (result != null) { // 过滤掉全局脚本
if (StringUtils.isBlank(result.getProjectId()) && dto.getExtendedParameters().containsKey(MsHashTreeService.PROJECT_ID)) { List<RequestResult> requestResults = dto.getRequestResults().stream()
result.setProjectId(this.getProjectIdByResultDTO(dto.getExtendedParameters().get(MsHashTreeService.PROJECT_ID).toString())); .filter(item -> !StringUtils.startsWithAny(item.getName(), "PRE_PROCESSOR_ENV_", "POST_PROCESSOR_ENV_"))
} .collect(Collectors.toList());
result.setResourceId(dto.getTestId());
apiExecutionInfoService.insertExecutionInfo(result); for (RequestResult item : requestResults) {
// 批量更新关联关系状态 ApiDefinitionExecResultWithBLOBs result = this.initResult(item, dto);
batchEditStatus(dto.getRunMode(), result.getStatus(), result.getId(), dto.getTestId(), planApiCaseMapper, batchApiTestCaseMapper); if (StringUtils.isBlank(result.getProjectId()) && dto.getExtendedParameters().containsKey(MsHashTreeService.PROJECT_ID)) {
} result.setProjectId(this.getProjectId(dto.getExtendedParameters().get(MsHashTreeService.PROJECT_ID).toString()));
if (result != null && !StringUtils.startsWithAny(dto.getRunMode(), NoticeConstants.Mode.SCHEDULE)) { }
User user = getUser(dto, result); result.setResourceId(dto.getTestId());
if (MapUtils.isNotEmpty(dto.getExtendedParameters()) // 更新结果数据
&& dto.getExtendedParameters().containsKey(CommonConstants.USER) batchExecResultMapper.updateByPrimaryKeySelective(result);
&& dto.getExtendedParameters().get(CommonConstants.USER) instanceof User) {
user = (User) dto.getExtendedParameters().get(CommonConstants.USER); apiExecutionInfoService.insertExecutionInfo(result);
} // 批量更新关联关系状态
// 发送通知 batchEditStatus(result.getStatus(), result.getId(), dto, planApiCaseMapper, batchApiTestCaseMapper);
result.setResourceId(dto.getTestId()); // 发送通知
sendNotice(result, user); if (!StringUtils.startsWithAny(dto.getRunMode(), NoticeConstants.Mode.SCHEDULE)) {
} User user = getUser(dto, result);
if (MapUtils.isNotEmpty(dto.getExtendedParameters())
&& dto.getExtendedParameters().containsKey(CommonConstants.USER)
&& dto.getExtendedParameters().get(CommonConstants.USER) instanceof User) {
user = (User) dto.getExtendedParameters().get(CommonConstants.USER);
} }
// 发送通知
result.setResourceId(dto.getTestId());
sendNotice(result, user);
} }
} }
} }
@ -168,7 +131,7 @@ public class ApiDefinitionExecResultService {
} }
} }
private String getProjectIdByResultDTO(String projectIdFromResultDTO) { private String getProjectId(String projectIdFromResultDTO) {
String returnStr = projectIdFromResultDTO; String returnStr = projectIdFromResultDTO;
if (StringUtils.startsWith(projectIdFromResultDTO, "[") && StringUtils.endsWith(projectIdFromResultDTO, "]")) { if (StringUtils.startsWith(projectIdFromResultDTO, "[") && StringUtils.endsWith(projectIdFromResultDTO, "]")) {
try { try {
@ -239,142 +202,33 @@ public class ApiDefinitionExecResultService {
} }
} }
public void setExecResult(String id, String status, Long time) { public void batchEditStatus(
TestPlanApiCase apiCase = new TestPlanApiCase(); String status, String reportId, ResultDTO dto,
apiCase.setId(id); TestPlanApiCaseMapper batchTestPlanApiCaseMapper,
apiCase.setStatus(status); ApiTestCaseMapper batchApiTestCaseMapper) {
apiCase.setUpdateTime(time); ApiRunMode runMode = ApiRunMode.fromString(dto.getRunMode());
testPlanApiCaseMapper.updateByPrimaryKeySelective(apiCase);
}
public void editStatus(ApiDefinitionExecResult saveResult, String type, String status, Long time, String reportId, String testId) { switch (runMode) {
String name = testId; case API_PLAN:
String version = StringUtils.EMPTY; case SCHEDULE_API_PLAN:
String projectId = StringUtils.EMPTY; case JENKINS_API_PLAN:
if (StringUtils.equalsAnyIgnoreCase(type, case MANUAL_PLAN:
ApiRunMode.API_PLAN.name(), TestPlanApiCase apiCase = new TestPlanApiCase();
ApiRunMode.SCHEDULE_API_PLAN.name(), apiCase.setId(dto.getTestId());
ApiRunMode.JENKINS_API_PLAN.name(), apiCase.setStatus(status);
ApiRunMode.MANUAL_PLAN.name())) { apiCase.setUpdateTime(System.currentTimeMillis());
TestPlanApiCase testPlanApiCase = testPlanApiCaseMapper.selectByPrimaryKey(testId); batchTestPlanApiCaseMapper.updateByPrimaryKeySelective(apiCase);
ApiTestCaseWithBLOBs caseWithBLOBs = null; redisTemplateService.unlock(dto.getTestId(), reportId);
if (testPlanApiCase != null) { break;
this.setExecResult(testId, status, time);
caseWithBLOBs = apiTestCaseMapper.selectByPrimaryKey(testPlanApiCase.getApiCaseId());
testPlanApiCase.setStatus(status);
testPlanApiCase.setUpdateTime(System.currentTimeMillis());
testPlanApiCaseMapper.updateByPrimaryKeySelective(testPlanApiCase);
if (LoggerUtil.getLogger().isDebugEnabled()) {
LoggerUtil.debug("更新测试计划用例【 " + testPlanApiCase.getId() + "");
}
redisTemplateService.unlock(testId, saveResult.getId());
}
if (caseWithBLOBs != null) {
name = caseWithBLOBs.getName();
version = caseWithBLOBs.getVersionId();
projectId = caseWithBLOBs.getProjectId();
}
} else {
ApiDefinition apiDefinition = apiDefinitionMapper.selectByPrimaryKey(testId);
if (apiDefinition != null) {
name = apiDefinition.getName();
projectId = apiDefinition.getProjectId();
} else {
ApiTestCaseWithBLOBs caseWithBLOBs = apiTestCaseMapper.selectByPrimaryKey(testId);
if (caseWithBLOBs != null) {
// 更新用例最后执行结果
caseWithBLOBs.setLastResultId(reportId);
caseWithBLOBs.setStatus(status);
caseWithBLOBs.setUpdateTime(System.currentTimeMillis());
apiTestCaseMapper.updateByPrimaryKey(caseWithBLOBs);
if (LoggerUtil.getLogger().isDebugEnabled()) { default:
LoggerUtil.debug("更新用例【 " + caseWithBLOBs.getId() + ""); ApiTestCaseWithBLOBs caseWithBLOBs = new ApiTestCaseWithBLOBs();
} caseWithBLOBs.setId(dto.getTestId());
name = caseWithBLOBs.getName(); caseWithBLOBs.setLastResultId(reportId);
version = caseWithBLOBs.getVersionId(); caseWithBLOBs.setStatus(status);
projectId = caseWithBLOBs.getProjectId(); caseWithBLOBs.setUpdateTime(System.currentTimeMillis());
redisTemplateService.unlock(testId, saveResult.getId()); batchApiTestCaseMapper.updateByPrimaryKeySelective(caseWithBLOBs);
} break;
}
}
if (StringUtils.isEmpty(saveResult.getProjectId()) && StringUtils.isNotEmpty(projectId)) {
saveResult.setProjectId(projectId);
}
saveResult.setVersionId(version);
saveResult.setName(name);
}
public void batchEditStatus(String type, String status, String reportId, String testId,
TestPlanApiCaseMapper batchTestPlanApiCaseMapper,
ApiTestCaseMapper batchApiTestCaseMapper) {
if (StringUtils.equalsAnyIgnoreCase(type,
ApiRunMode.API_PLAN.name(),
ApiRunMode.SCHEDULE_API_PLAN.name(),
ApiRunMode.JENKINS_API_PLAN.name(),
ApiRunMode.MANUAL_PLAN.name())) {
TestPlanApiCase apiCase = new TestPlanApiCase();
apiCase.setId(testId);
apiCase.setStatus(status);
apiCase.setUpdateTime(System.currentTimeMillis());
batchTestPlanApiCaseMapper.updateByPrimaryKeySelective(apiCase);
TestCaseReviewApiCase reviewApiCase = new TestCaseReviewApiCase();
reviewApiCase.setId(testId);
reviewApiCase.setStatus(status);
reviewApiCase.setUpdateTime(System.currentTimeMillis());
redisTemplateService.unlock(testId, reportId);
} else {
// 更新用例最后执行结果
ApiTestCaseWithBLOBs caseWithBLOBs = new ApiTestCaseWithBLOBs();
caseWithBLOBs.setId(testId);
caseWithBLOBs.setLastResultId(reportId);
caseWithBLOBs.setStatus(status);
caseWithBLOBs.setUpdateTime(System.currentTimeMillis());
batchApiTestCaseMapper.updateByPrimaryKeySelective(caseWithBLOBs);
}
}
/**
* 定时任务触发的保存逻辑
* 定时任务时userID要改为定时任务中的用户
*/
public void saveApiResultByScheduleTask(ResultDTO dto) {
if (CollectionUtils.isNotEmpty(dto.getRequestResults())) {
LoggerUtil.info("接收到API/CASE执行结果【 " + dto.getRequestResults().size() + " 】条");
for (RequestResult item : dto.getRequestResults()) {
LoggerUtil.info("执行结果【 " + item.getName() + " 】入库存储");
if (!StringUtils.startsWithAny(item.getName(), "PRE_PROCESSOR_ENV_", "POST_PROCESSOR_ENV_")) {
ApiDefinitionExecResult reportResult = this.editResult(item, dto.getReportId(), dto.getConsole(), dto.getRunMode(), dto.getTestId(), null);
if (MapUtils.isNotEmpty(dto.getExtendedParameters()) && dto.getExtendedParameters().containsKey(CommonConstants.USER_ID)) {
reportResult.setUserId(String.valueOf(dto.getExtendedParameters().get(CommonConstants.USER_ID)));
}
String triggerMode = StringUtils.EMPTY;
if (reportResult != null) {
triggerMode = reportResult.getTriggerMode();
}
if (StringUtils.equalsAny(dto.getRunMode(), ApiRunMode.SCHEDULE_API_PLAN.name(), ApiRunMode.JENKINS_API_PLAN.name())) {
TestPlanApiCase apiCase = testPlanApiCaseMapper.selectByPrimaryKey(dto.getTestId());
if (apiCase != null && redisTemplateService.has(dto.getTestId(), dto.getReportId())) {
String projectId = extTestPlanApiCaseMapper.selectProjectId(apiCase.getId());
ApiDefinition apiDefinition = extApiTestCaseMapper.selectApiBasicInfoByCaseId(apiCase.getId());
String version = apiDefinition == null ? "" : apiDefinition.getVersionId();
apiCaseExecutionInfoService.insertExecutionInfo(apiCase.getId(), reportResult.getStatus(), triggerMode, projectId, ExecutionExecuteTypeEnum.TEST_PLAN.name(), version);
apiCase.setStatus(reportResult.getStatus());
apiCase.setUpdateTime(System.currentTimeMillis());
testPlanApiCaseMapper.updateByPrimaryKeySelective(apiCase);
ApiTestCaseWithBLOBs apiTestCase = apiTestCaseMapper.selectByPrimaryKey(apiCase.getApiCaseId());
if (apiTestCase != null) {
apiTestCase.setLastResultId(dto.getReportId());
apiTestCaseMapper.updateByPrimaryKeySelective(apiTestCase);
}
redisTemplateService.unlock(dto.getTestId(), dto.getReportId());
}
} else {
this.setExecResult(dto.getTestId(), reportResult.getStatus(), item.getStartTime());
}
}
}
} }
} }
@ -388,41 +242,33 @@ public class ApiDefinitionExecResultService {
} }
} }
private ApiDefinitionExecResult editResult(RequestResult item, String reportId, String console, String type, String testId, ApiDefinitionExecResultMapper batchMapper) { private ApiDefinitionExecResultWithBLOBs initResult(RequestResult item, ResultDTO dto) {
if (!StringUtils.startsWithAny(item.getName(), "PRE_PROCESSOR_ENV_", "POST_PROCESSOR_ENV_")) { ApiDefinitionExecResultWithBLOBs saveResult = new ApiDefinitionExecResultWithBLOBs();
ApiDefinitionExecResultWithBLOBs saveResult = new ApiDefinitionExecResultWithBLOBs(); item.getResponseResult().setConsole(dto.getConsole());
item.getResponseResult().setConsole(console); saveResult.setId(dto.getReportId());
saveResult.setId(reportId); //对响应内容进行进一步解析如果有附加信息比如误报库信息则根据附加信息内的数据进行其他判读
//对响应内容进行进一步解析如果有附加信息比如误报库信息则根据附加信息内的数据进行其他判读 RequestResultExpandDTO expandDTO = ResponseUtil.parseByRequestResult(item);
RequestResultExpandDTO expandDTO = ResponseUtil.parseByRequestResult(item); String status = item.isSuccess() ? ApiReportStatus.SUCCESS.name() : ApiReportStatus.ERROR.name();
String status = item.isSuccess() ? ApiReportStatus.SUCCESS.name() : ApiReportStatus.ERROR.name(); if (MapUtils.isNotEmpty(expandDTO.getAttachInfoMap())) {
if (MapUtils.isNotEmpty(expandDTO.getAttachInfoMap())) { if (StringUtils.isNotEmpty(expandDTO.getStatus())) {
if (StringUtils.isNotEmpty(expandDTO.getStatus())) { status = expandDTO.getStatus();
status = expandDTO.getStatus();
}
saveResult.setContent(JSON.toJSONString(expandDTO));
} else {
saveResult.setContent(JSON.toJSONString(item));
} }
saveResult.setType(type); saveResult.setContent(JSON.toJSONString(expandDTO));
saveResult.setStatus(status); } else {
saveResult.setStartTime(item.getStartTime()); saveResult.setContent(JSON.toJSONString(item));
saveResult.setEndTime(item.getEndTime());
if (item.getStartTime() >= item.getEndTime()) {
saveResult.setEndTime(System.currentTimeMillis());
}
if (StringUtils.isNotEmpty(saveResult.getTriggerMode()) && saveResult.getTriggerMode().equals(CommonConstants.CASE)) {
saveResult.setTriggerMode(TriggerMode.MANUAL.name());
}
if (batchMapper == null) {
editStatus(saveResult, type, status, saveResult.getCreateTime(), saveResult.getId(), testId);
apiDefinitionExecResultMapper.updateByPrimaryKeySelective(saveResult);
} else {
batchMapper.updateByPrimaryKeySelective(saveResult);
}
return saveResult;
} }
return null; saveResult.setType(dto.getRunMode());
saveResult.setStatus(status);
saveResult.setStartTime(item.getStartTime());
saveResult.setEndTime(item.getEndTime());
if (item.getStartTime() >= item.getEndTime()) {
saveResult.setEndTime(System.currentTimeMillis());
}
if (StringUtils.isNotEmpty(saveResult.getTriggerMode()) && saveResult.getTriggerMode().equals(CommonConstants.CASE)) {
saveResult.setTriggerMode(TriggerMode.MANUAL.name());
}
saveResult.setResourceId(dto.getTestId());
return saveResult;
} }
public Map<String, String> selectReportResultByReportIds(Collection<String> values) { public Map<String, String> selectReportResultByReportIds(Collection<String> values) {

View File

@ -89,11 +89,6 @@ public class ApiScenarioReportService {
@Resource @Resource
private RedisTemplateService redisTemplateService; private RedisTemplateService redisTemplateService;
public void saveResult(ResultDTO dto) {
// 报告详情内容
apiScenarioReportResultService.save(dto.getReportId(), dto.getRequestResults());
}
public void batchSaveResult(List<ResultDTO> dtos) { public void batchSaveResult(List<ResultDTO> dtos) {
apiScenarioReportResultService.batchSave(dtos); apiScenarioReportResultService.batchSave(dtos);
} }

View File

@ -1,8 +1,41 @@
package io.metersphere.commons.constants; package io.metersphere.commons.constants;
import org.apache.commons.lang3.StringUtils;
public enum ApiRunMode { public enum ApiRunMode {
RUN, DEBUG, DEFINITION, TEST_CASE, SCENARIO, API_PLAN, JENKINS_API_PLAN, JENKINS_SCENARIO_PLAN, JENKINS_PERFORMANCE_TEST, JENKINS, RUN,
DEBUG,
DEFINITION,
TEST_CASE,
SCENARIO,
API_PLAN,
JENKINS_API_PLAN,
JENKINS_SCENARIO_PLAN,
JENKINS_PERFORMANCE_TEST,
JENKINS,
TEST_PLAN_PERFORMANCE_TEST, TEST_PLAN_PERFORMANCE_TEST,
SCENARIO_PLAN, API, SCHEDULE_API_PLAN, SCHEDULE_SCENARIO, SCHEDULE_SCENARIO_PLAN, SCHEDULE_PERFORMANCE_TEST, MANUAL_PLAN, SCENARIO_PLAN,
UI_SCENARIO, UI_SCENARIO_PLAN, UI_SCHEDULE_SCENARIO_PLAN, UI_JENKINS_SCENARIO_PLAN, UI_SCHEDULE_SCENARIO API,
SCHEDULE_API_PLAN,
SCHEDULE_SCENARIO,
SCHEDULE_SCENARIO_PLAN,
SCHEDULE_PERFORMANCE_TEST,
MANUAL_PLAN,
UI_SCENARIO,
UI_SCENARIO_PLAN,
UI_SCHEDULE_SCENARIO_PLAN,
UI_JENKINS_SCENARIO_PLAN,
UI_SCHEDULE_SCENARIO,
DEFAULT;
public static ApiRunMode fromString(String mode) {
if (StringUtils.isNotBlank(mode)) {
for (ApiRunMode runMode : ApiRunMode.values()) {
if (runMode.name().equalsIgnoreCase(mode)) {
return runMode;
}
}
}
return DEFAULT;
}
} }

View File

@ -24,4 +24,6 @@ public interface ExtTestPlanReportMapper {
void setApiBaseCountAndPassRateIsNullById(String id); void setApiBaseCountAndPassRateIsNullById(String id);
void updateAllStatus(); void updateAllStatus();
String selectLastReportByTestPlanId(@Param("testPlanId") String testPlanId);
} }

View File

@ -154,6 +154,10 @@
GROUP BY t.test_plan_id GROUP BY t.test_plan_id
</select> </select>
<select id="selectLastReportByTestPlanId" resultType="java.lang.String">
select `status` from test_plan_report where test_plan_id = #{testPlanId} ORDER BY create_time DESC LIMIT 1
</select>
<update id="setApiBaseCountAndPassRateIsNullById"> <update id="setApiBaseCountAndPassRateIsNullById">
update test_plan_report_content update test_plan_report_content

View File

@ -2,10 +2,12 @@ package io.metersphere.plan.service;
import io.metersphere.base.domain.TestPlanWithBLOBs; import io.metersphere.base.domain.TestPlanWithBLOBs;
import io.metersphere.base.mapper.TestPlanMapper; import io.metersphere.base.mapper.TestPlanMapper;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.JSON; import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.LogUtil; import io.metersphere.commons.utils.LogUtil;
import io.metersphere.constants.RunModeConstants; import io.metersphere.constants.RunModeConstants;
import io.metersphere.dto.*; import io.metersphere.dto.*;
import io.metersphere.i18n.Translator;
import io.metersphere.plan.dto.ExecutionWay; import io.metersphere.plan.dto.ExecutionWay;
import io.metersphere.plan.request.api.TestPlanRunRequest; import io.metersphere.plan.request.api.TestPlanRunRequest;
import io.metersphere.plan.service.remote.api.PlanTestPlanApiCaseService; import io.metersphere.plan.service.remote.api.PlanTestPlanApiCaseService;
@ -51,6 +53,11 @@ public class TestPlanExecuteService {
*/ */
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public String runTestPlan(String testPlanId, String projectId, String userId, String triggerMode, String planReportId, String executionWay, String apiRunConfig) { public String runTestPlan(String testPlanId, String projectId, String userId, String triggerMode, String planReportId, String executionWay, String apiRunConfig) {
// 校验测试计划是否在执行中
if (testPlanService.checkTestPlanIsRunning(testPlanId)) {
LogUtil.info("当前测试计划正在执行中,请稍后再试", testPlanId);
MSException.throwException(Translator.get("test_plan_run_message"));
}
RunModeConfigDTO runModeConfig = null; RunModeConfigDTO runModeConfig = null;
try { try {
runModeConfig = JSON.parseObject(apiRunConfig, RunModeConfigDTO.class); runModeConfig = JSON.parseObject(apiRunConfig, RunModeConfigDTO.class);

View File

@ -1769,4 +1769,8 @@ public class TestPlanReportService {
testPlanReportContentMapper.updateByExampleSelective(reportContentWithBLOBs, example); testPlanReportContentMapper.updateByExampleSelective(reportContentWithBLOBs, example);
} }
} }
public String selectLastReportByTestPlanId(String testPlanId) {
return extTestPlanReportMapper.selectLastReportByTestPlanId(testPlanId);
}
} }

View File

@ -161,7 +161,7 @@ public class TestPlanService {
private TestPlanService testPlanService; private TestPlanService testPlanService;
@Resource @Resource
private BaseTestResourcePoolService baseTestResourcePoolService; private BaseTestResourcePoolService baseTestResourcePoolService;
public TestPlan addTestPlan(AddTestPlanRequest testPlan) { public TestPlan addTestPlan(AddTestPlanRequest testPlan) {
if (getTestPlanByName(testPlan.getName()).size() > 0) { if (getTestPlanByName(testPlan.getName()).size() > 0) {
MSException.throwException(Translator.get("plan_name_already_exists")); MSException.throwException(Translator.get("plan_name_already_exists"));
@ -2359,4 +2359,9 @@ public class TestPlanService {
// 进行中0 < 测试进度 < 100% // 进行中0 < 测试进度 < 100%
return TestPlanStatus.Underway.name(); return TestPlanStatus.Underway.name();
} }
public boolean checkTestPlanIsRunning(String testPlanId) {
String status = testPlanReportService.selectLastReportByTestPlanId(testPlanId);
return StringUtils.equalsIgnoreCase(status, TestPlanReportStatus.RUNNING.name());
}
} }

View File

@ -240,3 +240,4 @@ rerun_warning=The report is being rerun, check it later
case_export_text_validate_tip=Contains extremely long text, currently supported up to %s! case_export_text_validate_tip=Contains extremely long text, currently supported up to %s!
case_import_table_header_missing=Header information is missing! case_import_table_header_missing=Header information is missing!
relate_resource=relate relate_resource=relate
test_plan_run_message=The current test plan is running, please try again later

View File

@ -211,3 +211,4 @@ rerun_warning=报告正在重跑中,稍后查看
case_export_text_validate_tip=包含超长文本,目前支持最大长度为 %s case_export_text_validate_tip=包含超长文本,目前支持最大长度为 %s
case_import_table_header_not_exist=缺少表头信息! case_import_table_header_not_exist=缺少表头信息!
relate_resource=关联 relate_resource=关联
test_plan_run_message=当前测试计划正在执行中,请稍后再试!

View File

@ -211,3 +211,4 @@ rerun_warning=報告正在重跑中,稻後查看
case_export_text_validate_tip=包含超長文本,目前支持最大長度為 %s case_export_text_validate_tip=包含超長文本,目前支持最大長度為 %s
case_import_table_header_not_exist=缺少表頭信息! case_import_table_header_not_exist=缺少表頭信息!
relate_resource=關聯 relate_resource=關聯
test_plan_run_message=當前測試計劃正在執行中,請稍後再試!