diff --git a/README.md b/README.md index 9048e41765..23dccbda7d 100755 --- a/README.md +++ b/README.md @@ -80,199 +80,225 @@ v1.1.0 是 v1.0.0 之后的功能版本。 ## 功能列表 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
测试跟踪项目管理多项目支持,测试用例、测试计划与项目关联
测试用例管理在线编辑用例
以树状形式展示项目的模块及其用例
自定义用例属性
快速导入用例到系统
测试用例评审基于已有用例发起评审
在线更新评审结果
支持多人在线添加评审评论
灵活的评审人分配形式
测试计划跟踪基于已有用例发起测试计划
在线更新用例执行结果
灵活的用例分配方式
在线生成测试报告,支持自定义测试报告模板
与平台中的接口测试、性能测试功能结合,自动更新关联用例的结果
记录测试用例关联的缺陷
缺陷记录支持关联到 Jira/TAPD 平台
测试报告支持分享、导出
接口测试测试脚本在线编辑接口测试内容
支持参数化测试
灵活多样的断言支持
支持多接口的场景化测试
测试场景复用
测试场景支持引用已有环境信息
测试环境信息管理
通过浏览器插件快速录制测试脚本
支持前后置 BeanShell/Python 脚本
上传并引用自定义 Jar 包
多协议支持,支持 HTTP、Dubbo、SQL、TCP 类型请求
支持等待时间、条件判断等逻辑控制功能
测试执行内置定时任务支持
通过 Jenkins 插件触发测试执行
多个接口测试一键合并执行
一键创建性能测试
测试报告测试执行后自动生成动态实时测试报告
测试报告导出
通过邮件、IM 工具等通知执行结果
性能测试测试脚本完全兼容 JMeter 脚本
在线调整压力参数
分布式压力测试
支持参数化测试
通过浏览器插件快速录制测试脚本
多协议支持
测试执行内置定时任务支持
通过 Jenkins 插件触发测试执行
测试报告测试执行后自动生成测试报告
丰富的测试报告展现形式
测试报告导出
查看测试日志详情
系统管理用户租户管理支持多级租户体系
支持多种租户角色
LDAP 认证对接
测试资源管理性能测试资源池管理
消息通知配置IM 工具通知(如企业微信、钉钉)
邮件通知配置
集成与扩展完善的 API 列表
支持对接 Jenkins 等持续集成工具
支持对接 Jira/TAPD 等缺陷管理工具
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
测试跟踪测试用例管理在线编辑用例
编辑窗口支持上传附件
查看与编辑窗口显示评审评论
以树状形式展示项目的模块及其用例
支持测试用例模块树拖拽排序
自定义用例等级/用例类型/测试方式
支持Excel/Xmind格式快速导入用例到系统
支持Excel格式快速导出用例到本地
测试用例评审基于已有用例发起评审
支持添加多个评审人
在线更新评审结果
支持多人在线添加评审评论
测试计划跟踪基于已有用例发起测试计划
支持在线更新用例执行结果
在线生成测试报告,支持自定义测试报告模板
与平台中的接口测试、性能测试功能联动,自动更新关联用例的结果
记录测试用例关联的缺陷
缺陷记录支持关联到 Jira/TAPD
支持PDF格式测试报告导出
接口测试接口定义在线编辑接口测试内容
支持 HTTP/Dubbo/SQL/TCP 类型接口请求
支持接口快捷调制
支持接口列表和用例列表切换显示
支持用例编辑窗口正则/jsonpath/Xpath等多种类型的断言规则
支持用例编辑窗口正则/jsonpath/Xpath类型的参数提取
支持用例编辑窗口前后置 BeanShell/Python 脚本
测试环境信息管理
支持单接口测试引用环境信息
支持通过浏览器插件快速录制测试脚本
支持Metersphere json/Postman/Swagger格式快速导入用例到系统
支持Metersphere json格式快速导出用例到本地
支持上传并引用自定义 Jar 包
接口自动化创建多接口的场景化测试
支持自定义场景标签
支持多层级场景嵌套结构
支持接口列表快速导入测试场景
支持测试场景复用
支持添加自定义请求/自定义脚本
支持添加等待时间/条件判断等多类型逻辑控制器
场景调试支持引用已有环境信息
支持定时任务
支持通过 Jenkins 插件触发测试执行
Jenkins 插件支持 Pipeline 方式调用
支持一键创建性能测试
测试报告测试执行后自动生成测试报告
支持PDF格式测试报告导出
性能测试性能测试脚本支持上传JMX/CSV/JAR格式文件创建性能测试
支持分线程组配置压力参数
支持下载 JTL 文件
支持通过浏览器插件快速录制测试脚本
测试执行内置定时任务支持
支持通过 Jenkins 插件触发测试执行
测试报告测试执行后自动生成动态实时测试报告
支持PDF格式测试报告导出
系统管理用户租户管理支持多级租户体系
支持多种租户角色
支持LDAP 认证对接
测试资源管理性能测试资源池管理
消息通知配置支持企业微信/钉钉等多种IM 工具通知配置
支持邮件通知配置
集成与扩展配置API 列表
支持对接 Jenkins 等持续集成工具
支持对接 Jira/TAPD 等缺陷管理工具
项目管理多项目支持,测试用例、测试计划与项目关联
+ 详细的版本规划请参考 [版本路线图](https://github.com/metersphere/metersphere/blob/master/ROADMAP.md) diff --git a/backend/src/main/java/io/metersphere/api/controller/APITestController.java b/backend/src/main/java/io/metersphere/api/controller/APITestController.java index 78bd6367af..e50b94d837 100644 --- a/backend/src/main/java/io/metersphere/api/controller/APITestController.java +++ b/backend/src/main/java/io/metersphere/api/controller/APITestController.java @@ -14,7 +14,6 @@ import io.metersphere.api.service.*; import io.metersphere.base.domain.ApiTest; import io.metersphere.base.domain.Schedule; import io.metersphere.commons.constants.RoleConstants; -import io.metersphere.commons.constants.ScheduleGroup; import io.metersphere.commons.utils.CronUtils; import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.Pager; @@ -59,7 +58,7 @@ public class APITestController { @Resource private ScheduleService scheduleService; @Resource - private APIReportService apiReportService; + private HistoricalDataUpgradeService historicalDataUpgradeService; @GetMapping("recent/{count}") public List recentTest(@PathVariable int count) { @@ -359,4 +358,8 @@ public class APITestController { schedule.setEnable(request.isEnable()); apiAutomationService.updateSchedule(schedule); } + @PostMapping(value = "/historicalDataUpgrade") + public String historicalDataUpgrade(@RequestBody SaveHistoricalDataUpgrade request) { + return historicalDataUpgradeService.upgrade(request); + } } diff --git a/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java b/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java index 2f8fbdbc06..0fee8b53b1 100644 --- a/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java +++ b/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java @@ -121,5 +121,6 @@ public class ApiAutomationController { public void createSchedule(@RequestBody Schedule request) { apiAutomationService.createSchedule(request); } + } diff --git a/backend/src/main/java/io/metersphere/api/dto/SaveHistoricalDataUpgrade.java b/backend/src/main/java/io/metersphere/api/dto/SaveHistoricalDataUpgrade.java new file mode 100644 index 0000000000..61e084790b --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/SaveHistoricalDataUpgrade.java @@ -0,0 +1,18 @@ +package io.metersphere.api.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +public class SaveHistoricalDataUpgrade { + private List testIds; + + private String projectId; + + private String modulePath; + + private String moduleId; +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsDubboSampler.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsDubboSampler.java index 6d97c94cc3..432d3e73e8 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsDubboSampler.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsDubboSampler.java @@ -31,7 +31,7 @@ public class MsDubboSampler extends MsTestElement { private String type = "DubboSampler"; @JSONField(ordinal = 52) - private String protocol; + private String protocol = "DUBBO"; @JsonProperty(value = "interface") @JSONField(ordinal = 53, name = "interface") private String _interface; diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java index 4770e79c04..dbfa8b8173 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java @@ -172,7 +172,7 @@ public class MsHTTPSamplerProxy extends MsTestElement { LogUtil.error(e); } // REST参数 - if (CollectionUtils.isNotEmpty(this.getArguments())) { + if (CollectionUtils.isNotEmpty(this.getRest())) { sampler.setArguments(httpArguments(this.getRest())); } // 请求参数 @@ -186,7 +186,9 @@ public class MsHTTPSamplerProxy extends MsTestElement { if (StringUtils.isNotEmpty(this.body.getType()) && this.body.getType().equals("Form Data")) { sampler.setDoMultipart(true); } - sampler.setArguments(httpArguments(bodyParams)); + if (CollectionUtils.isNotEmpty(bodyParams)) { + sampler.setArguments(httpArguments(bodyParams)); + } } } diff --git a/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java b/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java index 4065544e7b..bfc0729c12 100644 --- a/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java +++ b/backend/src/main/java/io/metersphere/api/dto/scenario/Body.java @@ -42,7 +42,11 @@ public class Body { return true; } else return false; } - + public boolean isOldKV() { + if (StringUtils.equals(type, KV)) { + return true; + } else return false; + } public List getBodyParams(HTTPSamplerProxy sampler, String requestId) { List body = new ArrayList<>(); if (this.isKV() || this.isBinary()) { diff --git a/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java b/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java new file mode 100644 index 0000000000..4e9358e6c2 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/service/HistoricalDataUpgradeService.java @@ -0,0 +1,335 @@ +package io.metersphere.api.service; + +import com.alibaba.fastjson.JSON; +import io.metersphere.api.dto.SaveHistoricalDataUpgrade; +import io.metersphere.api.dto.automation.ScenarioStatus; +import io.metersphere.api.dto.definition.request.MsScenario; +import io.metersphere.api.dto.definition.request.MsTestElement; +import io.metersphere.api.dto.definition.request.assertions.MsAssertions; +import io.metersphere.api.dto.definition.request.controller.MsIfController; +import io.metersphere.api.dto.definition.request.extract.MsExtract; +import io.metersphere.api.dto.definition.request.processors.post.MsJSR223PostProcessor; +import io.metersphere.api.dto.definition.request.processors.pre.MsJSR223PreProcessor; +import io.metersphere.api.dto.definition.request.sampler.MsDubboSampler; +import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy; +import io.metersphere.api.dto.definition.request.sampler.MsJDBCSampler; +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.request.*; +import io.metersphere.base.domain.*; +import io.metersphere.base.mapper.ApiScenarioMapper; +import io.metersphere.base.mapper.ApiTestMapper; +import io.metersphere.base.mapper.ext.ExtApiScenarioMapper; +import io.metersphere.commons.utils.BeanUtils; +import io.metersphere.commons.utils.DateUtils; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.commons.utils.SessionUtils; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.*; +import java.net.URL; +import java.util.*; + +@Service +@Transactional(rollbackFor = Exception.class) +public class HistoricalDataUpgradeService { + @Resource + private ApiTestMapper apiTestMapper; + @Resource + private ExtApiScenarioMapper extApiScenarioMapper; + @Resource + SqlSessionFactory sqlSessionFactory; + + private int getNextNum(String projectId) { + ApiScenario apiScenario = extApiScenarioMapper.getNextNum(projectId); + if (apiScenario == null) { + return 100001; + } else { + return Optional.of(apiScenario.getNum() + 1).orElse(100001); + } + } + + private MsScenario createScenario(Scenario oldScenario) { + MsScenario scenario = new MsScenario(); + scenario.setVariables(oldScenario.getVariables()); + scenario.setName(oldScenario.getName()); + scenario.setEnableCookieShare(oldScenario.isEnableCookieShare()); + scenario.setEnvironmentId(oldScenario.getEnvironmentId()); + scenario.setReferenced("Upgrade"); + scenario.setId(oldScenario.getId()); + scenario.setResourceId(UUID.randomUUID().toString()); + LinkedList testElements = new LinkedList<>(); + int index = 1; + for (Request request : oldScenario.getRequests()) { + // 条件控制器 + MsIfController ifController = null; + if (request.getController() != null && StringUtils.isNotEmpty(request.getController().getValue()) + && StringUtils.isNotEmpty(request.getController().getVariable())) { + ifController = new MsIfController(); + BeanUtils.copyBean(ifController, request.getController()); + ifController.setType("IfController"); + ifController.setName("IfController"); + ifController.setIndex(index + ""); + ifController.setResourceId(UUID.randomUUID().toString()); + } + // 等待控制器 + if (request.getTimer() != null && StringUtils.isNotEmpty(request.getTimer().getDelay())) { + MsConstantTimer constantTimer = new MsConstantTimer(); + BeanUtils.copyBean(constantTimer, request.getTimer()); + constantTimer.setType("ConstantTimer"); + constantTimer.setIndex(index + ""); + constantTimer.setResourceId(UUID.randomUUID().toString()); + testElements.add(constantTimer); + } + + MsTestElement element = null; + if (request instanceof HttpRequest) { + element = new MsHTTPSamplerProxy(); + HttpRequest request1 = (HttpRequest) request; + if (StringUtils.isEmpty(request1.getPath()) && StringUtils.isNotEmpty(request1.getUrl())) { + try { + URL urlObject = new URL(request1.getUrl()); + String envPath = StringUtils.equals(urlObject.getPath(), "/") ? "" : urlObject.getPath(); + request1.setPath(envPath); + } catch (Exception ex) { + LogUtil.error(ex.getMessage()); + } + } + if (request1.getBody() != null && request1.getBody().isOldKV()) { + request1.getBody().setType(Body.FORM_DATA); + } + BeanUtils.copyBean(element, request1); + ((MsHTTPSamplerProxy) element).setProtocol(RequestType.HTTP); + ((MsHTTPSamplerProxy) element).setArguments(request1.getParameters()); + element.setType("HTTPSamplerProxy"); + } + if (request instanceof DubboRequest) { + String requestJson = JSON.toJSONString(request); + element = JSON.parseObject(requestJson, MsDubboSampler.class); + element.setType("DubboSampler"); + } + if (request instanceof SqlRequest) { + element = new MsJDBCSampler(); + SqlRequest request1 = (SqlRequest) request; + BeanUtils.copyBean(element, request1); + element.setType("JDBCSampler"); + } + if (request instanceof TCPRequest) { + element = new MsTCPSampler(); + TCPRequest request1 = (TCPRequest) request; + BeanUtils.copyBean(element, request1); + element.setType("TCPSampler"); + } + element.setIndex(index + ""); + element.setResourceId(UUID.randomUUID().toString()); + LinkedList msTestElements = new LinkedList<>(); + // 断言规则 + if (request.getAssertions() != null && ((request.getAssertions().getDuration() != null && request.getAssertions().getDuration().getValue() > 0) || + CollectionUtils.isNotEmpty(request.getAssertions().getJsonPath()) || CollectionUtils.isNotEmpty(request.getAssertions().getJsr223()) || + CollectionUtils.isNotEmpty(request.getAssertions().getRegex()) || CollectionUtils.isNotEmpty(request.getAssertions().getXpath2()))) { + String assertions = JSON.toJSONString(request.getAssertions()); + MsAssertions msAssertions = JSON.parseObject(assertions, MsAssertions.class); + msAssertions.setType("Assertions"); + msAssertions.setIndex(index + ""); + msAssertions.setResourceId(UUID.randomUUID().toString()); + msTestElements.add(msAssertions); + } + // 提取规则 + if (request.getExtract() != null && (CollectionUtils.isNotEmpty(request.getExtract().getJson()) || + CollectionUtils.isNotEmpty(request.getExtract().getRegex()) || CollectionUtils.isNotEmpty(request.getExtract().getXpath()))) { + String extractJson = JSON.toJSONString(request.getExtract()); + MsExtract extract = JSON.parseObject(extractJson, MsExtract.class); + extract.setType("Extract"); + extract.setIndex(index + ""); + extract.setResourceId(UUID.randomUUID().toString()); + msTestElements.add(extract); + } + // 前置脚本 + if (request.getJsr223PreProcessor() != null && StringUtils.isNotEmpty(request.getJsr223PreProcessor().getScript())) { + String preJson = JSON.toJSONString(request.getJsr223PreProcessor()); + MsJSR223PreProcessor preProcessor = JSON.parseObject(preJson, MsJSR223PreProcessor.class); + preProcessor.setType("JSR223PreProcessor"); + preProcessor.setIndex(index + ""); + preProcessor.setResourceId(UUID.randomUUID().toString()); + msTestElements.add(preProcessor); + } + // 后置脚本 + if (request.getJsr223PostProcessor() != null && StringUtils.isNotEmpty(request.getJsr223PostProcessor().getScript())) { + String preJson = JSON.toJSONString(request.getJsr223PostProcessor()); + MsJSR223PostProcessor preProcessor = JSON.parseObject(preJson, MsJSR223PostProcessor.class); + preProcessor.setType("JSR223PostProcessor"); + preProcessor.setIndex(index + ""); + preProcessor.setResourceId(UUID.randomUUID().toString()); + msTestElements.add(preProcessor); + } + if (CollectionUtils.isNotEmpty(msTestElements)) { + element.setHashTree(msTestElements); + } + if (ifController != null) { + LinkedList elements = new LinkedList<>(); + elements.add(element); + ifController.setHashTree(elements); + testElements.add(ifController); + } else { + testElements.add(element); + } + index++; + } + scenario.setHashTree(testElements); + return scenario; + } + + private ApiScenarioWithBLOBs checkNameExist(Scenario oldScenario, String projectId, ApiScenarioMapper mapper) { + ApiScenarioExample example = new ApiScenarioExample(); + example.createCriteria().andIdEqualTo(oldScenario.getId()); + List list = mapper.selectByExampleWithBLOBs(example); + if (list.size() > 0) { + return list.get(0); + } + return null; + } + + private static final String BODY_FILE_DIR = "/opt/metersphere/data/body"; + + //文件的拷贝 + private static void copyFile(String sourcePath, String newPath) { + File readfile = new File(sourcePath); + File newFile = new File(newPath); + BufferedWriter bufferedWriter = null; + Writer writer = null; + FileOutputStream fileOutputStream = null; + BufferedReader bufferedReader = null; + try { + fileOutputStream = new FileOutputStream(newFile, true); + writer = new OutputStreamWriter(fileOutputStream, "UTF-8"); + bufferedWriter = new BufferedWriter(writer); + + bufferedReader = new BufferedReader(new FileReader(readfile)); + + String line = null; + while ((line = bufferedReader.readLine()) != null) { + bufferedWriter.write(line); + bufferedWriter.newLine(); + bufferedWriter.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (bufferedWriter != null) { + bufferedWriter.close(); + } + if (bufferedReader != null) { + bufferedReader.close(); + } + if (writer != null) { + writer.close(); + } + if (fileOutputStream != null) { + fileOutputStream.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static void copyDir(String sourcePathDir, String newPathDir) { + File start = new File(sourcePathDir); + File end = new File(newPathDir); + String[] filePath = start.list(); + 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); + } + } + } + + private void createBodyFiles(String testId) { + String dir = BODY_FILE_DIR + "/" + testId; + File testDir = new File(dir); + if (testDir.exists()) { + testDir.mkdirs(); + } + copyDir(dir, BODY_FILE_DIR); + } + + private void createApiScenarioWithBLOBs(SaveHistoricalDataUpgrade saveHistoricalDataUpgrade, Scenario oldScenario, String scenarioDefinition, ApiScenarioMapper mapper) { + if (StringUtils.isEmpty(oldScenario.getName())) { + oldScenario.setName("默认名称-" + DateUtils.getTimeStr(System.currentTimeMillis())); + } + ApiScenarioWithBLOBs scenario = checkNameExist(oldScenario, saveHistoricalDataUpgrade.getProjectId(), mapper); + if (scenario != null) { + scenario.setName(oldScenario.getName()); + scenario.setProjectId(saveHistoricalDataUpgrade.getProjectId()); + scenario.setTags(scenario.getTags()); + scenario.setLevel("P0"); + scenario.setModulePath(saveHistoricalDataUpgrade.getModulePath()); + scenario.setApiScenarioModuleId(saveHistoricalDataUpgrade.getModuleId()); + scenario.setPrincipal(Objects.requireNonNull(SessionUtils.getUser()).getId()); + scenario.setStepTotal(oldScenario.getRequests().size()); + scenario.setScenarioDefinition(scenarioDefinition); + scenario.setUpdateTime(System.currentTimeMillis()); + scenario.setStatus(ScenarioStatus.Underway.name()); + scenario.setUserId(SessionUtils.getUserId()); + scenario.setNum(getNextNum(saveHistoricalDataUpgrade.getProjectId())); + mapper.updateByPrimaryKeySelective(scenario); + } else { + scenario = new ApiScenarioWithBLOBs(); + scenario.setId(oldScenario.getId()); + scenario.setName(oldScenario.getName()); + scenario.setProjectId(saveHistoricalDataUpgrade.getProjectId()); + scenario.setTags(scenario.getTags()); + scenario.setLevel("P0"); + scenario.setModulePath(saveHistoricalDataUpgrade.getModulePath()); + scenario.setApiScenarioModuleId(saveHistoricalDataUpgrade.getModuleId()); + scenario.setPrincipal(Objects.requireNonNull(SessionUtils.getUser()).getId()); + scenario.setStepTotal(oldScenario.getRequests().size()); + scenario.setScenarioDefinition(scenarioDefinition); + scenario.setCreateTime(System.currentTimeMillis()); + scenario.setUpdateTime(System.currentTimeMillis()); + scenario.setStatus(ScenarioStatus.Underway.name()); + scenario.setUserId(SessionUtils.getUserId()); + scenario.setNum(getNextNum(saveHistoricalDataUpgrade.getProjectId())); + mapper.insert(scenario); + } + } + + public String upgrade(SaveHistoricalDataUpgrade saveHistoricalDataUpgrade) { + ApiTestExample example = new ApiTestExample(); + example.createCriteria().andIdIn(saveHistoricalDataUpgrade.getTestIds()); + List blobs = apiTestMapper.selectByExampleWithBLOBs(example); + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + ApiScenarioMapper mapper = sqlSession.getMapper(ApiScenarioMapper.class); + for (ApiTest test : blobs) { + // 附件迁移 + createBodyFiles(test.getId()); + + List scenarios = JSON.parseArray(test.getScenarioDefinition(), Scenario.class); + if (CollectionUtils.isNotEmpty(scenarios)) { + // 批量处理 + for (Scenario scenario : scenarios) { + MsScenario scenario1 = createScenario(scenario); + String scenarioDefinition = JSON.toJSONString(scenario1); + createApiScenarioWithBLOBs(saveHistoricalDataUpgrade, scenario, scenarioDefinition, mapper); + } + } + } + sqlSession.flushStatements(); + return null; + } +} diff --git a/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java b/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java index fe1f20966f..988c61955e 100644 --- a/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java +++ b/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java @@ -96,15 +96,27 @@ public class JmeterDocumentParser implements DocumentParser { processCheckoutArguments(ele); processCheckoutResponseAssertion(ele); } else if (nodeNameEquals(ele, CONCURRENCY_THREAD_GROUP)) { - processConcurrencyThreadGroup(ele); + processThreadGroupName(ele); processCheckoutTimer(ele); processCheckoutBackendListener(ele); } else if (nodeNameEquals(ele, VARIABLE_THROUGHPUT_TIMER)) { processVariableThroughputTimer(ele); } else if (nodeNameEquals(ele, THREAD_GROUP)) { - processThreadGroup(ele); - // - processConcurrencyThreadGroup(ele); + Object threadType = context.getProperty("threadType"); + if (threadType instanceof List) { + Object o = ((List) threadType).get(0); + ((List) threadType).remove(0); + if ("DURATION".equals(o)) { + processThreadGroup(ele); + } + if ("ITERATION".equals(o)) { + processIterationThreadGroup(ele); + } + } else { + processThreadGroup(ele); + } + + processThreadGroupName(ele); processCheckoutTimer(ele); processCheckoutBackendListener(ele); } else if (nodeNameEquals(ele, BACKEND_LISTENER)) { @@ -772,22 +784,120 @@ public class JmeterDocumentParser implements DocumentParser { */ removeChildren(threadGroup); // elementProp + Object targetLevels = context.getProperty("TargetLevel"); + String threads = "10"; + if (targetLevels instanceof List) { + Object o = ((List) targetLevels).get(0); + ((List) targetLevels).remove(0); + threads = o.toString(); + } + Object rampUps = context.getProperty("RampUp"); + String rampUp = "1"; + if (rampUps instanceof List) { + Object o = ((List) rampUps).get(0); + ((List) rampUps).remove(0); + rampUp = o.toString(); + } + Object steps = context.getProperty("Steps"); + String step = "2"; + if (steps instanceof List) { + Object o = ((List) steps).get(0); + ((List) steps).remove(0); + step = o.toString(); + } + Object holds = context.getProperty("Hold"); + String hold = "2"; + if (holds instanceof List) { + Object o = ((List) holds).get(0); + ((List) holds).remove(0); + hold = o.toString(); + } Element elementProp = document.createElement("elementProp"); elementProp.setAttribute("name", "ThreadGroup.main_controller"); elementProp.setAttribute("elementType", "com.blazemeter.jmeter.control.VirtualUserController"); threadGroup.appendChild(elementProp); - threadGroup.appendChild(createStringProp(document, "ThreadGroup.on_sample_error", "continue")); - threadGroup.appendChild(createStringProp(document, "TargetLevel", "2")); - threadGroup.appendChild(createStringProp(document, "RampUp", "12")); - threadGroup.appendChild(createStringProp(document, "Steps", "2")); - threadGroup.appendChild(createStringProp(document, "Hold", "1")); + threadGroup.appendChild(createStringProp(document, "TargetLevel", threads)); + threadGroup.appendChild(createStringProp(document, "RampUp", rampUp)); + threadGroup.appendChild(createStringProp(document, "Steps", step)); + threadGroup.appendChild(createStringProp(document, "Hold", hold)); threadGroup.appendChild(createStringProp(document, "LogFilename", "")); // bzm - Concurrency Thread Group "Thread Iterations Limit:" 设置为空 // threadGroup.appendChild(createStringProp(document, "Iterations", "1")); threadGroup.appendChild(createStringProp(document, "Unit", "S")); } + private void processIterationThreadGroup(Element threadGroup) { + // 检查 threadgroup 后面的hashtree是否为空 + Node hashTree = threadGroup.getNextSibling(); + while (!(hashTree instanceof Element)) { + hashTree = hashTree.getNextSibling(); + } + if (!hashTree.hasChildNodes()) { + MSException.throwException(Translator.get("jmx_content_valid")); + } + // 重命名 tagName + Document document = threadGroup.getOwnerDocument(); + removeChildren(threadGroup); + + // 选择按照迭代次数处理线程组 + /* + continue + + false + 1 + + 100 + 5 + true + 10 + + true + */ + // elementProp + Object targetLevels = context.getProperty("TargetLevel"); + String threads = "10"; + if (targetLevels instanceof List) { + Object o = ((List) targetLevels).get(0); + ((List) targetLevels).remove(0); + threads = o.toString(); + } + Object iterateNum = context.getProperty("iterateNum"); + String loops = "1"; + if (iterateNum instanceof List) { + Object o = ((List) iterateNum).get(0); + ((List) iterateNum).remove(0); + loops = o.toString(); + } + Object rampUps = context.getProperty("iterateRampUpTime"); + String rampUp = "10"; + if (rampUps instanceof List) { + Object o = ((List) rampUps).get(0); + ((List) rampUps).remove(0); + rampUp = o.toString(); + } + Element elementProp = document.createElement("elementProp"); + elementProp.setAttribute("name", "ThreadGroup.main_controller"); + elementProp.setAttribute("elementType", "LoopController"); + elementProp.setAttribute("guiclass", "LoopControlPanel"); + elementProp.setAttribute("testclass", "LoopController"); + elementProp.setAttribute("testname", "Loop Controller"); + elementProp.setAttribute("enabled", "true"); + elementProp.appendChild(createBoolProp(document, "LoopController.continue_forever", false)); + elementProp.appendChild(createStringProp(document, "LoopController.loops", loops)); + threadGroup.appendChild(elementProp); + + threadGroup.appendChild(createStringProp(document, "ThreadGroup.on_sample_error", "continue")); + threadGroup.appendChild(createStringProp(document, "ThreadGroup.num_threads", threads)); + threadGroup.appendChild(createStringProp(document, "ThreadGroup.ramp_time", rampUp)); + threadGroup.appendChild(createBoolProp(document, "ThreadGroup.scheduler", false)); // 不指定执行时间 + threadGroup.appendChild(createStringProp(document, "Hold", "1")); + threadGroup.appendChild(createStringProp(document, "ThreadGroup.duration", "10")); + threadGroup.appendChild(createStringProp(document, "ThreadGroup.delay", "")); + threadGroup.appendChild(createBoolProp(document, "ThreadGroup.same_user_on_next_iteration", true)); + } + + private void processCheckoutTimer(Element element) { /* @@ -856,37 +966,14 @@ public class JmeterDocumentParser implements DocumentParser { return unit; } - private void processConcurrencyThreadGroup(Element concurrencyThreadGroup) { - String testname = concurrencyThreadGroup.getAttribute("testname"); - concurrencyThreadGroup.setAttribute("testname", testname + "-" + context.getResourceIndex()); - if (concurrencyThreadGroup.getChildNodes().getLength() > 0) { - final NodeList childNodes = concurrencyThreadGroup.getChildNodes(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node node = childNodes.item(i); - if (node instanceof Element) { - Element ele = (Element) node; - if (invalid(ele)) { - continue; - } - - if (nodeNameEquals(ele, STRING_PROP)) { - parseStringProp(ele); - } - } - } - } + private void processThreadGroupName(Element threadGroup) { + String testname = threadGroup.getAttribute("testname"); + threadGroup.setAttribute("testname", testname + "-" + context.getResourceIndex()); } private void processVariableThroughputTimer(Element variableThroughputTimer) { - Object durations = context.getProperty("duration"); - Integer duration; - if (durations instanceof List) { - Object o = ((List) durations).get(0); - duration = (Integer) o; - ((List) durations).remove(0); - } else { - duration = (Integer) durations; - } + // 设置rps时长 + Integer duration = Integer.MAX_VALUE; Object rpsLimits = context.getProperty("rpsLimit"); String rpsLimit; if (rpsLimits instanceof List) { diff --git a/backend/src/main/java/io/metersphere/xpack b/backend/src/main/java/io/metersphere/xpack index 9f4a9bbf46..068127ce59 160000 --- a/backend/src/main/java/io/metersphere/xpack +++ b/backend/src/main/java/io/metersphere/xpack @@ -1 +1 @@ -Subproject commit 9f4a9bbf46fc1333dbcccea21f83e27e3ec10b1f +Subproject commit 068127ce59ea8b016434ed52a9de4a7a4b13bdb4 diff --git a/frontend/src/business/components/api/automation/scenario/ApiComponent.vue b/frontend/src/business/components/api/automation/scenario/ApiComponent.vue index 918abd1d24..7390d6edd0 100644 --- a/frontend/src/business/components/api/automation/scenario/ApiComponent.vue +++ b/frontend/src/business/components/api/automation/scenario/ApiComponent.vue @@ -58,8 +58,8 @@

{{$t('api_test.definition.request.req_param')}}

- - + +

{{$t('api_test.definition.request.res_param')}}

diff --git a/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue b/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue index b2fdd70c8c..3b6b8849e1 100644 --- a/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue +++ b/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue @@ -55,7 +55,7 @@ import MsApiAssertionRegex from "./ApiAssertionRegex"; import MsApiAssertionDuration from "./ApiAssertionDuration"; import MsApiAssertionJsonPath from "./ApiAssertionJsonPath"; - import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223"; + import MsApiAssertionJsr223 from "./ApiAssertionJsr223"; import MsApiAssertionXPath2 from "./ApiAssertionXPath2"; export default { diff --git a/frontend/src/business/components/api/definition/components/request/database/BasisParameters.vue b/frontend/src/business/components/api/definition/components/request/database/BasisParameters.vue index 3de55bde9a..9402456eee 100644 --- a/frontend/src/business/components/api/definition/components/request/database/BasisParameters.vue +++ b/frontend/src/business/components/api/definition/components/request/database/BasisParameters.vue @@ -63,18 +63,19 @@ -
- - - - - - - - - +
+
+ + + + + + + + +
diff --git a/frontend/src/business/components/api/definition/components/request/dubbo/BasisParameters.vue b/frontend/src/business/components/api/definition/components/request/dubbo/BasisParameters.vue index 9e3057827b..287cf0d22f 100644 --- a/frontend/src/business/components/api/definition/components/request/dubbo/BasisParameters.vue +++ b/frontend/src/business/components/api/definition/components/request/dubbo/BasisParameters.vue @@ -41,19 +41,19 @@
- -
- - - - - - - - - +
+
+ + + + + + + + +
diff --git a/frontend/src/business/components/api/test/ApiTestList.vue b/frontend/src/business/components/api/test/ApiTestList.vue index 35785fddbc..941b67b0aa 100644 --- a/frontend/src/business/components/api/test/ApiTestList.vue +++ b/frontend/src/business/components/api/test/ApiTestList.vue @@ -7,7 +7,7 @@ :title="$t('commons.test')" @create="create" :createTip="$t('load_test.create')" :runTip="$t('load_test.run')" :show-run="true" - @runTest="runTest"/> + @runTest="runTest" @historicalDataUpgrade="historicalDataUpgrade"/> @@ -54,7 +54,8 @@ - + @@ -72,13 +73,14 @@ import {TEST_CONFIGS} from "../../common/components/search/search-components"; import {ApiEvent, LIST_CHANGE} from "@/business/components/common/head/ListEvent"; import ApiCopyDialog from "./components/ApiCopyDialog"; + import MsUpgrade from "./Upgrade"; export default { components: { ApiCopyDialog, OneClickOperation, MsTableOperators, - MsApiTestStatus, MsMainContainer, MsContainer, MsTableHeader, MsTablePagination, MsTableOperator + MsApiTestStatus, MsMainContainer, MsContainer, MsTableHeader, MsTablePagination, MsTableOperator, MsUpgrade }, data() { return { @@ -204,6 +206,13 @@ _filter(filters, this.condition); this.init(); }, + historicalDataUpgrade() { + if (this.selectIds.size < 1) { + this.$warning(this.$t('test_track.plan_view.select_manipulate')); + } else { + this.$refs.upgrade.openOneClickOperation(); + } + } }, created() { this.init(); diff --git a/frontend/src/business/components/api/test/Upgrade.vue b/frontend/src/business/components/api/test/Upgrade.vue new file mode 100644 index 0000000000..5e0f9c2d92 --- /dev/null +++ b/frontend/src/business/components/api/test/Upgrade.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/frontend/src/business/components/common/components/MsTableHeader.vue b/frontend/src/business/components/common/components/MsTableHeader.vue index 09af2c28de..00c0f9d991 100644 --- a/frontend/src/business/components/common/components/MsTableHeader.vue +++ b/frontend/src/business/components/common/components/MsTableHeader.vue @@ -13,6 +13,8 @@ + @@ -84,6 +86,9 @@ }, runTest() { this.$emit('runTest') + }, + historicalDataUpgrade() { + this.$emit('historicalDataUpgrade'); } }, computed: { diff --git a/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue b/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue index 456f61e8d3..ee34766b45 100644 --- a/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue +++ b/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue @@ -21,45 +21,85 @@ size="mini"/>
- - + + + {{ $t('load_test.by_duration') }} + {{ $t('load_test.by_iteration') }} +
- - -   - - -
- - - - - - - +
+ + + +
+ + +   + + +
+ + + + + + + +
+
+ + + +
+ + +   + + +
+ + + + +
@@ -83,6 +123,10 @@ const STEPS = "Steps"; const DURATION = "duration"; const RPS_LIMIT = "rpsLimit"; const RPS_LIMIT_ENABLE = "rpsLimitEnable"; +const THREAD_TYPE = "threadType"; +const ITERATE_NUM = "iterateNum"; +const ITERATE_RAMP_UP = "iterateRampUpTime"; + const hexToRgba = function (hex, opacity) { return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ',' + parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')'; @@ -128,6 +172,9 @@ export default { case RAMP_UP: this.threadGroups[i].rampUpTime = item.value; break; + case ITERATE_RAMP_UP: + this.threadGroups[i].iterateRampUp = item.value; + break; case DURATION: if (item.unit) { this.threadGroups[i].duration = item.value; @@ -144,6 +191,12 @@ export default { case RPS_LIMIT_ENABLE: this.threadGroups[i].rpsLimitEnable = item.value; break; + case THREAD_TYPE: + this.threadGroups[i].threadType = item.value; + break; + case ITERATE_NUM: + this.threadGroups[i].iterateNum = item.value; + break; default: break; } @@ -157,6 +210,9 @@ export default { case RAMP_UP: this.threadGroups[0].rampUpTime = d.value; break; + case ITERATE_RAMP_UP: + this.threadGroups[0].iterateRampUp = d.value; + break; case DURATION: if (d.unit) { this.threadGroups[0].duration = d.value; @@ -173,6 +229,12 @@ export default { case RPS_LIMIT_ENABLE: this.threadGroups[0].rpsLimitEnable = d.value; break; + case THREAD_TYPE: + this.threadGroups[0].threadType = d.value; + break; + case ITERATE_NUM: + this.threadGroups[0].iterateNum = d.value; + break; default: break; } diff --git a/frontend/src/business/components/performance/test/EditPerformanceTest.vue b/frontend/src/business/components/performance/test/EditPerformanceTest.vue index 2e2769f1a1..8af0e56832 100644 --- a/frontend/src/business/components/performance/test/EditPerformanceTest.vue +++ b/frontend/src/business/components/performance/test/EditPerformanceTest.vue @@ -298,6 +298,9 @@ export default { tg.rampUpTime = tg.rampUpTime || 5; tg.step = tg.step || 5; tg.rpsLimit = tg.rpsLimit || 10; + tg.threadType = tg.threadType || 'DURATION'; + tg.iterateNum = tg.iterateNum || 1; + tg.iterateRampUp = tg.iterateRampUp || 10; handler.calculateChart(tg); }); } diff --git a/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue b/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue index c531212baa..88098c33af 100644 --- a/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue +++ b/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue @@ -33,45 +33,83 @@ size="mini"/>

- - + + + {{ $t('load_test.by_duration') }} + {{ $t('load_test.by_iteration') }} +
- - -   - - -
- - - - - - - +
+ + + +
+ + +   + + +
+ + + + + + + +
+
+ + + +
+ + +   + + +
+ + + + +
@@ -91,11 +129,14 @@ import {findTestPlan, findThreadGroup} from "@/business/components/performance/t const TARGET_LEVEL = "TargetLevel"; const RAMP_UP = "RampUp"; +const ITERATE_RAMP_UP = "iterateRampUpTime"; const STEPS = "Steps"; const DURATION = "duration"; const RPS_LIMIT = "rpsLimit"; const RPS_LIMIT_ENABLE = "rpsLimitEnable"; const HOLD = "Hold"; +const THREAD_TYPE = "threadType"; +const ITERATE_NUM = "iterateNum"; const hexToRgba = function (hex, opacity) { return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ',' @@ -186,6 +227,9 @@ export default { case RAMP_UP: this.threadGroups[i].rampUpTime = item.value; break; + case ITERATE_RAMP_UP: + this.threadGroups[i].iterateRampUp = item.value; + break; case DURATION: if (item.unit) { this.threadGroups[i].duration = item.value; @@ -202,9 +246,19 @@ export default { case RPS_LIMIT_ENABLE: this.threadGroups[i].rpsLimitEnable = item.value; break; + case THREAD_TYPE: + this.threadGroups[i].threadType = item.value; + break; + case ITERATE_NUM: + this.threadGroups[i].iterateNum = item.value; + break; default: break; } + // + this.$set(this.threadGroups[i], "threadType", this.threadGroups[i].threadType || 'DURATION'); + this.$set(this.threadGroups[i], "iterateNum", this.threadGroups[i].iterateNum || 1); + this.$set(this.threadGroups[i], "iterateRampUp", this.threadGroups[i].iterateRampUp || 10); }) this.calculateChart(this.threadGroups[i]); } else { @@ -215,6 +269,9 @@ export default { case RAMP_UP: this.threadGroups[0].rampUpTime = d.value; break; + case ITERATE_RAMP_UP: + this.threadGroups[0].iterateRampUp = d.value; + break; case DURATION: if (d.unit) { this.threadGroups[0].duration = d.value; @@ -231,9 +288,18 @@ export default { case RPS_LIMIT_ENABLE: this.threadGroups[0].rpsLimitEnable = d.value; break; + case THREAD_TYPE: + this.threadGroups[0].threadType = d.value; + break; + case ITERATE_NUM: + this.threadGroups[0].iterateNum = d.value; + break; default: break; } + this.$set(this.threadGroups[0], "threadType", this.threadGroups[0].threadType || 'DURATION'); + this.$set(this.threadGroups[0], "iterateNum", this.threadGroups[0].iterateNum || 1); + this.$set(this.threadGroups[0], "iterateRampUp", this.threadGroups[0].iterateRampUp || 10); this.calculateChart(this.threadGroups[0]); } } @@ -461,7 +527,7 @@ export default { for (let i = 0; i < this.threadGroups.length; i++) { if (!this.threadGroups[i].threadNumber || !this.threadGroups[i].duration - || !this.threadGroups[i].rampUpTime || !this.threadGroups[i].step) { + || !this.threadGroups[i].rampUpTime || !this.threadGroups[i].step || !this.threadGroups[i].iterateNum) { this.$warning(this.$t('load_test.pressure_config_params_is_empty')); this.$emit('changeActive', '1'); return false; @@ -488,6 +554,9 @@ export default { {key: RPS_LIMIT, value: this.threadGroups[i].rpsLimit}, {key: RPS_LIMIT_ENABLE, value: this.threadGroups[i].rpsLimitEnable}, {key: HOLD, value: this.threadGroups[i].duration - this.threadGroups[i].rampUpTime}, + {key: THREAD_TYPE, value: this.threadGroups[i].threadType}, + {key: ITERATE_NUM, value: this.threadGroups[i].iterateNum}, + {key: ITERATE_RAMP_UP, value: this.threadGroups[i].iterateRampUp}, ]); } return result; diff --git a/frontend/src/business/components/xpack b/frontend/src/business/components/xpack index 010ad7a5f0..7d43154a7c 160000 --- a/frontend/src/business/components/xpack +++ b/frontend/src/business/components/xpack @@ -1 +1 @@ -Subproject commit 010ad7a5f072a5e9d368c756a2473bbd20781433 +Subproject commit 7d43154a7c19732407a8e9ace8a7d1ea13c91f36 diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index 61611f4450..2891207808 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -438,6 +438,10 @@ export default { input_rps_limit: 'Please enter a limit', ramp_up_time_within: 'In', ramp_up_time_minutes: 'seconds, separate', + ramp_up_time_seconds: 'seconds add concurrent users', + iterate_num: 'Iterations: ', + by_iteration: 'By iterations', + by_duration: 'By duration', ramp_up_time_times: 'add concurrent users', advanced_config_error: 'Advanced configuration verification failed', domain_bind: 'Domain bind', diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 4e935f35b5..944fe712f7 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -434,6 +434,10 @@ export default { input_rps_limit: '请输入限制', ramp_up_time_within: '在', ramp_up_time_minutes: '秒内,分', + ramp_up_time_seconds: '秒内增加并发用户', + iterate_num: '迭代次数 (次): ', + by_iteration: '按迭代次数', + by_duration: '按持续时间', ramp_up_time_times: '次增加并发用户', advanced_config_error: '高级配置校验失败', domain_bind: '域名绑定', diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index 11fddfe1e5..5ba7888a5f 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -434,6 +434,10 @@ export default { input_rps_limit: '請輸入限制', ramp_up_time_within: '在', ramp_up_time_minutes: '秒內,分', + ramp_up_time_seconds: '秒內增加並發用戶', + iterate_num: '迭代次數 (次): ', + by_iteration: '按迭代次數', + by_duration: '按壓測時長', ramp_up_time_times: '次增加並發用戶', advanced_config_error: '高級配置校驗失敗', domain_bind: '域名綁定',