diff --git a/backend/src/main/java/io/metersphere/api/dto/EnvironmentDTO.java b/backend/src/main/java/io/metersphere/api/dto/EnvironmentDTO.java new file mode 100644 index 0000000000..8bff389e5d --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/EnvironmentDTO.java @@ -0,0 +1,10 @@ +package io.metersphere.api.dto; + +import io.metersphere.api.dto.scenario.DatabaseConfig; +import lombok.Data; + +@Data +public class EnvironmentDTO { + private String environmentId; + private DatabaseConfig databaseConfig; +} 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/jmeter/APIBackendListenerClient.java b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java index 79bb79e42d..feb3e61052 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java @@ -325,7 +325,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl requestResult.addPassAssertions(); } //xpath 提取错误会添加断言错误 - if (!responseAssertionResult.getMessage().contains("The required item type of the first operand of")) { + if (StringUtils.isBlank(responseAssertionResult.getMessage()) || !responseAssertionResult.getMessage().contains("The required item type of the first operand of")) { responseResult.getAssertions().add(responseAssertionResult); } } diff --git a/backend/src/main/java/io/metersphere/api/parse/Swagger3Parser.java b/backend/src/main/java/io/metersphere/api/parse/Swagger3Parser.java index ac6ae883fa..0498ec4ec2 100644 --- a/backend/src/main/java/io/metersphere/api/parse/Swagger3Parser.java +++ b/backend/src/main/java/io/metersphere/api/parse/Swagger3Parser.java @@ -333,6 +333,9 @@ public class Swagger3Parser extends SwaggerAbstractParser { } private Object parseSchemaProperties(Schema schema, Set refSet, Map infoMap) { + if (schema == null) { + return null; + } Map properties = schema.getProperties(); if (MapUtils.isEmpty(properties)) { return null; 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 4e9358e6c2..cb8aeb52fa 100644 --- a/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java +++ b/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java @@ -1,6 +1,8 @@ package io.metersphere.api.service; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.api.dto.EnvironmentDTO; import io.metersphere.api.dto.SaveHistoricalDataUpgrade; import io.metersphere.api.dto.automation.ScenarioStatus; import io.metersphere.api.dto.definition.request.MsScenario; @@ -17,6 +19,7 @@ import io.metersphere.api.dto.definition.request.sampler.MsTCPSampler; import io.metersphere.api.dto.definition.request.timer.MsConstantTimer; import io.metersphere.api.dto.scenario.Body; import io.metersphere.api.dto.scenario.Scenario; +import io.metersphere.api.dto.scenario.environment.EnvironmentConfig; import io.metersphere.api.dto.scenario.request.*; import io.metersphere.base.domain.*; import io.metersphere.base.mapper.ApiScenarioMapper; @@ -48,6 +51,9 @@ public class HistoricalDataUpgradeService { private ExtApiScenarioMapper extApiScenarioMapper; @Resource SqlSessionFactory sqlSessionFactory; + @Resource + ApiTestEnvironmentService apiTestEnvironmentService; + private Map environmentDTOMap; private int getNextNum(String projectId) { ApiScenario apiScenario = extApiScenarioMapper.getNextNum(projectId); @@ -110,6 +116,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) { @@ -121,6 +131,11 @@ public class HistoricalDataUpgradeService { element = new MsJDBCSampler(); SqlRequest request1 = (SqlRequest) request; BeanUtils.copyBean(element, request1); + EnvironmentDTO dto = environmentDTOMap.get(request1.getDataSource()); + if (dto != null) { + ((MsJDBCSampler) element).setEnvironmentId(dto.getEnvironmentId()); + ((MsJDBCSampler) element).setDataSource(dto.getDatabaseConfig()); + } element.setType("JDBCSampler"); } if (request instanceof TCPRequest) { @@ -250,11 +265,13 @@ public class HistoricalDataUpgradeService { if (!end.exists()) { end.mkdir(); } - for (String temp : filePath) { - //添加满足情况的条件 - if (new File(sourcePathDir + File.separator + temp).isFile()) { - //为文件则进行拷贝 - copyFile(sourcePathDir + File.separator + temp, newPathDir + File.separator + temp); + if (filePath != null) { + for (String temp : filePath) { + //添加满足情况的条件 + if (new File(sourcePathDir + File.separator + temp).isFile()) { + //为文件则进行拷贝 + copyFile(sourcePathDir + File.separator + temp, newPathDir + File.separator + temp); + } } } } @@ -310,6 +327,9 @@ public class HistoricalDataUpgradeService { } public String upgrade(SaveHistoricalDataUpgrade saveHistoricalDataUpgrade) { + // 初始化环境,获取数据源 + getDataSource(saveHistoricalDataUpgrade.getProjectId()); + ApiTestExample example = new ApiTestExample(); example.createCriteria().andIdIn(saveHistoricalDataUpgrade.getTestIds()); List blobs = apiTestMapper.selectByExampleWithBLOBs(example); @@ -332,4 +352,23 @@ public class HistoricalDataUpgradeService { sqlSession.flushStatements(); return null; } + + private void getDataSource(String projectId) { + List environments = apiTestEnvironmentService.list(projectId); + environmentDTOMap = new HashMap<>(); + if (CollectionUtils.isNotEmpty(environments)) { + environments.forEach(environment -> { + EnvironmentConfig envConfig = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class); + if (CollectionUtils.isNotEmpty(envConfig.getDatabaseConfigs())) { + envConfig.getDatabaseConfigs().forEach(item -> { + EnvironmentDTO dto = new EnvironmentDTO(); + dto.setDatabaseConfig(item); + dto.setEnvironmentId(environment.getId()); + environmentDTOMap.put(item.getId(), dto); + }); + } + }); + } + } + } diff --git a/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue b/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue index 0fcd3a79ab..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 2891207808..4629e2c072 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -544,6 +544,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", @@ -574,6 +575,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", @@ -1259,8 +1261,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: { @@ -1414,5 +1416,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 944fe712f7..b91cd2e0c4 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -543,6 +543,7 @@ export default { res_param: "响应内容", batch_delete: "批量删除", delete_confirm: "确认删除接口", + delete_confirm_step: "确认删除步骤", assertions_rule: "断言规则", response_header: "响应头", response_body: "响应体", @@ -573,6 +574,7 @@ export default { external_import: "外部导入", wait_controller: "等待控制器", if_controller: "条件控制器", + loop_controller: "循环控制器", scenario_import: "场景导入", customize_script: "自定义脚本", customize_req: "自定义请求", @@ -1260,8 +1262,8 @@ export default { host: '主机号不能为空', port: '端口号不能为空', account: '账户不能为空', - test_recipients:'测试收件人', - tip:'提示:仅用来作为测试邮件收件人', + test_recipients: '测试收件人', + tip: '提示:仅用来作为测试邮件收件人', }, i18n: { home: '首页', @@ -1414,5 +1416,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 5ba7888a5f..3007e1de51 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -543,6 +543,7 @@ export default { res_param: "響應内容", batch_delete: "批量删除", delete_confirm: "確認刪除接口", + delete_confirm_step: "確認刪除步骤", assertions_rule: "斷言規則", response_header: "響應頭", response_body: "響應體", @@ -573,6 +574,7 @@ export default { external_import: "外部導入", wait_controller: "等待控制器", if_controller: "條件控制器", + loop_controller: "循环控制器", scenario_import: "場景導入", customize_script: "自定義脚本", customize_req: "自定義請求", @@ -1259,8 +1261,8 @@ export default { host: '主機號不能為空', port: '端口號不能為空', account: '賬戶不能為空', - test_recipients:'測試收件人', - tip:'提示:僅用來作為測試郵件收件人', + test_recipients: '測試收件人', + tip: '提示:僅用來作為測試郵件收件人', }, i18n: { home: '首頁', @@ -1413,5 +1415,15 @@ export default { nothing: "无", preview: "预览", add_custom: "添加自定义属性" + }, + loop: { + loops_title: "次數循環", + foreach: "ForEach 循環", + while: "While 循環", + loops: "循環次数", + interval: "循環間隔", + proceed: "成功後繼續循環", + timeout: "循環超時時間", } + };