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

View File

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

View File

@ -99,4 +99,10 @@ public class RedisTemplateService {
} }
return false; 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

@ -33,7 +33,7 @@
v-xpack v-xpack
:project-id="projectId" :project-id="projectId"
:default-version="currentVersion" :default-version="currentVersion"
@changeVersion="currentVersionChange" /> @changeVersion="currentVersionChange"/>
</template> </template>
</scenario-relevance-api-list> </scenario-relevance-api-list>
@ -53,15 +53,15 @@
v-xpack v-xpack
:project-id="projectId" :project-id="projectId"
:default-version="currentVersion" :default-version="currentVersion"
@changeVersion="currentVersionChange" /> @changeVersion="currentVersionChange"/>
</template> </template>
</scenario-relevance-case-list> </scenario-relevance-case-list>
<template v-slot:headerBtn> <template v-slot:headerBtn>
<!-- 显示数量 --> <!-- 显示数量 -->
<table-select-count-bar :count="selectCounts" style="float: left; margin: 5px" /> <table-select-count-bar :count="selectCounts" style="float: left; margin: 5px"/>
<el-button size="mini" icon="el-icon-refresh" @click="refreshData" /> <el-button size="mini" icon="el-icon-refresh" @click="refreshData"/>
<el-button type="primary" @click="copy" :loading="buttonIsWorking" @keydown.enter.native.prevent size="mini"> <el-button type="primary" @click="copy" :loading="buttonIsWorking" @keydown.enter.native.prevent size="mini">
{{ $t('commons.copy') }} {{ $t('commons.copy') }}
</el-button> </el-button>
@ -79,9 +79,9 @@
</template> </template>
<script> <script>
import { getApiCaseWithBLOBs } from '@/api/api-test-case'; import {getApiCaseWithBLOBs} from '@/api/api-test-case';
import { apiListBatch } from '@/api/definition'; import {apiListBatch} from '@/api/definition';
import { getProjectVersions } from '@/api/xpack'; import {getProjectVersions} from '@/api/xpack';
import ScenarioRelevanceCaseList from './RelevanceCaseList'; import ScenarioRelevanceCaseList from './RelevanceCaseList';
import MsApiModule from '../../../definition/components/module/ApiModule'; import MsApiModule from '../../../definition/components/module/ApiModule';
import MsContainer from 'metersphere-frontend/src/components/MsContainer'; import MsContainer from 'metersphere-frontend/src/components/MsContainer';
@ -90,9 +90,9 @@ import MsMainContainer from 'metersphere-frontend/src/components/MsMainContainer
import ScenarioRelevanceApiList from './RelevanceApiList'; import ScenarioRelevanceApiList from './RelevanceApiList';
import RelevanceDialog from '@/business/commons/RelevanceDialog'; import RelevanceDialog from '@/business/commons/RelevanceDialog';
import TestCaseRelevanceBase from '@/business/commons/TestCaseRelevanceBase'; import TestCaseRelevanceBase from '@/business/commons/TestCaseRelevanceBase';
import { hasLicense } from 'metersphere-frontend/src/utils/permission'; import {hasLicense} from 'metersphere-frontend/src/utils/permission';
import TableSelectCountBar from '@/business/automation/scenario/api/TableSelectCountBar'; import TableSelectCountBar from '@/business/automation/scenario/api/TableSelectCountBar';
import { operationConfirm } from 'metersphere-frontend/src/utils'; import {operationConfirm} from 'metersphere-frontend/src/utils';
export default { export default {
name: 'ApiRelevance', name: 'ApiRelevance',
@ -260,11 +260,11 @@ export default {
this.versionFilters = response.data this.versionFilters = response.data
.filter((u) => u.id === currentVersion) .filter((u) => u.id === currentVersion)
.map((u) => { .map((u) => {
return { text: u.name, value: u.id }; return {text: u.name, value: u.id};
}); });
} else { } else {
this.versionFilters = response.data.map((u) => { this.versionFilters = response.data.map((u) => {
return { text: u.name, value: u.id }; return {text: u.name, value: u.id};
}); });
} }
}); });

View File

@ -15,7 +15,7 @@
@setModuleOptions="setModuleOptions" @setModuleOptions="setModuleOptions"
:is-read-only="true" :is-read-only="true"
:is-relevance="true" :is-relevance="true"
ref="nodeTree" /> ref="nodeTree"/>
</template> </template>
<relevance-api-list <relevance-api-list
@ -28,7 +28,7 @@
:is-script="isScript" :is-script="isScript"
:plan-id="planId" :plan-id="planId"
@isApiListEnableChange="isApiListEnableChange" @isApiListEnableChange="isApiListEnableChange"
ref="apiList" /> ref="apiList"/>
<relevance-case-list <relevance-case-list
v-if="!isApiListEnable" v-if="!isApiListEnable"
@ -40,19 +40,19 @@
:is-script="isScript" :is-script="isScript"
:plan-id="planId" :plan-id="planId"
@isApiListEnableChange="isApiListEnableChange" @isApiListEnableChange="isApiListEnableChange"
ref="apiCaseList" /> ref="apiCaseList"/>
</test-case-relevance-base> </test-case-relevance-base>
</template> </template>
<script> <script>
import { getApiCaseWithBLOBs } from '@/api/api-test-case'; import {getApiCaseWithBLOBs} from '@/api/api-test-case';
import { apiListBatch } from '@/api/definition'; import {apiListBatch} from '@/api/definition';
import RelevanceCaseList from '@/business/automation/scenario/api/RelevanceCaseList'; import RelevanceCaseList from '@/business/automation/scenario/api/RelevanceCaseList';
import RelevanceApiList from '@/business/automation/scenario/api/RelevanceApiList'; import RelevanceApiList from '@/business/automation/scenario/api/RelevanceApiList';
import MsApiModule from '@/business/definition/components/module/ApiModule'; import MsApiModule from '@/business/definition/components/module/ApiModule';
import { getEnvironmentById } from 'metersphere-frontend/src/api/environment'; import {getEnvironmentById} from 'metersphere-frontend/src/api/environment';
import TestCaseRelevanceBase from '@/business/commons/TestCaseRelevanceBase'; import TestCaseRelevanceBase from '@/business/commons/TestCaseRelevanceBase';
import { parseEnvironment } from '@/business/environment/model/EnvironmentModel'; import {parseEnvironment} from '@/business/environment/model/EnvironmentModel';
export default { export default {
name: 'ApiFuncRelevance', name: 'ApiFuncRelevance',
@ -75,7 +75,7 @@ export default {
condition: {}, condition: {},
currentRow: {}, currentRow: {},
projectId: '', projectId: '',
options: [{ value: 'HTTP', name: 'HTTP' }], options: [{value: 'HTTP', name: 'HTTP'}],
}; };
}, },
props: { props: {

View File

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

View File

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

View File

@ -724,6 +724,7 @@ const message = {
batch_add_to_ws: "批量添加到環境組", batch_add_to_ws: "批量添加到環境組",
choice_conflict: "環境選擇沖突,一個項目選擇一個對應環境!", choice_conflict: "環境選擇沖突,一個項目選擇一個對應環境!",
env_list: "環境列表", env_list: "環境列表",
case_env: "用例環境",
confirm: "確 定", confirm: "確 定",
please_select_env_for_current_scenario: "請為當前場景選擇一個運行環境!", please_select_env_for_current_scenario: "請為當前場景選擇一個運行環境!",
please_select_env_for_current_plan: "請為當前测试计划選擇一個運行環境!", 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: "${__machineIP}", description: "get the local machine IP address"},
{type: "Information", name: "${__machineName}", description: "get the local machine name"}, {type: "Information", name: "${__machineName}", description: "get the local machine name"},
{type: "Information", name: "${__time}", description: "return current time in various formats"}, {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: "${__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: "Information", name: "${__logn}", description: "log (or display) a message (empty return value)"},
{type: "Input", name: "${__StringFromFile}", description: "read a line from a file"}, {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: "${__XPath}", description: "Use an XPath expression to read from a file"},
{type: "Input", name: "${__StringToFile}", description: "write a string to a file"}, {type: "Input", name: "${__StringToFile}", description: "write a string to a file"},
{type: "Calculation", name: "${__counter}", description: "generate an incrementing number"}, {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: "${__digest}", description: "Generate a digest (SHA-1, SHA-256, MD5...)"},
{type: "Calculation", name: "${__intSum}", description: "add int numbers"}, {type: "Calculation", name: "${__intSum}", description: "add int numbers"},
{type: "Calculation", name: "${__longSum}", description: "add long numbers"}, {type: "Calculation", name: "${__longSum}", description: "add long numbers"},
{type: "Calculation", name: "${__Random}", description: "generate a random number"}, {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: "${__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: "${__RandomString}", description: "generate a random string"},
{type: "Calculation", name: "${__UUID}", description: "generate a random type 4 UUID"}, {type: "Calculation", name: "${__UUID}", description: "generate a random type 4 UUID"},
{type: "Scripting", name: "${__groovy}", description: "run an Apache Groovy script"}, {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: "${__unescape}", description: "Process strings containing Java escapes (e.g. \n & \t)"},
{type: "String", name: "${__unescapeHtml}", description: "Decode HTML-encoded strings"}, {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: "${__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"}, {type: "String", name: "${__TestPlanName}", description: "Return name of current test plan"},
] ]
@ -212,13 +228,14 @@ export const CONFIG_TYPE = {
ABNORMAL: "ABNORMAL" ABNORMAL: "ABNORMAL"
} }
export const WORKSTATION={ export const WORKSTATION = {
UPCOMING:"upcoming", UPCOMING: "upcoming",
FOCUS:"focus", FOCUS: "focus",
NODE:"node" NODE: "node"
} }
export const ENV_TYPE = { export const ENV_TYPE = {
DEFAULT: "DEFAULT",
JSON: "JSON", JSON: "JSON",
GROUP: "GROUP" GROUP: "GROUP"
} }
@ -295,15 +312,71 @@ export const TASK_DATA = [
name: "track", name: "track",
title: "side_task.test_tracking.title", title: "side_task.test_tracking.title",
percentage: 14, 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'], 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: [ 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: 1,
{ 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" }, name: "side_task.test_tracking.task_1",
{ 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" }, status: 1,
{ 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" }, permission: ['PROJECT_MANAGER:READ', 'WORKSPACE_PROJECT_MANAGER:READ'],
{ 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" }, api: [''],
{ 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" }, 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, rate: 1,
status: 0 status: 0
@ -313,15 +386,71 @@ export const TASK_DATA = [
name: "api", name: "api",
title: 'side_task.api_test.title', title: 'side_task.api_test.title',
percentage: 0, 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'], 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: [ 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: 1,
{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" }, name: "side_task.api_test.task_1",
{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" }, status: 0,
{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" }, path: '/api/definition',
{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" }, permission: ['PROJECT_API_DEFINITION:READ+CREATE_API'],
{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" }, 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, rate: 0,
status: 0 status: 0
@ -331,10 +460,26 @@ export const TASK_DATA = [
name: "performance", name: "performance",
title: 'side_task.performance_test.title', title: 'side_task.performance_test.title',
percentage: 0, percentage: 0,
permission: ['PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE',"PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE_BATCH",'PROJECT_PERFORMANCE_REPORT:READ'], permission: ['PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE', "PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE_BATCH", 'PROJECT_PERFORMANCE_REPORT:READ'],
taskData: [ 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, rate: 0,
status: 0 status: 0
@ -344,11 +489,35 @@ export const TASK_DATA = [
name: "project", name: "project",
title: 'side_task.project_setting.title', title: 'side_task.project_setting.title',
percentage: 0, percentage: 0,
permission: ['WORKSPACE_PROJECT_MANAGER:READ+CREATE','PROJECT_USER:READ+CREATE','PROJECT_ENVIRONMENT:READ+CREATE'], permission: ['WORKSPACE_PROJECT_MANAGER:READ+CREATE', 'PROJECT_USER:READ+CREATE', 'PROJECT_ENVIRONMENT:READ+CREATE'],
taskData: [ 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: 1,
{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" }, 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, rate: 0,
status: 0 status: 0
@ -358,11 +527,35 @@ export const TASK_DATA = [
name: "ui", name: "ui",
title: 'side_task.ui_test.title', title: 'side_task.ui_test.title',
percentage: 0, percentage: 0,
permission: ['PROJECT_UI_ELEMENT:READ+CREATE','PROJECT_UI_SCENARIO:READ+CREATE','PROJECT_UI_SCENARIO:READ+RUN','PROJECT_UI_SCENARIO:READ+DEBUG'], permission: ['PROJECT_UI_ELEMENT:READ+CREATE', 'PROJECT_UI_SCENARIO:READ+CREATE', 'PROJECT_UI_SCENARIO:READ+RUN', 'PROJECT_UI_SCENARIO:READ+DEBUG'],
taskData: [ 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: 1,
{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" }, 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, rate: 0,
status: 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.TestPlanLoadCaseMapper;
import io.metersphere.base.mapper.ext.BaseApiExecutionQueueMapper; import io.metersphere.base.mapper.ext.BaseApiExecutionQueueMapper;
import io.metersphere.commons.constants.KafkaTopicConstants; import io.metersphere.commons.constants.KafkaTopicConstants;
import io.metersphere.commons.constants.TestPlanExecuteCaseType;
import io.metersphere.commons.constants.TestPlanLoadCaseStatus; import io.metersphere.commons.constants.TestPlanLoadCaseStatus;
import io.metersphere.commons.constants.TriggerMode; import io.metersphere.commons.constants.TriggerMode;
import io.metersphere.commons.utils.BeanUtils; import io.metersphere.commons.utils.BeanUtils;
@ -14,6 +15,7 @@ import io.metersphere.constants.RunModeConstants;
import io.metersphere.dto.RunModeConfigDTO; import io.metersphere.dto.RunModeConfigDTO;
import io.metersphere.plan.exec.queue.DBTestQueue; import io.metersphere.plan.exec.queue.DBTestQueue;
import io.metersphere.request.RunTestPlanRequest; import io.metersphere.request.RunTestPlanRequest;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.utils.LoggerUtil; import io.metersphere.utils.LoggerUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
@ -175,13 +177,16 @@ public class PerfQueueService {
return queue; return queue;
} }
@Resource
private RedisTemplateService redisTemplateService;
@Transactional(propagation = Propagation.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
public DBTestQueue add(Object runObj, String poolId, String reportId, String reportType, String runMode, RunModeConfigDTO config) { public DBTestQueue add(Object runObj, String poolId, String testPlanReportId, String reportType, String runMode, RunModeConfigDTO config) {
LoggerUtil.info("报告【" + reportId + "】开始生成执行链"); LoggerUtil.info("报告【" + testPlanReportId + "】开始生成执行链");
if (config.getEnvMap() == null) { if (config.getEnvMap() == null) {
config.setEnvMap(new LinkedHashMap<>()); config.setEnvMap(new LinkedHashMap<>());
} }
ApiExecutionQueue executionQueue = getApiExecutionQueue(poolId, reportId, reportType, runMode, config); ApiExecutionQueue executionQueue = getApiExecutionQueue(poolId, testPlanReportId, reportType, runMode, config);
queueMapper.insert(executionQueue); queueMapper.insert(executionQueue);
DBTestQueue resQueue = new DBTestQueue(); DBTestQueue resQueue = new DBTestQueue();
BeanUtils.copyBean(resQueue, executionQueue); BeanUtils.copyBean(resQueue, executionQueue);
@ -196,7 +201,9 @@ public class PerfQueueService {
extApiExecutionQueueMapper.sqlInsert(queueDetails); extApiExecutionQueueMapper.sqlInsert(queueDetails);
} }
resQueue.setDetailMap(detailMap); resQueue.setDetailMap(detailMap);
LoggerUtil.info("报告【" + reportId + "】生成执行链结束"); LoggerUtil.info("报告【" + testPlanReportId + "】生成执行链结束");
//移除Redis中的标志
redisTemplateService.unlock(testPlanReportId, TestPlanExecuteCaseType.LOAD_CASE.name(), testPlanReportId);
return resQueue; return resQueue;
} }
@ -207,7 +214,7 @@ public class PerfQueueService {
executionQueue.setPoolId(poolId); executionQueue.setPoolId(poolId);
executionQueue.setFailure(config.isOnSampleError()); executionQueue.setFailure(config.isOnSampleError());
executionQueue.setReportId(reportId); executionQueue.setReportId(reportId);
executionQueue.setReportType(StringUtils.isNotEmpty(reportType) ? reportType : RunModeConstants.INDEPENDENCE.toString()); executionQueue.setReportType(TestPlanExecuteCaseType.LOAD_CASE.name());
executionQueue.setRunMode(runMode); executionQueue.setRunMode(runMode);
return executionQueue; return executionQueue;
} }

View File

@ -5,6 +5,7 @@ import io.metersphere.base.mapper.ApiExecutionQueueMapper;
import io.metersphere.commons.constants.KafkaTopicConstants; import io.metersphere.commons.constants.KafkaTopicConstants;
import io.metersphere.plan.service.AutomationCaseExecOverService; import io.metersphere.plan.service.AutomationCaseExecOverService;
import io.metersphere.plan.service.TestPlanReportService; import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.utils.LoggerUtil; import io.metersphere.utils.LoggerUtil;
import io.metersphere.utils.NamedThreadFactory; import io.metersphere.utils.NamedThreadFactory;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -28,6 +29,8 @@ public class ExecReportListener {
@Resource @Resource
private TestPlanReportService testPlanReportService; private TestPlanReportService testPlanReportService;
@Resource @Resource
private RedisTemplateService redisTemplateService;
@Resource
private AutomationCaseExecOverService automationCaseExecOverService; private AutomationCaseExecOverService automationCaseExecOverService;
// 线程池维护线程的最少数量 // 线程池维护线程的最少数量
@ -57,6 +60,7 @@ public class ExecReportListener {
task.setApiExecutionQueueDetailMapper(executionQueueDetailMapper); task.setApiExecutionQueueDetailMapper(executionQueueDetailMapper);
task.setAutomationCaseExecOverService(automationCaseExecOverService); task.setAutomationCaseExecOverService(automationCaseExecOverService);
task.setTestPlanReportService(testPlanReportService); task.setTestPlanReportService(testPlanReportService);
task.setRedisTemplateService(redisTemplateService);
task.setRecord(item); task.setRecord(item);
threadPool.execute(task); 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.domain.ApiExecutionQueueExample;
import io.metersphere.base.mapper.ApiExecutionQueueDetailMapper; import io.metersphere.base.mapper.ApiExecutionQueueDetailMapper;
import io.metersphere.base.mapper.ApiExecutionQueueMapper; import io.metersphere.base.mapper.ApiExecutionQueueMapper;
import io.metersphere.commons.constants.TestPlanExecuteCaseType;
import io.metersphere.commons.constants.TestPlanReportStatus; import io.metersphere.commons.constants.TestPlanReportStatus;
import io.metersphere.plan.service.AutomationCaseExecOverService; import io.metersphere.plan.service.AutomationCaseExecOverService;
import io.metersphere.plan.service.TestPlanReportService; import io.metersphere.plan.service.TestPlanReportService;
import io.metersphere.service.RedisTemplateService;
import io.metersphere.utils.LoggerUtil; import io.metersphere.utils.LoggerUtil;
import lombok.Data; import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -25,7 +27,7 @@ public class ExecReportListenerTask implements Runnable {
private ApiExecutionQueueDetailMapper apiExecutionQueueDetailMapper; private ApiExecutionQueueDetailMapper apiExecutionQueueDetailMapper;
private TestPlanReportService testPlanReportService; private TestPlanReportService testPlanReportService;
private AutomationCaseExecOverService automationCaseExecOverService; private AutomationCaseExecOverService automationCaseExecOverService;
private RedisTemplateService redisTemplateService;
@Override @Override
public void run() { public void run() {
@ -50,15 +52,15 @@ public class ExecReportListenerTask implements Runnable {
ApiExecutionQueueExample executionQueueExample = new ApiExecutionQueueExample(); ApiExecutionQueueExample executionQueueExample = new ApiExecutionQueueExample();
executionQueueExample.createCriteria().andReportIdEqualTo(testPlanReportId); executionQueueExample.createCriteria().andReportIdEqualTo(testPlanReportId);
List<ApiExecutionQueue> queues = apiExecutionQueueMapper.selectByExample(executionQueueExample); 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); LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name()); testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
} else { } else if (CollectionUtils.isNotEmpty(queues)) {
List<String> ids = queues.stream().map(ApiExecutionQueue::getId).collect(Collectors.toList()); List<String> ids = queues.stream().map(ApiExecutionQueue::getId).collect(Collectors.toList());
ApiExecutionQueueDetailExample detailExample = new ApiExecutionQueueDetailExample(); ApiExecutionQueueDetailExample detailExample = new ApiExecutionQueueDetailExample();
detailExample.createCriteria().andQueueIdIn(ids); detailExample.createCriteria().andQueueIdIn(ids);
long count = apiExecutionQueueDetailMapper.countByExample(detailExample); 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); LoggerUtil.info("Normal execution completes, update test plan report status" + testPlanReportId);
testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name()); testPlanReportService.testPlanExecuteOver(testPlanReportId, TestPlanReportStatus.COMPLETED.name());
LoggerUtil.info("Clear Queue" + ids); 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.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils; import org.mybatis.spring.SqlSessionUtils;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -622,6 +623,7 @@ public class TestPlanReportService {
} }
} }
@Async
public void testPlanUnExecute(TestPlanReport testPlanReport) { public void testPlanUnExecute(TestPlanReport testPlanReport) {
if (testPlanReport != null && !StringUtils.equalsIgnoreCase(testPlanReport.getStatus(), TestPlanReportStatus.COMPLETED.name())) { if (testPlanReport != null && !StringUtils.equalsIgnoreCase(testPlanReport.getStatus(), TestPlanReportStatus.COMPLETED.name())) {
testPlanReport.setIsApiCaseExecuting(false); 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.RunUiScenarioRequest;
import io.metersphere.plan.request.ui.TestPlanUiExecuteReportDTO; import io.metersphere.plan.request.ui.TestPlanUiExecuteReportDTO;
import io.metersphere.plan.request.ui.UiPlanReportRequest; 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.PlanApiAutomationService;
import io.metersphere.plan.service.remote.api.PlanTestPlanApiCaseService; import io.metersphere.plan.service.remote.api.PlanTestPlanApiCaseService;
import io.metersphere.plan.service.remote.api.PlanTestPlanScenarioCaseService; import io.metersphere.plan.service.remote.api.PlanTestPlanScenarioCaseService;
@ -495,6 +496,22 @@ public class TestPlanService {
request.setProjectId(request.getProjectId()); request.setProjectId(request.getProjectId());
} }
List<TestPlanDTOWithMetric> testPlanList = extTestPlanMapper.list(request); 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)) { if (CollectionUtils.isNotEmpty(testPlanList)) {
List<String> changeToFinishedIds = new ArrayList<>(); List<String> changeToFinishedIds = new ArrayList<>();
//检查定时任务的设置 //检查定时任务的设置
@ -556,17 +573,7 @@ public class TestPlanService {
public List<TestPlanDTOWithMetric> selectTestPlanMetricById(List<String> idList) { public List<TestPlanDTOWithMetric> selectTestPlanMetricById(List<String> idList) {
List<TestPlanDTOWithMetric> testPlanMetricList = this.calcTestPlanRateByIdList(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) { 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()); List<User> followUsers = this.getPlanFollow(testPlanMetric.getId());
testPlanMetric.setFollowUsers(followUsers); 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" @close="close"
:visible.sync="runModeVisible" :visible.sync="runModeVisible"
> >
<div class="env-container"> <div class="env-container">
<div> <div>
<div>{{ $t("commons.environment") }}</div> <div>{{ $t("commons.environment") }}</div>
@ -16,6 +17,7 @@
:project-env-map="projectEnvListMap" :project-env-map="projectEnvListMap"
:environment-type.sync="runConfig.environmentType" :environment-type.sync="runConfig.environmentType"
:has-option-group="true" :has-option-group="true"
:is-env-saved="isEnvSaved"
:group-id="runConfig.environmentGroupId" :group-id="runConfig.environmentGroupId"
@setProjectEnvMap="setProjectEnvMap" @setProjectEnvMap="setProjectEnvMap"
@setDefaultEnv="setDefaultEnv" @setDefaultEnv="setDefaultEnv"
@ -59,13 +61,12 @@
<div> <div>
<div class="mode-row">{{ $t("run_mode.other_config") }}</div> <div class="mode-row">{{ $t("run_mode.other_config") }}</div>
<div> <div>
<!-- 串行 --> <!-- 资源池 -->
<div <div
class="mode-row" class="mode-row"
v-if=" v-if="
runConfig.mode === 'serial' &&
testType === 'API' && testType === 'API' &&
haveOtherExecCase (haveOtherExecCase && !haveUICase)
" "
> >
<span>{{ $t("run_mode.run_with_resource_pool") }}: </span> <span>{{ $t("run_mode.run_with_resource_pool") }}: </span>
@ -83,31 +84,6 @@
</el-option> </el-option>
</el-select> </el-select>
</div> </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"> <div class="mode-row">
@ -124,7 +100,7 @@
class="el-icon-question" class="el-icon-question"
style="cursor: pointer" style="cursor: pointer"
/> </el-tooltip /> </el-tooltip
><br /> ><br/>
<span> <span>
{{ $t("run_mode.retry") }} {{ $t("run_mode.retry") }}
<el-input-number <el-input-number
@ -164,7 +140,8 @@
<el-button @click="close">{{ $t("commons.cancel") }}</el-button> <el-button @click="close">{{ $t("commons.cancel") }}</el-button>
<el-dropdown @command="handleCommand" style="margin-left: 5px"> <el-dropdown @command="handleCommand" style="margin-left: 5px">
<el-button type="primary"> <el-button type="primary">
{{ $t("api_test.run") {{
$t("api_test.run")
}}<i class="el-icon-arrow-down el-icon--right"></i> }}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
@ -180,7 +157,7 @@
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
<ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch" /> <ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch"/>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
@ -220,7 +197,7 @@ export default {
btnStyle: { btnStyle: {
width: "260px", width: "260px",
}, },
result: { loading: false }, result: {loading: false},
runModeVisible: false, runModeVisible: false,
testType: null, testType: null,
resourcePools: [], resourcePools: [],
@ -233,7 +210,7 @@ export default {
resourcePoolId: null, resourcePoolId: null,
envMap: new Map(), envMap: new Map(),
environmentGroupId: "", environmentGroupId: "",
environmentType: ENV_TYPE.JSON, environmentType: ENV_TYPE.DEFAULT,
retryEnable: false, retryEnable: false,
retryNum: 1, retryNum: 1,
browser: "CHROME", browser: "CHROME",
@ -241,6 +218,8 @@ export default {
}, },
projectList: [], projectList: [],
projectIds: new Set(), projectIds: new Set(),
//
isEnvSaved: true,
options: [ options: [
{ {
value: "confirmAndRun", value: "confirmAndRun",
@ -279,6 +258,7 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
//
haveOtherExecCase: { haveOtherExecCase: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -289,11 +269,22 @@ export default {
this.defaultEnvMap = {}; this.defaultEnvMap = {};
if (runModeConfig) { if (runModeConfig) {
this.runConfig = JSON.parse(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.envMap = new Map();
this.runConfig.testPlanDefaultEnvMap = {}; this.runConfig.testPlanDefaultEnvMap = {};
this.runConfig.onSampleError = this.runConfig.onSampleError =
this.runConfig.onSampleError === "true" || this.runConfig.onSampleError === "true" ||
this.runConfig.onSampleError === true; this.runConfig.onSampleError === true;
} else {
this.isEnvSaved = false;
//default
this.runConfig.environmentType = ENV_TYPE.DEFAULT;
} }
this.runModeVisible = true; this.runModeVisible = true;
this.testType = testType; this.testType = testType;
@ -408,7 +399,7 @@ export default {
this.$refs.envSelectPopover.open(); this.$refs.envSelectPopover.open();
}); });
} else if (this.type === "plan") { } else if (this.type === "plan") {
param = { id: this.planId }; param = {id: this.planId};
getPlanCaseEnv(param).then((res) => { getPlanCaseEnv(param).then((res) => {
let data = res.data; let data = res.data;
if (data) { if (data) {
@ -418,7 +409,7 @@ export default {
} }
} }
if (this.projectIds.size === 0) { if (this.projectIds.size === 0) {
param = { id: this.planId }; param = {id: this.planId};
getPlanCaseProjectIds(param).then((res) => { getPlanCaseProjectIds(param).then((res) => {
let data = res.data; let data = res.data;
if (data) { if (data) {
@ -459,11 +450,12 @@ export default {
</script> </script>
<style scoped> <style scoped>
.env-container{ .env-container {
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
padding-bottom: 1px; padding-bottom: 1px;
} }
.env-container .title { .env-container .title {
width: 100px; width: 100px;
min-width: 100px; min-width: 100px;
@ -486,6 +478,7 @@ export default {
.radio-change:deep(.el-radio__input.is-checked + .el-radio__label) { .radio-change:deep(.el-radio__input.is-checked + .el-radio__label) {
color: #606266 !important; color: #606266 !important;
} }
.radio-change:deep(.el-checkbox__input.is-checked + .el-checkbox__label) { .radio-change:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
color: #606266 !important; color: #606266 !important;
} }

View File

@ -75,23 +75,24 @@
</el-col> </el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>
<crontab-result :ex="form.cronValue" ref="crontabResult" /> <crontab-result :ex="form.cronValue" ref="crontabResult"/>
</el-form> </el-form>
<div v-if="haveUICase || haveOtherExecCase">
<div class="el-step__icon is-text" style="margin-right: 10px"> <div class="el-step__icon is-text" style="margin-right: 10px">
<div class="el-step__icon-inner">2</div> <div class="el-step__icon-inner">2</div>
</div> </div>
<span>{{ $t("load_test.runtime_config") }}</span> <span>{{ $t("load_test.runtime_config") }}</span>
<div class="ms-mode-div"> <div class="ms-mode-div">
<span class="ms-mode-span">{{ $t("run_mode.title") }}</span> <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="serial">{{ $t("run_mode.serial") }}</el-radio>
<el-radio label="parallel" <el-radio label="parallel"
>{{ $t("run_mode.parallel") }} >{{ $t("run_mode.parallel") }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</div> </div>
<div style="margin-top: 10px"> <div v-if="haveUICase" style="margin-top: 10px">
<span class="ms-mode-span">{{ $t("浏览器") }}</span> <span class="ms-mode-span">{{ $t("浏览器") }}</span>
<el-select <el-select
size="mini" size="mini"
@ -106,7 +107,7 @@
></el-option> ></el-option>
</el-select> </el-select>
</div> </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-row>
<el-col :span="3"> <el-col :span="3">
<span class="ms-mode-span" <span class="ms-mode-span"
@ -114,8 +115,8 @@
> >
</el-col> </el-col>
<el-col :span="18"> <el-col :span="18">
<div v-if="testType === 'API'"> <div v-if="haveOtherExecCase && testType === 'API'">
<sapn>{{ $t("run_mode.run_with_resource_pool") }}: </sapn> <sapn>{{ $t("run_mode.run_with_resource_pool") }}:</sapn>
<el-select <el-select
v-model="runConfig.resourcePoolId" v-model="runConfig.resourcePoolId"
size="mini" size="mini"
@ -132,7 +133,7 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </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-row>
<el-col :span="3"> <el-col :span="3">
<span class="ms-mode-span" <span class="ms-mode-span"
@ -140,7 +141,7 @@
> >
</el-col> </el-col>
<el-col :span="18"> <el-col :span="18">
<div v-if="testType === 'API'"> <div v-if="haveOtherExecCase && testType === 'API'">
<span> <span>
{{ $t("run_mode.run_with_resource_pool") }} : {{ $t("run_mode.run_with_resource_pool") }} :
</span> </span>
@ -180,7 +181,7 @@
<div slot="content"> <div slot="content">
{{ $t("run_mode.retry_message") }} {{ $t("run_mode.retry_message") }}
</div> </div>
<i class="el-icon-question" style="cursor: pointer" /> <i class="el-icon-question" style="cursor: pointer"/>
</el-tooltip> </el-tooltip>
<span> <span>
{{ $t("run_mode.retry") }} {{ $t("run_mode.retry") }}
@ -209,7 +210,7 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<div> <div v-if="haveUICase">
<el-row> <el-row>
<el-col :span="3"> &nbsp;</el-col> <el-col :span="3"> &nbsp;</el-col>
<el-col :span="18"> <el-col :span="18">
@ -221,7 +222,7 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</div>
<el-dialog <el-dialog
width="60%" width="60%"
:title="$t('schedule.generate_expression')" :title="$t('schedule.generate_expression')"
@ -250,21 +251,14 @@
</template> </template>
<script> <script>
import { import {getCurrentProjectID, getCurrentUser, getCurrentWorkspaceId,} from "metersphere-frontend/src/utils/token";
getCurrentProjectID, import {listenGoBack, removeGoBackListener,} from "metersphere-frontend/src/utils";
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 Crontab from "metersphere-frontend/src/components/cron/Crontab";
import CrontabResult from "metersphere-frontend/src/components/cron/CrontabResult"; import CrontabResult from "metersphere-frontend/src/components/cron/CrontabResult";
import { cronValidate } from "metersphere-frontend/src/utils/cron"; import {cronValidate} from "metersphere-frontend/src/utils/cron";
import MsScheduleNotification from "./ScheduleNotification"; import MsScheduleNotification from "./ScheduleNotification";
import ScheduleSwitch from "./ScheduleSwitch"; import ScheduleSwitch from "./ScheduleSwitch";
import { ENV_TYPE } from "metersphere-frontend/src/utils/constants"; import {ENV_TYPE} from "metersphere-frontend/src/utils/constants";
import MxNotification from "metersphere-frontend/src/components/MxNoticeTemplate"; import MxNotification from "metersphere-frontend/src/components/MxNoticeTemplate";
import { import {
createSchedule, createSchedule,
@ -272,14 +266,13 @@ import {
updateSchedule, updateSchedule,
updateScheduleEnableByPrimyKey, updateScheduleEnableByPrimyKey,
} from "@/api/remote/plan/test-plan"; } from "@/api/remote/plan/test-plan";
import { saveNotice } from "@/api/notice"; import {saveNotice} from "@/api/notice";
import { getProjectMember } from "@/api/user"; import {getProjectMember} from "@/api/user";
import { getQuotaValidResourcePools } from "@/api/remote/resource-pool"; import {getQuotaValidResourcePools} from "@/api/remote/resource-pool";
import { getProjectConfig } from "@/api/project"; import {getProjectConfig} from "@/api/project";
import { getSystemBaseSetting } from "metersphere-frontend/src/api/system";
function defaultCustomValidate() { function defaultCustomValidate() {
return { pass: true }; return {pass: true};
} }
export default { export default {
@ -308,6 +301,11 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
//
haveOtherExecCase: {
type: Boolean,
default: true,
},
}, },
watch: { watch: {
@ -351,7 +349,7 @@ export default {
activeName: "first", activeName: "first",
rules: { rules: {
cronValue: [ cronValue: [
{ required: true, validator: validateCron, trigger: "blur" }, {required: true, validator: validateCron, trigger: "blur"},
], ],
}, },
resourcePools: [], resourcePools: [],
@ -382,7 +380,7 @@ export default {
}; };
}, },
methods: { methods: {
async checkPool(){ async checkPool() {
let hasPool = false; let hasPool = false;
this.resourcePools.forEach(item => { this.resourcePools.forEach(item => {
if (item.id === this.runConfig.resourcePoolId) { if (item.id === this.runConfig.resourcePoolId) {

View File

@ -71,7 +71,7 @@
<span @click.stop="clickt = 'stop'"> <span @click.stop="clickt = 'stop'">
<el-dropdown class="test-case-status" @command="statusChange"> <el-dropdown class="test-case-status" @command="statusChange">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<plan-status-table-item :value="scope.row.status" /> <plan-status-table-item :value="scope.row.status"/>
</span> </span>
<el-dropdown-menu slot="dropdown" chang> <el-dropdown-menu slot="dropdown" chang>
<el-dropdown-item <el-dropdown-item
@ -122,8 +122,9 @@
<span v-if="scope.row.scheduleStatus === 'OPEN'"> <span v-if="scope.row.scheduleStatus === 'OPEN'">
<el-tooltip placement="bottom-start" effect="light"> <el-tooltip placement="bottom-start" effect="light">
<div slot="content"> <div slot="content">
{{ $t("home.table.run_rule") }}: {{ scope.row.scheduleCorn {{ $t("home.table.run_rule") }}: {{
}}<br /> scope.row.scheduleCorn
}}<br/>
{{ $t("test_track.plan.next_run_time") }}<span>{{ {{ $t("test_track.plan.next_run_time") }}<span>{{
scope.row.scheduleExecuteTime | datetimeFormat scope.row.scheduleExecuteTime | datetimeFormat
}}</span> }}</span>
@ -196,7 +197,7 @@
v-if="scope.row.isMetricLoadOver" v-if="scope.row.isMetricLoadOver"
:percentage="scope.row.testRate ? scope.row.testRate : 0" :percentage="scope.row.testRate ? scope.row.testRate : 0"
></el-progress> ></el-progress>
<i v-else class="el-icon-loading" /> <i v-else class="el-icon-loading"/>
</template> </template>
</ms-table-column> </ms-table-column>
<ms-table-column <ms-table-column
@ -208,7 +209,7 @@
> >
</ms-table-column> </ms-table-column>
<ms-tags-column :field="item" :fields-width="fieldsWidth" /> <ms-tags-column :field="item" :fields-width="fieldsWidth"/>
<ms-table-column <ms-table-column
prop="testPlanTestCaseCount" prop="testPlanTestCaseCount"
@ -261,7 +262,7 @@
<span v-if="scope.row.isMetricLoadOver">{{ <span v-if="scope.row.isMetricLoadOver">{{
scope.row.passRate scope.row.passRate
}}</span> }}</span>
<i v-else class="el-icon-loading" /> <i v-else class="el-icon-loading"/>
</template> </template>
</ms-table-column> </ms-table-column>
<ms-table-column <ms-table-column
@ -424,6 +425,7 @@
:plan-case-ids="[]" :plan-case-ids="[]"
:type="'plan'" :type="'plan'"
:have-u-i-case="haveUICase" :have-u-i-case="haveUICase"
:have-other-exec-case="haveOtherExecCase"
/> />
<ms-test-plan-schedule-batch-switch <ms-test-plan-schedule-batch-switch
ref="scheduleBatchSwitch" ref="scheduleBatchSwitch"
@ -439,8 +441,8 @@
:have-u-i-case="haveUICase" :have-u-i-case="haveUICase"
:have-other-exec-case="haveOtherExecCase" :have-other-exec-case="haveOtherExecCase"
/> />
<test-plan-report-review ref="testCaseReportView" /> <test-plan-report-review ref="testCaseReportView"/>
<ms-task-center ref="taskCenter" :show-menu="false" /> <ms-task-center ref="taskCenter" :show-menu="false"/>
<el-dialog <el-dialog
:visible.sync="showExecute" :visible.sync="showExecute"
destroy-on-close destroy-on-close
@ -454,10 +456,10 @@
<el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio> <el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio>
</el-radio-group> </el-radio-group>
</div> </div>
<br /> <br/>
<span>运行模式仅对测试计划间有效</span> <span>运行模式仅对测试计划间有效</span>
<template v-slot:footer> <template v-slot:footer>
<ms-dialog-footer @cancel="closeExecute" @confirm="handleRunBatch" /> <ms-dialog-footer @cancel="closeExecute" @confirm="handleRunBatch"/>
</template> </template>
</el-dialog> </el-dialog>
</el-card> </el-card>
@ -487,7 +489,7 @@ import HeaderLabelOperate from "metersphere-frontend/src/components/head/HeaderL
import MsTag from "metersphere-frontend/src/components/MsTag"; import MsTag from "metersphere-frontend/src/components/MsTag";
import MsTestPlanScheduleMaintain from "@/business/plan/components/ScheduleMaintain"; import MsTestPlanScheduleMaintain from "@/business/plan/components/ScheduleMaintain";
import {getCurrentProjectID, getCurrentUser, getCurrentUserId,} from "metersphere-frontend/src/utils/token"; 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 {operationConfirm} from "metersphere-frontend/src/utils";
import MsTestPlanRunModeWithEnv from "@/business/plan/common/TestPlanRunModeWithEnv"; import MsTestPlanRunModeWithEnv from "@/business/plan/common/TestPlanRunModeWithEnv";
import MsTaskCenter from "metersphere-frontend/src/components/task/TaskCenter"; import MsTaskCenter from "metersphere-frontend/src/components/task/TaskCenter";
@ -500,8 +502,6 @@ import {
testPlanEditFollows, testPlanEditFollows,
testPlanEditRunConfig, testPlanEditRunConfig,
testPlanGetEnableScheduleCount, testPlanGetEnableScheduleCount,
testPlanHaveExecCase,
testPlanHaveUiCase,
testPlanList, testPlanList,
testPlanMetric, testPlanMetric,
testPlanRun, testPlanRun,
@ -583,17 +583,17 @@ export default {
}, },
], ],
stageFilters: [ stageFilters: [
{ text: this.$t("test_track.plan.smoke_test"), value: "smoke" }, {text: this.$t("test_track.plan.smoke_test"), value: "smoke"},
{ text: this.$t("test_track.plan.system_test"), value: "system" }, {text: this.$t("test_track.plan.system_test"), value: "system"},
{ {
text: this.$t("test_track.plan.regression_test"), text: this.$t("test_track.plan.regression_test"),
value: "regression", value: "regression",
}, },
], ],
scheduleFilters: [ scheduleFilters: [
{ text: this.$t("test_track.plan.schedule_enabled"), value: "OPEN" }, {text: this.$t("test_track.plan.schedule_enabled"), value: "OPEN"},
{ text: this.$t("test_track.issue.status_closed"), value: "SHUT" }, {text: this.$t("test_track.issue.status_closed"), value: "SHUT"},
{ text: this.$t("schedule.not_set"), value: "NOTSET" }, {text: this.$t("schedule.not_set"), value: "NOTSET"},
], ],
currentPlanId: "", currentPlanId: "",
stageOption: [], stageOption: [],
@ -634,7 +634,7 @@ export default {
batchExecuteType: "serial", batchExecuteType: "serial",
//UI //UI
haveUICase: false, haveUICase: false,
//API/ //API
haveOtherExecCase: false, haveOtherExecCase: false,
}; };
}, },
@ -709,7 +709,7 @@ export default {
this.condition.projectId = getCurrentProjectID(); this.condition.projectId = getCurrentProjectID();
this.cardLoading = true; this.cardLoading = true;
testPlanList( testPlanList(
{ pageNum: this.currentPage, pageSize: this.pageSize }, {pageNum: this.currentPage, pageSize: this.pageSize},
this.condition this.condition
).then((response) => { ).then((response) => {
this.cardLoading = false; this.cardLoading = false;
@ -763,31 +763,6 @@ export default {
this.$set(item, "passed", metricData.passed); this.$set(item, "passed", metricData.passed);
this.$set(item, "tested", metricData.tested); this.$set(item, "tested", metricData.tested);
this.$set(item, "total", metricData.total); 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) { if (metricData.followUsers) {
let data = metricData.followUsers; let data = metricData.followUsers;
let follow = ""; let follow = "";
@ -820,7 +795,7 @@ export default {
}); });
}, },
resetTestPlanRow(item) { resetTestPlanRow(item) {
if (!isMetricLoadOver) { if (!item.isMetricLoadOver) {
return; return;
} }
this.$set(item, "isMetricLoadOver", true); this.$set(item, "isMetricLoadOver", true);
@ -834,11 +809,6 @@ export default {
this.$set(item, "passed", 0); this.$set(item, "passed", 0);
this.$set(item, "tested", 0); this.$set(item, "tested", 0);
this.$set(item, "total", 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) { copyData(status) {
return JSON.parse(JSON.stringify(this.dataMap.get(status))); return JSON.parse(JSON.stringify(this.dataMap.get(status)));
@ -928,7 +898,7 @@ export default {
handleRunBatch() { handleRunBatch() {
this.showExecute = false; this.showExecute = false;
let mode = this.batchExecuteType; let mode = this.batchExecuteType;
let param = { mode }; let param = {mode};
const ids = []; const ids = [];
if (this.condition.selectAll) { if (this.condition.selectAll) {
param.isAll = true; param.isAll = true;
@ -1049,8 +1019,19 @@ export default {
this.$refs.testCaseReportView.open(plan); this.$refs.testCaseReportView.open(plan);
}, },
async scheduleTask(row) { async scheduleTask(row) {
this.haveUICase = false;
this.haveOtherExecCase = false;
row.redirectFrom = "testPlan"; row.redirectFrom = "testPlan";
this.currentPlanId = row.id; 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); this.$refs.scheduleMaintain.open(row);
}, },
saveSortField(key, orders) { saveSortField(key, orders) {
@ -1074,18 +1055,27 @@ export default {
}); });
}, },
handleRun(row) { handleRun(row) {
this.haveUICase = false;
this.haveOtherExecCase = false;
this.currentPlanId = row.id; this.currentPlanId = row.id;
this.haveUIScenario().then(() => { let haveApiCase = row.testPlanApiCaseCount > 0;
testPlanHaveExecCase(row.id).then(async (res) => { let haveScenarioCase = row.testPlanApiScenarioCount > 0;
this.haveOtherExecCase = res.data; 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 //ui
if (this.haveOtherExecCase || this.haveUICase) {
this.$refs.runMode.open("API", row.runModeConfig); this.$refs.runMode.open("API", row.runModeConfig);
} else { } else {
//
this.$router.push("/track/plan/view/" + row.id); this.$router.push("/track/plan/view/" + row.id);
} }
});
});
}, },
_handleRun(config) { _handleRun(config) {
let defaultPlanEnvMap = config.testPlanDefaultEnvMap; let defaultPlanEnvMap = config.testPlanDefaultEnvMap;
@ -1183,18 +1173,18 @@ export default {
return; return;
} }
}, },
haveUIScenario() { // haveUIScenario() {
if (hasLicense()) { // if (hasLicense()) {
return new Promise((resolve) => { // return new Promise((resolve) => {
testPlanHaveUiCase(this.currentPlanId).then((r) => { // testPlanHaveUiCase(this.currentPlanId).then((r) => {
this.haveUICase = r.data; // this.haveUICase = r.data;
resolve(); // resolve();
}); // });
}); // });
} else { // } else {
return new Promise((resolve) => resolve()); // return new Promise((resolve) => resolve());
} // }
}, // },
}, },
}; };
</script> </script>

View File

@ -1,16 +1,23 @@
<template> <template>
<div> <div>
<!-- {{ JSON.stringify(eventData) }}-->
<el-radio-group <el-radio-group
v-model="radio" v-model="radio"
style="width: 100%" style="width: 100%"
@change="radioChange" @change="radioChange"
class="radio-change" 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">{{ <el-radio :label="ENV_TYPE.JSON">{{
$t("workspace.env_group.env_list") $t("workspace.env_group.env_list")
}}</el-radio> }}
</el-radio>
<el-radio :label="ENV_TYPE.GROUP" v-if="showEnvGroup" <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 }}<i class="el-icon-tickets mode-span" @click="viewGroup"></i
></el-radio> ></el-radio>
</el-radio-group> </el-radio-group>
@ -37,7 +44,7 @@
/> />
<span class="project-name" :title="getProjectName(pe.id)"> <span class="project-name" :title="getProjectName(pe.id)">
{{ getProjectName(pe.id) }} </span {{ getProjectName(pe.id) }} </span
><br /> ><br/>
<div v-if="pe.expendStatus === 'open'"> <div v-if="pe.expendStatus === 'open'">
<el-radio-group <el-radio-group
v-model="pe.envRadio" v-model="pe.envRadio"
@ -47,10 +54,12 @@
> >
<el-radio label="DEFAULT_ENV" style="margin-top: 7px">{{ <el-radio label="DEFAULT_ENV" style="margin-top: 7px">{{
$t("api_test.environment.default_environment") $t("api_test.environment.default_environment")
}}</el-radio> }}
</el-radio>
<el-radio label="CUSTOMIZE_ENV" style="margin-top: 7px">{{ <el-radio label="CUSTOMIZE_ENV" style="margin-top: 7px">{{
$t("api_test.environment.choose_new_environment") $t("api_test.environment.choose_new_environment")
}}</el-radio> }}
</el-radio>
</el-radio-group> </el-radio-group>
<el-tag <el-tag
v-show="!pe.showEnvSelect" v-show="!pe.showEnvSelect"
@ -58,7 +67,8 @@
:key="index" :key="index"
size="mini" size="mini"
style="margin-left: 0; margin-right: 2px; margin-top: 8px" style="margin-left: 0; margin-right: 2px; margin-top: 8px"
>{{ itemName }}</el-tag >{{ itemName }}
</el-tag
> >
<el-select <el-select
v-show="pe.showEnvSelect" v-show="pe.showEnvSelect"
@ -141,19 +151,16 @@
</template> </template>
<script> <script>
import { ENV_TYPE } from "metersphere-frontend/src/utils/constants"; import {ENV_TYPE} from "metersphere-frontend/src/utils/constants";
import { import {environmentGetALL, getEnvironmentOptions,} from "metersphere-frontend/src/api/environment";
environmentGetALL,
getEnvironmentOptions,
} from "metersphere-frontend/src/api/environment";
import MsTag from "metersphere-frontend/src/components/MsTag"; import MsTag from "metersphere-frontend/src/components/MsTag";
import EnvironmentGroup from "@/business/plan/env/EnvironmentGroupList"; import EnvironmentGroup from "@/business/plan/env/EnvironmentGroupList";
import { getEnvironmentByProjectId } from "@/api/remote/api/api-environment"; import {getEnvironmentByProjectId} from "@/api/remote/api/api-environment";
import { parseEnvironment } from "metersphere-frontend/src/model/EnvironmentModel"; import {parseEnvironment} from "metersphere-frontend/src/model/EnvironmentModel";
export default { export default {
name: "EnvSelectPopover", name: "EnvSelectPopover",
components: { MsTag, EnvironmentGroup }, components: {MsTag, EnvironmentGroup},
data() { data() {
return { return {
radio: this.environmentType, radio: this.environmentType,
@ -189,6 +196,11 @@ export default {
}, },
projectIds: Set, projectIds: Set,
projectList: Array, projectList: Array,
//
isEnvSaved: {
type: Boolean,
default: true,
},
projectEnvMap: Object, projectEnvMap: Object,
envMap: Map, envMap: Map,
environmentType: String, environmentType: String,
@ -233,7 +245,7 @@ export default {
this.groups = data ? data : []; this.groups = data ? data : [];
}); });
} else { } else {
getEnvironmentOptions({ projectIds: [...this.projectIds] }).then( getEnvironmentOptions({projectIds: [...this.projectIds]}).then(
(res) => { (res) => {
let groups = res.data; let groups = res.data;
this.disabledGroups = groups.filter( this.disabledGroups = groups.filter(

View File

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

View File

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

View File

@ -91,14 +91,14 @@
</template> </template>
<script> <script>
import NodeTree from "metersphere-frontend/src/components/module/MsNodeTree"; import NodeTree from "metersphere-frontend/src/components/module/MsNodeTree";
import MsTestPlanCommonComponent from "../base/TestPlanCommonComponent"; import MsTestPlanCommonComponent from "../base/TestPlanCommonComponent";
import TestPlanApiCaseList from "./TestPlanApiCaseList"; import TestPlanApiCaseList from "./TestPlanApiCaseList";
import TestCaseApiRelevance from "./TestCaseApiRelevance"; import TestCaseApiRelevance from "./TestCaseApiRelevance";
import MsTestPlanApiScenarioList from "./TestPlanApiScenarioList"; import MsTestPlanApiScenarioList from "./TestPlanApiScenarioList";
import TestCaseScenarioRelevance from "./TestCaseScenarioRelevance"; import TestCaseScenarioRelevance from "./TestCaseScenarioRelevance";
import MsApiModule from "@/business/plan/view/comonents/api/module/ApiModule"; import MsApiModule from "@/business/plan/view/comonents/api/module/ApiModule";
import MsApiScenarioModule from "@/business/plan/view/comonents/api/module/ApiScenarioModule"; import MsApiScenarioModule from "@/business/plan/view/comonents/api/module/ApiScenarioModule";
export default { export default {
name: "TestPlanApi", name: "TestPlanApi",
@ -140,19 +140,19 @@ export default {
this.selectNodeIds = []; this.selectNodeIds = [];
this.moduleOptions = {}; this.moduleOptions = {};
}, },
redirectCharType(){ redirectCharType() {
if(this.redirectCharType=='scenario'){ if (this.redirectCharType == 'scenario') {
this.model = 'scenario'; this.model = 'scenario';
}else{ } else {
this.model = 'api'; this.model = 'api';
} }
} }
}, },
methods: { methods: {
checkRedirectCharType(){ checkRedirectCharType() {
if(this.redirectCharType=='scenario'){ if (this.redirectCharType == 'scenario') {
this.model = 'scenario'; this.model = 'scenario';
}else{ } else {
this.model = 'api'; this.model = 'api';
} }
}, },

View File

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

View File

@ -34,13 +34,13 @@
<script> <script>
import TestCaseRelevanceBase from "../base/TestCaseRelevanceBase"; import TestCaseRelevanceBase from "../base/TestCaseRelevanceBase";
import { strMapToObj } from "metersphere-frontend/src/utils"; import {strMapToObj} from "metersphere-frontend/src/utils";
import { ENV_TYPE } from "metersphere-frontend/src/utils/constants"; import {ENV_TYPE} from "metersphere-frontend/src/utils/constants";
import RelevanceUiScenarioList from "@/business/plan/view/comonents/ui/RelevanceUiScenarioList"; import RelevanceUiScenarioList from "@/business/plan/view/comonents/ui/RelevanceUiScenarioList";
import { testPlanAutoCheck } from "@/api/remote/plan/test-plan"; import {testPlanAutoCheck} from "@/api/remote/plan/test-plan";
import { testPlanUiScenarioRelevanceListIds } from "@/api/remote/ui/test-plan-ui-scenario-case"; import {testPlanUiScenarioRelevanceListIds} from "@/api/remote/ui/test-plan-ui-scenario-case";
import UiScenarioModule from "@/business/plan/view/comonents/ui/UiScenarioModule"; import UiScenarioModule from "@/business/plan/view/comonents/ui/UiScenarioModule";
import { uiAutomationRelevance } from "@/api/remote/ui/api-scenario"; import {uiAutomationRelevance} from "@/api/remote/ui/api-scenario";
export default { export default {
name: "TestCaseUiScenarioRelevance", name: "TestCaseUiScenarioRelevance",
@ -163,12 +163,6 @@ export default {
return; return;
} }
if (!envMap || envMap.size == 0) {
this.isSaving = false;
this.$warning(this.$t("api_test.environment.select_environment"));
return;
}
let param = {}; let param = {};
param.planId = this.planId; param.planId = this.planId;
param.mapping = strMapToObj(map); param.mapping = strMapToObj(map);