feat(测试计划): 测试计划不再强制关联用例时选择环境

--story=1012486 --user=宋天阳
【货车之家】接口场景包含跨项目步骤时取消跨项目环境必选&测试计划关联接口、UI测试时取消运行环境必选
https://www.tapd.cn/55049933/s/1401625
This commit is contained in:
song-tianyang 2023-08-02 15:00:52 +08:00 committed by 建国
parent eed243d40c
commit dd92feeba2
28 changed files with 12863 additions and 12428 deletions

View File

@ -287,8 +287,16 @@ public class ApiExecuteService {
TestPlanApiCaseExample example = new TestPlanApiCaseExample();
example.createCriteria().andTestPlanIdEqualTo(request.getTestPlanId()).andApiCaseIdEqualTo(request.getCaseId());
List<TestPlanApiCase> list = testPlanApiCaseMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(list)) {
request.setEnvironmentId(list.get(0).getEnvironmentId());
element.setName(list.get(0).getId());
} else {
TestPlanApiCase apiCase = testPlanApiCaseMapper.selectByPrimaryKey(request.getCaseId());
if (apiCase != null) {
request.setEnvironmentId(apiCase.getEnvironmentId());
element.setName(request.getCaseId());
}
}
} else {
element.setName(request.getCaseId());
}

View File

@ -11,10 +11,7 @@ import io.metersphere.base.mapper.ext.BaseApiExecutionQueueMapper;
import io.metersphere.base.mapper.ext.ExtApiDefinitionExecResultMapper;
import io.metersphere.base.mapper.ext.ExtApiScenarioReportMapper;
import io.metersphere.base.mapper.plan.ext.ExtTestPlanApiCaseMapper;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.CommonConstants;
import io.metersphere.commons.constants.KafkaTopicConstants;
import io.metersphere.commons.constants.TestPlanReportStatus;
import io.metersphere.commons.constants.*;
import io.metersphere.commons.enums.ApiReportStatus;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.JSON;
@ -89,9 +86,11 @@ public class ApiExecutionQueueService {
Map<String, String> detailMap = new HashMap<>();
List<ApiExecutionQueueDetail> queueDetails = new LinkedList<>();
// 初始化API/用例队列
String redisLockType = TestPlanExecuteCaseType.SCENARIO.name();
if (StringUtils.equalsAnyIgnoreCase(type, ApiRunMode.DEFINITION.name(), ApiRunMode.API_PLAN.name())) {
Map<String, ApiDefinitionExecResult> runMap = (Map<String, ApiDefinitionExecResult>) runObj;
initApi(runMap, resQueue, config, detailMap, queueDetails);
redisLockType = TestPlanExecuteCaseType.API_CASE.name();
}
// 初始化场景
else {
@ -101,11 +100,16 @@ public class ApiExecutionQueueService {
if (CollectionUtils.isNotEmpty(queueDetails)) {
extApiExecutionQueueMapper.sqlInsert(queueDetails);
}
//redis移除key 执行测试计划时会添加key)
redisTemplateService.unlock(reportId, redisLockType, reportId);
resQueue.setDetailMap(detailMap);
LoggerUtil.info("报告【" + type + "】生成执行链结束", reportId);
return resQueue;
}
@Resource
private RedisTemplateService redisTemplateService;
private void initScenario(Map<String, RunModeDataDTO> runMap, DBTestQueue resQueue, RunModeConfigDTO config, Map<String, String> detailMap, List<ApiExecutionQueueDetail> queueDetails) {
final int[] sort = {0};
runMap.forEach((k, v) -> {

View File

@ -846,9 +846,8 @@ public class MockConfigService {
if (project != null) {
RequestMockParams requestMockParams = MockApiUtils.genRequestMockParamsFromHttpRequest(request, true);
String urlSuffix = this.getUrlSuffix(project.getSystemId(), request);
LogUtil.info("Mock urlSuffix:{}", urlSuffix);
LogUtil.info("Mock requestHeaderMap:{}", requestHeaderMap);
LogUtil.info("Mock requestMockParams:{}", JSON.toJSONString(requestMockParams));
LogUtil.info("Mock [" + url + "] Header:{}", requestHeaderMap);
LogUtil.info("Mock [" + url + "] request:{}", JSON.toJSONString(requestMockParams));
List<ApiDefinitionWithBLOBs> qualifiedApiList = apiDefinitionService.preparedUrl(project.getId(), method, urlSuffix, requestHeaderMap.get(MockApiHeaders.MOCK_API_RESOURCE_ID));
for (ApiDefinitionWithBLOBs api : qualifiedApiList) {
if (StringUtils.isEmpty(returnStr)) {
@ -872,6 +871,7 @@ public class MockConfigService {
response.setStatus(404);
returnStr = Translator.get("mock_warning");
}
LogUtil.info("Mock [" + url + "] response:{}", returnStr);
return returnStr;
}
@ -883,9 +883,8 @@ public class MockConfigService {
RequestMockParams requestMockParams = MockApiUtils.genRequestMockParamsFromHttpRequest(request, false);
String urlSuffix = this.getUrlSuffix(project.getSystemId(), request);
LogUtil.info("Mock urlSuffix:{}", urlSuffix);
LogUtil.info("Mock requestHeaderMap:{}", requestHeaderMap);
LogUtil.info("Mock requestMockParams:{}", JSON.toJSONString(requestMockParams));
LogUtil.info("Mock [" + url + "] Header:{}", requestHeaderMap);
LogUtil.info("Mock [" + url + "] request:{}", JSON.toJSONString(requestMockParams));
List<ApiDefinitionWithBLOBs> qualifiedApiList = apiDefinitionService.preparedUrl(project.getId(), method, urlSuffix, requestHeaderMap.get(MockApiHeaders.MOCK_API_RESOURCE_ID));
/*
GET/DELETE 这种通过url穿参数的接口在接口路径相同的情况下可能会出现这样的情况
@ -919,6 +918,7 @@ public class MockConfigService {
response.setStatus(404);
returnStr = Translator.get("mock_warning");
}
LogUtil.info("Mock [" + url + "] response:{}", returnStr);
return returnStr;
}

View File

@ -99,4 +99,10 @@ public class RedisTemplateService {
}
return false;
}
public void unlock(String testPlanReportId, String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
this.redisTemplate.execute(redisScript, Collections.singletonList(StringUtils.join(testPlanReportId, key)), new Object[]{value});
}
}

View File

@ -737,6 +737,7 @@ const message = {
batch_add_to_ws: "Add to environment group in bulk",
choice_conflict: "one project chooses a corresponding environment!",
env_list: "Environment List",
case_env: "Case environment",
confirm: "Confirm",
please_select_env_for_current_scenario:
"please select env for current scenario",

View File

@ -725,6 +725,7 @@ const message = {
batch_add_to_ws: "批量添加到环境组",
choice_conflict: "环境选择冲突,一个项目选择一个对应环境!",
env_list: "环境列表",
case_env: "用例环境",
confirm: "确 定",
please_select_env_for_current_scenario: "请为当前场景选择一个运行环境!",
please_select_env_for_current_plan: "请为当前测试计划选择一个运行环境!",

View File

@ -724,6 +724,7 @@ const message = {
batch_add_to_ws: "批量添加到環境組",
choice_conflict: "環境選擇沖突,一個項目選擇一個對應環境!",
env_list: "環境列表",
case_env: "用例環境",
confirm: "確 定",
please_select_env_for_current_scenario: "請為當前場景選擇一個運行環境!",
please_select_env_for_current_plan: "請為當前测试计划選擇一個運行環境!",

View File

@ -155,7 +155,11 @@ export const JMETER_FUNC = [
{type: "Information", name: "${__machineIP}", description: "get the local machine IP address"},
{type: "Information", name: "${__machineName}", description: "get the local machine name"},
{type: "Information", name: "${__time}", description: "return current time in various formats"},
{type: "Information", name: "${__timeShift}", description: "return a date in various formats with the specified amount of seconds/minutes/hours/days added"},
{
type: "Information",
name: "${__timeShift}",
description: "return a date in various formats with the specified amount of seconds/minutes/hours/days added"
},
{type: "Information", name: "${__log}", description: "log (or display) a message (and return the value)"},
{type: "Information", name: "${__logn}", description: "log (or display) a message (empty return value)"},
{type: "Input", name: "${__StringFromFile}", description: "read a line from a file"},
@ -164,13 +168,21 @@ export const JMETER_FUNC = [
{type: "Input", name: "${__XPath}", description: "Use an XPath expression to read from a file"},
{type: "Input", name: "${__StringToFile}", description: "write a string to a file"},
{type: "Calculation", name: "${__counter}", description: "generate an incrementing number"},
{type: "Formatting", name: "${__dateTimeConvert}", description: "Convert a date or time from source to target format"},
{
type: "Formatting",
name: "${__dateTimeConvert}",
description: "Convert a date or time from source to target format"
},
{type: "Calculation", name: "${__digest}", description: "Generate a digest (SHA-1, SHA-256, MD5...)"},
{type: "Calculation", name: "${__intSum}", description: "add int numbers"},
{type: "Calculation", name: "${__longSum}", description: "add long numbers"},
{type: "Calculation", name: "${__Random}", description: "generate a random number"},
{type: "Calculation", name: "${__RandomDate}", description: "generate random date within a specific date range"},
{type: "Calculation", name: "${__RandomFromMultipleVars}", description: "extracts an element from the values of a set of variables separated by |"},
{
type: "Calculation",
name: "${__RandomFromMultipleVars}",
description: "extracts an element from the values of a set of variables separated by |"
},
{type: "Calculation", name: "${__RandomString}", description: "generate a random string"},
{type: "Calculation", name: "${__UUID}", description: "generate a random type 4 UUID"},
{type: "Scripting", name: "${__groovy}", description: "run an Apache Groovy script"},
@ -196,7 +208,11 @@ export const JMETER_FUNC = [
{type: "String", name: "${__unescape}", description: "Process strings containing Java escapes (e.g. \n & \t)"},
{type: "String", name: "${__unescapeHtml}", description: "Decode HTML-encoded strings"},
{type: "String", name: "${__urldecode}", description: "Decode a application/x-www-form-urlencoded string"},
{type: "String", name: "${__urlencode}", description: "Encode a string to a application/x-www-form-urlencoded string"},
{
type: "String",
name: "${__urlencode}",
description: "Encode a string to a application/x-www-form-urlencoded string"
},
{type: "String", name: "${__TestPlanName}", description: "Return name of current test plan"},
]
@ -219,6 +235,7 @@ export const WORKSTATION={
}
export const ENV_TYPE = {
DEFAULT: "DEFAULT",
JSON: "JSON",
GROUP: "GROUP"
}
@ -297,13 +314,69 @@ export const TASK_DATA = [
percentage: 14,
permission: ['PROJECT_MANAGER:READ', 'WORKSPACE_PROJECT_MANAGER:READ', 'PROJECT_TRACK_CASE:READ+CREATE', 'PROJECT_TRACK_REVIEW:READ+CREATE', 'PROJECT_TRACK_REVIEW:READ+COMMENT', 'PROJECT_TRACK_PLAN:READ+CREATE', 'PROJECT_TRACK_PLAN:READ+RELEVANCE_OR_CANCEL', 'PROJECT_TRACK_ISSUE:READ+CREATE', 'PROJECT_TRACK_CASE:READ+BATCH_ADD_PUBLIC'],
taskData: [
{ id: 1, name: "side_task.test_tracking.task_1", status: 1, permission: ['PROJECT_MANAGER:READ', 'WORKSPACE_PROJECT_MANAGER:READ'], api: [''], path: '/setting/project/:type', url: "" },
{ id: 2, name: "side_task.test_tracking.task_2", status: 0, permission: ['PROJECT_TRACK_CASE:READ+CREATE'], api: ["/test/case/add"], path: '/track/case/all', url: "/assets/guide/track/task-2.gif" },
{ id: 3, name: "side_task.test_tracking.task_3", status: 0, permission: ['PROJECT_TRACK_REVIEW:READ+CREATE'], api: ["/test/case/review/save"], path: '/track/review/all', url: "/assets/guide/track/task-3.gif" },
{ id: 4, name: "side_task.test_tracking.task_4", status: 0, permission: ['PROJECT_TRACK_REVIEW:READ+COMMENT'], api: ["/test/case/comment/save"], path: '/track/review/all', url: "/assets/guide/track/task-4.gif" },
{ id: 5, name: "side_task.test_tracking.task_5", status: 0, permission: ['PROJECT_TRACK_PLAN:READ+CREATE'], api: ["/test/plan/add"], path: '/track/plan/all', url: "/assets/guide/track/task-5.gif" },
{ id: 6, name: "side_task.test_tracking.task_6", status: 0, permission: ['PROJECT_TRACK_PLAN:READ+RELEVANCE_OR_CANCEL'], api: ["/test/plan/relevance"], path: '/track/plan/all', url: "/assets/guide/track/task-6.gif" },
{ id: 7, name: "side_task.test_tracking.task_7", status: 0, permission: ['PROJECT_TRACK_ISSUE:READ+CREATE','PROJECT_TRACK_CASE:READ+BATCH_ADD_PUBLIC'], api: ["issues/add","test/case/issues/relate"], path: '/track/issue', url: "/assets/guide/track/task-7.gif" },
{
id: 1,
name: "side_task.test_tracking.task_1",
status: 1,
permission: ['PROJECT_MANAGER:READ', 'WORKSPACE_PROJECT_MANAGER:READ'],
api: [''],
path: '/setting/project/:type',
url: ""
},
{
id: 2,
name: "side_task.test_tracking.task_2",
status: 0,
permission: ['PROJECT_TRACK_CASE:READ+CREATE'],
api: ["/test/case/add"],
path: '/track/case/all',
url: "/assets/guide/track/task-2.gif"
},
{
id: 3,
name: "side_task.test_tracking.task_3",
status: 0,
permission: ['PROJECT_TRACK_REVIEW:READ+CREATE'],
api: ["/test/case/review/save"],
path: '/track/review/all',
url: "/assets/guide/track/task-3.gif"
},
{
id: 4,
name: "side_task.test_tracking.task_4",
status: 0,
permission: ['PROJECT_TRACK_REVIEW:READ+COMMENT'],
api: ["/test/case/comment/save"],
path: '/track/review/all',
url: "/assets/guide/track/task-4.gif"
},
{
id: 5,
name: "side_task.test_tracking.task_5",
status: 0,
permission: ['PROJECT_TRACK_PLAN:READ+CREATE'],
api: ["/test/plan/add"],
path: '/track/plan/all',
url: "/assets/guide/track/task-5.gif"
},
{
id: 6,
name: "side_task.test_tracking.task_6",
status: 0,
permission: ['PROJECT_TRACK_PLAN:READ+RELEVANCE_OR_CANCEL'],
api: ["/test/plan/relevance"],
path: '/track/plan/all',
url: "/assets/guide/track/task-6.gif"
},
{
id: 7,
name: "side_task.test_tracking.task_7",
status: 0,
permission: ['PROJECT_TRACK_ISSUE:READ+CREATE', 'PROJECT_TRACK_CASE:READ+BATCH_ADD_PUBLIC'],
api: ["issues/add", "test/case/issues/relate"],
path: '/track/issue',
url: "/assets/guide/track/task-7.gif"
},
],
rate: 1,
status: 0
@ -315,13 +388,69 @@ export const TASK_DATA = [
percentage: 0,
permission: ['PROJECT_API_DEFINITION:READ+CREATE_API', 'PROJECT_API_DEFINITION:READ+IMPORT_API', 'PROJECT_API_DEFINITION:READ+DEBUG', 'PROJECT_API_DEFINITION:READ+CREATE_CASE', 'PROJECT_API_DEFINITION:READ', 'PROJECT_API_SCENARIO:READ+CREATE', 'PROJECT_API_SCENARIO:READ+SCHEDULE'],
taskData: [
{id: 1, name: "side_task.api_test.task_1", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+CREATE_API'], api: ["/api/definition/create"], url: "/assets/guide/api/task-1.gif" },
{id: 2, name: "side_task.api_test.task_2", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+IMPORT_API'], api: ["/api/definition/import"], url: "/assets/guide/api/task-2.gif" },
{id: 3, name: "side_task.api_test.task_3", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+DEBUG'], api: ["/api/definition/run/debug"], url: "/assets/guide/api/task-3.gif" },
{id: 4, name: "side_task.api_test.task_4", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+CREATE_CASE'], api: ["/api/testcase/create"], url: "/assets/guide/api/task-4.gif" },
{id: 5, name: "side_task.api_test.task_5", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ'], api: ["/share/generate/api/document"], url: "/assets/guide/api/task-5.gif" },
{id: 6, name: "side_task.api_test.task_6", status: 0, path: '/api/automation', permission: ['PROJECT_API_SCENARIO:READ+CREATE'], api: ["/api/automation/create"], url: "/assets/guide/api/task-6.gif" },
{id: 7, name: "side_task.api_test.task_7", status: 0, path: '/api/automation', permission: ['PROJECT_API_SCENARIO:READ+SCHEDULE'], api: ["/api/automation/schedule/create"], url: "/assets/guide/api/task-7.gif" },
{
id: 1,
name: "side_task.api_test.task_1",
status: 0,
path: '/api/definition',
permission: ['PROJECT_API_DEFINITION:READ+CREATE_API'],
api: ["/api/definition/create"],
url: "/assets/guide/api/task-1.gif"
},
{
id: 2,
name: "side_task.api_test.task_2",
status: 0,
path: '/api/definition',
permission: ['PROJECT_API_DEFINITION:READ+IMPORT_API'],
api: ["/api/definition/import"],
url: "/assets/guide/api/task-2.gif"
},
{
id: 3,
name: "side_task.api_test.task_3",
status: 0,
path: '/api/definition',
permission: ['PROJECT_API_DEFINITION:READ+DEBUG'],
api: ["/api/definition/run/debug"],
url: "/assets/guide/api/task-3.gif"
},
{
id: 4,
name: "side_task.api_test.task_4",
status: 0,
path: '/api/definition',
permission: ['PROJECT_API_DEFINITION:READ+CREATE_CASE'],
api: ["/api/testcase/create"],
url: "/assets/guide/api/task-4.gif"
},
{
id: 5,
name: "side_task.api_test.task_5",
status: 0,
path: '/api/definition',
permission: ['PROJECT_API_DEFINITION:READ'],
api: ["/share/generate/api/document"],
url: "/assets/guide/api/task-5.gif"
},
{
id: 6,
name: "side_task.api_test.task_6",
status: 0,
path: '/api/automation',
permission: ['PROJECT_API_SCENARIO:READ+CREATE'],
api: ["/api/automation/create"],
url: "/assets/guide/api/task-6.gif"
},
{
id: 7,
name: "side_task.api_test.task_7",
status: 0,
path: '/api/automation',
permission: ['PROJECT_API_SCENARIO:READ+SCHEDULE'],
api: ["/api/automation/schedule/create"],
url: "/assets/guide/api/task-7.gif"
},
],
rate: 0,
status: 0
@ -333,8 +462,24 @@ export const TASK_DATA = [
percentage: 0,
permission: ['PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE', "PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE_BATCH", 'PROJECT_PERFORMANCE_REPORT:READ'],
taskData: [
{id: 1, name: 'side_task.performance_test.task_1', status: 0, path: '/performance/test/all', permission: ['PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE',"PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE_BATCH"], api: ["/performance/save"], url: "/assets/guide/performance/task-1.gif" },
{id: 2, name: 'side_task.performance_test.task_2', status: 0, path: '/performance/report/all', permission: ['PROJECT_PERFORMANCE_REPORT:READ'], api: ["/share/generate/expired"], url: "/assets/guide/performance/task-2.gif" },
{
id: 1,
name: 'side_task.performance_test.task_1',
status: 0,
path: '/performance/test/all',
permission: ['PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE', "PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE_BATCH"],
api: ["/performance/save"],
url: "/assets/guide/performance/task-1.gif"
},
{
id: 2,
name: 'side_task.performance_test.task_2',
status: 0,
path: '/performance/report/all',
permission: ['PROJECT_PERFORMANCE_REPORT:READ'],
api: ["/share/generate/expired"],
url: "/assets/guide/performance/task-2.gif"
},
],
rate: 0,
status: 0
@ -346,9 +491,33 @@ export const TASK_DATA = [
percentage: 0,
permission: ['WORKSPACE_PROJECT_MANAGER:READ+CREATE', 'PROJECT_USER:READ+CREATE', 'PROJECT_ENVIRONMENT:READ+CREATE'],
taskData: [
{id: 1, name: 'side_task.project_setting.task_1', status: 0, permission: ['WORKSPACE_PROJECT_MANAGER:READ+CREATE'], api: ["/project/add"], path: '/setting/project/:type', url: "/assets/guide/project/task-1.gif" },
{id: 2, name: 'side_task.project_setting.task_2', status: 0, permission: ['PROJECT_USER:READ+CREATE'], api: ["/project/member/add","/setting/user/project/member/add"], path: '/project/member', url: "/assets/guide/project/task-2.gif" },
{id: 3, name: 'side_task.project_setting.task_3', status: 0, permission: ['PROJECT_ENVIRONMENT:READ+CREATE'], api: ["/environment/add"], path: '/project/env', url: "/assets/guide/project/task-3.gif" },
{
id: 1,
name: 'side_task.project_setting.task_1',
status: 0,
permission: ['WORKSPACE_PROJECT_MANAGER:READ+CREATE'],
api: ["/project/add"],
path: '/setting/project/:type',
url: "/assets/guide/project/task-1.gif"
},
{
id: 2,
name: 'side_task.project_setting.task_2',
status: 0,
permission: ['PROJECT_USER:READ+CREATE'],
api: ["/project/member/add", "/setting/user/project/member/add"],
path: '/project/member',
url: "/assets/guide/project/task-2.gif"
},
{
id: 3,
name: 'side_task.project_setting.task_3',
status: 0,
permission: ['PROJECT_ENVIRONMENT:READ+CREATE'],
api: ["/environment/add"],
path: '/project/env',
url: "/assets/guide/project/task-3.gif"
},
],
rate: 0,
status: 0
@ -360,9 +529,33 @@ export const TASK_DATA = [
percentage: 0,
permission: ['PROJECT_UI_ELEMENT:READ+CREATE', 'PROJECT_UI_SCENARIO:READ+CREATE', 'PROJECT_UI_SCENARIO:READ+RUN', 'PROJECT_UI_SCENARIO:READ+DEBUG'],
taskData: [
{id: 1, name: 'side_task.ui_test.task_1', status: 0, permission: ['PROJECT_UI_ELEMENT:READ+CREATE'], api: ["/ui/element/add"], path: '/ui/element', url: "/assets/guide/ui/task-1.gif" },
{id: 2, name: 'side_task.ui_test.task_2', status: 0, permission: ['PROJECT_UI_SCENARIO:READ+CREATE'], api: ["/ui/automation/create"], path: '/ui/automation', url: "/assets/guide/ui/task-2.gif" },
{id: 2, name: 'side_task.ui_test.task_3', status: 0, permission: ['PROJECT_UI_SCENARIO:READ+RUN','PROJECT_UI_SCENARIO:READ+DEBUG'], api: ["/ui/automation/run/debug"], path: '/ui/automation', url: "/assets/guide/ui/task-3.gif" },
{
id: 1,
name: 'side_task.ui_test.task_1',
status: 0,
permission: ['PROJECT_UI_ELEMENT:READ+CREATE'],
api: ["/ui/element/add"],
path: '/ui/element',
url: "/assets/guide/ui/task-1.gif"
},
{
id: 2,
name: 'side_task.ui_test.task_2',
status: 0,
permission: ['PROJECT_UI_SCENARIO:READ+CREATE'],
api: ["/ui/automation/create"],
path: '/ui/automation',
url: "/assets/guide/ui/task-2.gif"
},
{
id: 2,
name: 'side_task.ui_test.task_3',
status: 0,
permission: ['PROJECT_UI_SCENARIO:READ+RUN', 'PROJECT_UI_SCENARIO:READ+DEBUG'],
api: ["/ui/automation/run/debug"],
path: '/ui/automation',
url: "/assets/guide/ui/task-3.gif"
},
],
rate: 0,
status: 0

View File

@ -0,0 +1,5 @@
package io.metersphere.commons.constants;
public enum TestPlanExecuteCaseType {
API_CASE, SCENARIO, UI_SCENARIO, LOAD_CASE
}

View File

@ -0,0 +1,93 @@
package io.metersphere.service;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.utils.LoggerUtil;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class RedisTemplateService {
public static final long TIME_OUT = 480;
@Resource
private RedisTemplate<String, Object> redisTemplate;
public boolean setIfAbsent(String key, String value) {
try {
return redisTemplate.opsForValue().setIfAbsent(key, value);
} catch (Exception e) {
LoggerUtil.error(key, e);
return true;
}
}
public Object get(String key) {
try {
return redisTemplate.opsForValue().get(key);
} catch (Exception e) {
LoggerUtil.error(key, e);
}
return null;
}
public boolean delete(String key) {
try {
return redisTemplate.delete(key);
} catch (Exception e) {
LoggerUtil.error(key, e);
return false;
}
}
/**
* 加锁
*/
public boolean lock(String testPlanReportId, String key, String value) {
Boolean hasReport = redisTemplate.opsForValue().setIfAbsent(
StringUtils.join(testPlanReportId, key),
value,
TIME_OUT,
TimeUnit.MINUTES);
if (Boolean.FALSE.equals(hasReport)) {
redisTemplate.opsForValue().setIfPresent(
StringUtils.join(testPlanReportId, key),
value,
TIME_OUT,
TimeUnit.MINUTES);
return false;
} else {
return true;
}
}
public boolean has(String testPlanReportId, String key, String reportId) {
try {
Object value = redisTemplate.opsForValue().get(StringUtils.join(testPlanReportId, key));
return ObjectUtils.isNotEmpty(value) && StringUtils.equals(reportId, String.valueOf(value));
} catch (Exception e) {
LogUtil.error(e);
}
return false;
}
/**
* 解锁
*/
public boolean unlock(String testPlanReportId, String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(StringUtils.join(testPlanReportId, key)), value);
if (Objects.equals(1L, result)) {
return true;
}
return false;
}
}

View File

@ -6,6 +6,7 @@ import io.metersphere.base.mapper.ApiExecutionQueueMapper;
import io.metersphere.base.mapper.TestPlanLoadCaseMapper;
import io.metersphere.base.mapper.ext.BaseApiExecutionQueueMapper;
import io.metersphere.commons.constants.KafkaTopicConstants;
import io.metersphere.commons.constants.TestPlanExecuteCaseType;
import io.metersphere.commons.constants.TestPlanLoadCaseStatus;
import io.metersphere.commons.constants.TriggerMode;
import io.metersphere.commons.utils.BeanUtils;
@ -14,6 +15,7 @@ import io.metersphere.constants.RunModeConstants;
import io.metersphere.dto.RunModeConfigDTO;
import io.metersphere.plan.exec.queue.DBTestQueue;
import io.metersphere.request.RunTestPlanRequest;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.utils.LoggerUtil;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
@ -175,13 +177,16 @@ public class PerfQueueService {
return queue;
}
@Resource
private RedisTemplateService redisTemplateService;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public DBTestQueue add(Object runObj, String poolId, String reportId, String reportType, String runMode, RunModeConfigDTO config) {
LoggerUtil.info("报告【" + reportId + "】开始生成执行链");
public DBTestQueue add(Object runObj, String poolId, String testPlanReportId, String reportType, String runMode, RunModeConfigDTO config) {
LoggerUtil.info("报告【" + testPlanReportId + "】开始生成执行链");
if (config.getEnvMap() == null) {
config.setEnvMap(new LinkedHashMap<>());
}
ApiExecutionQueue executionQueue = getApiExecutionQueue(poolId, reportId, reportType, runMode, config);
ApiExecutionQueue executionQueue = getApiExecutionQueue(poolId, testPlanReportId, reportType, runMode, config);
queueMapper.insert(executionQueue);
DBTestQueue resQueue = new DBTestQueue();
BeanUtils.copyBean(resQueue, executionQueue);
@ -196,7 +201,9 @@ public class PerfQueueService {
extApiExecutionQueueMapper.sqlInsert(queueDetails);
}
resQueue.setDetailMap(detailMap);
LoggerUtil.info("报告【" + reportId + "】生成执行链结束");
LoggerUtil.info("报告【" + testPlanReportId + "】生成执行链结束");
//移除Redis中的标志
redisTemplateService.unlock(testPlanReportId, TestPlanExecuteCaseType.LOAD_CASE.name(), testPlanReportId);
return resQueue;
}
@ -207,7 +214,7 @@ public class PerfQueueService {
executionQueue.setPoolId(poolId);
executionQueue.setFailure(config.isOnSampleError());
executionQueue.setReportId(reportId);
executionQueue.setReportType(StringUtils.isNotEmpty(reportType) ? reportType : RunModeConstants.INDEPENDENCE.toString());
executionQueue.setReportType(TestPlanExecuteCaseType.LOAD_CASE.name());
executionQueue.setRunMode(runMode);
return executionQueue;
}

View File

@ -5,6 +5,7 @@ import io.metersphere.base.mapper.ApiExecutionQueueMapper;
import io.metersphere.commons.constants.KafkaTopicConstants;
import io.metersphere.plan.service.AutomationCaseExecOverService;
import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.utils.LoggerUtil;
import io.metersphere.utils.NamedThreadFactory;
import jakarta.annotation.Resource;
@ -28,6 +29,8 @@ public class ExecReportListener {
@Resource
private TestPlanReportService testPlanReportService;
@Resource
private RedisTemplateService redisTemplateService;
@Resource
private AutomationCaseExecOverService automationCaseExecOverService;
// 线程池维护线程的最少数量
@ -57,6 +60,7 @@ public class ExecReportListener {
task.setApiExecutionQueueDetailMapper(executionQueueDetailMapper);
task.setAutomationCaseExecOverService(automationCaseExecOverService);
task.setTestPlanReportService(testPlanReportService);
task.setRedisTemplateService(redisTemplateService);
task.setRecord(item);
threadPool.execute(task);
});

View File

@ -5,14 +5,16 @@ 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.TestPlanExecuteCaseType;
import io.metersphere.commons.constants.TestPlanReportStatus;
import io.metersphere.plan.service.AutomationCaseExecOverService;
import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.utils.LoggerUtil;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
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;
@ -25,7 +27,7 @@ public class ExecReportListenerTask implements Runnable {
private ApiExecutionQueueDetailMapper apiExecutionQueueDetailMapper;
private TestPlanReportService testPlanReportService;
private AutomationCaseExecOverService automationCaseExecOverService;
private RedisTemplateService redisTemplateService;
@Override
public void run() {
@ -50,15 +52,15 @@ public class ExecReportListenerTask implements Runnable {
ApiExecutionQueueExample executionQueueExample = new ApiExecutionQueueExample();
executionQueueExample.createCriteria().andReportIdEqualTo(testPlanReportId);
List<ApiExecutionQueue> queues = apiExecutionQueueMapper.selectByExample(executionQueueExample);
if (CollectionUtils.isEmpty(queues)) {
if (CollectionUtils.isEmpty(queues) && this.isTestPlanIsEmptyInRedis(testPlanReportId)) {
LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
} else {
} else if (CollectionUtils.isNotEmpty(queues)) {
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) {
if (count == 0 && this.isTestPlanIsEmptyInRedis(testPlanReportId)) {
LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
LoggerUtil.info("Clear Queue" + ids);
@ -68,4 +70,15 @@ public class ExecReportListenerTask implements Runnable {
}
}
}
/**
* 测试计划执行时会将运行标志放入redis中当测试计划执行队列入库后会将redis中的标志清除
*/
private boolean isTestPlanIsEmptyInRedis(String testPlanReportId) {
Object scenarioObj = redisTemplateService.get(testPlanReportId + TestPlanExecuteCaseType.SCENARIO);
Object apiCaseObj = redisTemplateService.get(testPlanReportId + TestPlanExecuteCaseType.API_CASE);
Object uiObj = redisTemplateService.get(testPlanReportId + TestPlanExecuteCaseType.UI_SCENARIO);
Object loadObj = redisTemplateService.get(testPlanReportId + TestPlanExecuteCaseType.LOAD_CASE);
return ObjectUtils.isEmpty(scenarioObj) && ObjectUtils.isEmpty(apiCaseObj) && ObjectUtils.isEmpty(uiObj) && ObjectUtils.isEmpty(loadObj);
}
}

View File

@ -1,221 +0,0 @@
package io.metersphere.plan.service;
import io.metersphere.base.domain.TestPlanWithBLOBs;
import io.metersphere.base.mapper.TestPlanMapper;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.constants.RunModeConstants;
import io.metersphere.dto.*;
import io.metersphere.i18n.Translator;
import io.metersphere.plan.dto.ExecutionWay;
import io.metersphere.plan.request.api.TestPlanRunRequest;
import io.metersphere.plan.service.remote.api.PlanTestPlanApiCaseService;
import io.metersphere.plan.service.remote.api.PlanTestPlanScenarioCaseService;
import io.metersphere.plan.service.remote.performance.PerfExecService;
import io.metersphere.plan.service.remote.ui.PlanTestPlanUiScenarioCaseService;
import io.metersphere.plan.utils.TestPlanRequestUtil;
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.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
@Transactional
public class TestPlanExecuteService {
@Resource
@Lazy
private TestPlanService testPlanService;
@Resource
private TestPlanReportService testPlanReportService;
@Resource
private PlanTestPlanApiCaseService planTestPlanApiCaseService;
@Resource
private PlanTestPlanScenarioCaseService planTestPlanScenarioCaseService;
@Resource
private PerfExecService perfExecService;
@Resource
private PlanTestPlanUiScenarioCaseService planTestPlanUiScenarioCaseService;
@Resource
private TestPlanMapper testPlanMapper;
/**
* 执行测试计划流程是会调用其它服务的执行方法并通过kafka传递信息给test-track服务来判断测试计划是否执行结束
* 执行方法采用单独的事务控制执行完了就提交让测试报告以及包括执行内容的数据及时入库
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public String runTestPlan(String testPlanId, String projectId, String userId, String triggerMode, String planReportId, String executionWay, String apiRunConfig) {
// 校验测试计划是否在执行中
if (testPlanService.checkTestPlanIsRunning(testPlanId)) {
LogUtil.info("当前测试计划正在执行中,请稍后再试", testPlanId);
MSException.throwException(Translator.get("test_plan_run_message"));
}
RunModeConfigDTO runModeConfig = null;
try {
runModeConfig = JSON.parseObject(apiRunConfig, RunModeConfigDTO.class);
} catch (Exception e) {
LogUtil.error(e);
}
if (runModeConfig == null) {
runModeConfig = this.buildRunModeConfigDTO();
}
//环境参数为空时依据测试计划保存的环境执行
if (((StringUtils.equals("GROUP", runModeConfig.getEnvironmentType()) && StringUtils.isBlank(runModeConfig.getEnvironmentGroupId()))
|| (!StringUtils.equals("GROUP", runModeConfig.getEnvironmentType()) && MapUtils.isEmpty(runModeConfig.getEnvMap()) && MapUtils.isEmpty(runModeConfig.getTestPlanDefaultEnvMap())))
&& !StringUtils.equals(executionWay, ExecutionWay.RUN.name())) {
TestPlanWithBLOBs testPlanWithBLOBs = testPlanMapper.selectByPrimaryKey(testPlanId);
if (StringUtils.isNotEmpty(testPlanWithBLOBs.getRunModeConfig())) {
try {
Map json = JSON.parseMap(testPlanWithBLOBs.getRunModeConfig());
TestPlanRequestUtil.changeStringToBoolean(json);
TestPlanRunRequest testPlanRunRequest = JSON.parseObject(JSON.toJSONString(json), TestPlanRunRequest.class);
if (testPlanRunRequest != null) {
String envType = testPlanRunRequest.getEnvironmentType();
Map<String, String> envMap = testPlanRunRequest.getEnvMap();
String environmentGroupId = testPlanRunRequest.getEnvironmentGroupId();
runModeConfig = testPlanService.getRunModeConfigDTO(testPlanRunRequest, envType, envMap, environmentGroupId, testPlanId);
runModeConfig.setTestPlanDefaultEnvMap(testPlanRunRequest.getTestPlanDefaultEnvMap());
if (!testPlanRunRequest.isRunWithinResourcePool()) {
runModeConfig.setResourcePoolId(null);
}
}
} catch (Exception e) {
LogUtil.error("获取测试计划保存的环境信息出错!", e);
}
}
}
if (planReportId == null) {
planReportId = UUID.randomUUID().toString();
}
if (testPlanService.haveExecCase(testPlanId, true)) {
testPlanService.verifyPool(projectId, runModeConfig);
}
//创建测试报告然后返回的ID重新赋值为resourceID作为后续的参数
TestPlanScheduleReportInfoDTO reportInfoDTO = testPlanService.genTestPlanReport(planReportId, testPlanId, userId, triggerMode, runModeConfig);
LoggerUtil.info("预生成测试计划报告【" + reportInfoDTO.getTestPlanReport() != null ? reportInfoDTO.getTestPlanReport().getName() : StringUtils.EMPTY + "】计划报告ID[" + planReportId + "]");
List<TestPlanApiDTO> apiTestCases = null;
List<TestPlanScenarioDTO> scenarioCases = null;
List<TestPlanUiScenarioDTO> uiScenarios = null;
Map<String, String> loadCaseReportMap = null;
if (MapUtils.isNotEmpty(reportInfoDTO.getApiTestCaseDataMap())) {
try {
apiTestCases = planTestPlanApiCaseService.getFailureListByIds(reportInfoDTO.getApiTestCaseDataMap().keySet());
} catch (Exception e) {
LogUtil.error("测试计划执行查询接口用例失败!", e);
}
}
if (MapUtils.isNotEmpty(reportInfoDTO.getPlanScenarioIdMap())) {
try {
scenarioCases = planTestPlanScenarioCaseService.getFailureListByIds(reportInfoDTO.getPlanScenarioIdMap().keySet());
} catch (Exception e) {
LogUtil.error("测试计划执行查询场景用例失败!", e);
}
}
if (MapUtils.isNotEmpty(reportInfoDTO.getUiScenarioIdMap())) {
try {
uiScenarios = planTestPlanUiScenarioCaseService.getFailureListByIds(reportInfoDTO.getUiScenarioIdMap().keySet());
} catch (Exception e) {
LogUtil.error("测试计划执行查询UI用例失败!", e);
}
}
boolean haveApiCaseExec = false, haveScenarioCaseExec = false, haveLoadCaseExec = false, haveUICaseExec = false;
if (CollectionUtils.isNotEmpty(apiTestCases)) {
//执行接口案例任务
LoggerUtil.info("开始执行测试计划接口用例 " + planReportId);
try {
Map<String, String> apiCaseReportMap = testPlanService.executeApiTestCase(triggerMode, planReportId, userId, testPlanId, runModeConfig);
if (MapUtils.isNotEmpty(apiCaseReportMap)) {
haveApiCaseExec = true;
for (TestPlanApiDTO dto : apiTestCases) {
dto.setReportId(apiCaseReportMap.get(dto.getId()));
}
}
} catch (Exception e) {
apiTestCases = null;
LoggerUtil.info("测试报告" + planReportId + "本次执行测试计划接口用例失败! ", e);
}
}
if (CollectionUtils.isNotEmpty(scenarioCases)) {
//执行场景执行任务
LoggerUtil.info("开始执行测试计划场景用例 " + planReportId);
try {
Map<String, String> scenarioReportMap = testPlanService.executeScenarioCase(planReportId, testPlanId, projectId, runModeConfig, triggerMode, userId, reportInfoDTO.getPlanScenarioIdMap());
if (MapUtils.isNotEmpty(scenarioReportMap)) {
haveScenarioCaseExec = true;
List<TestPlanScenarioDTO> removeDTO = new ArrayList<>();
for (TestPlanScenarioDTO dto : scenarioCases) {
if (scenarioReportMap.containsKey(dto.getId())) {
dto.setReportId(scenarioReportMap.get(dto.getId()));
} else {
removeDTO.add(dto);
}
}
if (CollectionUtils.isNotEmpty(removeDTO)) {
scenarioCases.removeAll(removeDTO);
}
}
} catch (Exception e) {
scenarioCases = null;
LoggerUtil.info("测试报告" + planReportId + "本次执行测试计划场景用例失败! ", e);
}
}
if (MapUtils.isNotEmpty(reportInfoDTO.getPerformanceIdMap())) {
//执行性能测试任务
LoggerUtil.info("开始执行测试计划性能用例 " + planReportId);
try {
loadCaseReportMap = perfExecService.executeLoadCase(planReportId, runModeConfig, testPlanService.transformationPerfTriggerMode(triggerMode), reportInfoDTO.getPerformanceIdMap());
if (MapUtils.isNotEmpty(loadCaseReportMap)) {
haveLoadCaseExec = true;
}
} catch (Exception e) {
LoggerUtil.info("测试报告" + planReportId + "本次执行测试计划性能用例失败! ", e);
}
}
if (CollectionUtils.isNotEmpty(uiScenarios)) {
//执行UI场景执行任务
LoggerUtil.info("开始执行测试计划 UI 场景用例 " + planReportId);
try {
Map<String, String> uiScenarioReportMap = testPlanService.executeUiScenarioCase(planReportId, testPlanId, projectId, runModeConfig, triggerMode, userId, reportInfoDTO.getUiScenarioIdMap());
if (MapUtils.isNotEmpty(uiScenarioReportMap)) {
haveUICaseExec = true;
for (TestPlanUiScenarioDTO dto : uiScenarios) {
dto.setReportId(uiScenarioReportMap.get(dto.getId()));
}
}
} catch (Exception e) {
uiScenarios = null;
LoggerUtil.info("测试报告" + planReportId + "本次执行测试计划 UI 用例失败! ", e);
}
}
LoggerUtil.info("开始生成测试计划报告内容 " + planReportId);
testPlanReportService.createTestPlanReportContentReportIds(planReportId, apiTestCases, scenarioCases, uiScenarios, loadCaseReportMap);
if (!haveApiCaseExec && !haveScenarioCaseExec && !haveLoadCaseExec && !haveUICaseExec) {
//如果没有执行的自动化用例调用结束测试计划的方法 因为方法中包含着测试计划执行队列的处理逻辑
testPlanReportService.testPlanUnExecute(reportInfoDTO.getTestPlanReport());
}
return planReportId;
}
private RunModeConfigDTO buildRunModeConfigDTO() {
RunModeConfigDTO runModeConfig = new RunModeConfigDTO();
runModeConfig.setMode(RunModeConstants.SERIAL.name());
runModeConfig.setReportType("iddReport");
runModeConfig.setEnvMap(new HashMap<>());
runModeConfig.setOnSampleError(false);
return runModeConfig;
}
}

View File

@ -49,6 +49,7 @@ import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -622,6 +623,7 @@ public class TestPlanReportService {
}
}
@Async
public void testPlanUnExecute(TestPlanReport testPlanReport) {
if (testPlanReport != null && !StringUtils.equalsIgnoreCase(testPlanReport.getStatus(), TestPlanReportStatus.COMPLETED.name())) {
testPlanReport.setIsApiCaseExecuting(false);

View File

@ -40,6 +40,7 @@ import io.metersphere.plan.request.performance.LoadPlanReportDTO;
import io.metersphere.plan.request.ui.RunUiScenarioRequest;
import io.metersphere.plan.request.ui.TestPlanUiExecuteReportDTO;
import io.metersphere.plan.request.ui.UiPlanReportRequest;
import io.metersphere.plan.service.execute.TestPlanExecuteService;
import io.metersphere.plan.service.remote.api.PlanApiAutomationService;
import io.metersphere.plan.service.remote.api.PlanTestPlanApiCaseService;
import io.metersphere.plan.service.remote.api.PlanTestPlanScenarioCaseService;
@ -495,6 +496,22 @@ public class TestPlanService {
request.setProjectId(request.getProjectId());
}
List<TestPlanDTOWithMetric> testPlanList = extTestPlanMapper.list(request);
//统计测试计划的测试用例数
List<String> testPlanIdList = testPlanList.stream().map(TestPlanDTOWithMetric::getId).collect(Collectors.toList());
Map<String, ParamsDTO> planTestCaseCountMap = extTestPlanMapper.testPlanTestCaseCount(testPlanIdList);
Map<String, ParamsDTO> planApiCaseMap = extTestPlanMapper.testPlanApiCaseCount(testPlanIdList);
Map<String, ParamsDTO> planApiScenarioMap = extTestPlanMapper.testPlanApiScenarioCount(testPlanIdList);
Map<String, ParamsDTO> planUiScenarioMap = extTestPlanMapper.testPlanUiScenarioCount(testPlanIdList);
Map<String, ParamsDTO> planLoadCaseMap = extTestPlanMapper.testPlanLoadCaseCount(testPlanIdList);
for (TestPlanDTOWithMetric testPlanMetric : testPlanList) {
testPlanMetric.setTestPlanTestCaseCount(planTestCaseCountMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planTestCaseCountMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planTestCaseCountMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanApiCaseCount(planApiCaseMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planApiCaseMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planApiCaseMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanApiScenarioCount(planApiScenarioMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planApiScenarioMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planApiScenarioMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanUiScenarioCount(planUiScenarioMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planUiScenarioMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planUiScenarioMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanLoadCaseCount(planLoadCaseMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planLoadCaseMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planLoadCaseMap.get(testPlanMetric.getId()).getValue()));
}
if (CollectionUtils.isNotEmpty(testPlanList)) {
List<String> changeToFinishedIds = new ArrayList<>();
//检查定时任务的设置
@ -556,17 +573,7 @@ public class TestPlanService {
public List<TestPlanDTOWithMetric> selectTestPlanMetricById(List<String> idList) {
List<TestPlanDTOWithMetric> testPlanMetricList = this.calcTestPlanRateByIdList(idList);
Map<String, ParamsDTO> planTestCaseCountMap = extTestPlanMapper.testPlanTestCaseCount(idList);
Map<String, ParamsDTO> planApiCaseMap = extTestPlanMapper.testPlanApiCaseCount(idList);
Map<String, ParamsDTO> planApiScenarioMap = extTestPlanMapper.testPlanApiScenarioCount(idList);
Map<String, ParamsDTO> planUiScenarioMap = extTestPlanMapper.testPlanUiScenarioCount(idList);
Map<String, ParamsDTO> planLoadCaseMap = extTestPlanMapper.testPlanLoadCaseCount(idList);
for (TestPlanDTOWithMetric testPlanMetric : testPlanMetricList) {
testPlanMetric.setTestPlanTestCaseCount(planTestCaseCountMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planTestCaseCountMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planTestCaseCountMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanApiCaseCount(planApiCaseMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planApiCaseMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planApiCaseMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanApiScenarioCount(planApiScenarioMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planApiScenarioMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planApiScenarioMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanUiScenarioCount(planUiScenarioMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planUiScenarioMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planUiScenarioMap.get(testPlanMetric.getId()).getValue()));
testPlanMetric.setTestPlanLoadCaseCount(planLoadCaseMap.get(testPlanMetric.getId()) == null ? 0 : Integer.parseInt(planLoadCaseMap.get(testPlanMetric.getId()).getValue() == null ? "0" : planLoadCaseMap.get(testPlanMetric.getId()).getValue()));
List<User> followUsers = this.getPlanFollow(testPlanMetric.getId());
testPlanMetric.setFollowUsers(followUsers);
}

View File

@ -0,0 +1,352 @@
package io.metersphere.plan.service.execute;
import com.esotericsoftware.minlog.Log;
import io.metersphere.base.domain.TestPlanReport;
import io.metersphere.base.domain.TestPlanWithBLOBs;
import io.metersphere.base.mapper.TestPlanMapper;
import io.metersphere.commons.constants.TestPlanExecuteCaseType;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.constants.RunModeConstants;
import io.metersphere.dto.*;
import io.metersphere.i18n.Translator;
import io.metersphere.plan.dto.ExecutionWay;
import io.metersphere.plan.request.api.TestPlanRunRequest;
import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.plan.service.TestPlanService;
import io.metersphere.plan.service.remote.api.PlanTestPlanApiCaseService;
import io.metersphere.plan.service.remote.api.PlanTestPlanScenarioCaseService;
import io.metersphere.plan.service.remote.performance.PerfExecService;
import io.metersphere.plan.service.remote.ui.PlanTestPlanUiScenarioCaseService;
import io.metersphere.plan.utils.TestPlanRequestUtil;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.utils.LoggerUtil;
import jakarta.annotation.Resource;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.CountDownLatch;
@Service
@Transactional(rollbackFor = Exception.class)
public class TestPlanExecuteService {
@Resource
@Lazy
private TestPlanService testPlanService;
@Resource
private TestPlanReportService testPlanReportService;
@Resource
private PlanTestPlanApiCaseService planTestPlanApiCaseService;
@Resource
private PlanTestPlanScenarioCaseService planTestPlanScenarioCaseService;
@Resource
private PerfExecService perfExecService;
@Resource
private PlanTestPlanUiScenarioCaseService planTestPlanUiScenarioCaseService;
@Resource
private TestPlanMapper testPlanMapper;
@Resource
private RedisTemplateService redisTemplateService;
/**
* 执行测试计划流程是会调用其它服务的执行方法并通过kafka传递信息给test-track服务来判断测试计划是否执行结束
* 执行方法采用单独的事务控制执行完了就提交让测试报告以及包括执行内容的数据及时入库
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public String runTestPlan(String testPlanId, String projectId, String userId, String triggerMode, String planReportId, String executionWay, String apiRunConfig) {
//获取运行模式
RunModeConfigDTO runModeConfig = this.getRunModeConfig(apiRunConfig, executionWay, testPlanId);
if (StringUtils.isEmpty(planReportId)) {
planReportId = UUID.randomUUID().toString();
}
TestPlanReport testPlanReport = null;
try {
this.checkTestPlanCanRunning(testPlanId, projectId, runModeConfig);
//创建测试报告然后返回的ID重新赋值为resourceID作为后续的参数
TestPlanScheduleReportInfoDTO reportInfoDTO = testPlanService.genTestPlanReport(planReportId, testPlanId, userId, triggerMode, runModeConfig);
testPlanReport = reportInfoDTO.getTestPlanReport();
LoggerUtil.info("预生成测试计划报告【" + (reportInfoDTO.getTestPlanReport() != null ? reportInfoDTO.getTestPlanReport().getName() : StringUtils.EMPTY) + "】计划报告ID[" + planReportId + "]");
this.execute(reportInfoDTO, runModeConfig, triggerMode, projectId, userId);
} catch (Exception e) {
//如果执行失败要保证执行队列是否不被影响
if (testPlanReport == null) {
testPlanReport = new TestPlanReport();
testPlanReport.setId(planReportId);
}
testPlanReportService.testPlanUnExecute(testPlanReport);
Log.error("执行测试计划失败!", e);
}
return planReportId;
}
private void checkTestPlanCanRunning(String testPlanId, String projectId, RunModeConfigDTO runModeConfig) throws Exception {
// 校验测试计划是否在执行中
if (testPlanService.checkTestPlanIsRunning(testPlanId)) {
LogUtil.info("当前测试计划正在执行中,请稍后再试", testPlanId);
MSException.throwException(Translator.get("test_plan_run_message"));
}
//检查执行资源池
if (testPlanService.haveExecCase(testPlanId, true)) {
testPlanService.verifyPool(projectId, runModeConfig);
}
}
private void execute(TestPlanScheduleReportInfoDTO reportInfoDTO, RunModeConfigDTO runModeConfig, String triggerMode, String projectId, String executeUser) throws Exception {
CaseExecuteResult caseExecuteResult = new CaseExecuteResult();
CountDownLatch countDownLatch = this.countDownExecute(reportInfoDTO, caseExecuteResult, runModeConfig, triggerMode, projectId, executeUser);
countDownLatch.await();
LoggerUtil.info("开始生成测试计划报告内容 " + reportInfoDTO.getTestPlanReport().getId());
testPlanReportService.createTestPlanReportContentReportIds(reportInfoDTO.getTestPlanReport().getId(),
caseExecuteResult.getApiCaseDTO(), caseExecuteResult.getScenarioCases(), caseExecuteResult.getUiScenarios(), caseExecuteResult.getLoadCaseReportMap());
if (!caseExecuteResult.isExecuting()) {
MSException.throwException("测试计划执行失败不存在可执行的用例报告ID:[" + reportInfoDTO.getTestPlanReport().getTestPlanId() + "]");
}
}
private CountDownLatch countDownExecute(TestPlanScheduleReportInfoDTO reportInfoDTO, CaseExecuteResult caseExecuteResult, RunModeConfigDTO runModeConfig, String triggerMode, String projectId, String executeUser) {
CountDownLatch countDownLatch = new CountDownLatch(4);
try {
this.executeApiCase(caseExecuteResult, reportInfoDTO.getApiTestCaseDataMap(), triggerMode,
reportInfoDTO.getTestPlanReport().getId(), reportInfoDTO.getTestPlanReport().getTestPlanId(), executeUser, runModeConfig);
} catch (Exception e) {
LogUtil.error(e);
} finally {
countDownLatch.countDown();
}
try {
this.executeScenarioCase(caseExecuteResult, reportInfoDTO.getPlanScenarioIdMap(), triggerMode,
reportInfoDTO.getTestPlanReport().getId(), reportInfoDTO.getTestPlanReport().getTestPlanId(), projectId, executeUser, runModeConfig);
} catch (Exception e) {
LogUtil.error(e);
} finally {
countDownLatch.countDown();
}
try {
this.executeUiCase(caseExecuteResult, reportInfoDTO.getUiScenarioIdMap(), triggerMode,
reportInfoDTO.getTestPlanReport().getId(), reportInfoDTO.getTestPlanReport().getTestPlanId(), projectId, executeUser, runModeConfig);
} catch (Exception e) {
LogUtil.error(e);
} finally {
countDownLatch.countDown();
}
try {
this.executeLoadCase(caseExecuteResult, reportInfoDTO.getPerformanceIdMap(), triggerMode,
reportInfoDTO.getTestPlanReport().getId(), runModeConfig);
} catch (Exception e) {
LogUtil.error(e);
} finally {
countDownLatch.countDown();
}
return countDownLatch;
}
private void executeApiCase(CaseExecuteResult executeResult, Map<String, String> executeCase, String triggerMode, String testPlanReportId, String testPlanId, String executeUser, RunModeConfigDTO runModeConfig) {
boolean executing = false;
List<TestPlanApiDTO> apiTestCases = null;
if (MapUtils.isNotEmpty(executeCase)) {
try {
apiTestCases = planTestPlanApiCaseService.getFailureListByIds(executeCase.keySet());
} catch (Exception e) {
LogUtil.error("测试计划执行查询接口用例失败!", e);
}
}
if (CollectionUtils.isNotEmpty(apiTestCases)) {
//执行接口案例任务
LoggerUtil.info("开始执行测试计划接口用例 " + testPlanReportId);
try {
redisTemplateService.lock(testPlanReportId, TestPlanExecuteCaseType.API_CASE.name(), testPlanReportId);
Map<String, String> apiCaseReportMap = testPlanService.executeApiTestCase(triggerMode, testPlanReportId, executeUser, testPlanId, runModeConfig);
if (MapUtils.isNotEmpty(apiCaseReportMap)) {
executing = true;
for (TestPlanApiDTO dto : apiTestCases) {
dto.setReportId(apiCaseReportMap.get(dto.getId()));
}
}
} catch (Exception e) {
redisTemplateService.unlock(testPlanReportId, TestPlanExecuteCaseType.API_CASE.name(), testPlanReportId);
apiTestCases = null;
LoggerUtil.info("测试报告" + testPlanReportId + "本次执行测试计划接口用例失败! ", e);
}
}
executeResult.setApiCaseExecuting(executing);
executeResult.setApiCaseDTO(apiTestCases);
}
private void executeScenarioCase(CaseExecuteResult executeResult, Map<String, String> executeCase, String triggerMode, String testPlanReportId, String testPlanId, String projectId, String executeUser, RunModeConfigDTO runModeConfig) {
boolean executing = false;
List<TestPlanScenarioDTO> scenarioCases = null;
if (MapUtils.isNotEmpty(executeCase)) {
try {
scenarioCases = planTestPlanScenarioCaseService.getFailureListByIds(executeCase.keySet());
} catch (Exception e) {
LogUtil.error("测试计划执行查询场景用例失败!", e);
}
}
if (CollectionUtils.isNotEmpty(scenarioCases)) {
//执行场景执行任务
LoggerUtil.info("开始执行测试计划场景用例 " + testPlanReportId);
try {
redisTemplateService.lock(testPlanReportId, TestPlanExecuteCaseType.SCENARIO.name(), testPlanReportId);
Map<String, String> scenarioReportMap = testPlanService.executeScenarioCase(testPlanReportId, testPlanId, projectId, runModeConfig, triggerMode, executeUser, executeCase);
if (MapUtils.isNotEmpty(scenarioReportMap)) {
executing = true;
List<TestPlanScenarioDTO> removeDTO = new ArrayList<>();
for (TestPlanScenarioDTO dto : scenarioCases) {
if (scenarioReportMap.containsKey(dto.getId())) {
dto.setReportId(scenarioReportMap.get(dto.getId()));
} else {
removeDTO.add(dto);
}
}
if (CollectionUtils.isNotEmpty(removeDTO)) {
scenarioCases.removeAll(removeDTO);
}
}
} catch (Exception e) {
redisTemplateService.unlock(testPlanReportId, TestPlanExecuteCaseType.SCENARIO.name(), testPlanReportId);
scenarioCases = null;
LoggerUtil.info("测试报告" + testPlanReportId + "本次执行测试计划场景用例失败! ", e);
}
}
executeResult.setScenarioCases(scenarioCases);
executeResult.setScenarioExecuting(executing);
}
private void executeUiCase(CaseExecuteResult executeResult, Map<String, String> executeCase, String triggerMode, String testPlanReportId, String testPlanId, String projectId, String executeUser, RunModeConfigDTO runModeConfig) {
boolean executing = false;
List<TestPlanUiScenarioDTO> uiScenarios = null;
if (MapUtils.isNotEmpty(executeCase)) {
try {
uiScenarios = planTestPlanUiScenarioCaseService.getFailureListByIds(executeCase.keySet());
} catch (Exception e) {
LogUtil.error("测试计划执行查询UI用例失败!", e);
}
}
if (CollectionUtils.isNotEmpty(uiScenarios)) {
//执行UI场景执行任务
LoggerUtil.info("开始执行测试计划 UI 场景用例 " + testPlanReportId);
try {
redisTemplateService.lock(testPlanReportId, TestPlanExecuteCaseType.UI_SCENARIO.name(), testPlanReportId);
Map<String, String> uiScenarioReportMap = testPlanService.executeUiScenarioCase(testPlanReportId, testPlanId, projectId, runModeConfig, triggerMode, executeUser, executeCase);
if (MapUtils.isNotEmpty(uiScenarioReportMap)) {
executing = true;
for (TestPlanUiScenarioDTO dto : uiScenarios) {
dto.setReportId(uiScenarioReportMap.get(dto.getId()));
}
}
} catch (Exception e) {
redisTemplateService.unlock(testPlanReportId, TestPlanExecuteCaseType.UI_SCENARIO.name(), testPlanReportId);
uiScenarios = null;
LoggerUtil.info("测试报告" + testPlanReportId + "本次执行测试计划 UI 用例失败! ", e);
}
}
executeResult.setUiScenarios(uiScenarios);
executeResult.setUiScenarioExecuting(executing);
}
private void executeLoadCase(CaseExecuteResult executeResult, Map<String, String> executeCase, String triggerMode, String testPlanReportId, RunModeConfigDTO runModeConfig) {
boolean executing = false;
Map<String, String> loadCaseReportMap = null;
if (MapUtils.isNotEmpty(executeCase)) {
//执行性能测试任务
LoggerUtil.info("开始执行测试计划性能用例 " + testPlanReportId);
try {
redisTemplateService.lock(testPlanReportId, TestPlanExecuteCaseType.LOAD_CASE.name(), testPlanReportId);
loadCaseReportMap = perfExecService.executeLoadCase(testPlanReportId, runModeConfig, testPlanService.transformationPerfTriggerMode(triggerMode), executeCase);
if (MapUtils.isNotEmpty(loadCaseReportMap)) {
executing = true;
}
} catch (Exception e) {
redisTemplateService.unlock(testPlanReportId, TestPlanExecuteCaseType.LOAD_CASE.name(), testPlanReportId);
LoggerUtil.info("测试报告" + testPlanReportId + "本次执行测试计划性能用例失败! ", e);
}
}
executeResult.setLoadCaseReportMap(loadCaseReportMap);
executeResult.setLoadCaseExecuting(executing);
}
private RunModeConfigDTO getRunModeConfig(String apiRunConfig, String executionWay, String testPlanId) {
RunModeConfigDTO runModeConfig = null;
try {
runModeConfig = JSON.parseObject(apiRunConfig, RunModeConfigDTO.class);
} catch (Exception e) {
LogUtil.error(e);
}
if (runModeConfig == null) {
runModeConfig = this.buildRunModeConfigDTO();
}
//环境参数为空时依据测试计划保存的环境执行
if (((StringUtils.equals("GROUP", runModeConfig.getEnvironmentType()) && StringUtils.isBlank(runModeConfig.getEnvironmentGroupId()))
|| (!StringUtils.equals("GROUP", runModeConfig.getEnvironmentType()) && MapUtils.isEmpty(runModeConfig.getEnvMap()) && MapUtils.isEmpty(runModeConfig.getTestPlanDefaultEnvMap())))
&& !StringUtils.equals(executionWay, ExecutionWay.RUN.name())) {
TestPlanWithBLOBs testPlanWithBLOBs = testPlanMapper.selectByPrimaryKey(testPlanId);
if (StringUtils.isNotEmpty(testPlanWithBLOBs.getRunModeConfig())) {
try {
Map json = JSON.parseMap(testPlanWithBLOBs.getRunModeConfig());
TestPlanRequestUtil.changeStringToBoolean(json);
TestPlanRunRequest testPlanRunRequest = JSON.parseObject(JSON.toJSONString(json), TestPlanRunRequest.class);
if (testPlanRunRequest != null) {
String envType = testPlanRunRequest.getEnvironmentType();
Map<String, String> envMap = testPlanRunRequest.getEnvMap();
String environmentGroupId = testPlanRunRequest.getEnvironmentGroupId();
runModeConfig = testPlanService.getRunModeConfigDTO(testPlanRunRequest, envType, envMap, environmentGroupId, testPlanId);
runModeConfig.setTestPlanDefaultEnvMap(testPlanRunRequest.getTestPlanDefaultEnvMap());
if (!testPlanRunRequest.isRunWithinResourcePool()) {
runModeConfig.setResourcePoolId(null);
}
}
} catch (Exception e) {
LogUtil.error("获取测试计划保存的环境信息出错!", e);
}
}
}
return runModeConfig;
}
private RunModeConfigDTO buildRunModeConfigDTO() {
RunModeConfigDTO runModeConfig = new RunModeConfigDTO();
runModeConfig.setMode(RunModeConstants.SERIAL.name());
runModeConfig.setReportType("iddReport");
runModeConfig.setEnvMap(new HashMap<>());
runModeConfig.setOnSampleError(false);
return runModeConfig;
}
}
@Data
class CaseExecuteResult {
private boolean apiCaseExecuting;
private boolean scenarioExecuting;
private boolean uiScenarioExecuting;
private boolean loadCaseExecuting;
private List<TestPlanApiDTO> apiCaseDTO;
private List<TestPlanScenarioDTO> scenarioCases;
private List<TestPlanUiScenarioDTO> uiScenarios;
private Map<String, String> loadCaseReportMap;
public boolean isExecuting() {
return apiCaseExecuting || scenarioExecuting || uiScenarioExecuting || loadCaseExecuting;
}
}

View File

@ -7,6 +7,7 @@
@close="close"
:visible.sync="runModeVisible"
>
<div class="env-container">
<div>
<div>{{ $t("commons.environment") }}</div>
@ -16,6 +17,7 @@
:project-env-map="projectEnvListMap"
:environment-type.sync="runConfig.environmentType"
:has-option-group="true"
:is-env-saved="isEnvSaved"
:group-id="runConfig.environmentGroupId"
@setProjectEnvMap="setProjectEnvMap"
@setDefaultEnv="setDefaultEnv"
@ -59,13 +61,12 @@
<div>
<div class="mode-row">{{ $t("run_mode.other_config") }}</div>
<div>
<!-- 串行 -->
<!-- 资源池 -->
<div
class="mode-row"
v-if="
runConfig.mode === 'serial' &&
testType === 'API' &&
haveOtherExecCase
(haveOtherExecCase && !haveUICase)
"
>
<span>{{ $t("run_mode.run_with_resource_pool") }}: </span>
@ -83,31 +84,6 @@
</el-option>
</el-select>
</div>
<!-- 并行 -->
<div
class="mode-row"
v-if="
runConfig.mode === 'parallel' &&
testType === 'API' &&
haveOtherExecCase
"
>
<span>{{ $t("run_mode.run_with_resource_pool") }}: </span>
<el-select
v-model="runConfig.resourcePoolId"
size="mini"
style="width: 100%; margin-top: 8px"
>
<el-option
v-for="item in resourcePools"
:key="item.id"
:label="item.name"
:disabled="!item.api"
:value="item.id"
>
</el-option>
</el-select>
</div>
<!-- 失败重试 -->
<div class="mode-row">
@ -164,7 +140,8 @@
<el-button @click="close">{{ $t("commons.cancel") }}</el-button>
<el-dropdown @command="handleCommand" style="margin-left: 5px">
<el-button type="primary">
{{ $t("api_test.run")
{{
$t("api_test.run")
}}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
@ -233,7 +210,7 @@ export default {
resourcePoolId: null,
envMap: new Map(),
environmentGroupId: "",
environmentType: ENV_TYPE.JSON,
environmentType: ENV_TYPE.DEFAULT,
retryEnable: false,
retryNum: 1,
browser: "CHROME",
@ -241,6 +218,8 @@ export default {
},
projectList: [],
projectIds: new Set(),
//
isEnvSaved: true,
options: [
{
value: "confirmAndRun",
@ -279,6 +258,7 @@ export default {
type: Boolean,
default: false,
},
//
haveOtherExecCase: {
type: Boolean,
default: true,
@ -289,11 +269,22 @@ export default {
this.defaultEnvMap = {};
if (runModeConfig) {
this.runConfig = JSON.parse(runModeConfig);
if (!this.runConfig.envMap || JSON.stringify(this.runConfig.envMap) === "{}") {
this.isEnvSaved = false;
this.runConfig.environmentType = ENV_TYPE.DEFAULT;
} else {
this.isEnvSaved = true;
this.runConfig.environmentType = ENV_TYPE.JSON;
}
this.runConfig.envMap = new Map();
this.runConfig.testPlanDefaultEnvMap = {};
this.runConfig.onSampleError =
this.runConfig.onSampleError === "true" ||
this.runConfig.onSampleError === true;
} else {
this.isEnvSaved = false;
//default
this.runConfig.environmentType = ENV_TYPE.DEFAULT;
}
this.runModeVisible = true;
this.testType = testType;
@ -464,6 +455,7 @@ export default {
overflow-y: auto;
padding-bottom: 1px;
}
.env-container .title {
width: 100px;
min-width: 100px;
@ -486,6 +478,7 @@ export default {
.radio-change:deep(.el-radio__input.is-checked + .el-radio__label) {
color: #606266 !important;
}
.radio-change:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
color: #606266 !important;
}

View File

@ -78,20 +78,21 @@
<crontab-result :ex="form.cronValue" ref="crontabResult"/>
</el-form>
<div v-if="haveUICase || haveOtherExecCase">
<div class="el-step__icon is-text" style="margin-right: 10px">
<div class="el-step__icon-inner">2</div>
</div>
<span>{{ $t("load_test.runtime_config") }}</span>
<div class="ms-mode-div">
<span class="ms-mode-span">{{ $t("run_mode.title") }}</span>
<el-radio-group v-model="runConfig.mode" @change="changeMode">
<el-radio-group v-if="haveUICase || haveOtherExecCase" v-model="runConfig.mode" @change="changeMode">
<el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio>
<el-radio label="parallel"
>{{ $t("run_mode.parallel") }}
</el-radio>
</el-radio-group>
</div>
<div style="margin-top: 10px">
<div v-if="haveUICase" style="margin-top: 10px">
<span class="ms-mode-span">{{ $t("浏览器") }}</span>
<el-select
size="mini"
@ -106,7 +107,7 @@
></el-option>
</el-select>
</div>
<div class="ms-mode-div" v-if="runConfig.mode === 'serial'">
<div class="ms-mode-div" v-if="(haveUICase || haveOtherExecCase) && runConfig.mode === 'serial'">
<el-row>
<el-col :span="3">
<span class="ms-mode-span"
@ -114,7 +115,7 @@
>
</el-col>
<el-col :span="18">
<div v-if="testType === 'API'">
<div v-if="haveOtherExecCase && testType === 'API'">
<sapn>{{ $t("run_mode.run_with_resource_pool") }}:</sapn>
<el-select
v-model="runConfig.resourcePoolId"
@ -132,7 +133,7 @@
</el-col>
</el-row>
</div>
<div class="ms-mode-div" v-if="runConfig.mode === 'parallel'">
<div class="ms-mode-div" v-if="(haveUICase || haveOtherExecCase) && runConfig.mode === 'parallel'">
<el-row>
<el-col :span="3">
<span class="ms-mode-span"
@ -140,7 +141,7 @@
>
</el-col>
<el-col :span="18">
<div v-if="testType === 'API'">
<div v-if="haveOtherExecCase && testType === 'API'">
<span>
{{ $t("run_mode.run_with_resource_pool") }} :
</span>
@ -209,7 +210,7 @@
</el-col>
</el-row>
</div>
<div>
<div v-if="haveUICase">
<el-row>
<el-col :span="3"> &nbsp;</el-col>
<el-col :span="18">
@ -221,7 +222,7 @@
</el-col>
</el-row>
</div>
</div>
<el-dialog
width="60%"
:title="$t('schedule.generate_expression')"
@ -250,15 +251,8 @@
</template>
<script>
import {
getCurrentProjectID,
getCurrentUser,
getCurrentWorkspaceId,
} from "metersphere-frontend/src/utils/token";
import {
listenGoBack,
removeGoBackListener,
} from "metersphere-frontend/src/utils";
import {getCurrentProjectID, getCurrentUser, getCurrentWorkspaceId,} from "metersphere-frontend/src/utils/token";
import {listenGoBack, removeGoBackListener,} from "metersphere-frontend/src/utils";
import Crontab from "metersphere-frontend/src/components/cron/Crontab";
import CrontabResult from "metersphere-frontend/src/components/cron/CrontabResult";
import {cronValidate} from "metersphere-frontend/src/utils/cron";
@ -276,7 +270,6 @@ import { saveNotice } from "@/api/notice";
import {getProjectMember} from "@/api/user";
import {getQuotaValidResourcePools} from "@/api/remote/resource-pool";
import {getProjectConfig} from "@/api/project";
import { getSystemBaseSetting } from "metersphere-frontend/src/api/system";
function defaultCustomValidate() {
return {pass: true};
@ -308,6 +301,11 @@ export default {
type: Boolean,
default: false,
},
//
haveOtherExecCase: {
type: Boolean,
default: true,
},
},
watch: {

View File

@ -122,7 +122,8 @@
<span v-if="scope.row.scheduleStatus === 'OPEN'">
<el-tooltip placement="bottom-start" effect="light">
<div slot="content">
{{ $t("home.table.run_rule") }}: {{ scope.row.scheduleCorn
{{ $t("home.table.run_rule") }}: {{
scope.row.scheduleCorn
}}<br/>
{{ $t("test_track.plan.next_run_time") }}<span>{{
scope.row.scheduleExecuteTime | datetimeFormat
@ -424,6 +425,7 @@
:plan-case-ids="[]"
:type="'plan'"
:have-u-i-case="haveUICase"
:have-other-exec-case="haveOtherExecCase"
/>
<ms-test-plan-schedule-batch-switch
ref="scheduleBatchSwitch"
@ -487,7 +489,7 @@ import HeaderLabelOperate from "metersphere-frontend/src/components/head/HeaderL
import MsTag from "metersphere-frontend/src/components/MsTag";
import MsTestPlanScheduleMaintain from "@/business/plan/components/ScheduleMaintain";
import {getCurrentProjectID, getCurrentUser, getCurrentUserId,} from "metersphere-frontend/src/utils/token";
import {hasLicense, hasPermission,} from "metersphere-frontend/src/utils/permission";
import {hasPermission,} from "metersphere-frontend/src/utils/permission";
import {operationConfirm} from "metersphere-frontend/src/utils";
import MsTestPlanRunModeWithEnv from "@/business/plan/common/TestPlanRunModeWithEnv";
import MsTaskCenter from "metersphere-frontend/src/components/task/TaskCenter";
@ -500,8 +502,6 @@ import {
testPlanEditFollows,
testPlanEditRunConfig,
testPlanGetEnableScheduleCount,
testPlanHaveExecCase,
testPlanHaveUiCase,
testPlanList,
testPlanMetric,
testPlanRun,
@ -634,7 +634,7 @@ export default {
batchExecuteType: "serial",
//UI
haveUICase: false,
//API/
//API
haveOtherExecCase: false,
};
},
@ -763,31 +763,6 @@ export default {
this.$set(item, "passed", metricData.passed);
this.$set(item, "tested", metricData.tested);
this.$set(item, "total", metricData.total);
this.$set(
item,
"testPlanTestCaseCount",
metricData.testPlanTestCaseCount
);
this.$set(
item,
"testPlanApiCaseCount",
metricData.testPlanApiCaseCount
);
this.$set(
item,
"testPlanApiScenarioCount",
metricData.testPlanApiScenarioCount
);
this.$set(
item,
"testPlanUiScenarioCount",
metricData.testPlanUiScenarioCount
);
this.$set(
item,
"testPlanLoadCaseCount",
metricData.testPlanLoadCaseCount
);
if (metricData.followUsers) {
let data = metricData.followUsers;
let follow = "";
@ -820,7 +795,7 @@ export default {
});
},
resetTestPlanRow(item) {
if (!isMetricLoadOver) {
if (!item.isMetricLoadOver) {
return;
}
this.$set(item, "isMetricLoadOver", true);
@ -834,11 +809,6 @@ export default {
this.$set(item, "passed", 0);
this.$set(item, "tested", 0);
this.$set(item, "total", 0);
this.$set(item, "testPlanTestCaseCount", 0);
this.$set(item, "testPlanApiCaseCount", 0);
this.$set(item, "testPlanApiScenarioCount", 0);
this.$set(item, "testPlanUiScenarioCount", 0);
this.$set(item, "testPlanLoadCaseCount", 0);
},
copyData(status) {
return JSON.parse(JSON.stringify(this.dataMap.get(status)));
@ -1049,8 +1019,19 @@ export default {
this.$refs.testCaseReportView.open(plan);
},
async scheduleTask(row) {
this.haveUICase = false;
this.haveOtherExecCase = false;
row.redirectFrom = "testPlan";
this.currentPlanId = row.id;
this.haveUICase = row.testPlanUiScenarioCount > 0;
let haveApiCase = row.testPlanApiCaseCount > 0;
let haveScenarioCase = row.testPlanApiScenarioCount > 0;
if (haveApiCase || haveScenarioCase) {
this.haveOtherExecCase = true;
}
this.$refs.scheduleMaintain.open(row);
},
saveSortField(key, orders) {
@ -1074,18 +1055,27 @@ export default {
});
},
handleRun(row) {
this.haveUICase = false;
this.haveOtherExecCase = false;
this.currentPlanId = row.id;
this.haveUIScenario().then(() => {
testPlanHaveExecCase(row.id).then(async (res) => {
this.haveOtherExecCase = res.data;
let haveApiCase = row.testPlanApiCaseCount > 0;
let haveScenarioCase = row.testPlanApiScenarioCount > 0;
let haveLoadCase = row.testPlanLoadCaseCount > 0;
this.haveUICase = row.testPlanUiScenarioCount > 0;
if (!this.haveUICase && !haveApiCase && !haveScenarioCase && haveLoadCase) {
//
this.$refs.runMode.handleCommand("run");
} else if (haveApiCase || haveScenarioCase || this.haveUICase) {
this.haveOtherExecCase = true;
//ui
if (this.haveOtherExecCase || this.haveUICase) {
this.$refs.runMode.open("API", row.runModeConfig);
} else {
//
this.$router.push("/track/plan/view/" + row.id);
}
});
});
},
_handleRun(config) {
let defaultPlanEnvMap = config.testPlanDefaultEnvMap;
@ -1183,18 +1173,18 @@ export default {
return;
}
},
haveUIScenario() {
if (hasLicense()) {
return new Promise((resolve) => {
testPlanHaveUiCase(this.currentPlanId).then((r) => {
this.haveUICase = r.data;
resolve();
});
});
} else {
return new Promise((resolve) => resolve());
}
},
// haveUIScenario() {
// if (hasLicense()) {
// return new Promise((resolve) => {
// testPlanHaveUiCase(this.currentPlanId).then((r) => {
// this.haveUICase = r.data;
// resolve();
// });
// });
// } else {
// return new Promise((resolve) => resolve());
// }
// },
},
};
</script>

View File

@ -1,16 +1,23 @@
<template>
<div>
<!-- {{ JSON.stringify(eventData) }}-->
<el-radio-group
v-model="radio"
style="width: 100%"
@change="radioChange"
class="radio-change"
>
<el-radio v-show="!isEnvSaved" :label="ENV_TYPE.DEFAULT">{{
$t("workspace.env_group.case_env")
}}
</el-radio>
<el-radio :label="ENV_TYPE.JSON">{{
$t("workspace.env_group.env_list")
}}</el-radio>
}}
</el-radio>
<el-radio :label="ENV_TYPE.GROUP" v-if="showEnvGroup"
>{{ $t("workspace.env_group.name")
>{{
$t("workspace.env_group.name")
}}<i class="el-icon-tickets mode-span" @click="viewGroup"></i
></el-radio>
</el-radio-group>
@ -47,10 +54,12 @@
>
<el-radio label="DEFAULT_ENV" style="margin-top: 7px">{{
$t("api_test.environment.default_environment")
}}</el-radio>
}}
</el-radio>
<el-radio label="CUSTOMIZE_ENV" style="margin-top: 7px">{{
$t("api_test.environment.choose_new_environment")
}}</el-radio>
}}
</el-radio>
</el-radio-group>
<el-tag
v-show="!pe.showEnvSelect"
@ -58,7 +67,8 @@
:key="index"
size="mini"
style="margin-left: 0; margin-right: 2px; margin-top: 8px"
>{{ itemName }}</el-tag
>{{ itemName }}
</el-tag
>
<el-select
v-show="pe.showEnvSelect"
@ -142,10 +152,7 @@
<script>
import {ENV_TYPE} from "metersphere-frontend/src/utils/constants";
import {
environmentGetALL,
getEnvironmentOptions,
} from "metersphere-frontend/src/api/environment";
import {environmentGetALL, getEnvironmentOptions,} from "metersphere-frontend/src/api/environment";
import MsTag from "metersphere-frontend/src/components/MsTag";
import EnvironmentGroup from "@/business/plan/env/EnvironmentGroupList";
import {getEnvironmentByProjectId} from "@/api/remote/api/api-environment";
@ -189,6 +196,11 @@ export default {
},
projectIds: Set,
projectList: Array,
//
isEnvSaved: {
type: Boolean,
default: true,
},
projectEnvMap: Object,
envMap: Map,
environmentType: String,

View File

@ -75,14 +75,8 @@
<script>
import TestCaseRelevanceBase from "../base/TestCaseRelevanceBase";
import MxVersionSelect from "metersphere-frontend/src/components/version/MxVersionSelect";
import {
apiDefinitionListBatch,
apiDefinitionRelevance,
} from "@/api/remote/api/api-definition";
import {
apiTestCaseListBlobs,
apiTestCaseRelevance,
} from "@/api/remote/api/api-case";
import {apiDefinitionListBatch, apiDefinitionRelevance,} from "@/api/remote/api/api-definition";
import {apiTestCaseListBlobs, apiTestCaseRelevance,} from "@/api/remote/api/api-case";
import RelevanceApiList from "@/business/plan/view/comonents/api/RelevanceApiList";
import RelevanceCaseList from "@/business/plan/view/comonents/api/RelevanceCaseList";
import MsApiModule from "@/business/plan/view/comonents/api/module/ApiModule";
@ -244,13 +238,6 @@ export default {
postRelevance(relevanceList, environmentId, selectIds, protocol) {
let param = {};
if (protocol !== "DUBBO") {
if (!environmentId) {
this.isSaving = false;
this.$warning(this.$t("api_test.environment.select_environment"));
return;
}
}
if (selectIds.length < 1) {
this.isSaving = false;
this.$warning(this.$t("test_track.plan_view.please_choose_test_case"));

View File

@ -36,12 +36,7 @@
import TestCaseRelevanceBase from "../base/TestCaseRelevanceBase";
import RelevanceScenarioList from "./RelevanceScenarioList";
import {ENV_TYPE} from "metersphere-frontend/src/utils/constants";
import {
getCurrentProjectID,
hasLicense,
strMapToObj,
} from "@/business/utils/sdk-utils";
import { getVersionFilters } from "@/business/utils/sdk-utils";
import {getCurrentProjectID, getVersionFilters, hasLicense, strMapToObj,} from "@/business/utils/sdk-utils";
import {testPlanAutoCheck} from "@/api/remote/plan/test-plan";
import {scenarioRelevance} from "@/api/remote/plan/test-plan-scenario";
import MsApiScenarioModule from "@/business/plan/view/comonents/api/module/ApiScenarioModule";
@ -137,11 +132,6 @@ export default {
},
async saveCaseRelevance() {
this.isSaving = true;
const sign = await this.$refs.apiScenarioList.checkEnv();
if (!sign) {
this.isSaving = false;
return false;
}
let selectIds = [];
let selectRows = this.$refs.apiScenarioList.selectRows;
const envMap = this.$refs.apiScenarioList.projectEnvMap;
@ -157,15 +147,7 @@ export default {
selectRows.forEach((row) => {
selectIds.push(row.id);
});
if (envType === ENV_TYPE.JSON && (!envMap || envMap.size < 1)) {
this.isSaving = false;
this.$warning(this.$t("api_test.environment.select_environment"));
return false;
} else if (envType === ENV_TYPE.GROUP && !envGroupId) {
this.isSaving = false;
this.$warning(this.$t("api_test.environment.select_environment"));
return false;
}
let param = {};
param.planId = this.planId;
param.mapping = strMapToObj(map);

View File

@ -124,6 +124,9 @@ export default {
}
}
},
mounted() {
this.initProtocol();
},
watch: {
'condition.filterText'() {
this.filter();

View File

@ -163,12 +163,6 @@ export default {
return;
}
if (!envMap || envMap.size == 0) {
this.isSaving = false;
this.$warning(this.$t("api_test.environment.select_environment"));
return;
}
let param = {};
param.planId = this.planId;
param.mapping = strMapToObj(map);