mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-11-30 11:08:38 +08:00
feat(测试计划): 测试规划,失败重试
--story=1015333 --user=陈建星 【测试计划】完成剩余功能 https://www.tapd.cn/55049933/s/1544401
This commit is contained in:
parent
01c7a365e0
commit
9353382732
@ -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 -> {
|
||||
for (AbstractMsTestElement child : element.getChildren()) {
|
||||
child.setParent(element);
|
||||
getConverterFunc.apply(child.getClass()).toHashTree(tree, child, config);
|
||||
});
|
||||
// 拦截器拦截
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置步骤标识
|
||||
* 当前步骤唯一标识,结果和步骤匹配的关键
|
||||
|
@ -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);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package io.metersphere.plan.enums;
|
||||
package io.metersphere.sdk.constants;
|
||||
|
||||
public enum RetryType {
|
||||
|
@ -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)
|
||||
|
@ -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 实现类对象
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 等信息
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user