fix(测试跟踪): 优化测试跟踪首页失败用例top10的查询方式和逻辑

--bug=1020830 --user=宋天阳 [测试跟踪]github#205762.4.1msctl uninstall+msctl
reload服务全部healthy登录测试跟踪首页报错 https://www.tapd.cn/55049933/s/1348104
This commit is contained in:
song-tianyang 2023-03-10 14:54:52 +08:00 committed by 刘瑞斌
parent ee0a5f1a49
commit fba936030c
32 changed files with 4704 additions and 3981 deletions

View File

@ -1,7 +1,6 @@
package io.metersphere.base.mapper.ext;
import io.metersphere.api.dto.QueryAPIReportRequest;
import io.metersphere.api.dto.datacount.ExecutedCaseInfoResult;
import io.metersphere.base.domain.ApiDefinitionExecResult;
import io.metersphere.base.domain.ApiDefinitionExecResultExpand;
import io.metersphere.base.domain.ApiDefinitionExecResultWithBLOBs;
@ -26,8 +25,6 @@ public interface ExtApiDefinitionExecResultMapper {
long countByProjectIDAndCreateInThisWeek(@Param("projectId") String projectId, @Param("version") String version, @Param("firstDayTimestamp") long firstDayTimestamp, @Param("lastDayTimestamp") long lastDayTimestamp);
List<ExecutedCaseInfoResult> findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(@Param("projectId") String projectId, @Param("versionId") String version, @Param("selectFunctionCase") boolean selectFunctionCase, @Param("startTimestamp") long startTimestamp, @Param("limitNumber") int limitNumber);
String selectExecResult(String resourceId);
ApiDefinitionExecResultWithBLOBs selectPlanApiMaxResultByTestIdAndType(String resourceId, String type);

View File

@ -39,105 +39,6 @@
WHERE integrated_report_id = #{0}
</select>
<select id="findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber"
resultType="io.metersphere.api.dto.datacount.ExecutedCaseInfoResult">
SELECT * FROM (
SELECT *
FROM (
SELECT testCase.testPlanCaseID AS testPlanCaseID,
testCase.id AS id,
testCase.testCaseName AS caseName,
testCase.testPlanName AS testPlan,
testCase.testPlanId AS testPlanId,
caseErrorCountData.dataCountNumber AS failureTimes,
'apiCase' AS caseType
FROM (SELECT testPlanCase.id AS testPlanCaseID,
apiCase.id AS id,
apiCase.`name` AS testCaseName,
testPlan.id AS testPlanId,
testPlan.`name` AS testPlanName
FROM api_test_case apiCase
INNER JOIN test_plan_api_case testPlanCase ON testPlanCase.api_case_id = apiCase.id
INNER JOIN test_plan testPlan ON testPlan.id = testPlanCase.test_plan_id
WHERE (
(apiCase.`status` IS NULL OR apiCase.`status` != 'Trash')
AND apiCase.project_id = #{projectId}
)) testCase
INNER JOIN (SELECT executionInfo.source_id AS sourceId,
COUNT(executionInfo.id) AS dataCountNumber
FROM api_case_execution_info executionInfo
INNER JOIN test_plan_api_case testPlanCase
ON executionInfo.source_id = testPlanCase.id
WHERE executionInfo.`result` = 'ERROR'
AND executionInfo.create_time > #{startTimestamp}
<if test="versionId != null">
AND executionInfo.version = #{versionId}
</if>
GROUP BY source_id) caseErrorCountData
ON caseErrorCountData.sourceId = testCase.testPlanCaseID
UNION
SELECT scene.id AS testCaseID,
scene.id AS id,
scene.`name` AS caseName,
apiScene.testPlanName AS testPlan,
apiScene.testPlanId AS testPlanId,
count(executionInfo.id) AS failureTimes,
'scenario' AS caseType
FROM scenario_execution_info executionInfo
INNER JOIN (SELECT testPlanScenario.id,
testPlanScenario.api_scenario_id,
testPlan.id AS testPlanId,
testPlan.`name` AS testPlanName
FROM test_plan_api_scenario testPlanScenario
INNER JOIN test_plan testPlan ON testPlan.id = testPlanScenario.test_plan_id) apiScene
ON apiScene.id = executionInfo.source_id
INNER JOIN api_scenario scene ON scene.id = apiScene.api_scenario_id
WHERE scene.project_id = #{projectId}
AND scene.`status` != 'Trash'
AND ( executionInfo.result = 'ERROR' )
AND executionInfo.create_time >= #{startTimestamp}
<if test="versionId != null">
AND executionInfo.version = #{versionId}
</if>
GROUP BY
scene.id,apiScene.testPlanId
<if test="selectFunctionCase == true">
UNION
SELECT
testCase.id AS testCaseID,
testCase.id AS id,
testCase.`name` AS caseName,
testCasePlan.testPlanName AS testPlan,
testCasePlan.testPlanId AS testPlanId,
count( executionInfo.id ) AS failureTimes,
'testCase' AS caseType
FROM
function_case_execution_info executionInfo
INNER JOIN (
SELECT
testPlanTestCase.id,
testPlanTestCase.case_id,
testPlan.id AS testPlanId,
testPlan.`name` AS testPlanName
FROM
test_plan_test_case testPlanTestCase
INNER JOIN test_plan testPlan ON testPlan.id = testPlanTestCase.plan_id
) testCasePlan ON testCasePlan.id = executionInfo.source_id
INNER JOIN test_case testCase ON testCase.id = testCasePlan.case_id
WHERE
testCase.project_id = #{projectId}
AND testCase.`status` != 'Trash'
AND ( executionInfo.result = 'Failure' )
AND executionInfo.create_time >= #{startTimestamp}
GROUP BY
testCase.id
</if>
) showTable
ORDER BY showTable.failureTimes DESC LIMIT ${limitNumber}
) pageTable
</select>
<select id="selectExecResult" resultType="java.lang.String">
select ader.status
from api_definition_exec_result ader

View File

@ -6,11 +6,9 @@ import io.metersphere.api.dto.DubboProvider;
import io.metersphere.api.dto.JmxInfoDTO;
import io.metersphere.api.dto.RegistryCenter;
import io.metersphere.api.dto.datacount.ApiDataCountResult;
import io.metersphere.api.dto.datacount.ExecutedCaseInfoResult;
import io.metersphere.api.dto.datacount.response.ApiDataCountDTO;
import io.metersphere.api.dto.datacount.response.CoveredDTO;
import io.metersphere.api.dto.datacount.response.ExecuteResultCountDTO;
import io.metersphere.api.dto.datacount.response.ExecutedCaseInfoDTO;
import io.metersphere.api.dto.definition.RunDefinitionRequest;
import io.metersphere.api.dto.export.ScenarioToPerformanceInfoDTO;
import io.metersphere.base.domain.ApiDefinition;
@ -32,7 +30,6 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -251,31 +248,6 @@ public class ApiHomeController {
return PageUtils.setPageInfo(page, resultList);
}
@GetMapping("/failure/case/about/plan/{projectId}/{versionId}/{selectFunctionCase}/{limitNumber}/{goPage}/{pageSize}")
public Pager<List<ExecutedCaseInfoDTO>> failureCaseAboutTestPlan(@PathVariable String projectId, @PathVariable String versionId, @PathVariable boolean selectFunctionCase,
@PathVariable int limitNumber, @PathVariable int goPage, @PathVariable int pageSize) {
versionId = this.initializationVersionId(versionId);
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
List<ExecutedCaseInfoResult> selectDataList = apiDefinitionExecResultService.findFailureCaseInfoByProjectIDAndLimitNumberInSevenDays(projectId, versionId, selectFunctionCase, limitNumber);
List<ExecutedCaseInfoDTO> returnList = new ArrayList<>(selectDataList.size());
for (int dataIndex = 0; dataIndex < selectDataList.size(); dataIndex++) {
ExecutedCaseInfoDTO dataDTO = new ExecutedCaseInfoDTO();
dataDTO.setSortIndex(dataIndex + 1);
ExecutedCaseInfoResult selectData = selectDataList.get(dataIndex);
dataDTO.setCaseID(selectData.getTestCaseID());
dataDTO.setCaseName(selectData.getCaseName());
dataDTO.setTestPlan(selectData.getTestPlan());
dataDTO.setFailureTimes(selectData.getFailureTimes());
dataDTO.setTestPlanId(selectData.getTestPlanId());
dataDTO.setCaseType(selectData.getCaseType());
dataDTO.setId(selectData.getId());
dataDTO.setTestPlanDTOList(selectData.getTestPlanDTOList());
returnList.add(dataDTO);
}
return PageUtils.setPageInfo(page, returnList);
}
//初始化版本ID
private String initializationVersionId(String versionId) {
if (StringUtils.equalsIgnoreCase(versionId, "default")) {

View File

@ -2,7 +2,6 @@ package io.metersphere.service.definition;
import io.metersphere.api.dto.QueryAPIReportRequest;
import io.metersphere.api.dto.RequestResultExpandDTO;
import io.metersphere.api.dto.datacount.ExecutedCaseInfoResult;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtApiDefinitionExecResultMapper;
@ -355,39 +354,6 @@ public class ApiDefinitionExecResultService {
}
}
public List<ExecutedCaseInfoResult> findFailureCaseInfoByProjectIDAndLimitNumberInSevenDays(String projectId, String versionId, boolean selectFunctionCase, int limitNumber) {
//获取7天之前的日期
Date startDay = DateUtils.dateSum(new Date(), -6);
//将日期转化为 00:00:00 的时间戳
Date startTime = null;
try {
startTime = DateUtils.getDayStartTime(startDay);
} catch (Exception e) {
LogUtil.error("解析日期出错!", e);
}
if (startTime == null) {
return new ArrayList<>(0);
} else {
List<ExecutedCaseInfoResult> list = extApiDefinitionExecResultMapper.findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(projectId, versionId, selectFunctionCase, startTime.getTime(), limitNumber);
List<ExecutedCaseInfoResult> returnList = new ArrayList<>(limitNumber);
for (int i = 0; i < list.size(); i++) {
if (i < limitNumber) {
//开始遍历查询TestPlan信息 --> 提供前台做超链接
ExecutedCaseInfoResult item = list.get(i);
returnList.add(item);
} else {
break;
}
}
return returnList;
}
}
private ApiDefinitionExecResult editResult(RequestResult item, String reportId, String console, String type, String testId, ApiDefinitionExecResultMapper batchMapper) {
if (!StringUtils.startsWithAny(item.getName(), "PRE_PROCESSOR_ENV_", "POST_PROCESSOR_ENV_")) {
ApiDefinitionExecResultWithBLOBs saveResult = new ApiDefinitionExecResultWithBLOBs();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
package io.metersphere.commons.utils;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class NamedThreadFactory implements ThreadFactory {
private static AtomicInteger tag = new AtomicInteger(0);
private String name;
public NamedThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(this.name + "" + tag.getAndIncrement());
return thread;
}
}

View File

@ -2,6 +2,7 @@ package io.metersphere.base.mapper.ext;
import io.metersphere.base.domain.ApiDefinitionExecResult;
import io.metersphere.base.domain.TestPlanApiCase;
import io.metersphere.dto.ExecutedCaseInfoResult;
import io.metersphere.plan.dto.CaseExecResult;
import io.metersphere.plan.dto.TestPlanApiCaseInfoDTO;
import org.apache.ibatis.annotations.Param;
@ -17,6 +18,8 @@ public interface ExtTestPlanApiCaseMapper {
@Select("SELECT id,test_plan_id,api_case_id,status FROM test_plan_api_case WHERE id = #{0} ")
TestPlanApiCase selectBaseInfoById(String testId);
List<ExecutedCaseInfoResult> findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(@Param("projectId") String projectId, @Param("versionId") String version, @Param("startTimestamp") long startTimestamp, @Param("limitNumber") int limitNumber);
List<ApiDefinitionExecResult> selectReportStatusByReportIds(@Param("ids") List<String> apiReportIdList);
}

View File

@ -28,4 +28,42 @@
</foreach>
</select>
<select id="findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber"
resultType="io.metersphere.dto.ExecutedCaseInfoResult">
SELECT * FROM (
SELECT testCase.testPlanCaseID AS testPlanCaseID,
testCase.id AS id,
testCase.testCaseName AS caseName,
testCase.testPlanName AS testPlan,
testCase.testPlanId AS testPlanId,
caseErrorCountData.dataCountNumber AS failureTimes,
'apiCase' AS caseType
FROM (
SELECT testPlanCase.id AS testPlanCaseID,
apiCase.id AS id,
apiCase.`name` AS testCaseName,
testPlan.id AS testPlanId,
testPlan.`name` AS testPlanName
FROM api_test_case apiCase
INNER JOIN test_plan_api_case testPlanCase ON testPlanCase.api_case_id = apiCase.id
INNER JOIN test_plan testPlan ON testPlan.id = testPlanCase.test_plan_id
WHERE
(apiCase.`status` IS NULL OR apiCase.`status` != 'Trash')
AND apiCase.project_id = #{projectId}
) testCase
INNER JOIN (SELECT executionInfo.source_id AS sourceId,
COUNT(executionInfo.id) AS dataCountNumber
FROM api_case_execution_info executionInfo
INNER JOIN test_plan_api_case testPlanCase ON executionInfo.source_id = testPlanCase.id
WHERE executionInfo.`result` = 'ERROR'
AND executionInfo.create_time > #{startTimestamp}
<if test="versionId != null">
AND executionInfo.version = #{versionId}
</if>
GROUP BY source_id
) caseErrorCountData
ON caseErrorCountData.sourceId = testCase.testPlanCaseID
) showTable ORDER BY showTable.failureTimes DESC LIMIT ${limitNumber}
</select>
</mapper>

View File

@ -2,6 +2,7 @@ package io.metersphere.base.mapper.ext;
import io.metersphere.base.domain.ApiScenarioReport;
import io.metersphere.base.domain.TestPlanApiScenario;
import io.metersphere.dto.ExecutedCaseInfoResult;
import io.metersphere.plan.dto.CaseExecResult;
import io.metersphere.plan.dto.TestPlanApiScenarioInfoDTO;
import org.apache.ibatis.annotations.Param;
@ -20,4 +21,6 @@ public interface ExtTestPlanScenarioCaseMapper {
TestPlanApiScenario selectBaseInfoById(String testId);
List<ApiScenarioReport> selectReportStatusByReportIds(@Param("ids") List<String> scenarioReportIdList);
List<ExecutedCaseInfoResult> findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(@Param("projectId") String projectId, @Param("versionId") String version, @Param("startTimestamp") long startTimestamp, @Param("limitNumber") int limitNumber);
}

View File

@ -46,4 +46,32 @@
#{value}
</foreach>
</select>
<select id="findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber"
resultType="io.metersphere.dto.ExecutedCaseInfoResult">
SELECT * FROM (
SELECT scene.id AS testCaseID,
scene.id AS id,
scene.`name` AS caseName,
apiScene.testPlanName AS testPlan,
apiScene.testPlanId AS testPlanId,
count(executionInfo.id) AS failureTimes,
'scenario' AS caseType
FROM scenario_execution_info executionInfo
INNER JOIN (
SELECT testPlanScenario.id, testPlanScenario.api_scenario_id, testPlan.id AS testPlanId, testPlan.`name` AS testPlanName
FROM test_plan_api_scenario testPlanScenario
INNER JOIN test_plan testPlan ON testPlan.id = testPlanScenario.test_plan_id
) apiScene ON apiScene.id = executionInfo.source_id
INNER JOIN api_scenario scene ON scene.id = apiScene.api_scenario_id
WHERE scene.project_id = #{projectId}
AND scene.`status` != 'Trash'
AND ( executionInfo.result = 'ERROR' )
AND executionInfo.create_time >= #{startTimestamp}
<if test="versionId != null">
AND executionInfo.version = #{versionId}
</if>
GROUP BY scene.id,apiScene.testPlanId
) showTable ORDER BY showTable.failureTimes DESC LIMIT ${limitNumber}
</select>
</mapper>

View File

@ -7,6 +7,7 @@ import io.metersphere.plan.request.function.QueryTestPlanCaseRequest;
import io.metersphere.plan.request.function.TestPlanFuncCaseConditions;
import io.metersphere.request.BaseQueryRequest;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@ -76,4 +77,9 @@ public interface ExtTestPlanTestCaseMapper {
void updateExecResultByTestCaseIdAndTestPlanId(@Param("testCaseId") String testCaseId, @Param("testPlanId") String testPlanId, @Param("execResult") String execResult);
List<TestPlanTestCase> selectByAutomationCaseIdAndTestPlanId(@Param("automationCaseId") String automationCaseId, @Param("test_plan_id") String testPlanId);
List<ExecutedCaseInfoResult> findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(@Param("projectId") String projectId, @Param("versionId") String version, @Param("startTimestamp") long startTimestamp, @Param("limitNumber") int limitNumber);
@Select("SELECT id FROM test_plan_test_case WHERE plan_id = #{planId} AND case_id = #{caseId}")
List<String> selectIdByTestCaseIdAndTestPlanId(@Param("caseId") String caseId, @Param("planId") String planId);
}

View File

@ -666,5 +666,36 @@
UPDATE test_plan_test_case SET status = #{execResult}
WHERE case_id = #{testCaseId} AND plan_id = #{testPlanId}
</update>
<select id="findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber"
resultType="io.metersphere.dto.ExecutedCaseInfoResult">
SELECT * FROM (
SELECT
testCase.id AS testCaseID,
testCase.id AS id,
testCase.`name` AS caseName,
testCasePlan.testPlanName AS testPlan,
testCasePlan.testPlanId AS testPlanId,
count( executionInfo.id ) AS failureTimes,
'testCase' AS caseType
FROM
function_case_execution_info executionInfo
INNER JOIN (
SELECT
testPlanTestCase.id,
testPlanTestCase.case_id,
testPlan.id AS testPlanId,
testPlan.`name` AS testPlanName
FROM
test_plan_test_case testPlanTestCase
INNER JOIN test_plan testPlan ON testPlan.id = testPlanTestCase.plan_id
) testCasePlan ON testCasePlan.id = executionInfo.source_id
INNER JOIN test_case testCase ON testCase.id = testCasePlan.case_id
WHERE
testCase.project_id = #{projectId}
AND testCase.`status` != 'Trash'
AND ( executionInfo.result = 'Failure' )
AND executionInfo.create_time >= #{startTimestamp}
GROUP BY testCase.id
) showTable ORDER BY showTable.failureTimes DESC LIMIT ${limitNumber}
</select>
</mapper>

View File

@ -0,0 +1,61 @@
package io.metersphere.config;
import jakarta.annotation.Resource;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import org.springframework.kafka.listener.ContainerProperties;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class KafkaConfig {
@Resource
private KafkaProperties kafkaProperties;
@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, Object>> batchFactory() {
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerConfigs()));
//并发数量
factory.setConcurrency(1);
//开启批量监听
factory.setBatchListener(true);
factory.getContainerProperties().setPollTimeout(5000L);
//设置提交偏移量的方式
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
public Map<String, Object> consumerConfigs() {
Map<String, Object> producerProps = new HashMap<>();
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaProperties.getBootstrapServers());
producerProps.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, kafkaProperties.getMaxRequestSize());
// 批量一次最大拉取数据量
producerProps.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100);
// 消费者每次去kafka拉取数据最大间隔服务端会认为消费者已离线触发reBalance
producerProps.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 900000);
// 心跳检查
producerProps.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 5000);
// 手动提交 配置 false
producerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
producerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
producerProps.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);
producerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
producerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return producerProps;
}
}

View File

@ -1,23 +1,27 @@
package io.metersphere.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.TestCase;
import io.metersphere.commons.constants.MicroServiceName;
import io.metersphere.dto.BugStatistics;
import io.metersphere.dto.TrackCountResult;
import io.metersphere.dto.TrackStatisticsDTO;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.dto.*;
import io.metersphere.i18n.Translator;
import io.metersphere.plan.dto.ChartsData;
import io.metersphere.service.TestCaseService;
import io.metersphere.service.TrackService;
import io.metersphere.utils.DiscoveryUtil;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
@RestController
@ -58,6 +62,32 @@ public class TrackController {
return statistics;
}
@GetMapping("/failure/case/about/plan/{projectId}/{versionId}/{limitNumber}/{goPage}/{pageSize}")
public Pager<List<ExecutedCaseInfoDTO>> failureCaseAboutTestPlan(@PathVariable String projectId, @PathVariable String versionId,
@PathVariable int limitNumber, @PathVariable int goPage, @PathVariable int pageSize) {
if (StringUtils.equalsIgnoreCase(versionId, "default")) {
versionId = null;
}
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
List<ExecutedCaseInfoResult> selectDataList = trackService.findFailureCaseInfoByProjectIDAndLimitNumberInSevenDays(projectId, versionId, limitNumber);
List<ExecutedCaseInfoDTO> returnList = new ArrayList<>(selectDataList.size());
for (int dataIndex = 0; dataIndex < selectDataList.size(); dataIndex++) {
ExecutedCaseInfoDTO dataDTO = new ExecutedCaseInfoDTO();
dataDTO.setSortIndex(dataIndex + 1);
ExecutedCaseInfoResult selectData = selectDataList.get(dataIndex);
dataDTO.setCaseID(selectData.getTestCaseID());
dataDTO.setCaseName(selectData.getCaseName());
dataDTO.setTestPlan(selectData.getTestPlan());
dataDTO.setFailureTimes(selectData.getFailureTimes());
dataDTO.setTestPlanId(selectData.getTestPlanId());
dataDTO.setCaseType(selectData.getCaseType());
dataDTO.setId(selectData.getId());
dataDTO.setTestPlanDTOList(selectData.getTestPlanDTOList());
returnList.add(dataDTO);
}
return PageUtils.setPageInfo(page, returnList);
}
@GetMapping("/relevance/count/{projectId}")
public TrackStatisticsDTO getRelevanceCount(@PathVariable String projectId) {
TrackStatisticsDTO statistics = new TrackStatisticsDTO();

View File

@ -0,0 +1,31 @@
package io.metersphere.dto;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 已执行的案例
*/
@Getter
@Setter
public class ExecutedCaseInfoDTO {
//排名
private int sortIndex;
//案例名称
private String caseName;
//所属测试计划
private String testPlan;
private String testPlanId;
//失败次数
private Long failureTimes;
//案例类型
private String caseType;
//案例ID -- 目前被用为案例-测试计划 关联表ID
private String caseID;
//ID
private String id;
//测试计划集合
private List<TestPlanDTO> testPlanDTOList;
}

View File

@ -0,0 +1,26 @@
package io.metersphere.dto;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 已执行的案例
*/
@Getter
@Setter
public class ExecutedCaseInfoResult {
private String testCaseID;
private String id;
//案例名称
private String caseName;
//所属测试计划
private String testPlan;
private String testPlanId;
//失败次数
private Long failureTimes;
//案例类型
private String caseType;
private List<TestPlanDTO> testPlanDTOList;
}

View File

@ -0,0 +1,38 @@
package io.metersphere.dto;
import io.metersphere.base.domain.TestPlanWithBLOBs;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class TestPlanDTO extends TestPlanWithBLOBs {
private String projectName;
private String userName;
private List<String> projectIds;
private List<String> principals;
private List<String> follows;
/**
* 定时任务ID
*/
private String scheduleId;
/**
* 定时任务是否开启
*/
private boolean scheduleOpen;
/**
* 定时任务状态
*/
private String scheduleStatus;
/**
* 定时任务规则
*/
private String scheduleCorn;
/**
* 定时任务下一次执行时间
*/
private Long scheduleExecuteTime;
private String workspaceName;
}

View File

@ -1,24 +1,22 @@
package io.metersphere.listener;
import io.metersphere.base.domain.ApiExecutionQueue;
import io.metersphere.base.domain.ApiExecutionQueueDetailExample;
import io.metersphere.base.domain.ApiExecutionQueueExample;
import io.metersphere.base.mapper.ApiExecutionQueueDetailMapper;
import io.metersphere.base.mapper.ApiExecutionQueueMapper;
import io.metersphere.commons.constants.KafkaTopicConstants;
import io.metersphere.commons.constants.TestPlanReportStatus;
import io.metersphere.plan.service.AutomationCaseExecOverService;
import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.utils.LoggerUtil;
import io.metersphere.utils.NamedThreadFactory;
import jakarta.annotation.Resource;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List;
import java.util.stream.Collectors;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Component
public class ExecReportListener {
@ -32,45 +30,40 @@ public class ExecReportListener {
@Resource
private AutomationCaseExecOverService automationCaseExecOverService;
@KafkaListener(id = CONSUME_ID, topics = KafkaTopicConstants.TEST_PLAN_REPORT_TOPIC, groupId = "${spring.application.name}")
public void consume(ConsumerRecord<?, String> record) {
Object testIdObj = record.key();
if (ObjectUtils.isEmpty(testIdObj)) {
LoggerUtil.info("Execute message. received", record.value());
this.testPlanReportTestEnded(record.value());
} else {
LoggerUtil.info("Execute message. key:[" + testIdObj.toString() + "], received", record.value());
this.automationCaseTestEnd(testIdObj.toString());
}
// 线程池维护线程的最少数量
private final static int CORE_POOL_SIZE = 5;
// 线程池维护线程的最大数量
private final static int MAX_POOL_SIZE = 5;
// 线程池维护线程所允许的空闲时间
private final static int KEEP_ALIVE_TIME = 1;
// 线程池所使用的缓冲队列大小
private final static int WORK_QUEUE_SIZE = 10000;
private final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue(WORK_QUEUE_SIZE),
new NamedThreadFactory("MS-TEST-PLAN-CASE-EXEC-LISTENER-TASK"));
}
@KafkaListener(id = CONSUME_ID, topics = KafkaTopicConstants.TEST_PLAN_REPORT_TOPIC, groupId = "${spring.application.name}", containerFactory = "batchFactory")
public void consume(List<ConsumerRecord<?, String>> records, Acknowledgment ack) {
public void automationCaseTestEnd(String testId) {
//自动化用例执行完成之后的后续操作
automationCaseExecOverService.automationCaseExecOver(testId);
}
public void testPlanReportTestEnded(String testPlanReportId) {
// 检查测试计划中其他队列是否结束
ApiExecutionQueueExample executionQueueExample = new ApiExecutionQueueExample();
executionQueueExample.createCriteria().andReportIdEqualTo(testPlanReportId);
List<ApiExecutionQueue> queues = queueMapper.selectByExample(executionQueueExample);
if (CollectionUtils.isEmpty(queues)) {
LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
} else {
List<String> ids = queues.stream().map(ApiExecutionQueue::getId).collect(Collectors.toList());
ApiExecutionQueueDetailExample detailExample = new ApiExecutionQueueDetailExample();
detailExample.createCriteria().andQueueIdIn(ids);
long count = executionQueueDetailMapper.countByExample(detailExample);
if (count == 0) {
LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
LoggerUtil.info("Clear Queue" + ids);
ApiExecutionQueueExample queueExample = new ApiExecutionQueueExample();
queueExample.createCriteria().andIdIn(ids);
queueMapper.deleteByExample(queueExample);
}
try {
records.forEach(item -> {
LoggerUtil.info("接收到报告【key:" + item.key() + ",value:" + item.value() + "】,加入到结果处理队列");
ExecReportListenerTask task = new ExecReportListenerTask();
task.setApiExecutionQueueMapper(queueMapper);
task.setApiExecutionQueueDetailMapper(executionQueueDetailMapper);
task.setAutomationCaseExecOverService(automationCaseExecOverService);
task.setTestPlanReportService(testPlanReportService);
task.setRecord(item);
threadPool.execute(task);
});
} catch (Exception e) {
LoggerUtil.error("测试计划自动化用例结束后-KAFKA消费失败", e);
} finally {
ack.acknowledge();
}
}
}

View File

@ -0,0 +1,71 @@
package io.metersphere.listener;
import io.metersphere.base.domain.ApiExecutionQueue;
import io.metersphere.base.domain.ApiExecutionQueueDetailExample;
import io.metersphere.base.domain.ApiExecutionQueueExample;
import io.metersphere.base.mapper.ApiExecutionQueueDetailMapper;
import io.metersphere.base.mapper.ApiExecutionQueueMapper;
import io.metersphere.commons.constants.TestPlanReportStatus;
import io.metersphere.plan.service.AutomationCaseExecOverService;
import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.utils.LoggerUtil;
import lombok.Data;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List;
import java.util.stream.Collectors;
@Data
public class ExecReportListenerTask implements Runnable {
private ConsumerRecord<?, String> record;
protected ApiExecutionQueueMapper apiExecutionQueueMapper;
private ApiExecutionQueueDetailMapper apiExecutionQueueDetailMapper;
private TestPlanReportService testPlanReportService;
private AutomationCaseExecOverService automationCaseExecOverService;
@Override
public void run() {
Object testIdObj = record.key();
if (ObjectUtils.isEmpty(testIdObj)) {
LoggerUtil.info("Execute message. received", record.value());
this.testPlanReportTestEnded(record.value());
} else {
LoggerUtil.info("Execute message. key:[" + testIdObj.toString() + "], received", record.value());
this.automationCaseTestEnd(testIdObj.toString());
}
}
public void automationCaseTestEnd(String testId) {
//自动化用例执行完成之后的后续操作
automationCaseExecOverService.automationCaseExecOver(testId);
}
public void testPlanReportTestEnded(String testPlanReportId) {
// 检查测试计划中其他队列是否结束
ApiExecutionQueueExample executionQueueExample = new ApiExecutionQueueExample();
executionQueueExample.createCriteria().andReportIdEqualTo(testPlanReportId);
List<ApiExecutionQueue> queues = apiExecutionQueueMapper.selectByExample(executionQueueExample);
if (CollectionUtils.isEmpty(queues)) {
LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
} else {
List<String> ids = queues.stream().map(ApiExecutionQueue::getId).collect(Collectors.toList());
ApiExecutionQueueDetailExample detailExample = new ApiExecutionQueueDetailExample();
detailExample.createCriteria().andQueueIdIn(ids);
long count = apiExecutionQueueDetailMapper.countByExample(detailExample);
if (count == 0) {
LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
LoggerUtil.info("Clear Queue" + ids);
ApiExecutionQueueExample queueExample = new ApiExecutionQueueExample();
queueExample.createCriteria().andIdIn(ids);
apiExecutionQueueMapper.deleteByExample(queueExample);
}
}
}
}

View File

@ -4,9 +4,13 @@ import io.metersphere.base.domain.TestPlanApiCase;
import io.metersphere.base.domain.TestPlanApiScenario;
import io.metersphere.base.domain.TestPlanLoadCase;
import io.metersphere.base.domain.TestPlanUiScenario;
import io.metersphere.base.mapper.ext.*;
import io.metersphere.base.mapper.ext.ExtTestPlanApiCaseMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanLoadCaseMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanScenarioCaseMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanUiCaseMapper;
import io.metersphere.dto.TestPlanCaseStatusDTO;
import io.metersphere.utils.JsonUtils;
import io.metersphere.utils.LoggerUtil;
import io.metersphere.websocket.UICaseStatusHandleSocket;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
@ -71,8 +75,13 @@ public class AutomationCaseExecOverService {
testCaseSyncStatusService.updateFunctionCaseStatusByAutomationCaseId(automationCaseId, planId, triggerCaseExecResult);
}
if (testPlanUiScenario != null) {
//UI执行完成发送Socket
UICaseStatusHandleSocket.sendMessageSingle(planId, JsonUtils.toJSONString(TestPlanCaseStatusDTO.builder().planCaseId(testPlanUiScenario.getId()).planCaseStatus(triggerCaseExecResult)));
try {
//UI执行完成发送Socket
UICaseStatusHandleSocket.sendMessageSingle(planId, JsonUtils.toJSONString(TestPlanCaseStatusDTO.builder().planCaseId(testPlanUiScenario.getId()).planCaseStatus(triggerCaseExecResult)));
} catch (Exception e) {
LoggerUtil.error("ui执行完成发送socket失败", e);
}
}
}
}

View File

@ -16,11 +16,14 @@ import io.metersphere.plan.enums.FunctionCaseExecResult;
import io.metersphere.plan.enums.TestCaseReleevanceType;
import io.metersphere.plan.utils.TestCaseSyncStatusUtil;
import io.metersphere.service.BaseUserService;
import io.metersphere.service.FunctionCaseExecutionInfoService;
import io.metersphere.utils.BatchProcessingUtil;
import io.metersphere.utils.LoggerUtil;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -49,6 +52,8 @@ public class TestCaseSyncStatusService {
private BaseUserService baseUserService;
@Resource
private TestCaseCommentMapper testCaseCommentMapper;
@Resource
private FunctionCaseExecutionInfoService functionCaseExecutionInfoService;
//通过自动化用例的状态获取最新的功能用例状态
public void getTestCaseStatusByTestPlanExecuteOver(List<PlanReportCaseDTO> testPlanCaseList, List<TestPlanApiDTO> apiAllCases, List<TestPlanScenarioDTO> scenarioAllCases, List<TestPlanLoadCaseDTO> loadAllCases, List<TestPlanUiScenarioDTO> uiAllCases) {
@ -100,64 +105,72 @@ public class TestCaseSyncStatusService {
}
if (MapUtils.isNotEmpty(successCaseMap)) {
extTestPlanTestCaseMapper.updateExecResultByTestPlanCaseIdList(new ArrayList<>(successCaseMap.keySet()), FunctionCaseExecResult.SUCCESS.toString());
functionCaseExecutionInfoService.insertExecutionInfoByIdList(new ArrayList<>(successCaseMap.keySet()), FunctionCaseExecResult.SUCCESS.toString());
}
if (MapUtils.isNotEmpty(errorCaseMap)) {
extTestPlanTestCaseMapper.updateExecResultByTestPlanCaseIdList(new ArrayList<>(errorCaseMap.keySet()), FunctionCaseExecResult.ERROR.toString());
functionCaseExecutionInfoService.insertExecutionInfoByIdList(new ArrayList<>(errorCaseMap.keySet()), FunctionCaseExecResult.ERROR.toString());
}
if (MapUtils.isNotEmpty(blockingCaseMap)) {
extTestPlanTestCaseMapper.updateExecResultByTestPlanCaseIdList(new ArrayList<>(blockingCaseMap.keySet()), FunctionCaseExecResult.BLOCKING.toString());
functionCaseExecutionInfoService.insertExecutionInfoByIdList(new ArrayList<>(blockingCaseMap.keySet()), FunctionCaseExecResult.BLOCKING.toString());
this.addTestCaseComment(operator, testPlanName, blockingCaseMap, FunctionCaseExecResult.BLOCKING.toString());
}
}
}
@Async
public void updateFunctionCaseStatusByAutomationCaseId(String automationCaseId, String testPlanId, String triggerCaseRunResult) {
TestPlan testPlan = testPlanMapper.selectByPrimaryKey(testPlanId);
if (testPlan != null && testPlan.getAutomaticStatusUpdate()) {
HttpHeaderUtils.runAsUser(baseUserService.getUserDTO(testPlan.getCreator()));
try {
TestPlan testPlan = testPlanMapper.selectByPrimaryKey(testPlanId);
if (testPlan != null && testPlan.getAutomaticStatusUpdate()) {
HttpHeaderUtils.runAsUser(baseUserService.getUserDTO(testPlan.getCreator()));
Set<String> testCaseIdSet = new HashSet<>();
List<TestPlanTestCase> testPlanTestCaseList = extTestPlanTestCaseMapper.selectByAutomationCaseIdAndTestPlanId(automationCaseId, testPlanId);
testPlanTestCaseList.forEach(item -> testCaseIdSet.add(item.getCaseId()));
Set<String> testCaseIdSet = new HashSet<>();
List<TestPlanTestCase> testPlanTestCaseList = extTestPlanTestCaseMapper.selectByAutomationCaseIdAndTestPlanId(automationCaseId, testPlanId);
testPlanTestCaseList.forEach(item -> testCaseIdSet.add(item.getCaseId()));
if (CollectionUtils.isNotEmpty(testCaseIdSet)) {
TestCaseTestExample testCaseTestExample = new TestCaseTestExample();
testCaseTestExample.createCriteria().andTestCaseIdIn(new ArrayList<>(testCaseIdSet));
List<TestCaseTest> testCaseTestList = testCaseTestMapper.selectByExample(testCaseTestExample);
Map<String, List<TestCaseTest>> testCaseTestMap = testCaseTestList.stream().collect(Collectors.groupingBy(TestCaseTest::getTestCaseId));
if (CollectionUtils.isNotEmpty(testCaseIdSet)) {
TestCaseTestExample testCaseTestExample = new TestCaseTestExample();
testCaseTestExample.createCriteria().andTestCaseIdIn(new ArrayList<>(testCaseIdSet));
List<TestCaseTest> testCaseTestList = testCaseTestMapper.selectByExample(testCaseTestExample);
Map<String, List<TestCaseTest>> testCaseTestMap = testCaseTestList.stream().collect(Collectors.groupingBy(TestCaseTest::getTestCaseId));
for (Map.Entry<String, List<TestCaseTest>> entry : testCaseTestMap.entrySet()) {
TestCaseRelevanceCasesRequest request = this.generateRelevanceCasesRequest(testPlanId, entry.getKey(), entry.getValue());
CaseExecResult priorityResult = TestCaseSyncStatusUtil.getTestCaseExecResultByRelevance(request.getTestCaseTestList(),
request.getApiAllCaseMap(), request.getScenarioAllCaseMap(), request.getLoadAllCaseMap(), request.getUiAllCaseMap());
if (priorityResult == null) {
continue;
}
for (Map.Entry<String, List<TestCaseTest>> entry : testCaseTestMap.entrySet()) {
TestCaseRelevanceCasesRequest request = this.generateRelevanceCasesRequest(testPlanId, entry.getKey(), entry.getValue());
CaseExecResult priorityResult = TestCaseSyncStatusUtil.getTestCaseExecResultByRelevance(request.getTestCaseTestList(),
request.getApiAllCaseMap(), request.getScenarioAllCaseMap(), request.getLoadAllCaseMap(), request.getUiAllCaseMap());
if (priorityResult == null) {
continue;
}
String automationCaseResult = null;
if (priorityResult != null && StringUtils.isNotEmpty(priorityResult.getExecResult())) {
if (StringUtils.equalsIgnoreCase(ApiReportStatus.ERROR.name(), priorityResult.getExecResult())) {
automationCaseResult = ApiReportStatus.ERROR.name();
priorityResult.setExecResult(FunctionCaseExecResult.ERROR.toString());
} else if (StringUtils.equalsIgnoreCase(ApiReportStatus.FAKE_ERROR.name(), priorityResult.getExecResult())) {
automationCaseResult = ApiReportStatus.FAKE_ERROR.name();
priorityResult.setExecResult(FunctionCaseExecResult.BLOCKING.toString());
} else if (StringUtils.equalsIgnoreCase(ApiReportStatus.SUCCESS.name(), priorityResult.getExecResult())) {
priorityResult.setExecResult(FunctionCaseExecResult.SUCCESS.toString());
String automationCaseResult = null;
if (priorityResult != null && StringUtils.isNotEmpty(priorityResult.getExecResult())) {
if (StringUtils.equalsIgnoreCase(ApiReportStatus.ERROR.name(), priorityResult.getExecResult())) {
automationCaseResult = ApiReportStatus.ERROR.name();
priorityResult.setExecResult(FunctionCaseExecResult.ERROR.toString());
} else if (StringUtils.equalsIgnoreCase(ApiReportStatus.FAKE_ERROR.name(), priorityResult.getExecResult())) {
automationCaseResult = ApiReportStatus.FAKE_ERROR.name();
priorityResult.setExecResult(FunctionCaseExecResult.BLOCKING.toString());
} else if (StringUtils.equalsIgnoreCase(ApiReportStatus.SUCCESS.name(), priorityResult.getExecResult())) {
priorityResult.setExecResult(FunctionCaseExecResult.SUCCESS.toString());
}
}
//通过 triggerCaseRunResult(触发操作的用例的执行结果) 进行判断会不会直接影响最终结果如果是在改变功能用例状态时也要增加一条评论
extTestPlanTestCaseMapper.updateExecResultByTestCaseIdAndTestPlanId(entry.getKey(), testPlanId, priorityResult.getExecResult());
//记录功能用例执行信息
functionCaseExecutionInfoService.insertExecutionInfoByCaseIdAndPlanId(entry.getKey(), testPlanId, priorityResult.getExecResult());
if (StringUtils.equalsIgnoreCase(triggerCaseRunResult, automationCaseResult) && !StringUtils.equalsIgnoreCase(triggerCaseRunResult, ApiReportStatus.SUCCESS.name())) {
this.addTestCaseComment(testPlan.getCreator(), testPlan.getName(), entry.getKey(), priorityResult.getCaseName(), FunctionCaseExecResult.BLOCKING.toString());
}
}
//通过 triggerCaseRunResult(触发操作的用例的执行结果) 进行判断会不会直接影响最终结果如果是在改变功能用例状态时也要增加一条评论
extTestPlanTestCaseMapper.updateExecResultByTestCaseIdAndTestPlanId(entry.getKey(), testPlanId, priorityResult.getExecResult());
if (StringUtils.equalsIgnoreCase(triggerCaseRunResult, automationCaseResult) && !StringUtils.equalsIgnoreCase(triggerCaseRunResult, ApiReportStatus.SUCCESS.name())) {
this.addTestCaseComment(testPlan.getCreator(), testPlan.getName(), entry.getKey(), priorityResult.getCaseName(), FunctionCaseExecResult.BLOCKING.toString());
}
}
HttpHeaderUtils.clearUser();
}
HttpHeaderUtils.clearUser();
} catch (Exception e) {
LoggerUtil.error("更新功能用例状态出错!", e);
}
}

View File

@ -14,6 +14,7 @@ import io.metersphere.excel.constants.TestPlanTestCaseStatus;
import io.metersphere.i18n.Translator;
import io.metersphere.log.vo.OperatingLogDetails;
import io.metersphere.plan.constant.ApiReportStatus;
import io.metersphere.plan.dto.TestPlanDTO;
import io.metersphere.plan.dto.*;
import io.metersphere.plan.request.QueryTestPlanRequest;
import io.metersphere.plan.request.TestPlanReportSaveRequest;

View File

@ -22,6 +22,7 @@ import io.metersphere.log.utils.ReflexObjectUtil;
import io.metersphere.log.vo.DetailColumn;
import io.metersphere.log.vo.OperatingLogDetails;
import io.metersphere.log.vo.track.TestPlanReference;
import io.metersphere.plan.dto.TestPlanDTO;
import io.metersphere.plan.dto.*;
import io.metersphere.plan.job.TestPlanTestJob;
import io.metersphere.plan.request.AddTestPlanRequest;

View File

@ -3,12 +3,13 @@ package io.metersphere.service;
import io.metersphere.base.domain.FunctionCaseExecutionInfo;
import io.metersphere.base.domain.FunctionCaseExecutionInfoExample;
import io.metersphere.base.mapper.FunctionCaseExecutionInfoMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanTestCaseMapper;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.UUID;
@ -17,6 +18,8 @@ import java.util.UUID;
public class FunctionCaseExecutionInfoService {
@Resource
private FunctionCaseExecutionInfoMapper functionCaseExecutionInfoMapper;
@Resource
private ExtTestPlanTestCaseMapper extTestPlanTestCaseMapper;
public void insertExecutionInfo(String caseId, String result) {
if (!StringUtils.isAnyEmpty(caseId, result)) {
@ -29,6 +32,23 @@ public class FunctionCaseExecutionInfoService {
}
}
public void insertExecutionInfoByIdList(List<String> caseIdList, String result) {
if (CollectionUtils.isNotEmpty(caseIdList)) {
caseIdList.forEach(item -> {
this.insertExecutionInfo(item, result);
});
}
}
public void insertExecutionInfoByCaseIdAndPlanId(String caseId, String planId, String result) {
if (!StringUtils.isAnyEmpty(caseId, planId, result)) {
List<String> testPlanTestCaseIdList = extTestPlanTestCaseMapper.selectIdByTestCaseIdAndTestPlanId(caseId, planId);
testPlanTestCaseIdList.forEach(item -> {
this.insertExecutionInfo(item, result);
});
}
}
public void deleteBySourceIdList(List<String> ids) {
if (CollectionUtils.isNotEmpty(ids)) {
FunctionCaseExecutionInfoExample example = new FunctionCaseExecutionInfoExample();

View File

@ -3,18 +3,16 @@ package io.metersphere.service;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.CustomField;
import io.metersphere.base.domain.TestPlan;
import io.metersphere.base.domain.TestPlanExample;
import io.metersphere.base.mapper.TestPlanMapper;
import io.metersphere.base.mapper.ext.ExtIssuesMapper;
import io.metersphere.base.mapper.ext.ExtTestCaseMapper;
import io.metersphere.base.mapper.ext.*;
import io.metersphere.commons.constants.CustomFieldScene;
import io.metersphere.commons.utils.DateUtils;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.constants.IssueStatus;
import io.metersphere.constants.SystemCustomField;
import io.metersphere.dto.BugStatistics;
import io.metersphere.dto.TestPlanBugCount;
import io.metersphere.dto.ExecutedCaseInfoResult;
import io.metersphere.dto.TestPlanDTOWithMetric;
import io.metersphere.dto.TrackCountResult;
import io.metersphere.i18n.Translator;
@ -23,13 +21,13 @@ import io.metersphere.plan.service.TestPlanService;
import io.metersphere.request.testcase.TrackCount;
import io.metersphere.xpack.track.dto.IssuesDao;
import io.metersphere.xpack.track.dto.request.IssuesRequest;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.*;
@ -49,7 +47,12 @@ public class TrackService {
private BaseCustomFieldService baseCustomFieldService;
@Resource
private TestPlanService testPlanService;
@Resource
private ExtTestPlanTestCaseMapper extTestPlanTestCaseMapper;
@Resource
private ExtTestPlanApiCaseMapper extTestPlanApiCaseMapper;
@Resource
private ExtTestPlanScenarioCaseMapper extTestPlanScenarioCaseMapper;
@Resource
private ExtIssuesMapper extIssuesMapper;
@ -304,4 +307,40 @@ public class TrackService {
testPlanService.calcTestPlanRate(testPlan);
return testPlan.getPassRate();
}
public List<ExecutedCaseInfoResult> findFailureCaseInfoByProjectIDAndLimitNumberInSevenDays(String projectId, String versionId, int limitNumber) {
//获取7天之前的日期
Date startDay = DateUtils.dateSum(new Date(), -6);
//将日期转化为 00:00:00 的时间戳
Date startTime = null;
try {
startTime = DateUtils.getDayStartTime(startDay);
} catch (Exception e) {
LogUtil.error("解析日期出错!", e);
}
if (startTime == null) {
return new ArrayList<>(0);
} else {
List<ExecutedCaseInfoResult> returnList = new ArrayList<>(limitNumber);
ArrayList<ExecutedCaseInfoResult> allCaseExecList = new ArrayList<>();
allCaseExecList.addAll(extTestPlanTestCaseMapper.findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(projectId, versionId, startTime.getTime(), limitNumber));
allCaseExecList.addAll(extTestPlanApiCaseMapper.findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(projectId, versionId, startTime.getTime(), limitNumber));
allCaseExecList.addAll(extTestPlanScenarioCaseMapper.findFailureCaseInTestPlanByProjectIDAndExecuteTimeAndLimitNumber(projectId, versionId, startTime.getTime(), limitNumber));
if (CollectionUtils.isNotEmpty(allCaseExecList)) {
allCaseExecList.sort(Comparator.comparing(ExecutedCaseInfoResult::getFailureTimes).reversed());
for (int i = 0; i < allCaseExecList.size(); i++) {
if (i < limitNumber) {
ExecutedCaseInfoResult item = allCaseExecList.get(i);
returnList.add(item);
} else {
break;
}
}
}
return returnList;
}
}
}

View File

@ -0,0 +1,20 @@
package io.metersphere.utils;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class NamedThreadFactory implements ThreadFactory {
private static AtomicInteger tag = new AtomicInteger(0);
private String name;
public NamedThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(this.name + "" + tag.getAndIncrement());
return thread;
}
}

View File

@ -1,16 +0,0 @@
import { get } from "@/business/utils/sdk-utils";
const BASE_URL = "/home/";
export function homeTestPlanFailureCaseGet(
projectId,
selectFunctionCase,
limitNumber,
currentPage,
pageSize
) {
return get(
BASE_URL +
`failure/case/about/plan/${projectId}/default/${selectFunctionCase}/${limitNumber}/${currentPage}/${pageSize}`
);
}

View File

@ -1,9 +1,20 @@
import {post, get} from "metersphere-frontend/src/plugins/request";
import { get, post } from "metersphere-frontend/src/plugins/request";
export function getTrackCount(selectProjectId) {
return get("/track/count/" + selectProjectId);
}
export function homeTestPlanFailureCaseGet(
projectId,
limitNumber,
currentPage,
pageSize
) {
return get(
`/track/failure/case/about/plan/${projectId}/default/${limitNumber}/${currentPage}/${pageSize}`
);
}
export function getTrackRelevanceCount(selectProjectId) {
return get("/track/relevance/count/" + selectProjectId);
}
@ -12,8 +23,21 @@ export function getTrackCaseBar(selectProjectId) {
return get("/track/case/bar/" + selectProjectId);
}
export function getTrackRunningTask(selectProjectId, currentPage, pageSize, param) {
return post("/task/center/runningTask/" + selectProjectId + "/" + currentPage + "/" + pageSize, param);
export function getTrackRunningTask(
selectProjectId,
currentPage,
pageSize,
param
) {
return post(
"/task/center/runningTask/" +
selectProjectId +
"/" +
currentPage +
"/" +
pageSize,
param
);
}
export function getTrackBugCount(selectProjectId) {
@ -21,9 +45,10 @@ export function getTrackBugCount(selectProjectId) {
}
export function formatNumber(param) {
let num = (param || 0).toString(), result = '';
let num = (param || 0).toString(),
result = "";
while (num.length > 3) {
result = ',' + num.slice(-3) + result;
result = "," + num.slice(-3) + result;
num = num.slice(0, num.length - 3);
}
if (num) {
@ -31,4 +56,3 @@ export function formatNumber(param) {
}
return result;
}

View File

@ -23,10 +23,7 @@
<el-row style="margin-top: 16px">
<el-col style="background-color: #ffffff">
<ms-failure-test-case-list
:select-function-case="true"
@redirectPage="redirectPage"
/>
<ms-failure-test-case-list @redirectPage="redirectPage" />
</el-col>
</el-row>

View File

@ -1,32 +1,64 @@
<template>
<div style="margin: 24px" class="failure-case-table">
<span class="table-title">
{{ $t('api_test.home_page.failed_case_list.title') }}
{{ $t("api_test.home_page.failed_case_list.title") }}
</span>
<div style="margin-top: 16px" v-loading="loading" element-loading-background="#FFFFFF">
<div v-show="loadError"
style="width: 100%; height: 300px; display: flex; flex-direction: column; justify-content: center;align-items: center">
<img style="height: 100px;width: 100px;"
src="/assets/module/figma/icon_load_error.svg"/>
<span class="addition-info-title" style="color: #646A73">{{ $t("home.dashboard.public.load_error") }}</span>
<div
style="margin-top: 16px"
v-loading="loading"
element-loading-background="#FFFFFF"
>
<div
v-show="loadError"
style="
width: 100%;
height: 300px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
"
>
<img
style="height: 100px; width: 100px"
src="/assets/module/figma/icon_load_error.svg"
/>
<span class="addition-info-title" style="color: #646a73">{{
$t("home.dashboard.public.load_error")
}}</span>
</div>
<div v-show="!loadError">
<el-table :data="tableData" class="adjust-table table-content"
header-cell-class-name="home-table-cell" style="min-height: 228px">
<el-table
:data="tableData"
class="adjust-table table-content"
header-cell-class-name="home-table-cell"
style="min-height: 228px"
>
<el-table-column
type="index"
:label="$t('home.case.index')"
show-overflow-tooltip
width="100px"/>
width="100px"
/>
<el-table-column
prop="caseName"
:label="$t('home.case.case_name')"
min-width="200px">
<template v-slot:default="{row}">
<el-link style="color: #783887; width: 100%;" :underline="false" type="info" @click="redirect(row.caseType,row.id)"
:disabled="(row.caseType === 'apiCase' && apiCaseReadOnly) || (row.caseType === 'scenario' && apiScenarioReadOnly) ||
(row.caseType === 'load' && loadCaseReadOnly) || (row.caseType === 'testCase' && testCaseReadOnly)">
min-width="200px"
>
<template v-slot:default="{ row }">
<el-link
style="color: #783887; width: 100%"
:underline="false"
type="info"
@click="redirect(row.caseType, row.id)"
:disabled="
(row.caseType === 'apiCase' && apiCaseReadOnly) ||
(row.caseType === 'scenario' && apiScenarioReadOnly) ||
(row.caseType === 'load' && loadCaseReadOnly) ||
(row.caseType === 'testCase' && testCaseReadOnly)
"
>
{{ row.caseName }}
</el-link>
</template>
@ -37,18 +69,28 @@
:label="$t('home.case.case_type')"
show-overflow-tooltip
column-key="caseType"
width="150px">
width="150px"
>
<template v-slot:default="scope">
<basic-case-type-label :value="scope.row.caseType"></basic-case-type-label>
<basic-case-type-label
:value="scope.row.caseType"
></basic-case-type-label>
</template>
</el-table-column>
<el-table-column
prop="testPlan"
:label="$t('home.case.test_plan')"
width="300px">
<template v-slot:default="{row}">
<el-link style="color: #783887; width: 100%;" :underline="false" type="info" @click="redirect('testPlanEdit',row.testPlanId)" v-permission-disable="['PROJECT_TRACK_PLAN:READ']">
width="300px"
>
<template v-slot:default="{ row }">
<el-link
style="color: #783887; width: 100%"
:underline="false"
type="info"
@click="redirect('testPlanEdit', row.testPlanId)"
v-permission-disable="['PROJECT_TRACK_PLAN:READ']"
>
{{ row.testPlan }}
</el-link>
</template>
@ -58,19 +100,38 @@
prop="failureTimes"
:label="$t('home.case.failure_times')"
show-overflow-tooltip
width="350px"/>
width="350px"
/>
<template #empty>
<div
style="width: 100%;height: 238px;display: flex;flex-direction: column;justify-content: center;align-items: center">
<img style="height: 100px;width: 100px;margin-bottom: 8px"
src="/assets/module/figma/icon_none.svg"/>
<span class="addition-info-title">{{ $t("home.dashboard.public.no_data") }}</span>
style="
width: 100%;
height: 238px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
"
>
<img
style="height: 100px; width: 100px; margin-bottom: 8px"
src="/assets/module/figma/icon_none.svg"
/>
<span class="addition-info-title">{{
$t("home.dashboard.public.no_data")
}}</span>
</div>
</template>
</el-table>
<home-pagination v-if="tableData.length > 0" :change="search" :current-page.sync="currentPage" :page-size.sync="pageSize" layout="prev, pager, next, sizes"
:total="total"/>
<home-pagination
v-if="tableData.length > 0"
:change="search"
:current-page.sync="currentPage"
:page-size.sync="pageSize"
layout="prev, pager, next, sizes"
:total="total"
/>
</div>
</div>
</div>
@ -78,11 +139,11 @@
<script>
import MsTag from "metersphere-frontend/src/components/MsTag";
import {getCurrentProjectID} from "metersphere-frontend/src/utils/token";
import {homeTestPlanFailureCaseGet} from "@/api/remote/api/api-home";
import {hasPermission} from "@/business/utils/sdk-utils";
import { getCurrentProjectID } from "metersphere-frontend/src/utils/token";
import { hasPermission } from "@/business/utils/sdk-utils";
import HomePagination from "@/business/home/components/pagination/HomePagination";
import BasicCaseTypeLabel from "metersphere-frontend/src/components/BasicCaseTypeLabel";
import { homeTestPlanFailureCaseGet } from "@/api/track";
export default {
name: "MsFailureTestCaseList",
@ -101,10 +162,7 @@ export default {
currentPage: 1,
pageSize: 5,
total: 0,
}
},
props: {
selectFunctionCase: Boolean,
};
},
computed: {
projectId() {
@ -116,13 +174,19 @@ export default {
if (this.projectId) {
this.loading = true;
this.loadError = false;
homeTestPlanFailureCaseGet(this.projectId, this.selectFunctionCase, 10, this.currentPage, this.pageSize)
homeTestPlanFailureCaseGet(
this.projectId,
10,
this.currentPage,
this.pageSize
)
.then((r) => {
this.loading = false;
this.loadError = false;
this.total = r.data.itemCount;
this.tableData = r.data.listObject;
}).catch(() => {
})
.catch(() => {
this.loading = false;
this.loadError = true;
});
@ -131,28 +195,33 @@ export default {
redirect(pageType, param) {
switch (pageType) {
case "testPlanEdit":
this.$emit('redirectPage', 'testPlanEdit', null, param);
this.$emit("redirectPage", "testPlanEdit", null, param);
break;
case "apiCase":
this.$emit('redirectPage', 'api', 'apiTestCase', 'single:' + param);
this.$emit("redirectPage", "api", "apiTestCase", "single:" + param);
break;
case "scenario":
this.$emit('redirectPage', 'scenarioWithQuery', 'scenario', 'edit:' + param);
this.$emit(
"redirectPage",
"scenarioWithQuery",
"scenario",
"edit:" + param
);
break;
case "testCase":
this.$emit('redirectPage', 'testCase', 'case', 'single:' + param);
this.$emit("redirectPage", "testCase", "case", "single:" + param);
break;
}
}
},
},
activated() {
this.search();
this.testCaseReadOnly = !hasPermission('PROJECT_TRACK_CASE:READ');
this.apiCaseReadOnly = !hasPermission('PROJECT_API_DEFINITION:READ');
this.apiScenarioReadOnly = !hasPermission('PROJECT_API_SCENARIO:READ');
this.loadCaseReadOnly = !hasPermission('PROJECT_PERFORMANCE_TEST:READ');
}
}
this.testCaseReadOnly = !hasPermission("PROJECT_TRACK_CASE:READ");
this.apiCaseReadOnly = !hasPermission("PROJECT_API_DEFINITION:READ");
this.apiScenarioReadOnly = !hasPermission("PROJECT_API_SCENARIO:READ");
this.loadCaseReadOnly = !hasPermission("PROJECT_PERFORMANCE_TEST:READ");
},
};
</script>
<style scoped>