feat(测试计划): 测试计划定时任务功能开发

This commit is contained in:
Jianguo-Genius 2024-06-05 20:12:51 +08:00 committed by 刘瑞斌
parent dc8d772658
commit 4e35887b1e
18 changed files with 536 additions and 8 deletions

View File

@ -130,4 +130,21 @@ CREATE INDEX idx_exec_status ON api_scenario_report(exec_status);
-- set innodb lock wait timeout to default -- set innodb lock wait timeout to default
SET SESSION innodb_lock_wait_timeout = DEFAULT; SET SESSION innodb_lock_wait_timeout = DEFAULT;
-- 测试计划队列表
CREATE TABLE IF NOT EXISTS test_plan_execution_queue
(
`id` VARCHAR(50) NOT NULL COMMENT 'ID',
`execute_queue_id` VARCHAR(50) NOT NULL COMMENT '执行队列唯一ID',
`test_plan_id` VARCHAR(50) NOT NULL COMMENT '测试计划id',
`pos` BIGINT NOT NULL COMMENT '排序',
`prepare_report_id` VARCHAR(50) NOT NULL COMMENT '预生成报告ID',
`run_mode` VARCHAR(10) NOT NULL COMMENT '运行模式(SERIAL/PARALLEL)',
`create_user` VARCHAR(50) NOT NULL COMMENT '操作人',
`create_time` BIGINT NOT NULL COMMENT '操作时间',
PRIMARY KEY (id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT = '测试计划执行队列';

View File

@ -0,0 +1,14 @@
package io.metersphere.sdk.dto.queue;
import lombok.Data;
@Data
public class TestPlanExecutionQueue {
private String queueId;
private String testPlanId;
private long pos;
private String runMode;
private String prepareReportId;
private String createUser;
private long createTime;
}

View File

@ -114,3 +114,4 @@ test_plan.cannot.archived=测试计划不符合归档操作条件
test_plan_module_already_exists=同名模块已存在 test_plan_module_already_exists=同名模块已存在
test_plan_report_name_length_range=报告名称长度过长 test_plan_report_name_length_range=报告名称长度过长
test_plan_allocation_type_param_error=测试集所属分类参数错误 test_plan_allocation_type_param_error=测试集所属分类参数错误
test_plan_schedule=测试计划-定時任務

View File

@ -117,3 +117,4 @@ test_plan.cannot.archived=Test plan cannot be archived
test_plan_module_already_exists=The module with the same name already exists test_plan_module_already_exists=The module with the same name already exists
test_plan_report_name_length_range=The report name is too long test_plan_report_name_length_range=The report name is too long
test_plan_allocation_type_param_error=The parameter of the allocation type is not correct test_plan_allocation_type_param_error=The parameter of the allocation type is not correct
test_plan_schedule=Test plan schedule

View File

@ -117,3 +117,4 @@ test_plan.cannot.archived=测试计划不符合归档操作条件
test_plan_module_already_exists=同名模块已存在 test_plan_module_already_exists=同名模块已存在
test_plan_report_name_length_range=报告名称长度过长 test_plan_report_name_length_range=报告名称长度过长
test_plan_allocation_type_param_error=测试集所属分类参数错误 test_plan_allocation_type_param_error=测试集所属分类参数错误
test_plan_schedule=测试计划-定時任務

View File

@ -116,3 +116,4 @@ test_plan.cannot.archived=測試計劃不符合歸檔操作條件
test_plan_module_already_exists=同名模塊已存在 test_plan_module_already_exists=同名模塊已存在
test_plan_report_name_length_range=报告名称长度过长 test_plan_report_name_length_range=报告名称长度过长
test_plan_allocation_type_param_error=測試集所屬分類參數錯誤 test_plan_allocation_type_param_error=測試集所屬分類參數錯誤
test_plan_schedule=測試計劃-定時任務

View File

@ -0,0 +1,21 @@
package io.metersphere.system.dto.request.schedule;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class BaseScheduleConfigRequest {
@NotBlank(message = "{api_scenario.id.not_blank}")
@Schema(description = "定时任务资源ID")
@Size(min = 1, max = 50, message = "{api_scenario.id.length_range}")
private String resourceId;
@Schema(description = "启用/禁用")
private boolean enable;
@Schema(description = "Cron表达式")
@NotBlank
private String cron;
}

View File

@ -1,5 +1,6 @@
package io.metersphere.plan.controller; package io.metersphere.plan.controller;
import io.metersphere.api.service.scenario.ApiScenarioLogService;
import io.metersphere.plan.constants.TestPlanResourceConfig; import io.metersphere.plan.constants.TestPlanResourceConfig;
import io.metersphere.plan.domain.TestPlan; import io.metersphere.plan.domain.TestPlan;
import io.metersphere.plan.dto.request.*; import io.metersphere.plan.dto.request.*;
@ -11,6 +12,7 @@ import io.metersphere.plan.service.*;
import io.metersphere.sdk.constants.HttpMethodConstants; import io.metersphere.sdk.constants.HttpMethodConstants;
import io.metersphere.sdk.constants.PermissionConstants; import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.system.dto.LogInsertModule; import io.metersphere.system.dto.LogInsertModule;
import io.metersphere.system.dto.request.schedule.BaseScheduleConfigRequest;
import io.metersphere.system.dto.sdk.request.PosRequest; import io.metersphere.system.dto.sdk.request.PosRequest;
import io.metersphere.system.log.annotation.Log; import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogType; import io.metersphere.system.log.constants.OperationLogType;
@ -229,4 +231,24 @@ public class TestPlanController {
testPlanManagementService.checkModuleIsOpen(request.getMoveId(), TestPlanResourceConfig.CHECK_TYPE_TEST_PLAN, Collections.singletonList(TestPlanResourceConfig.CONFIG_TEST_PLAN)); testPlanManagementService.checkModuleIsOpen(request.getMoveId(), TestPlanResourceConfig.CHECK_TYPE_TEST_PLAN, Collections.singletonList(TestPlanResourceConfig.CONFIG_TEST_PLAN));
return testPlanService.sortInGroup(request, new LogInsertModule(SessionUtils.getUserId(), "/test-plan/move", HttpMethodConstants.POST.name())); return testPlanService.sortInGroup(request, new LogInsertModule(SessionUtils.getUserId(), "/test-plan/move", HttpMethodConstants.POST.name()));
} }
@PostMapping(value = "/schedule-config")
@Operation(summary = "接口测试-接口场景管理-定时任务配置")
@RequiresPermissions(PermissionConstants.TEST_PLAN_READ_EXECUTE)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.scheduleLog(#testPlanId)", msClass = TestPlanLogService.class)
@CheckOwner(resourceId = "#request.getResourceId()", resourceType = "test_plan")
public String scheduleConfig(@Validated @RequestBody BaseScheduleConfigRequest request) {
testPlanManagementService.checkModuleIsOpen(request.getResourceId(), TestPlanResourceConfig.CHECK_TYPE_TEST_PLAN, Collections.singletonList(TestPlanResourceConfig.CONFIG_TEST_PLAN));
return testPlanService.scheduleConfig(request, SessionUtils.getUserId());
}
@GetMapping(value = "/schedule-config-delete/{testPlanId}")
@Operation(summary = "接口测试-接口场景管理-删除定时任务配置")
@RequiresPermissions(PermissionConstants.TEST_PLAN_READ_EXECUTE)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.scheduleLog(#testPlanId)", msClass = ApiScenarioLogService.class)
@CheckOwner(resourceId = "#testPlanId", resourceType = "test_plan")
public void deleteScheduleConfig(@PathVariable String testPlanId) {
testPlanManagementService.checkModuleIsOpen(testPlanId, TestPlanResourceConfig.CHECK_TYPE_TEST_PLAN, Collections.singletonList(TestPlanResourceConfig.CONFIG_TEST_PLAN));
testPlanService.deleteScheduleConfig(testPlanId);
}
} }

View File

@ -0,0 +1,45 @@
package io.metersphere.plan.controller;
import io.metersphere.plan.constants.TestPlanResourceConfig;
import io.metersphere.plan.dto.request.TestPlanExecuteRequest;
import io.metersphere.plan.service.TestPlanExecuteService;
import io.metersphere.plan.service.TestPlanLogService;
import io.metersphere.plan.service.TestPlanManagementService;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.security.CheckOwner;
import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
@RestController
@RequestMapping("/test-plan-execute")
@Tag(name = "测试计划执行")
public class TestPlanExecuteController {
@Resource
private TestPlanManagementService testPlanManagementService;
@Resource
private TestPlanExecuteService testPlanExecuteService;
@PostMapping("/start")
@Operation(summary = "测试计划-开始自行")
@RequiresPermissions(PermissionConstants.TEST_PLAN_READ_UPDATE)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
@Log(type = OperationLogType.EXECUTE, expression = "#msClass.batchEditLog(#request)", msClass = TestPlanLogService.class)
public void startExecute(@Validated @RequestBody TestPlanExecuteRequest request) {
testPlanManagementService.checkModuleIsOpen(request.getProjectId(), TestPlanResourceConfig.CHECK_TYPE_PROJECT, Collections.singletonList(TestPlanResourceConfig.CONFIG_TEST_PLAN));
testPlanExecuteService.execute(request, SessionUtils.getUserId());
}
}

View File

@ -0,0 +1,26 @@
package io.metersphere.plan.dto.request;
import io.metersphere.sdk.constants.ApiBatchRunMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
/**
* @author wx
*/
@Data
public class TestPlanExecuteRequest {
@Schema(description = "项目ID", required = true)
@NotBlank(message = "{project.id.not_blank}")
private String projectId;
@Schema(description = "执行ID")
List<String> executeIds;
@Schema(description = "执行模式", allowableValues = {"SERIAL", "PARALLEL"}, requiredMode = Schema.RequiredMode.REQUIRED)
private String executeMode = ApiBatchRunMode.SERIAL.name();
}

View File

@ -0,0 +1,32 @@
package io.metersphere.plan.job;
import io.metersphere.plan.dto.request.TestPlanExecuteRequest;
import io.metersphere.plan.service.TestPlanExecuteService;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.system.schedule.BaseScheduleJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.TriggerKey;
import java.util.Collections;
public class TestPlanScheduleJob extends BaseScheduleJob {
@Override
protected void businessExecute(JobExecutionContext context) {
TestPlanExecuteService testPlanExecuteService = CommonBeanFactory.getBean(TestPlanExecuteService.class);
assert testPlanExecuteService != null;
testPlanExecuteService.execute(new TestPlanExecuteRequest() {{
this.setExecuteIds(Collections.singletonList(resourceId));
}}, userId);
}
public static JobKey getJobKey(String testPlanId) {
return new JobKey(testPlanId, TestPlanScheduleJob.class.getName());
}
public static TriggerKey getTriggerKey(String testPlanId) {
return new TriggerKey(testPlanId, TestPlanScheduleJob.class.getName());
}
}

View File

@ -50,4 +50,6 @@ public interface ExtTestPlanMapper {
long selectMaxPosByGroupId(String groupId); long selectMaxPosByGroupId(String groupId);
List<TestPlanResponse> selectByGroupIds(@Param("groupIds") List<String> groupIds); List<TestPlanResponse> selectByGroupIds(@Param("groupIds") List<String> groupIds);
List<String> selectRightfulIdsForExecute(@Param("ids") List<String> ids);
} }

View File

@ -451,6 +451,13 @@
FROM test_plan FROM test_plan
WHERE group_id = #{0} WHERE group_id = #{0}
</select> </select>
<select id="selectRightfulIdsForExecute" resultType="java.lang.String">
SELECT id FROM test_plan WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND status != 'ARCHIVED'
</select>
<update id="batchUpdate"> <update id="batchUpdate">
update test_plan update test_plan

View File

@ -0,0 +1,134 @@
package io.metersphere.plan.service;
import io.metersphere.plan.domain.TestPlan;
import io.metersphere.plan.dto.request.TestPlanExecuteRequest;
import io.metersphere.plan.mapper.TestPlanMapper;
import io.metersphere.sdk.constants.ApiBatchRunMode;
import io.metersphere.sdk.dto.queue.TestPlanExecutionQueue;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional(rollbackFor = Exception.class)
public class TestPlanExecuteService {
@Resource
private TestPlanMapper testPlanMapper;
@Resource
private TestPlanService testPlanService;
@Resource
private RedisTemplate<String, String> redisTemplate;
public static final String TEST_PLAN_QUEUE_PREFIX = "queue:test-plan:";
private TestPlanExecutionQueue genQueue(String testPlanId, String queueId, long pos, String userId, String executeMode) {
TestPlanExecutionQueue testPlanExecutionQueue = new TestPlanExecutionQueue();
testPlanExecutionQueue.setTestPlanId(testPlanId);
testPlanExecutionQueue.setQueueId(queueId);
testPlanExecutionQueue.setPos(pos);
testPlanExecutionQueue.setPrepareReportId(IDGenerator.nextStr());
testPlanExecutionQueue.setCreateUser(userId);
testPlanExecutionQueue.setCreateTime(System.currentTimeMillis());
testPlanExecutionQueue.setRunMode(executeMode);
return testPlanExecutionQueue;
}
public void execute(TestPlanExecuteRequest request, String userId) {
List<String> rightfulIds = testPlanService.selectRightfulIds(request.getExecuteIds());
if (CollectionUtils.isNotEmpty(rightfulIds)) {
//遍历原始ID只挑选符合条件的ID进行防止顺序错乱
String executeMode = request.getExecuteMode();
String queueId = IDGenerator.nextStr();
long pos = 1;
List<TestPlanExecutionQueue> testPlanExecutionQueues = new ArrayList<>();
for (String testPlanId : request.getExecuteIds()) {
List<TestPlan> childList = testPlanService.selectChildPlanByGroupId(testPlanId);
if (CollectionUtils.isNotEmpty(childList)) {
for (TestPlan child : childList) {
testPlanExecutionQueues.add(genQueue(child.getId(), queueId, pos++, userId, executeMode));
}
} else {
testPlanExecutionQueues.add(genQueue(testPlanId, queueId, pos++, userId, executeMode));
}
}
if (StringUtils.equalsIgnoreCase(request.getExecuteMode(), ApiBatchRunMode.SERIAL.name())) {
//串行
testPlanExecutionQueues.forEach(testPlanExecutionQueue -> {
redisTemplate.opsForList().rightPush(TEST_PLAN_QUEUE_PREFIX + queueId, JSON.toJSONString(testPlanExecutionQueue));
});
try {
executeByExecutionQueue(getNextDetail(queueId));
} catch (Exception ignore) {
}
} else {
//并行
testPlanExecutionQueues.forEach(testPlanExecutionQueue -> {
executeByExecutionQueue(testPlanExecutionQueue);
});
}
}
}
public void executeByExecutionQueue(TestPlanExecutionQueue queue) {
Thread.startVirtualThread(() -> {
TestPlan testPlan = testPlanMapper.selectByPrimaryKey(queue.getTestPlanId());
// todo 获取测试规划通过测试规划执行方式确定用例的执行方式
});
}
/**
* 获取下一个节点
*/
public TestPlanExecutionQueue getNextDetail(String queueId) throws Exception {
String queueKey = TEST_PLAN_QUEUE_PREFIX + queueId;
ListOperations<String, String> listOps = redisTemplate.opsForList();
String queueDetail = listOps.leftPop(queueKey);
if (StringUtils.isBlank(queueDetail)) {
// 重试3次获取
for (int i = 0; i < 3; i++) {
queueDetail = redisTemplate.opsForList().leftPop(queueKey);
if (StringUtils.isNotBlank(queueDetail)) {
break;
}
Thread.sleep(1000);
}
}
if (StringUtils.isNotBlank(queueDetail)) {
Long size = size(queueId);
if (size == null || size == 0) {
// 最后一个节点清理队列
deleteQueue(queueId);
}
return JSON.parseObject(queueDetail, TestPlanExecutionQueue.class);
}
// 整体获取完清理队列
deleteQueue(queueId);
return null;
}
public void deleteQueue(String queueId) {
redisTemplate.delete(TEST_PLAN_QUEUE_PREFIX + queueId);
}
public Long size(String queueId) {
ListOperations<String, String> listOps = redisTemplate.opsForList();
String queueKey = TEST_PLAN_QUEUE_PREFIX + queueId;
return listOps.size(queueKey);
}
}

View File

@ -41,6 +41,20 @@ public class TestPlanLogService {
@Resource @Resource
private TestPlanMapper testPlanMapper; private TestPlanMapper testPlanMapper;
public LogDTO scheduleLog(String id) {
TestPlan testPlan = testPlanMapper.selectByPrimaryKey(id);
Project project = projectMapper.selectByPrimaryKey(testPlan.getProjectId());
LogDTO dto = LogDTOBuilder.builder()
.projectId(project.getId())
.organizationId(project.getOrganizationId())
.type(OperationLogType.UPDATE.name())
.module(OperationLogModule.TEST_PLAN)
.sourceId(testPlan.getId())
.content(Translator.get("test_plan_schedule") + ":" + testPlan.getName())
.build().getLogDTO();
return dto;
}
/** /**
* 新增计划日志 * 新增计划日志
* *

View File

@ -4,6 +4,7 @@ import io.metersphere.plan.domain.*;
import io.metersphere.plan.dto.request.*; import io.metersphere.plan.dto.request.*;
import io.metersphere.plan.dto.response.TestPlanDetailResponse; import io.metersphere.plan.dto.response.TestPlanDetailResponse;
import io.metersphere.plan.dto.response.TestPlanOperationResponse; import io.metersphere.plan.dto.response.TestPlanOperationResponse;
import io.metersphere.plan.job.TestPlanScheduleJob;
import io.metersphere.plan.mapper.*; import io.metersphere.plan.mapper.*;
import io.metersphere.sdk.constants.*; import io.metersphere.sdk.constants.*;
import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.exception.MSException;
@ -15,12 +16,15 @@ import io.metersphere.system.domain.ScheduleExample;
import io.metersphere.system.domain.TestPlanModuleExample; import io.metersphere.system.domain.TestPlanModuleExample;
import io.metersphere.system.domain.User; import io.metersphere.system.domain.User;
import io.metersphere.system.dto.LogInsertModule; import io.metersphere.system.dto.LogInsertModule;
import io.metersphere.system.dto.request.ScheduleConfig;
import io.metersphere.system.dto.request.schedule.BaseScheduleConfigRequest;
import io.metersphere.system.dto.sdk.request.PosRequest; import io.metersphere.system.dto.sdk.request.PosRequest;
import io.metersphere.system.log.constants.OperationLogType; import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.mapper.ScheduleMapper; import io.metersphere.system.mapper.ScheduleMapper;
import io.metersphere.system.mapper.TestPlanModuleMapper; import io.metersphere.system.mapper.TestPlanModuleMapper;
import io.metersphere.system.mapper.UserMapper; import io.metersphere.system.mapper.UserMapper;
import io.metersphere.system.notice.constants.NoticeConstants; import io.metersphere.system.notice.constants.NoticeConstants;
import io.metersphere.system.schedule.ScheduleService;
import io.metersphere.system.uid.IDGenerator; import io.metersphere.system.uid.IDGenerator;
import io.metersphere.system.uid.NumGenerator; import io.metersphere.system.uid.NumGenerator;
import io.metersphere.system.utils.BatchProcessUtils; import io.metersphere.system.utils.BatchProcessUtils;
@ -81,6 +85,8 @@ public class TestPlanService extends TestPlanBaseUtilsService {
private TestPlanSendNoticeService testPlanSendNoticeService; private TestPlanSendNoticeService testPlanSendNoticeService;
@Resource @Resource
private TestPlanCaseExecuteHistoryMapper testPlanCaseExecuteHistoryMapper; private TestPlanCaseExecuteHistoryMapper testPlanCaseExecuteHistoryMapper;
@Resource
private ScheduleService scheduleService;
private static final int MAX_TAG_SIZE = 10; private static final int MAX_TAG_SIZE = 10;
@ -199,11 +205,11 @@ public class TestPlanService extends TestPlanBaseUtilsService {
List<String> allDeleteIds = ListUtils.union(deleteGroupIds, deleteGroupPlanIds); List<String> allDeleteIds = ListUtils.union(deleteGroupIds, deleteGroupPlanIds);
if (CollectionUtils.isNotEmpty(allDeleteIds)) { if (CollectionUtils.isNotEmpty(allDeleteIds)) {
// 级联删除子计划关联的资源(计划组不存在关联的资源,但是存在报告) // 级联删除子计划关联的资源(计划组不存在关联的资源,但是存在报告)
this.cascadeDeleteTestPlanIds(deleteGroupPlanIds, testPlanReportService); this.cascadeDeleteTestPlanIds(allDeleteIds, testPlanReportService);
testPlanExample.clear();
testPlanExample.createCriteria().andIdIn(allDeleteIds);
testPlanMapper.deleteByExample(testPlanExample);
} }
testPlanExample.clear();
testPlanExample.createCriteria().andIdIn(allDeleteIds);
testPlanMapper.deleteByExample(testPlanExample);
}); });
} }
} }
@ -694,4 +700,41 @@ public class TestPlanService extends TestPlanBaseUtilsService {
testPlanLogService.saveMoveLog(testPlanMapper.selectByPrimaryKey(request.getMoveId()), request.getMoveId(), logInsertModule); testPlanLogService.saveMoveLog(testPlanMapper.selectByPrimaryKey(request.getMoveId()), request.getMoveId(), logInsertModule);
return new TestPlanOperationResponse(1); return new TestPlanOperationResponse(1);
} }
public String scheduleConfig(BaseScheduleConfigRequest request, String operator) {
TestPlan testPlan = testPlanMapper.selectByPrimaryKey(request.getResourceId());
if (testPlan == null) {
throw new MSException(Translator.get("test_plan.not.exist"));
}
ScheduleConfig scheduleConfig = ScheduleConfig.builder()
.resourceId(testPlan.getId())
.key(testPlan.getId())
.projectId(testPlan.getProjectId())
.name(testPlan.getName())
.enable(request.isEnable())
.cron(request.getCron())
.resourceType(ScheduleResourceType.TEST_PLAN.name())
.build();
return scheduleService.scheduleConfig(
scheduleConfig,
TestPlanScheduleJob.getJobKey(testPlan.getId()),
TestPlanScheduleJob.getTriggerKey(testPlan.getId()),
TestPlanScheduleJob.class,
operator);
}
public void deleteScheduleConfig(String testPlanId) {
scheduleService.deleteByResourceId(testPlanId, TestPlanScheduleJob.getJobKey(testPlanId), TestPlanScheduleJob.getTriggerKey(testPlanId));
}
public List<String> selectRightfulIds(List<String> executeIds) {
return extTestPlanMapper.selectNotArchivedIds(executeIds);
}
public List<TestPlan> selectChildPlanByGroupId(String testPlanId) {
TestPlanExample example = new TestPlanExample();
example.createCriteria().andGroupIdEqualTo(testPlanId).andStatusNotEqualTo(TEST_PLAN_STATUS_ARCHIVED);
example.setOrderByClause("pos asc");
return testPlanMapper.selectByExample(example);
}
} }

View File

@ -26,6 +26,7 @@ import io.metersphere.system.controller.handler.ResultHolder;
import io.metersphere.system.domain.TestPlanModule; import io.metersphere.system.domain.TestPlanModule;
import io.metersphere.system.domain.TestPlanModuleExample; import io.metersphere.system.domain.TestPlanModuleExample;
import io.metersphere.system.dto.AddProjectRequest; import io.metersphere.system.dto.AddProjectRequest;
import io.metersphere.system.dto.request.schedule.BaseScheduleConfigRequest;
import io.metersphere.system.dto.sdk.BaseTreeNode; import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.dto.sdk.enums.MoveTypeEnum; import io.metersphere.system.dto.sdk.enums.MoveTypeEnum;
import io.metersphere.system.dto.sdk.request.NodeMoveRequest; import io.metersphere.system.dto.sdk.request.NodeMoveRequest;
@ -115,6 +116,9 @@ public class TestPlanTests extends BaseTest {
private static final String URL_POST_TEST_PLAN_SORT = "/test-plan/sort"; private static final String URL_POST_TEST_PLAN_SORT = "/test-plan/sort";
private static final String URL_POST_TEST_PLAN_UPDATE = "/test-plan/update"; private static final String URL_POST_TEST_PLAN_UPDATE = "/test-plan/update";
private static final String URL_POST_TEST_PLAN_BATCH_DELETE = "/test-plan/batch-delete"; private static final String URL_POST_TEST_PLAN_BATCH_DELETE = "/test-plan/batch-delete";
private static final String URL_POST_TEST_PLAN_SCHEDULE = "/test-plan/schedule-config";
private static final String URL_POST_TEST_PLAN_SCHEDULE_DELETE = "/test-plan/schedule-config-delete/%s";
private static final String URL_POST_TEST_PLAN_EXECUTE = "/test-plan-execute/start";
//测试计划资源-功能用例 //测试计划资源-功能用例
private static final String URL_POST_RESOURCE_CASE_ASSOCIATION = "/test-plan/association"; private static final String URL_POST_RESOURCE_CASE_ASSOCIATION = "/test-plan/association";
@ -1312,6 +1316,123 @@ public class TestPlanTests extends BaseTest {
} }
@Test
@Order(61)
public void scheduleTest() throws Exception {
//为测试计划组创建
BaseScheduleConfigRequest request = new BaseScheduleConfigRequest();
request.setResourceId(groupTestPlanId7);
request.setEnable(true);
request.setCron("0 0 0 * * ?");
//先测试一下没有开启模块时接口能否使用
testPlanTestService.removeProjectModule(project, PROJECT_MODULE, "testPlan");
this.requestPost(URL_POST_TEST_PLAN_SCHEDULE, request).andExpect(status().is5xxServerError());
this.requestGet(String.format(URL_POST_TEST_PLAN_SCHEDULE_DELETE, groupTestPlanId7)).andExpect(status().is5xxServerError());
//恢复
testPlanTestService.resetProjectModule(project, PROJECT_MODULE);
MvcResult result = this.requestPostAndReturn(URL_POST_TEST_PLAN_SCHEDULE, request);
ResultHolder resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class);
String scheduleId = resultHolder.getData().toString();
testPlanTestService.checkSchedule(scheduleId, groupTestPlanId7, request.isEnable());
//增加日志检查
LOG_CHECK_LIST.add(
new CheckLogModel(groupTestPlanId7, OperationLogType.UPDATE, null)
);
//关闭
request.setEnable(false);
result = this.requestPostAndReturn(URL_POST_TEST_PLAN_SCHEDULE, request);
resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class);
String newScheduleId = resultHolder.getData().toString();
//检查两个scheduleId是否相同
Assertions.assertEquals(scheduleId, newScheduleId);
testPlanTestService.checkSchedule(newScheduleId, groupTestPlanId7, request.isEnable());
//测试各种corn表达式用于校验正则的准确性
String[] cornStrArr = new String[]{
"0 0 12 * * ?", //每天中午12点触发
"0 15 10 ? * *", //每天上午10:15触发
"0 15 10 * * ?", //每天上午10:15触发
"0 15 10 * * ? *",//每天上午10:15触发
"0 15 10 * * ? 2048",//2008年的每天上午10:15触发
"0 * 10 * * ?",//每天上午10:00至10:59期间的每1分钟触发
"0 0/5 10 * * ?",//每天上午10:00至10:55期间的每5分钟触发
"0 0/5 10,16 * * ?",//每天上午10:00至10:55期间和下午4:00至4:55期间的每5分钟触发
"0 0-5 10 * * ?",//每天上午10:00至10:05期间的每1分钟触发
"0 10,14,18 15 ? 3 WED",//每年三月的星期三的下午2:10和2:18触发
"0 10 15 ? * MON-FRI",//每个周一周二周三周四周五的下午3:10触发
"0 15 10 15 * ?",//每月15日上午10:15触发
"0 15 10 L * ?", //每月最后一日的上午10:15触发
"0 15 10 ? * 6L", //每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2024-2026", //从2024年至2026年每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3", //每月的第三个星期五上午10:15触发
};
//每种corn表达式开启关闭都测试一遍检查是否能正常开关定时任务
for (String corn : cornStrArr) {
request = new BaseScheduleConfigRequest();
request.setResourceId(groupTestPlanId7);
request.setEnable(true);
request.setCron(corn);
result = this.requestPostAndReturn(URL_POST_TEST_PLAN_SCHEDULE, request);
resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class);
scheduleId = resultHolder.getData().toString();
testPlanTestService.checkSchedule(scheduleId, groupTestPlanId7, request.isEnable());
request = new BaseScheduleConfigRequest();
request.setResourceId(groupTestPlanId7);
request.setEnable(false);
request.setCron(corn);
result = this.requestPostAndReturn(URL_POST_TEST_PLAN_SCHEDULE, request);
resultHolder = JSON.parseObject(result.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class);
scheduleId = resultHolder.getData().toString();
testPlanTestService.checkSchedule(scheduleId, groupTestPlanId7, request.isEnable());
}
//校验权限
this.requestPostPermissionTest(PermissionConstants.TEST_PLAN_READ_EXECUTE, URL_POST_TEST_PLAN_SCHEDULE, request);
//反例scenarioId不存在
request = new BaseScheduleConfigRequest();
request.setCron("0 0 0 * * ?");
this.requestPost(URL_POST_TEST_PLAN_SCHEDULE, request).andExpect(status().isBadRequest());
request.setResourceId(IDGenerator.nextStr());
this.requestPost(URL_POST_TEST_PLAN_SCHEDULE, request).andExpect(status().is5xxServerError());
//反例不配置cron表达式
request = new BaseScheduleConfigRequest();
request.setResourceId(IDGenerator.nextStr());
this.requestPost(URL_POST_TEST_PLAN_SCHEDULE, request).andExpect(status().isBadRequest());
//反例配置错误的cron表达式测试是否会关闭定时任务
request = new BaseScheduleConfigRequest();
request.setResourceId(IDGenerator.nextStr());
request.setEnable(true);
request.setCron(IDGenerator.nextStr());
this.requestPost(URL_POST_TEST_PLAN_SCHEDULE, request).andExpect(status().is5xxServerError());
//测试删除
this.requestGetWithOk(String.format(URL_POST_TEST_PLAN_SCHEDULE_DELETE, groupTestPlanId7));
testPlanTestService.checkScheduleIsRemove(groupTestPlanId7);
}
@Test
@Order(71)
public void executeTest() throws Exception {
TestPlanExecuteRequest executeRequest = new TestPlanExecuteRequest();
executeRequest.setExecuteIds(Collections.singletonList(groupTestPlanId7));
executeRequest.setProjectId(project.getId());
//串行
this.requestPostWithOk(URL_POST_TEST_PLAN_EXECUTE, executeRequest);
//并行
executeRequest.setExecuteMode(ApiBatchRunMode.PARALLEL.name());
this.requestPostWithOk(URL_POST_TEST_PLAN_EXECUTE, executeRequest);
}
@Test @Test
@Order(81) @Order(81)
public void copyTestPlan() throws Exception { public void copyTestPlan() throws Exception {

View File

@ -10,6 +10,7 @@ import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.mapper.FunctionalCaseMapper; import io.metersphere.functional.mapper.FunctionalCaseMapper;
import io.metersphere.plan.domain.*; import io.metersphere.plan.domain.*;
import io.metersphere.plan.dto.request.TestPlanUpdateRequest; import io.metersphere.plan.dto.request.TestPlanUpdateRequest;
import io.metersphere.plan.job.TestPlanScheduleJob;
import io.metersphere.plan.mapper.*; import io.metersphere.plan.mapper.*;
import io.metersphere.project.domain.Project; import io.metersphere.project.domain.Project;
import io.metersphere.project.mapper.ProjectMapper; import io.metersphere.project.mapper.ProjectMapper;
@ -18,6 +19,7 @@ import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.SubListUtils; import io.metersphere.sdk.util.SubListUtils;
import io.metersphere.system.controller.handler.ResultHolder; import io.metersphere.system.controller.handler.ResultHolder;
import io.metersphere.system.domain.TestPlanModuleExample; import io.metersphere.system.domain.TestPlanModuleExample;
import io.metersphere.system.mapper.ExtScheduleMapper;
import io.metersphere.system.uid.IDGenerator; import io.metersphere.system.uid.IDGenerator;
import io.metersphere.system.uid.NumGenerator; import io.metersphere.system.uid.NumGenerator;
import io.metersphere.system.utils.Pager; import io.metersphere.system.utils.Pager;
@ -25,6 +27,8 @@ import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
@ -447,4 +451,26 @@ public class TestPlanTestService {
Assertions.assertEquals(result.getTotal(), allData); Assertions.assertEquals(result.getTotal(), allData);
} }
} }
@Resource
private ExtScheduleMapper extScheduleMapper;
@Resource
private Scheduler scheduler;
/*
校验定时任务是否成功开启
1.schedule表中存在数据且开启状态符合isEnable
2.开启状态下 qrtz_triggers qrtz_cron_triggers 表存在对应的数据
3.关闭状态下 qrtz_triggers qrtz_cron_triggers 表不存在对应的数据
*/
public void checkSchedule(String scheduleId, String resourceId, boolean isEnable) throws Exception {
Assertions.assertEquals(extScheduleMapper.countByIdAndEnable(scheduleId, isEnable), 1L);
Assertions.assertEquals(scheduler.checkExists(TestPlanScheduleJob.getJobKey(resourceId)), isEnable);
}
public void checkScheduleIsRemove(String resourceId) throws SchedulerException {
Assertions.assertEquals(extScheduleMapper.countByResourceId(resourceId), 0L);
Assertions.assertEquals(scheduler.checkExists(TestPlanScheduleJob.getJobKey(resourceId)), false);
}
} }