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/service/ApiAutomationService.java b/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java index 33626513da..1694d72efe 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; @@ -710,6 +711,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"); @@ -1031,4 +1036,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/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/components/case/ApiCaseList.vue b/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue index f63c1e8f92..d1e3834198 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) { 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..19f64837d2 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}} + + + {{interfaceCoverage}} + + + @@ -120,6 +133,7 @@ export default { props:{ sceneCountData:{}, + interfaceCoverage:String, }, methods: { @@ -141,6 +155,13 @@ export default { margin:20px auto; } +.rows-count-number{ + font-family:'ArialMT', 'Arial', sans-serif; + font-size:30px; + color: var(--count_number); + margin:20px auto; +} + .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: {