feat(测试计划): 测试计划定时任务功能开发
This commit is contained in:
parent
dc8d772658
commit
4e35887b1e
|
@ -130,4 +130,21 @@ CREATE INDEX idx_exec_status ON api_scenario_report(exec_status);
|
|||
-- set innodb lock wait timeout to 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 = '测试计划执行队列';
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -113,4 +113,5 @@ test_plan.is.archived=测试计划已归档
|
|||
test_plan.cannot.archived=测试计划不符合归档操作条件
|
||||
test_plan_module_already_exists=同名模块已存在
|
||||
test_plan_report_name_length_range=报告名称长度过长
|
||||
test_plan_allocation_type_param_error=测试集所属分类参数错误
|
||||
test_plan_allocation_type_param_error=测试集所属分类参数错误
|
||||
test_plan_schedule=测试计划-定時任務
|
|
@ -116,4 +116,5 @@ test_plan.is.archived=Test plan has been archived
|
|||
test_plan.cannot.archived=Test plan cannot be archived
|
||||
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_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
|
|
@ -116,4 +116,5 @@ test_plan.is.archived=测试计划已归档
|
|||
test_plan.cannot.archived=测试计划不符合归档操作条件
|
||||
test_plan_module_already_exists=同名模块已存在
|
||||
test_plan_report_name_length_range=报告名称长度过长
|
||||
test_plan_allocation_type_param_error=测试集所属分类参数错误
|
||||
test_plan_allocation_type_param_error=测试集所属分类参数错误
|
||||
test_plan_schedule=测试计划-定時任務
|
|
@ -115,4 +115,5 @@ test_plan.is.archived=測試計劃已歸檔
|
|||
test_plan.cannot.archived=測試計劃不符合歸檔操作條件
|
||||
test_plan_module_already_exists=同名模塊已存在
|
||||
test_plan_report_name_length_range=报告名称长度过长
|
||||
test_plan_allocation_type_param_error=測試集所屬分類參數錯誤
|
||||
test_plan_allocation_type_param_error=測試集所屬分類參數錯誤
|
||||
test_plan_schedule=測試計劃-定時任務
|
|
@ -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;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package io.metersphere.plan.controller;
|
||||
|
||||
import io.metersphere.api.service.scenario.ApiScenarioLogService;
|
||||
import io.metersphere.plan.constants.TestPlanResourceConfig;
|
||||
import io.metersphere.plan.domain.TestPlan;
|
||||
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.PermissionConstants;
|
||||
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.log.annotation.Log;
|
||||
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));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -50,4 +50,6 @@ public interface ExtTestPlanMapper {
|
|||
long selectMaxPosByGroupId(String groupId);
|
||||
|
||||
List<TestPlanResponse> selectByGroupIds(@Param("groupIds") List<String> groupIds);
|
||||
|
||||
List<String> selectRightfulIdsForExecute(@Param("ids") List<String> ids);
|
||||
}
|
||||
|
|
|
@ -451,6 +451,13 @@
|
|||
FROM test_plan
|
||||
WHERE group_id = #{0}
|
||||
</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 test_plan
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -41,6 +41,20 @@ public class TestPlanLogService {
|
|||
@Resource
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增计划日志
|
||||
*
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.metersphere.plan.domain.*;
|
|||
import io.metersphere.plan.dto.request.*;
|
||||
import io.metersphere.plan.dto.response.TestPlanDetailResponse;
|
||||
import io.metersphere.plan.dto.response.TestPlanOperationResponse;
|
||||
import io.metersphere.plan.job.TestPlanScheduleJob;
|
||||
import io.metersphere.plan.mapper.*;
|
||||
import io.metersphere.sdk.constants.*;
|
||||
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.User;
|
||||
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.log.constants.OperationLogType;
|
||||
import io.metersphere.system.mapper.ScheduleMapper;
|
||||
import io.metersphere.system.mapper.TestPlanModuleMapper;
|
||||
import io.metersphere.system.mapper.UserMapper;
|
||||
import io.metersphere.system.notice.constants.NoticeConstants;
|
||||
import io.metersphere.system.schedule.ScheduleService;
|
||||
import io.metersphere.system.uid.IDGenerator;
|
||||
import io.metersphere.system.uid.NumGenerator;
|
||||
import io.metersphere.system.utils.BatchProcessUtils;
|
||||
|
@ -81,6 +85,8 @@ public class TestPlanService extends TestPlanBaseUtilsService {
|
|||
private TestPlanSendNoticeService testPlanSendNoticeService;
|
||||
@Resource
|
||||
private TestPlanCaseExecuteHistoryMapper testPlanCaseExecuteHistoryMapper;
|
||||
@Resource
|
||||
private ScheduleService scheduleService;
|
||||
|
||||
private static final int MAX_TAG_SIZE = 10;
|
||||
|
||||
|
@ -199,11 +205,11 @@ public class TestPlanService extends TestPlanBaseUtilsService {
|
|||
List<String> allDeleteIds = ListUtils.union(deleteGroupIds, deleteGroupPlanIds);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import io.metersphere.system.controller.handler.ResultHolder;
|
|||
import io.metersphere.system.domain.TestPlanModule;
|
||||
import io.metersphere.system.domain.TestPlanModuleExample;
|
||||
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.enums.MoveTypeEnum;
|
||||
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_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_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";
|
||||
|
@ -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
|
||||
@Order(81)
|
||||
public void copyTestPlan() throws Exception {
|
||||
|
|
|
@ -10,6 +10,7 @@ import io.metersphere.functional.domain.FunctionalCase;
|
|||
import io.metersphere.functional.mapper.FunctionalCaseMapper;
|
||||
import io.metersphere.plan.domain.*;
|
||||
import io.metersphere.plan.dto.request.TestPlanUpdateRequest;
|
||||
import io.metersphere.plan.job.TestPlanScheduleJob;
|
||||
import io.metersphere.plan.mapper.*;
|
||||
import io.metersphere.project.domain.Project;
|
||||
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.system.controller.handler.ResultHolder;
|
||||
import io.metersphere.system.domain.TestPlanModuleExample;
|
||||
import io.metersphere.system.mapper.ExtScheduleMapper;
|
||||
import io.metersphere.system.uid.IDGenerator;
|
||||
import io.metersphere.system.uid.NumGenerator;
|
||||
import io.metersphere.system.utils.Pager;
|
||||
|
@ -25,6 +27,8 @@ import jakarta.annotation.Resource;
|
|||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -447,4 +451,26 @@ public class TestPlanTestService {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue