feat(测试计划): 测试规划,失败重试

--story=1015333 --user=陈建星 【测试计划】完成剩余功能 https://www.tapd.cn/55049933/s/1544401
This commit is contained in:
AgAngle 2024-07-04 16:01:07 +08:00 committed by Craftsman
parent 01c7a365e0
commit 9353382732
13 changed files with 192 additions and 19 deletions

View File

@ -35,6 +35,10 @@ public abstract class AbstractJmeterElementConverter<T extends MsTestElement> im
* 解析子步骤前的前置处理函数
*/
private static List<AbstractJmeterElementConverter> childPostConverters = new ArrayList<>();
/**
* 解析子步骤前的前置处理函数
*/
private static List<JmeterElementConvertInterceptor> convertInterceptors = new ArrayList<>();
public static void registerChildPreConverters(AbstractJmeterElementConverter converter) {
childPreConverters.add(converter);
@ -44,6 +48,10 @@ public abstract class AbstractJmeterElementConverter<T extends MsTestElement> im
childPostConverters.add(converter);
}
public static void registerConvertInterceptor(JmeterElementConvertInterceptor interceptor) {
convertInterceptors.add(interceptor);
}
public AbstractJmeterElementConverter() {
Type genericSuperclass = getClass().getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType parameterizedType) {
@ -64,15 +72,24 @@ public abstract class AbstractJmeterElementConverter<T extends MsTestElement> im
if (element != null && element.getChildren() != null) {
// 解析子步骤前的前置处理函数
childPreConverters.forEach(processor -> processor.toHashTree(tree, element, config));
element.getChildren().forEach(child -> {
child.setParent(element);
getConverterFunc.apply(child.getClass()).toHashTree(tree, child, config);
});
for (AbstractMsTestElement child : element.getChildren()) {
child.setParent(element);
// 拦截器拦截
HashTree wrapperTree = intercept(config, child, tree);
getConverterFunc.apply(child.getClass()).toHashTree(wrapperTree, child, config);
}
// 解析子步骤后的后置处理函数
childPostConverters.forEach(processor -> processor.toHashTree(tree, element, config));
}
}
public static HashTree intercept(ParameterConfig config, AbstractMsTestElement testElement, HashTree hashTree) {
for (JmeterElementConvertInterceptor convertInterceptor : convertInterceptors) {
return convertInterceptor.intercept(hashTree, testElement, config);
}
return hashTree;
}
/**
* 设置步骤标识
* 当前步骤唯一标识结果和步骤匹配的关键

View File

@ -0,0 +1,13 @@
package io.metersphere.plugin.api.spi;
import io.metersphere.plugin.api.dto.ParameterConfig;
import org.apache.jorphan.collections.HashTree;
/**
* @Author: jianxing
* @CreateTime: 2024-06-16 19:23
*/
public interface JmeterElementConvertInterceptor {
HashTree intercept(HashTree tree, MsTestElement element, ParameterConfig config);
}

View File

@ -1,4 +1,4 @@
package io.metersphere.plan.enums;
package io.metersphere.sdk.constants;
public enum RetryType {

View File

@ -10,15 +10,10 @@ public class ApiRunRetryConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 失败重试类型(步骤/场景)
*/
private String retryType;
/**
* 失败重试次数
*/
private Integer retryTimes;
private Integer retryTimes = 0;
/**
* 失败重试间隔(单位: ms)

View File

@ -5,6 +5,7 @@ import io.metersphere.plugin.api.dto.ParameterConfig;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.project.dto.environment.EnvironmentInfoDTO;
import io.metersphere.project.dto.environment.GlobalParams;
import io.metersphere.sdk.dto.api.task.ApiRunRetryConfig;
import lombok.Data;
import java.util.HashMap;
@ -32,6 +33,14 @@ public class ApiParamConfig extends ParameterConfig {
* 全局参数
*/
private GlobalParams globalParams;
/**
* 是否失败重试
*/
private Boolean retryOnFail = false;
/**
* 失败重试配置
*/
private ApiRunRetryConfig retryConfig;
/**
* AbstractMsTestElement 实现类与插件 ID 的映射
* key AbstractMsTestElement 实现类对象

View File

@ -6,6 +6,7 @@ import io.metersphere.api.dto.scenario.ScenarioOtherConfig;
import io.metersphere.api.parser.TestElementParser;
import io.metersphere.api.utils.JmeterElementConverterRegister;
import io.metersphere.plugin.api.dto.ParameterConfig;
import io.metersphere.plugin.api.spi.AbstractJmeterElementConverter;
import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.project.dto.environment.EnvironmentInfoDTO;
@ -67,8 +68,11 @@ public class JmeterTestElementParser implements TestElementParser {
Optional.ofNullable(userParameters).ifPresent(groupTree::add);
}
// 拦截器拦截
HashTree wrapperTree = AbstractJmeterElementConverter.intercept(config, msTestElement, groupTree);
// 解析 msTestElement
JmeterElementConverterRegister.getConverter(msTestElement.getClass()).toHashTree(groupTree, msTestElement, config);
JmeterElementConverterRegister.getConverter(msTestElement.getClass()).toHashTree(wrapperTree, msTestElement, config);
// 添加 debugSampler放最后才能采集到变量信息
groupTree.add(getDebugSampler());

View File

@ -0,0 +1,128 @@
package io.metersphere.api.parser.jmeter.interceptor;
import io.metersphere.api.dto.ApiParamConfig;
import io.metersphere.api.dto.request.controller.MsLoopController;
import io.metersphere.api.parser.jmeter.constants.JmeterAlias;
import io.metersphere.plugin.api.dto.ParameterConfig;
import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.plugin.api.spi.JmeterElementConvertInterceptor;
import io.metersphere.plugin.api.spi.MsTestElement;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.control.WhileController;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.visualizers.JSR223Listener;
import org.apache.jorphan.collections.HashTree;
import java.util.UUID;
/**
* @Author: jianxing
* @CreateTime: 2024-06-16 19:34
*/
public class RetryInterceptor implements JmeterElementConvertInterceptor {
private final String template = """
String retryId = "%s";
try {
String retryValueName = "VARS_" + retryId;
String retryTimes = "%s";
if (prev.isSuccess()) {
vars.put(retryId, "STOPPED");
}
if (vars.get(retryValueName) == null) {
vars.put(retryValueName, "0");
} else {
int retryNum = Integer.parseInt(vars.get(retryValueName));
retryNum++;
log.info("重试:" + retryNum);
prev.setSampleLabel("MsRetry_" + retryNum + "_" + prev.getSampleLabel());
vars.put(retryValueName, String.valueOf(retryNum));
}
if (vars.get(retryValueName).equals(retryTimes)) {
vars.put(retryId, "STOPPED");
}
} catch (Exception e) {
e.printStackTrace();
vars.put(retryId, "STOPPED");
}
""";
@Override
public HashTree intercept(HashTree tree, MsTestElement element, ParameterConfig config) {
AbstractMsTestElement abstractMsTestElement = (AbstractMsTestElement) element;
ApiParamConfig apiParamConfig = (ApiParamConfig) config;
if (isRetryEnable(apiParamConfig) && isRetryElement(element) && !isInLoop(abstractMsTestElement)) {
return addRetryWhileController(tree, abstractMsTestElement.getName(), apiParamConfig.getRetryConfig().getRetryTimes());
}
return tree;
}
public HashTree addRetryWhileController(HashTree tree, String name, int retryTimes) {
String retryId = UUID.randomUUID().toString();
String whileCondition = String.format("""
${__jexl3("${%s}" != "STOPPED")}
""", retryId);
HashTree hashTree = tree.add(getRetryWhileController(whileCondition, name));
// 添加超时处理防止死循环
JSR223Listener postProcessor = new JSR223Listener();
postProcessor.setName("Retry-controller");
postProcessor.setProperty(TestElement.TEST_CLASS, JSR223Listener.class.getName());
postProcessor.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass(JmeterAlias.TEST_BEAN_GUI));
postProcessor.setProperty("scriptLanguage", "groovy");
postProcessor.setProperty("script", getRetryScript(retryId, retryTimes));
hashTree.add(postProcessor);
return hashTree;
}
private WhileController getRetryWhileController(String condition, String name) {
if (StringUtils.isEmpty(condition)) {
return null;
}
WhileController controller = new WhileController();
controller.setEnabled(true);
controller.setName(StringUtils.join("RetryWhile_", name));
controller.setProperty(TestElement.TEST_CLASS, WhileController.class.getName());
controller.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("WhileControllerGui"));
controller.setCondition(condition);
return controller;
}
private String getRetryScript(String retryId, int retryTimes) {
return String.format(template,
retryId,
retryTimes
);
}
private boolean isRetryEnable(ApiParamConfig apiParamConfig) {
return BooleanUtils.isTrue(apiParamConfig.getRetryOnFail()) && apiParamConfig.getRetryConfig().getRetryTimes() > 0;
}
/**
* 需要重试的组件
* @param element
* @return
*/
private boolean isRetryElement(MsTestElement element) {
if (element instanceof AbstractMsProtocolTestElement) {
return true;
}
return false;
}
public boolean isInLoop(AbstractMsTestElement msTestElement) {
if (msTestElement != null) {
if (msTestElement instanceof MsLoopController) {
return true;
}
if (msTestElement.getParent() != null) {
return isInLoop(msTestElement.getParent());
}
}
return false;
}
}

View File

@ -780,6 +780,8 @@ public class ApiTestCaseService extends MoveNodeService {
ApiDefinition apiDefinition = apiDefinitionMapper.selectByPrimaryKey(apiTestCase.getApiDefinitionId());
ApiTestCaseBlob apiTestCaseBlob = apiTestCaseBlobMapper.selectByPrimaryKey(apiTestCase.getId());
ApiParamConfig apiParamConfig = apiExecuteService.getApiParamConfig(taskItem.getReportId(), apiTestCase.getProjectId());
apiParamConfig.setRetryOnFail(request.getRunModeConfig().getRetryOnFail());
apiParamConfig.setRetryConfig(request.getRunModeConfig().getRetryConfig());
AbstractMsTestElement msTestElement = ApiDataUtils.parseObject(new String(apiTestCaseBlob.getRequest()), AbstractMsTestElement.class);
// 设置 method 等信息

View File

@ -283,6 +283,12 @@ public class ApiScenarioReportService {
} else {
step.setStatus(ExecStatus.PENDING.name());
}
// 重试的话取最后一次的状态
ApiScenarioReportStepDTO lastDetail = details.getLast();
if (lastDetail.getName().contains("MsRetry_")) {
step.setStatus(lastDetail.getStatus());
}
}
step.setChildren(details);
} else if (CollectionUtils.isNotEmpty(details)) {

View File

@ -310,6 +310,8 @@ public class ApiScenarioRunService {
ApiScenarioParamConfig parseConfig = getApiScenarioParamConfig(apiScenarioDetail.getProjectId(), parseParam, tmpParam.getScenarioParseEnvInfo());
parseConfig.setReportId(reportId);
parseConfig.setRetryOnFail(request.getRunModeConfig().getRetryOnFail());
parseConfig.setRetryConfig(request.getRunModeConfig().getRetryConfig());
String script = apiExecuteService.parseExecuteScript(runRequest.getTestElement(), parseConfig);

View File

@ -6,6 +6,7 @@ import io.metersphere.api.parser.jmeter.controller.MsConstantTimerControllerConv
import io.metersphere.api.parser.jmeter.controller.MsIfControllerConverter;
import io.metersphere.api.parser.jmeter.controller.MsLoopControllerConverter;
import io.metersphere.api.parser.jmeter.controller.MsOnceOnlyControllerConverter;
import io.metersphere.api.parser.jmeter.interceptor.RetryInterceptor;
import io.metersphere.plugin.api.spi.AbstractJmeterElementConverter;
import io.metersphere.plugin.api.spi.MsTestElement;
import io.metersphere.plugin.sdk.util.PluginLogUtils;
@ -42,6 +43,9 @@ public class JmeterElementConverterRegister {
register(MsLoopControllerConverter.class);
register(MsOnceOnlyControllerConverter.class);
register(MsConstantTimerControllerConverter.class);
// 注册转换器拦截器
AbstractJmeterElementConverter.registerConvertInterceptor(new RetryInterceptor());
}
/**

View File

@ -45,10 +45,6 @@ public class TestPlanApiCaseController {
@Resource
private TestPlanApiCaseBatchRunService testPlanApiCaseBatchRunService;
@Resource
private TestPlanManagementService testPlanManagementService;
@Resource
private TestPlanService testPlanService;
@Resource
private ApiReportService apiReportService;
@PostMapping(value = "/sort")

View File

@ -12,7 +12,6 @@ import io.metersphere.plan.dto.response.TestPlanOperationResponse;
import io.metersphere.plan.service.TestPlanApiScenarioBatchRunService;
import io.metersphere.plan.service.TestPlanApiScenarioLogService;
import io.metersphere.plan.service.TestPlanApiScenarioService;
import io.metersphere.plan.service.TestPlanService;
import io.metersphere.sdk.constants.HttpMethodConstants;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.dto.api.task.TaskRequestDTO;
@ -46,8 +45,6 @@ public class TestPlanApiScenarioController {
@Resource
private TestPlanApiScenarioBatchRunService testPlanApiScenarioBatchRunService;
@Resource
private TestPlanService testPlanService;
@Resource
private ApiScenarioReportService apiScenarioReportService;
@PostMapping("/page")