diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java index 54f8dc608e..d5e6ee61b7 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/MsTestElement.java @@ -11,6 +11,7 @@ import io.metersphere.api.dto.definition.request.assertions.MsAssertions; import io.metersphere.api.dto.definition.request.auth.MsAuthManager; import io.metersphere.api.dto.definition.request.configurations.MsHeaderManager; import io.metersphere.api.dto.definition.request.controller.MsIfController; +import io.metersphere.api.dto.definition.request.controller.MsLoopController; import io.metersphere.api.dto.definition.request.extract.MsExtract; import io.metersphere.api.dto.definition.request.processors.MsJSR223Processor; import io.metersphere.api.dto.definition.request.processors.post.MsJSR223PostProcessor; @@ -56,11 +57,12 @@ import java.util.List; @JsonSubTypes.Type(value = MsConstantTimer.class, name = "ConstantTimer"), @JsonSubTypes.Type(value = MsIfController.class, name = "IfController"), @JsonSubTypes.Type(value = MsScenario.class, name = "scenario"), + @JsonSubTypes.Type(value = MsLoopController.class, name = "LoopController"), }) @JSONType(seeAlso = {MsHTTPSamplerProxy.class, MsHeaderManager.class, MsJSR223Processor.class, MsJSR223PostProcessor.class, MsJSR223PreProcessor.class, MsTestPlan.class, MsThreadGroup.class, AuthManager.class, MsAssertions.class, - MsExtract.class, MsTCPSampler.class, MsDubboSampler.class, MsJDBCSampler.class, MsConstantTimer.class, MsIfController.class, MsScenario.class}, typeKey = "type") + MsExtract.class, MsTCPSampler.class, MsDubboSampler.class, MsJDBCSampler.class, MsConstantTimer.class, MsIfController.class, MsScenario.class, MsLoopController.class}, typeKey = "type") @Data public abstract class MsTestElement { private String type; diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/MsLoopController.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/MsLoopController.java new file mode 100644 index 0000000000..7f5390287a --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/MsLoopController.java @@ -0,0 +1,126 @@ +package io.metersphere.api.dto.definition.request.controller; + +import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson.annotation.JSONType; +import io.metersphere.api.dto.definition.request.MsTestElement; +import io.metersphere.api.dto.definition.request.ParameterConfig; +import io.metersphere.api.dto.definition.request.controller.loop.CountController; +import io.metersphere.api.dto.definition.request.controller.loop.MsForEachController; +import io.metersphere.api.dto.definition.request.controller.loop.MsWhileController; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.control.ForeachController; +import org.apache.jmeter.control.GenericController; +import org.apache.jmeter.control.LoopController; +import org.apache.jmeter.control.WhileController; +import org.apache.jmeter.save.SaveService; +import org.apache.jmeter.testelement.TestElement; +import org.apache.jorphan.collections.HashTree; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@JSONType(typeName = "LoopController") +public class MsLoopController extends MsTestElement { + private String type = "LoopController"; + @JSONField(ordinal = 20) + private String loopType; + + @JSONField(ordinal = 21) + private CountController countController; + + @JSONField(ordinal = 22) + private MsForEachController forEachController; + + @JSONField(ordinal = 23) + private MsWhileController whileController; + + public void toHashTree(HashTree tree, List hashTree, ParameterConfig config) { + if (!this.isEnable()) { + return; + } + GenericController controller = controller(); + if (controller == null) + return; + + final HashTree groupTree = tree.add(controller); + if (CollectionUtils.isNotEmpty(hashTree)) { + hashTree.forEach(el -> { + el.toHashTree(groupTree, el.getHashTree(), config); + }); + } + } + + private LoopController loopController() { + LoopController loopController = new LoopController(); + loopController.setEnabled(true); + loopController.setName(this.getLabel()); + loopController.setProperty(TestElement.TEST_CLASS, LoopController.class.getName()); + loopController.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("LoopControlPanel")); + loopController.setContinueForever(countController.isProceed()); + loopController.setLoops(countController.getLoops()); + return loopController; + } + + public String getCondition() { + String variable = "\"" + this.whileController.getVariable() + "\""; + String operator = this.whileController.getOperator(); + String value = "\"" + this.whileController.getValue() + "\""; + + if (StringUtils.contains(operator, "~")) { + value = "\".*" + this.whileController.getValue() + ".*\""; + } + + if (StringUtils.equals(operator, "is empty")) { + variable = "empty(" + variable + ")"; + operator = ""; + value = ""; + } + + if (StringUtils.equals(operator, "is not empty")) { + variable = "!empty(" + variable + ")"; + operator = ""; + value = ""; + } + return "${__jexl3(" + variable + operator + value + ")}"; + } + + private WhileController whileController() { + WhileController controller = new WhileController(); + controller.setEnabled(true); + controller.setName(this.getLabel()); + controller.setProperty(TestElement.TEST_CLASS, WhileController.class.getName()); + controller.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("WhileControllerGui")); + controller.setCondition(getCondition()); + return controller; + } + + private ForeachController foreachController() { + ForeachController controller = new ForeachController(); + controller.setEnabled(true); + controller.setName(this.getLabel()); + controller.setProperty(TestElement.TEST_CLASS, ForeachController.class.getName()); + controller.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ForeachControlPanel")); + controller.setInputVal(this.forEachController.getInputVal()); + controller.setReturnVal(this.forEachController.getReturnVal()); + controller.setUseSeparator(true); + return controller; + } + + private GenericController controller() { + if (StringUtils.equals(this.loopType, "WHILE") && this.whileController != null) { + return whileController(); + } + if (StringUtils.equals(this.loopType, "FOREACH") && this.forEachController != null) { + return foreachController(); + } + if (StringUtils.equals(this.loopType, "LOOP_COUNT") && this.countController != null) { + return loopController(); + } + return null; + } + +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/CountController.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/CountController.java new file mode 100644 index 0000000000..808b67cc9e --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/CountController.java @@ -0,0 +1,10 @@ +package io.metersphere.api.dto.definition.request.controller.loop; + +import lombok.Data; + +@Data +public class CountController { + private int loops; + private int interval; + private boolean proceed; +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/MsForEachController.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/MsForEachController.java new file mode 100644 index 0000000000..30a4a59e7f --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/MsForEachController.java @@ -0,0 +1,10 @@ +package io.metersphere.api.dto.definition.request.controller.loop; + +import lombok.Data; + +@Data +public class MsForEachController { + private String inputVal; + private String returnVal; + private String interval; +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/MsWhileController.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/MsWhileController.java new file mode 100644 index 0000000000..7781abbb0b --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/controller/loop/MsWhileController.java @@ -0,0 +1,11 @@ +package io.metersphere.api.dto.definition.request.controller.loop; + +import lombok.Data; + +@Data +public class MsWhileController { + private String variable; + private String operator; + private String value; + private int timeout; +} diff --git a/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java b/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java index c21cecceaf..8220ca72e5 100644 --- a/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java +++ b/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java @@ -110,6 +110,10 @@ public class HistoricalDataUpgradeService { BeanUtils.copyBean(element, request1); ((MsHTTPSamplerProxy) element).setProtocol(RequestType.HTTP); ((MsHTTPSamplerProxy) element).setArguments(request1.getParameters()); + if (StringUtils.isEmpty(element.getName())) { + element.setName(request1.getPath()); + } + element.setType("HTTPSamplerProxy"); } if (request instanceof DubboRequest) { diff --git a/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue b/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue index a68e317c9a..ebe3dc5fb4 100644 --- a/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue +++ b/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue @@ -152,6 +152,8 @@ + + @@ -168,7 +170,8 @@ - + @@ -227,7 +230,7 @@ + + diff --git a/frontend/src/business/components/api/automation/scenario/Setting.js b/frontend/src/business/components/api/automation/scenario/Setting.js index 46fbef2fb8..a25c6d9f72 100644 --- a/frontend/src/business/components/api/automation/scenario/Setting.js +++ b/frontend/src/business/components/api/automation/scenario/Setting.js @@ -1,5 +1,5 @@ export const ELEMENTS = new Map([ - ['ALL', ["scenario", "HTTPSamplerProxy", "DubboSampler", "JDBCSampler", "TCPSampler", "OT_IMPORT", "IfController", "ConstantTimer", "JSR223Processor", "CustomizeReq"]], + ['ALL', ["scenario", "HTTPSamplerProxy", "DubboSampler", "JDBCSampler", "TCPSampler", "OT_IMPORT", "IfController", "LoopController", "ConstantTimer", "JSR223Processor", "CustomizeReq"]], ['scenario', ["HTTPSamplerProxy", "DubboSampler", "JDBCSampler", "TCPSampler", "CASE", "OT_IMPORT", "IfController", "ConstantTimer", "JSR223Processor", "CustomizeReq"]], ['HTTPSamplerProxy', ["ConstantTimer", "JSR223PreProcessor", "JSR223PostProcessor", "Assertions", "Extract"]], ['DubboSampler', ["ConstantTimer", "JSR223PreProcessor", "JSR223PostProcessor", "Assertions", "Extract"]], @@ -7,6 +7,7 @@ export const ELEMENTS = new Map([ ['TCPSampler', ["ConstantTimer", "JSR223PreProcessor", "JSR223PostProcessor", "Assertions", "Extract"]], ['OT_IMPORT', ["ConstantTimer", "JSR223PreProcessor", "JSR223PostProcessor", "Assertions", "Extract"]], ['IfController', ["IfController", "HTTPSamplerProxy", "DubboSampler", "JDBCSampler", "TCPSampler", "OT_IMPORT", "ConstantTimer", "JSR223Processor", "JSR223PreProcessor", "JSR223PostProcessor", "Assertions", "Extract", "CustomizeReq"]], + ['LoopController', ["IfController", "HTTPSamplerProxy", "DubboSampler", "JDBCSampler", "TCPSampler", "OT_IMPORT", "ConstantTimer", "JSR223Processor", "JSR223PreProcessor", "JSR223PostProcessor", "Assertions", "Extract", "CustomizeReq"]], ['ConstantTimer', []], ['JSR223Processor', ["ConstantTimer", "JSR223PreProcessor", "JSR223PostProcessor", "Assertions", "Extract"]], ['JSR223PreProcessor', []], @@ -27,6 +28,7 @@ export const ELEMENT_TYPE = { JSR223PostProcessor: "JSR223PostProcessor", Assertions: "Assertions", Extract: "Extract", - CustomizeReq: "CustomizeReq" + CustomizeReq: "CustomizeReq", + LoopController: "LoopController" } diff --git a/frontend/src/business/components/api/definition/model/ApiTestModel.js b/frontend/src/business/components/api/definition/model/ApiTestModel.js index 1be3b0c169..f6ef40f9f2 100644 --- a/frontend/src/business/components/api/definition/model/ApiTestModel.js +++ b/frontend/src/business/components/api/definition/model/ApiTestModel.js @@ -1016,6 +1016,38 @@ export class IfController extends Controller { } } +export class LoopController extends Controller { + constructor(options = {}) { + super("LoopController", options); + this.type = "LoopController"; + this.active = false; + this.loopType = "LOOP_COUNT"; + this.countController = {loops: 0, interval: 0, proceed: false}; + this.forEachController = {inputVal: "", returnVal: "", interval: 0}; + this.whileController = {variable: "", operator: "", value: "", timeout: 0}; + this.hashTree = []; + this.set(options); + } + + isValid() { + if (!!this.operator && this.operator.indexOf("empty") > 0) { + return !!this.variable && !!this.operator; + } + return !!this.variable && !!this.operator && !!this.value; + } + + label() { + if (this.isValid()) { + let label = this.variable; + if (this.operator) label += " " + this.operator; + if (this.value) label += " " + this.value; + return label; + } + return ""; + } +} + + export class Timer extends BaseConfig { static TYPES = { CONSTANT_TIMER: "Constant Timer", diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index 61611f4450..d53f7e78ab 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -540,6 +540,7 @@ export default { res_param: "Response content", batch_delete: "Batch deletion", delete_confirm: "Confirm deletion", + delete_confirm_step: "Confirm deletion step", assertions_rule: "Assertion rule", response_header: "Response header", response_body: "Response body", @@ -570,6 +571,7 @@ export default { external_import: "External import", wait_controller: "Wait controller", if_controller: "If controller", + loop_controller: "Loop Controller", scenario_import: "Scenario import", customize_script: "Customize script", customize_req: "Customize req", @@ -1255,8 +1257,8 @@ export default { host: 'Host number cannot be empty', port: 'Port cannot be empty', account: 'Account cannot be empty', - test_recipients:'Test recipients', - tip:'Tip: use as test mail recipient only', + test_recipients: 'Test recipients', + tip: 'Tip: use as test mail recipient only', }, i18n: { @@ -1410,5 +1412,15 @@ export default { nothing: "Nothing", preview: "Preview", add_custom: "Add Custom Prop" + }, + loop: { + loops_title: "loops", + foreach: "ForEach", + while: "While", + loops: "loops", + interval: "interval", + proceed: "proceed", + timeout: "timeout", } + }; diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 4e935f35b5..3ae529230a 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -539,6 +539,7 @@ export default { res_param: "响应内容", batch_delete: "批量删除", delete_confirm: "确认删除接口", + delete_confirm_step: "确认删除步骤", assertions_rule: "断言规则", response_header: "响应头", response_body: "响应体", @@ -569,6 +570,7 @@ export default { external_import: "外部导入", wait_controller: "等待控制器", if_controller: "条件控制器", + loop_controller: "循环控制器", scenario_import: "场景导入", customize_script: "自定义脚本", customize_req: "自定义请求", @@ -1256,8 +1258,8 @@ export default { host: '主机号不能为空', port: '端口号不能为空', account: '账户不能为空', - test_recipients:'测试收件人', - tip:'提示:仅用来作为测试邮件收件人', + test_recipients: '测试收件人', + tip: '提示:仅用来作为测试邮件收件人', }, i18n: { home: '首页', @@ -1410,5 +1412,14 @@ export default { nothing: "无", preview: "预览", add_custom: "添加自定义属性" + }, + loop: { + loops_title: "次数循环", + foreach: "ForEach 循环", + while: "While 循环", + loops: "循环次数", + interval: "循环间隔", + proceed: "成功后继续循环", + timeout: "循环超时时间", } }; diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index 11fddfe1e5..6e5e37511b 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -539,6 +539,7 @@ export default { res_param: "響應内容", batch_delete: "批量删除", delete_confirm: "確認刪除接口", + delete_confirm_step: "確認刪除步骤", assertions_rule: "斷言規則", response_header: "響應頭", response_body: "響應體", @@ -569,6 +570,7 @@ export default { external_import: "外部導入", wait_controller: "等待控制器", if_controller: "條件控制器", + loop_controller: "循环控制器", scenario_import: "場景導入", customize_script: "自定義脚本", customize_req: "自定義請求", @@ -1255,8 +1257,8 @@ export default { host: '主機號不能為空', port: '端口號不能為空', account: '賬戶不能為空', - test_recipients:'測試收件人', - tip:'提示:僅用來作為測試郵件收件人', + test_recipients: '測試收件人', + tip: '提示:僅用來作為測試郵件收件人', }, i18n: { home: '首頁', @@ -1409,5 +1411,15 @@ export default { nothing: "无", preview: "预览", add_custom: "添加自定义属性" + }, + loop: { + loops_title: "次數循環", + foreach: "ForEach 循環", + while: "While 循環", + loops: "循環次数", + interval: "循環間隔", + proceed: "成功後繼續循環", + timeout: "循環超時時間", } + };