diff --git a/backend/pom.xml b/backend/pom.xml index c54fc88b7c..7c5cb8a95b 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -139,7 +139,7 @@ org.springdoc springdoc-openapi-ui - 1.2.32 + 1.5.6 @@ -280,7 +280,7 @@ io.swagger.parser.v3 swagger-parser - 2.0.22 + 2.0.24 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 d43ea2acb9..185abbd176 100644 --- a/backend/src/main/java/io/metersphere/api/controller/APITestController.java +++ b/backend/src/main/java/io/metersphere/api/controller/APITestController.java @@ -12,8 +12,7 @@ import io.metersphere.api.dto.datacount.response.TaskInfoResult; import io.metersphere.api.dto.definition.RunDefinitionRequest; import io.metersphere.api.dto.scenario.request.dubbo.RegistryCenter; import io.metersphere.api.service.*; -import io.metersphere.base.domain.ApiTest; -import io.metersphere.base.domain.Schedule; +import io.metersphere.base.domain.*; import io.metersphere.commons.constants.RoleConstants; import io.metersphere.commons.constants.ScheduleGroup; import io.metersphere.commons.utils.CronUtils; @@ -30,13 +29,15 @@ import org.apache.commons.lang3.StringUtils; import org.apache.jorphan.collections.HashTree; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresRoles; -import org.python.core.AstList; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.text.DecimalFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; import static io.metersphere.commons.utils.JsonPathUtils.getListJson; @@ -251,9 +252,7 @@ public class APITestController { @GetMapping("/testSceneInfoCount/{projectId}") public ApiDataCountDTO testSceneInfoCount(@PathVariable String projectId) { - ApiDataCountDTO apiCountResult = new ApiDataCountDTO(); - long scenarioCountNumber = apiAutomationService.countScenarioByProjectID(projectId); apiCountResult.setAllApiDataCountNumber(scenarioCountNumber); @@ -272,17 +271,46 @@ public class APITestController { //未执行、未通过、已通过 List countResultByRunResult = apiAutomationService.countRunResultByProjectID(projectId); apiCountResult.countRunResult(countResultByRunResult); - long allCount = apiCountResult.getUnexecuteCount() + apiCountResult.getExecutionPassCount() + apiCountResult.getExecutionFailedCount(); - + DecimalFormat df = new DecimalFormat("0.0"); if (allCount != 0) { + //通过率 float coverageRageNumber = (float) apiCountResult.getExecutionPassCount() * 100 / allCount; - DecimalFormat df = new DecimalFormat("0.0"); apiCountResult.setPassRage(df.format(coverageRageNumber) + "%"); } - return apiCountResult; + } + @GetMapping("/countInterfaceCoverage/{projectId}") + public String countInterfaceCoverage(@PathVariable String projectId) { + String returnStr = "100%"; + /** + * 接口覆盖率 + * 复制的接口定义/复制或引用的单接口用例/ 添加的自定义请求 url 路径与现有的接口定义一致的请求 + */ + long startTime1 = System.currentTimeMillis(); + List allScenarioInfoList = apiAutomationService.selectIdAndScenarioByProjectId(projectId); + long startTime2 = System.currentTimeMillis(); + System.out.println("Search data time : " + (startTime2 - startTime1)); + startTime1 = System.currentTimeMillis(); + List allEffectiveApiIdList = apiDefinitionService.selectEffectiveIdByProjectId(projectId); + startTime2 = System.currentTimeMillis(); + System.out.println("Search data time (api info) : " + (startTime2 - startTime1)); + List allEffectiveApiCaseList = apiTestCaseService.selectEffectiveTestCaseByProjectId(projectId); + long startTime3 = System.currentTimeMillis(); + System.out.println("Search data time (case info): " + (startTime3 - startTime2)); + + try { + startTime1 = System.currentTimeMillis(); + float intetfaceCoverageRageNumber = apiAutomationService.countInterfaceCoverage(allScenarioInfoList, allEffectiveApiIdList, allEffectiveApiCaseList); + startTime2 = System.currentTimeMillis(); + System.out.println("Count data time : " + (startTime2 - startTime1)); + DecimalFormat df = new DecimalFormat("0.0"); + returnStr = df.format(intetfaceCoverageRageNumber) + "%"; + }catch (Exception e){ + e.printStackTrace(); + } + return returnStr; } @GetMapping("/scheduleTaskInfoCount/{projectId}") @@ -346,11 +374,11 @@ public class APITestController { @GetMapping("/runningTask/{projectID}/{callFrom}") public List runningTask(@PathVariable String projectID, @PathVariable String callFrom) { List typeFilter = new ArrayList<>(); - if(StringUtils.equals(callFrom, "api_test")) { // 接口测试首页显示的运行中定时任务,只要这3种,不需要 性能测试、api_test(旧版) + if (StringUtils.equals(callFrom, "api_test")) { // 接口测试首页显示的运行中定时任务,只要这3种,不需要 性能测试、api_test(旧版) typeFilter.add(ScheduleGroup.API_SCENARIO_TEST.name()); typeFilter.add(ScheduleGroup.SWAGGER_IMPORT.name()); typeFilter.add(ScheduleGroup.TEST_PLAN_TEST.name()); - } else if(StringUtils.equals(callFrom, "track_home")) { // 测试跟踪首页只显示测试计划的定时任务 + } else if (StringUtils.equals(callFrom, "track_home")) { // 测试跟踪首页只显示测试计划的定时任务 typeFilter.add(ScheduleGroup.TEST_PLAN_TEST.name()); } List resultList = scheduleService.findRunningTaskInfoByProjectID(projectID, typeFilter); @@ -386,7 +414,7 @@ public class APITestController { String testName = runRequest.getName(); //将jmx处理封装为通用方法 - JmxInfoDTO dto = apiTestService.updateJmxString(jmxString,testName,false); + JmxInfoDTO dto = apiTestService.updateJmxString(jmxString, testName, false); dto.setName(runRequest.getName() + ".jmx"); return dto; } diff --git a/backend/src/main/java/io/metersphere/api/dto/datacount/response/ApiDataCountDTO.java b/backend/src/main/java/io/metersphere/api/dto/datacount/response/ApiDataCountDTO.java index 1ed49369c7..088cb896f5 100644 --- a/backend/src/main/java/io/metersphere/api/dto/datacount/response/ApiDataCountDTO.java +++ b/backend/src/main/java/io/metersphere/api/dto/datacount/response/ApiDataCountDTO.java @@ -111,6 +111,11 @@ public class ApiDataCountDTO { */ private String successRage = " 0%"; + /** + * 接口覆盖率 + */ + private String interfaceCoverage = " 0%"; + public ApiDataCountDTO(){} /** diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/parse/Swagger3Parser.java b/backend/src/main/java/io/metersphere/api/dto/definition/parse/Swagger3Parser.java index dd25a0c8b5..920a8f09b4 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/parse/Swagger3Parser.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/parse/Swagger3Parser.java @@ -320,6 +320,9 @@ public class Swagger3Parser extends SwaggerAbstractParser { } private Object parseSchema(Schema schema, Set refSet, Map infoMap) { + if (schema == null) { + return new JSONObject(); + } infoMap.put(schema.getName(), schema); if (StringUtils.isNotBlank(schema.get$ref())) { if (refSet.contains(schema.get$ref())) { 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 74e804c3f0..f46e4d1162 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 @@ -160,14 +160,16 @@ public class MsHTTPSamplerProxy extends MsTestElement { sampler.setProtocol(urlObject.getProtocol()); sampler.setPath(urlObject.getPath()); } else { - String configStr = config.getConfig().get(this.getProjectId()).getHttpConfig().getSocket(); - sampler.setDomain(configStr); - if (config.getConfig().get(this.getProjectId()).getHttpConfig().getPort() > 0) { - sampler.setDomain(config.getConfig().get(this.getProjectId()).getHttpConfig().getDomain()); + sampler.setDomain(config.getConfig().get(this.getProjectId()).getHttpConfig().getDomain()); + url = config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol() + "://" + config.getConfig().get(this.getProjectId()).getHttpConfig().getSocket(); + URL urlObject = new URL(url); + String envPath = StringUtils.equals(urlObject.getPath(), "/") ? "" : urlObject.getPath(); + if (StringUtils.isNotBlank(this.getPath())) { + envPath += this.getPath(); } sampler.setPort(config.getConfig().get(this.getProjectId()).getHttpConfig().getPort()); sampler.setProtocol(config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol()); - sampler.setPath(this.getPath()); + sampler.setPath(envPath); } String envPath = sampler.getPath(); if (CollectionUtils.isNotEmpty(this.getRest()) && this.isRest()) { @@ -187,7 +189,7 @@ public class MsHTTPSamplerProxy extends MsTestElement { } } else { String url = this.getUrl(); - if (!url.startsWith("http://") && !url.startsWith("https://")) { + if (StringUtils.isNotEmpty(url) && !url.startsWith("http://") && !url.startsWith("https://")) { url = "http://" + url; } if (StringUtils.isNotEmpty(this.getPort()) && this.getPort().startsWith("${")) { diff --git a/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java b/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java index 33626513da..c4c71db6f1 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java @@ -1,6 +1,7 @@ package io.metersphere.api.service; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONPath; import com.fasterxml.jackson.core.type.TypeReference; @@ -29,6 +30,7 @@ import io.metersphere.commons.utils.*; import io.metersphere.controller.request.ScheduleRequest; import io.metersphere.i18n.Translator; import io.metersphere.job.sechedule.ApiScenarioTestJob; +import io.metersphere.job.sechedule.SwaggerUrlImportJob; import io.metersphere.job.sechedule.TestPlanTestJob; import io.metersphere.service.ScheduleService; import io.metersphere.track.dto.TestPlanDTO; @@ -230,6 +232,7 @@ public class ApiAutomationService { scenario.setPrincipal(request.getPrincipal()); scenario.setStepTotal(request.getStepTotal()); scenario.setUpdateTime(System.currentTimeMillis()); + scenario.setDescription(request.getDescription()); scenario.setScenarioDefinition(JSON.toJSONString(request.getScenarioDefinition())); if (StringUtils.isNotEmpty(request.getStatus())) { scenario.setStatus(request.getStatus()); @@ -710,6 +713,10 @@ public class ApiAutomationService { return extApiScenarioMapper.countByProjectID(projectId); } + public List selectIdAndScenarioByProjectId(String projectId) { + return extApiScenarioMapper.selectIdAndScenarioByProjectId(projectId); + } + public long countScenarioByProjectIDAndCreatInThisWeek(String projectId) { Map startAndEndDateInWeek = DateUtils.getWeedFirstTimeAndLastTime(new Date()); Date firstTime = startAndEndDateInWeek.get("firstTime"); @@ -811,7 +818,9 @@ public class ApiAutomationService { if (StringUtils.equals(request.getGroup(), ScheduleGroup.TEST_PLAN_TEST.name())) { scheduleService.addOrUpdateCronJob( request, TestPlanTestJob.getJobKey(request.getResourceId()), TestPlanTestJob.getTriggerKey(request.getResourceId()), TestPlanTestJob.class); - } else { + }else if(StringUtils.equals(request.getGroup(), ScheduleGroup.SWAGGER_IMPORT.name())){ + scheduleService.addOrUpdateCronJob(request, SwaggerUrlImportJob.getJobKey(request.getResourceId()), SwaggerUrlImportJob.getTriggerKey(request.getResourceId()), SwaggerUrlImportJob.class); + } else{ scheduleService.addOrUpdateCronJob( request, ApiScenarioTestJob.getJobKey(request.getResourceId()), ApiScenarioTestJob.getTriggerKey(request.getResourceId()), ApiScenarioTestJob.class); } @@ -1031,4 +1040,131 @@ public class ApiAutomationService { this.deleteBatch(request.getIds()); } + + /** + * 统计接口覆盖率 + * 1.场景中复制的接口 + * 2.场景中引用/复制的案例 + * 3.场景中的自定义路径与接口定义中的匹配 + * + * @param allScenarioInfoList 场景集合(id / scenario大字段 必须有数据) + * @param allEffectiveApiList 接口集合(id / path 必须有数据) + * @param allEffectiveApiCaseList 案例集合(id /api_definition_id 必须有数据) + * @return + */ + public float countInterfaceCoverage(List allScenarioInfoList, List allEffectiveApiList, List allEffectiveApiCaseList) { +// float coverageRageNumber = (float) apiCountResult.getExecutionPassCount() * 100 / allCount; + float intetfaceCoverage = 0; + if (allEffectiveApiList == null || allEffectiveApiList.isEmpty()) { + return 100; + } + + /** + * 前置工作: + * 1。将接口集合转化数据结构: map> urlMap 用来做3的筛选 + * 2。将案例集合转化数据结构:map> caseIdMap 用来做2的筛选 + * 3。将接口集合转化数据结构: List allApiIdList 用来做1的筛选 + * 4。自定义List coveragedIdList 已覆盖的id集合。 最终计算公式是 coveragedIdList/allApiIdList + * + * 解析allScenarioList的scenarioDefinition字段。 + * 1。提取每个步骤的url。 在 urlMap筛选 + * 2。提取每个步骤的id. 在caseIdMap 和 allApiIdList。 + */ + Map> urlMap = new HashMap<>(); + List allApiIdList = new ArrayList<>(); + Map> caseIdMap = new HashMap<>(); + for (ApiDefinition model : allEffectiveApiList) { + String url = model.getPath(); + String id = model.getId(); + allApiIdList.add(id); + if (urlMap.containsKey(url)) { + urlMap.get(url).add(id); + } else { + List list = new ArrayList<>(); + list.add(id); + urlMap.put(url, list); + } + } + for (ApiTestCase model : allEffectiveApiCaseList){ + String caseId = model.getId(); + String apiId = model.getApiDefinitionId(); + if (urlMap.containsKey(caseId)) { + urlMap.get(caseId).add(apiId); + } else { + List list = new ArrayList<>(); + list.add(apiId); + urlMap.put(caseId, list); + } + } + + if (allApiIdList.isEmpty()) { + return 100; + } + + List urlList = new ArrayList<>(); + List idList= new ArrayList<>(); + + for (ApiScenarioWithBLOBs model : allScenarioInfoList) { + String scenarioDefiniton = model.getScenarioDefinition(); + this.addUrlAndIdToList(scenarioDefiniton,urlList,idList); + } + + List containsApiIdList = new ArrayList<>(); + + for (String url : urlList) { + List apiIdList = urlMap.get(url); + if(apiIdList!=null ){ + for (String api : apiIdList) { + if(!containsApiIdList.contains(api)){ + containsApiIdList.add(api); + } + } + } + } + + for (String id : idList) { + List apiIdList = caseIdMap.get(id); + if(apiIdList!=null ){ + for (String api : apiIdList) { + if(!containsApiIdList.contains(api)){ + containsApiIdList.add(api); + } + } + } + + if(allApiIdList.contains(id)){ + if(!containsApiIdList.contains(id)){ + containsApiIdList.add(id); + } + } + } + + float coverageRageNumber = (float) containsApiIdList.size() * 100 / allApiIdList.size(); + return coverageRageNumber; + } + + private void addUrlAndIdToList(String scenarioDefiniton, List urlList, List idList) { + try { + JSONObject scenarioObj = JSONObject.parseObject(scenarioDefiniton); + if(scenarioObj.containsKey("hashTree")){ + JSONArray hashArr = scenarioObj.getJSONArray("hashTree"); + for (int i = 0; i < hashArr.size(); i++) { + JSONObject elementObj = hashArr.getJSONObject(i); + if(elementObj.containsKey("id")){ + String id = elementObj.getString("id"); + idList.add(id); + } + if(elementObj.containsKey("url")){ + String url = elementObj.getString("url"); + urlList.add(url); + } + if(elementObj.containsKey("path")){ + String path = elementObj.getString("path"); + urlList.add(path); + } + } + } + }catch (Exception e){ + } + } } diff --git a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java index 7884538fcc..eb3847aaf5 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java @@ -912,4 +912,8 @@ public class ApiDefinitionService { } return apiExportResult; } + + public List selectEffectiveIdByProjectId(String projectId) { + return extApiDefinitionMapper.selectEffectiveIdByProjectId(projectId); + } } diff --git a/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java b/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java index aa7f0724a4..341c68747e 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java @@ -648,4 +648,8 @@ public class ApiTestCaseService { public ApiDefinitionExecResult getInfo(String id){ return apiDefinitionExecResultMapper.selectByPrimaryKey(id); } + + public List selectEffectiveTestCaseByProjectId(String projectId) { + return extApiTestCaseMapper.selectEffectiveTestCaseByProjectId(projectId); + } } diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.java index 44cf4c6abe..3230662b65 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.java +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.java @@ -38,4 +38,6 @@ public interface ExtApiDefinitionMapper { List listRelevance(@Param("request")ApiDefinitionRequest request); List listRelevanceReview(@Param("request")ApiDefinitionRequest request); List selectIds(@Param("request") BaseQueryRequest query); + + List selectEffectiveIdByProjectId(String projectId); } \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.xml index bfaa0afcd0..7067ce9a49 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiDefinitionMapper.xml @@ -465,6 +465,11 @@ + diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.java index 6bd9153db2..d7d397f6bb 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.java +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.java @@ -6,7 +6,6 @@ import io.metersphere.api.dto.datacount.ApiDataCountResult; import io.metersphere.base.domain.ApiScenario; import io.metersphere.base.domain.ApiScenarioExample; import io.metersphere.base.domain.ApiScenarioWithBLOBs; -import io.metersphere.controller.request.BaseQueryRequest; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -28,6 +27,8 @@ public interface ExtApiScenarioMapper { long countByProjectID(String projectId); + List selectIdAndScenarioByProjectId(String projectId); + long countByProjectIDAndCreatInThisWeek(@Param("projectId") String projectId, @Param("firstDayTimestamp") long firstDayTimestamp, @Param("lastDayTimestamp") long lastDayTimestamp); List countRunResultByProjectID(String projectId); diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.xml index c7db6225fe..db83f076af 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiScenarioMapper.xml @@ -304,6 +304,9 @@ + select name from api_test_case where project_id = #{projectId} + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/config/KafkaProperties.java b/backend/src/main/java/io/metersphere/config/KafkaProperties.java index d158b64fcc..63ade0be9f 100644 --- a/backend/src/main/java/io/metersphere/config/KafkaProperties.java +++ b/backend/src/main/java/io/metersphere/config/KafkaProperties.java @@ -11,6 +11,7 @@ public class KafkaProperties { public static final String KAFKA_PREFIX = "kafka"; private String acks = "0"; // 不要设置all + private String expectedDelayEndTime = "30000"; // 30s private String topic; private String fields; private String timestamp; diff --git a/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java index 05615842c0..f1d2d639c5 100644 --- a/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java +++ b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java @@ -67,6 +67,16 @@ public class TestCaseDataListener extends EasyExcelListener { break; } } + //增加字数校验,每一层不能超过100字 + for (int i = 0; i < nodes.length; i++) { + String nodeStr = nodes[i]; + if(StringUtils.isNotEmpty(nodeStr)){ + if(nodeStr.trim().length()>100){ + stringBuilder.append(Translator.get("module") + Translator.get("test_track.length_less_than") + "100:"+nodeStr); + break; + } + } + } } // if (StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) { diff --git a/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java b/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java index 6ad3c39089..4a80c5fa12 100644 --- a/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java +++ b/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java @@ -25,8 +25,9 @@ public class JmeterFileController { @GetMapping("download") public ResponseEntity downloadJmeterFiles(@RequestParam("testId") String testId, @RequestParam("resourceId") String resourceId, - @RequestParam("ratio") double ratio, @RequestParam("startTime") long startTime, + @RequestParam("ratio") double ratio, @RequestParam("reportId") String reportId, @RequestParam("resourceIndex") int resourceIndex) { + long startTime = System.currentTimeMillis(); byte[] bytes = jmeterFileService.downloadZip(testId, resourceId, ratio, startTime, reportId, resourceIndex); return ResponseEntity.ok() .contentType(MediaType.parseMediaType("application/octet-stream")) 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 a8d33e4391..790be02d19 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 @@ -29,6 +29,7 @@ public class JmeterDocumentParser implements DocumentParser { private final static String HASH_TREE_ELEMENT = "hashTree"; private final static String TEST_PLAN = "TestPlan"; private final static String STRING_PROP = "stringProp"; + private final static String BOOL_PROP = "boolProp"; private final static String COLLECTION_PROP = "collectionProp"; private final static String CONCURRENCY_THREAD_GROUP = "com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup"; private final static String VARIABLE_THROUGHPUT_TIMER = "kg.apc.jmeter.timers.VariableThroughputTimer"; @@ -94,6 +95,7 @@ public class JmeterDocumentParser implements DocumentParser { processCheckoutDnsCacheManager(ele); processCheckoutArguments(ele); processCheckoutResponseAssertion(ele); + processCheckoutSerializeThreadgroups(ele); } else if (nodeNameEquals(ele, CONCURRENCY_THREAD_GROUP)) { processThreadGroupName(ele); processCheckoutTimer(ele); @@ -141,6 +143,21 @@ public class JmeterDocumentParser implements DocumentParser { } } + private void processCheckoutSerializeThreadgroups(Element element) { + NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node item = childNodes.item(i); + if (nodeNameEquals(item, BOOL_PROP)) { + String serializeName = ((Element) item).getAttribute("name"); + if (StringUtils.equals(serializeName, "TestPlan.serialize_threadgroups")) { + // 保存线程组是否是顺序执行 + context.addProperty("serialize_threadgroups", item.getTextContent()); + break; + } + } + } + } + private void processArgumentFiles(Element element) { NodeList childNodes = element.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { @@ -479,7 +496,7 @@ public class JmeterDocumentParser implements DocumentParser { item.appendChild(elementProp); } } - if (item instanceof Element && nodeNameEquals(item, "boolProp") + if (item instanceof Element && nodeNameEquals(item, BOOL_PROP) && org.apache.commons.lang3.StringUtils.equals(((Element) item).getAttribute("name"), "DNSCacheManager.isCustomResolver")) { item.getFirstChild().setNodeValue("true"); } @@ -511,7 +528,7 @@ public class JmeterDocumentParser implements DocumentParser { } private Element createBoolProp(Document document, String name, boolean value) { - Element boolProp = document.createElement("boolProp"); + Element boolProp = document.createElement(BOOL_PROP); boolProp.setAttribute("name", name); boolProp.appendChild(document.createTextNode(String.valueOf(value))); return boolProp; @@ -569,6 +586,8 @@ public class JmeterDocumentParser implements DocumentParser { collectionProp.appendChild(createKafkaProp(document, "test.name", context.getTestName())); collectionProp.appendChild(createKafkaProp(document, "test.startTime", context.getStartTime().toString())); collectionProp.appendChild(createKafkaProp(document, "test.reportId", context.getReportId())); + collectionProp.appendChild(createKafkaProp(document, "test.expectedEndTime", (String) context.getProperty("expectedEndTime"))); + collectionProp.appendChild(createKafkaProp(document, "test.expectedDelayEndTime", kafkaProperties.getExpectedDelayEndTime())); // 30s elementProp.appendChild(collectionProp); // set elementProp @@ -710,6 +729,8 @@ public class JmeterDocumentParser implements DocumentParser { default: break; } + // 处理预计结束时间 + processExpectedEndTime(duration); threadGroup.setAttribute("enabled", enabled); if (BooleanUtils.toBoolean(deleted)) { @@ -815,6 +836,8 @@ public class JmeterDocumentParser implements DocumentParser { default: break; } + // 处理预计结束时间 + processExpectedEndTime(hold); threadGroup.setAttribute("enabled", enabled); if (BooleanUtils.toBoolean(deleted)) { @@ -835,6 +858,27 @@ public class JmeterDocumentParser implements DocumentParser { threadGroup.appendChild(createStringProp(document, "Unit", "S")); } + private void processExpectedEndTime(String duration) { + long startTime = context.getStartTime(); + Long d = Long.parseLong(duration); + Object serialize = context.getProperty("TestPlan.serialize_threadgroups"); + String expectedEndTime = (String) context.getProperty("expectedEndTime"); + if (StringUtils.isBlank(expectedEndTime)) { + expectedEndTime = startTime + ""; + } + long endTime = Long.parseLong(expectedEndTime); + + if (BooleanUtils.toBoolean((String) serialize)) { + // 顺序执行线程组 + context.addProperty("expectedEndTime", String.valueOf(endTime + d * 1000)); + } else { + // 同时执行线程组 + if (endTime < startTime + d * 1000) { + context.addProperty("expectedEndTime", String.valueOf(startTime + d * 1000)); + } + } + } + private void processIterationThreadGroup(Element threadGroup) { // 检查 threadgroup 后面的hashtree是否为空 Node hashTree = threadGroup.getNextSibling(); @@ -903,8 +947,11 @@ public class JmeterDocumentParser implements DocumentParser { threadGroup.appendChild(createStringProp(document, "ThreadGroup.duration", "10")); threadGroup.appendChild(createStringProp(document, "ThreadGroup.delay", "")); threadGroup.appendChild(createBoolProp(document, "ThreadGroup.same_user_on_next_iteration", true)); - } + // 处理预计结束时间, (按照迭代次数 * 线程数)s + String duration = String.valueOf(Long.parseLong(loops) * Long.parseLong(threads)); + processExpectedEndTime(duration); + } private void processCheckoutTimer(Element element) { /* diff --git a/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java b/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java index 68736be502..3942c92a74 100644 --- a/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java +++ b/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java @@ -91,8 +91,8 @@ public class TestPlanController { @PostMapping("/edit") @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) - public void editTestPlan(@RequestBody TestPlanDTO testPlanDTO) { - testPlanService.editTestPlan(testPlanDTO, true); + public String editTestPlan(@RequestBody TestPlanDTO testPlanDTO) { + return testPlanService.editTestPlan(testPlanDTO, true); } @PostMapping("/edit/status/{planId}") diff --git a/backend/src/main/java/io/metersphere/track/service/TestPlanService.java b/backend/src/main/java/io/metersphere/track/service/TestPlanService.java index 7d9c4047ac..e1da5af7a8 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestPlanService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestPlanService.java @@ -176,7 +176,7 @@ public class TestPlanService { return Optional.ofNullable(testPlanMapper.selectByPrimaryKey(testPlanId)).orElse(new TestPlan()); } - public int editTestPlan(TestPlanDTO testPlan, Boolean isSendMessage) { + public String editTestPlan(TestPlanDTO testPlan, Boolean isSendMessage) { checkTestPlanExist(testPlan); TestPlan res = testPlanMapper.selectByPrimaryKey(testPlan.getId()); // 先查一次库 testPlan.setUpdateTime(System.currentTimeMillis()); @@ -230,7 +230,7 @@ public class TestPlanService { .build(); noticeSendService.send(NoticeConstants.TaskType.TEST_PLAN_TASK, noticeModel); } - return i; + return testPlan.getId(); } //计划内容 diff --git a/frontend/src/business/components/api/definition/ApiDefinition.vue b/frontend/src/business/components/api/definition/ApiDefinition.vue index 3abdc02222..9f1fcbda0d 100644 --- a/frontend/src/business/components/api/definition/ApiDefinition.vue +++ b/frontend/src/business/components/api/definition/ApiDefinition.vue @@ -392,8 +392,8 @@ } }, runTest(data) { - this.setTabTitle(data); this.handleTabsEdit(this.$t("commons.api"), "TEST", data); + this.setTabTitle(data); }, saveApi(data) { this.setTabTitle(data); diff --git a/frontend/src/business/components/api/definition/components/Run.vue b/frontend/src/business/components/api/definition/components/Run.vue index 734419b84f..3ef992d3e9 100644 --- a/frontend/src/business/components/api/definition/components/Run.vue +++ b/frontend/src/business/components/api/definition/components/Run.vue @@ -58,22 +58,26 @@ } }, run() { - let testPlan = new TestPlan(); - let threadGroup = new ThreadGroup(); - threadGroup.hashTree = []; - testPlan.hashTree = [threadGroup]; - this.runData.forEach(item => { - threadGroup.hashTree.push(item); - }) - - let projectId = ""; + let projectId = this.$store.state.projectId; // 如果envMap不存在,是单接口调用 if (!this.envMap || this.envMap.size === 0) { projectId = this.$store.state.projectId; } else { // 场景步骤下接口调用 - projectId = this.runData.projectId; + if(this.runData.projectId){ + projectId = this.runData.projectId; + } } + + let testPlan = new TestPlan(); + let threadGroup = new ThreadGroup(); + threadGroup.hashTree = []; + testPlan.hashTree = [threadGroup]; + this.runData.forEach(item => { + item.projectId = projectId; + threadGroup.hashTree.push(item); + }) + let reqObj = {id: this.reportId, testElement: testPlan, type: this.type,projectId: projectId, environmentMap: strMapToObj(this.envMap)}; let bodyFiles = getBodyUploadFiles(reqObj, this.runData); let url = ""; diff --git a/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue b/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue index 227102ac01..ec53fe028a 100644 --- a/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue +++ b/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue @@ -27,7 +27,7 @@ + @blur="saveTestCase(apiCase,true)" :placeholder="$t('commons.input_name')" ref="nameEdit"/> {{ apiCase.id ? apiCase.name : '' }} @@ -48,7 +48,7 @@
- +
@@ -291,7 +291,7 @@ } }); }, - saveCase(row) { + saveCase(row,hideAlert) { let tmp = JSON.parse(JSON.stringify(row)); this.isShowInput = false; if (this.validate(tmp)) { @@ -328,16 +328,18 @@ row.createTime = data.createTime; row.updateTime = data.updateTime; if (!row.message) { - this.$success(this.$t('commons.save_success')); - this.$emit('refresh'); + if(!hideAlert){ + this.$success(this.$t('commons.save_success')); + this.$emit('refresh'); + } } }); }, - saveTestCase(row) { + saveTestCase(row,hideAlert) { if (this.api.saved) { this.addModule(row); } else { - this.saveCase(row); + this.saveCase(row,hideAlert); } }, showInput(row) { diff --git a/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue b/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue index f63c1e8f92..a6db51ab96 100644 --- a/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue +++ b/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue @@ -152,6 +152,7 @@ saveApiAndCase(api) { this.visible = true; this.api = api; + this.currentApi = api; this.addCase(); }, setEnvironment(environment) { @@ -305,6 +306,8 @@ this.$warning(this.$t('api_test.environment.select_environment')); return; } + this.envMap = new Map(); + this.envMap.set(this.$store.state.projectId,this.environment); this.runData = []; this.batchLoadingIds = []; this.selectdCases = []; diff --git a/frontend/src/business/components/api/definition/components/list/ApiList.vue b/frontend/src/business/components/api/definition/components/list/ApiList.vue index c13957ac04..8844291a23 100644 --- a/frontend/src/business/components/api/definition/components/list/ApiList.vue +++ b/frontend/src/business/components/api/definition/components/list/ApiList.vue @@ -248,6 +248,7 @@ import {Api_List} from "@/business/components/common/model/JsonData"; import HeaderCustom from "@/business/components/common/head/HeaderCustom"; import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate"; import {Body} from "@/business/components/api/definition/model/ApiTestModel"; +import {buildNodePath} from "@/business/components/api/definition/model/NodeTree"; export default { @@ -768,13 +769,21 @@ export default { }); }, buildApiPath(apis) { - apis.forEach((api) => { + try { + let options = []; this.moduleOptions.forEach(item => { - if (api.moduleId === item.id) { - api.modulePath = item.path; - } + buildNodePath(item, {path: ''}, options); }); - }); + apis.forEach((api) => { + options.forEach((item) => { + if (api.moduleId === item.id) { + api.modulePath = item.path; + } + }) + }); + } catch (e) { + console.log(e); + } }, sort(column) { // 每次只对一个字段排序 diff --git a/frontend/src/business/components/api/definition/components/module/ApiModule.vue b/frontend/src/business/components/api/definition/components/module/ApiModule.vue index 22a88d9139..1d2e735ff6 100644 --- a/frontend/src/business/components/api/definition/components/module/ApiModule.vue +++ b/frontend/src/business/components/api/definition/components/module/ApiModule.vue @@ -133,16 +133,11 @@ this.data.forEach(node => { buildTree(node, {path: ''}); }); + this.$emit('setModuleOptions', this.data); this.$emit('setNodeTree', this.data); if (this.$refs.nodeTree) { this.$refs.nodeTree.filter(this.condition.filterText); } - let moduleOptions = []; - this.data.forEach(node => { - buildNodePath(node, {path: ''}, moduleOptions); - }); - this.moduleOptions = moduleOptions; - this.$emit('setModuleOptions', moduleOptions); } }); }, diff --git a/frontend/src/business/components/api/homepage/ApiTestHomePage.vue b/frontend/src/business/components/api/homepage/ApiTestHomePage.vue index dd5236a8e8..1bd4373d58 100644 --- a/frontend/src/business/components/api/homepage/ApiTestHomePage.vue +++ b/frontend/src/business/components/api/homepage/ApiTestHomePage.vue @@ -34,7 +34,7 @@ - + @@ -82,6 +82,7 @@ export default { sceneCountData: {}, testCaseCountData: {}, scheduleTaskCountData: {}, + interfaceCoverage: "waitting...", tipsType: "1", result: {}, } @@ -109,6 +110,10 @@ export default { this.sceneCountData = response.data; }); + this.$get("/api/countInterfaceCoverage/" + selectProjectId, response => { + this.interfaceCoverage = response.data; + }); + this.$get("/api/testCaseInfoCount/" + selectProjectId, response => { this.testCaseCountData = response.data; }); diff --git a/frontend/src/business/components/api/homepage/components/SceneInfoCard.vue b/frontend/src/business/components/api/homepage/components/SceneInfoCard.vue index e048e5c050..130628bdf8 100644 --- a/frontend/src/business/components/api/homepage/components/SceneInfoCard.vue +++ b/frontend/src/business/components/api/homepage/components/SceneInfoCard.vue @@ -52,16 +52,29 @@ - - + + {{$t('api_test.home_page.detail_card.rate.pass')+":"}} - - + + {{sceneCountData.passRage}} + + + {{$t('api_test.home_page.detail_card.rate.interface_coverage')+":"}} + + + + + + + {{interfaceCoverage}} + + + @@ -120,6 +133,7 @@ export default { props:{ sceneCountData:{}, + interfaceCoverage:String, }, methods: { @@ -141,6 +155,17 @@ export default { margin:20px auto; } +.rows-count-number{ + font-family:'ArialMT', 'Arial', sans-serif; + font-size:30px; + color: var(--count_number); + margin:20px auto; +} +.lading-icon{ + font-size: 25px; + color: var(--count_number); + font-weight: bold; +} .main-number-show { width: 100px; height: 100px; diff --git a/frontend/src/business/components/api/test/Upgrade.vue b/frontend/src/business/components/api/test/Upgrade.vue index 0c3b568be8..40f70cbd05 100644 --- a/frontend/src/business/components/api/test/Upgrade.vue +++ b/frontend/src/business/components/api/test/Upgrade.vue @@ -11,7 +11,6 @@ >> .el-textarea__inner { border-style: hidden; background-color: white; - color: #606266; + color: #060505; } .cast_label { @@ -785,4 +785,5 @@ p { height: 550px; overflow: auto; } + diff --git a/frontend/src/business/components/track/review/view/components/TestReviewTestCaseEdit.vue b/frontend/src/business/components/track/review/view/components/TestReviewTestCaseEdit.vue index e2aab0f143..eb03657f6b 100644 --- a/frontend/src/business/components/track/review/view/components/TestReviewTestCaseEdit.vue +++ b/frontend/src/business/components/track/review/view/components/TestReviewTestCaseEdit.vue @@ -549,4 +549,10 @@ export default { .comment-card >>> .el-card__body { height: calc(100vh - 120px); } + +.tb-edit >>> .el-textarea__inner { + border-style: hidden; + background-color: white; + color: #060505; +} diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index 94837b728c..3ffb614514 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -1001,6 +1001,7 @@ export default { coverage: "Coverage rate", pass: "Pass rate", success: "Success rate", + interface_coverage: "Interface coverage", }, }, api_details_card: { diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 251d71ac48..5039195966 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -1005,6 +1005,7 @@ export default { coverage: "覆盖率", pass: "通过率", success: "成功率", + interface_coverage: "接口覆盖率", }, }, api_details_card: { diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index e9f8dddcd4..2a6ca86ab7 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -1003,6 +1003,7 @@ export default { coverage: "覆蓋率", pass: "通過率", success: "成功率", + interface_coverage: "接口覆蓋率", }, }, api_details_card: {