feat(测试计划): 增加测试类型的失败停止和测试集的失败停止设置冲突时的失败停止支持&场景导出的页面优化

--bug=1047945 --user=宋天阳 【测试计划】-场景串行+失败停止,测试点A串行+失败不停止,测试点B并行,执行后,测试点2的场景全部执行了 https://www.tapd.cn/55049933/s/1597277
This commit is contained in:
Jianguo-Genius 2024-10-24 14:38:53 +08:00 committed by Craftsman
parent a53c2b75bf
commit f2312e8da8
15 changed files with 88 additions and 33 deletions

View File

@ -31,6 +31,8 @@ public class ApiScenarioBatchExportRequest extends ApiScenarioBatchRequest imple
@Schema(description = "排序字段model中的字段 : asc/desc") @Schema(description = "排序字段model中的字段 : asc/desc")
private Map<@Valid @Pattern(regexp = "^[A-Za-z]+$") String, @Valid @NotBlank String> sort; private Map<@Valid @Pattern(regexp = "^[A-Za-z]+$") String, @Valid @NotBlank String> sort;
private boolean exportAllRelatedData;
public String getSortString() { public String getSortString() {
if (sort == null || sort.isEmpty()) { if (sort == null || sort.isEmpty()) {
return null; return null;

View File

@ -1,7 +1,6 @@
package io.metersphere.api.service; package io.metersphere.api.service;
import io.metersphere.api.constants.ApiDefinitionStatus; import io.metersphere.api.constants.ApiDefinitionStatus;
import io.metersphere.api.constants.ApiScenarioExportType;
import io.metersphere.api.constants.ApiScenarioStepType; import io.metersphere.api.constants.ApiScenarioStepType;
import io.metersphere.api.domain.*; import io.metersphere.api.domain.*;
import io.metersphere.api.dto.ApiFile; import io.metersphere.api.dto.ApiFile;
@ -76,6 +75,7 @@ import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -1023,11 +1023,11 @@ public class ApiScenarioDataTransferService {
Map<String, String> moduleMap = this.apiScenarioModuleService.getTree(request.getProjectId()).stream().collect(Collectors.toMap(BaseTreeNode::getId, BaseTreeNode::getPath)); Map<String, String> moduleMap = this.apiScenarioModuleService.getTree(request.getProjectId()).stream().collect(Collectors.toMap(BaseTreeNode::getId, BaseTreeNode::getPath));
String fileFolder = tmpDir.getPath() + File.separatorChar + request.getFileId(); String fileFolder = tmpDir.getPath() + File.separatorChar + request.getFileId();
int fileIndex = 1; AtomicInteger fileIndex = new AtomicInteger(1);
SubListUtils.dealForSubList(ids, 500, subList -> { SubListUtils.dealForSubList(ids, 500, subList -> {
request.setSelectIds(subList); request.setSelectIds(subList);
ApiScenarioExportResponse exportResponse = this.genMetersphereExportResponse(request, moduleMap, exportType, userId); ApiScenarioExportResponse exportResponse = this.genMetersphereExportResponse(request, moduleMap);
TempFileUtils.writeExportFile(fileFolder + File.separatorChar + "scenario_export_" + fileIndex + ".ms", exportResponse); TempFileUtils.writeExportFile(fileFolder + File.separatorChar + "scenario_" + fileIndex.getAndIncrement() + ".ms", exportResponse);
}); });
File zipFile = MsFileUtils.zipFile(tmpDir.getPath(), request.getFileId()); File zipFile = MsFileUtils.zipFile(tmpDir.getPath(), request.getFileId());
if (zipFile == null) { if (zipFile == null) {
@ -1058,13 +1058,13 @@ public class ApiScenarioDataTransferService {
} }
} }
private ApiScenarioExportResponse genMetersphereExportResponse(ApiScenarioBatchExportRequest request, Map<String, String> moduleMap, String exportType, String userId) { private ApiScenarioExportResponse genMetersphereExportResponse(ApiScenarioBatchExportRequest request, Map<String, String> moduleMap) {
Project project = projectMapper.selectByPrimaryKey(request.getProjectId()); Project project = projectMapper.selectByPrimaryKey(request.getProjectId());
MetersphereApiScenarioExportResponse response = apiScenarioService.selectAndSortScenarioDetailWithIds(request.getSelectIds(), moduleMap); MetersphereApiScenarioExportResponse response = apiScenarioService.selectAndSortScenarioDetailWithIds(request.getSelectIds(), moduleMap);
response.setProjectId(project.getId()); response.setProjectId(project.getId());
response.setOrganizationId(project.getOrganizationId()); response.setOrganizationId(project.getOrganizationId());
if (StringUtils.equalsIgnoreCase(ApiScenarioExportType.METERSPHERE_ALL_DATA.name(), exportType)) { if (request.isExportAllRelatedData()) {
// 全量导出导出引用的apiapiCase // 全量导出导出引用的apiapiCase
List<String> apiDefinitionIdList = new ArrayList<>(); List<String> apiDefinitionIdList = new ArrayList<>();
List<String> apiCaseIdList = new ArrayList<>(); List<String> apiCaseIdList = new ArrayList<>();

View File

@ -39,6 +39,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -133,11 +134,12 @@ public class ApiDefinitionExportService {
Map<String, String> moduleMap = this.buildModuleIdPathMap(request.getProjectId()); Map<String, String> moduleMap = this.buildModuleIdPathMap(request.getProjectId());
String fileFolder = tmpDir.getPath() + File.separatorChar + request.getFileId(); String fileFolder = tmpDir.getPath() + File.separatorChar + request.getFileId();
int fileIndex = 1;
AtomicInteger fileIndex = new AtomicInteger(1);
SubListUtils.dealForSubList(ids, 1000, subList -> { SubListUtils.dealForSubList(ids, 1000, subList -> {
request.setSelectIds(subList); request.setSelectIds(subList);
ApiDefinitionExportResponse exportResponse = this.genApiExportResponse(request, moduleMap, exportType, userId); ApiDefinitionExportResponse exportResponse = this.genApiExportResponse(request, moduleMap, exportType, userId);
TempFileUtils.writeExportFile(fileFolder + File.separatorChar + fileIndex + ".json", exportResponse); TempFileUtils.writeExportFile(fileFolder + File.separatorChar + "API_" + fileIndex.getAndIncrement() + ".json", exportResponse);
}); });
File zipFile = MsFileUtils.zipFile(tmpDir.getPath(), request.getFileId()); File zipFile = MsFileUtils.zipFile(tmpDir.getPath(), request.getFileId());
if (zipFile == null) { if (zipFile == null) {

View File

@ -1,5 +1,6 @@
package io.metersphere.plan.mapper; package io.metersphere.plan.mapper;
import io.metersphere.plan.domain.TestPlanCollection;
import io.metersphere.plan.dto.TestPlanCollectionConfigDTO; import io.metersphere.plan.dto.TestPlanCollectionConfigDTO;
import io.metersphere.plan.dto.TestPlanCollectionEnvDTO; import io.metersphere.plan.dto.TestPlanCollectionEnvDTO;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@ -17,4 +18,6 @@ public interface ExtTestPlanCollectionMapper {
String selectDefaultCollectionId(@Param("testPlanId")String newTestPlanId,@Param("type") String key); String selectDefaultCollectionId(@Param("testPlanId")String newTestPlanId,@Param("type") String key);
boolean getParentStopOnFailure(String collectionId); boolean getParentStopOnFailure(String collectionId);
List<TestPlanCollection> selectByItemParentId(String collectionId);
} }

View File

@ -64,4 +64,10 @@
from test_plan_collection from test_plan_collection
where id IN (select parent_id from test_plan_collection where id = #{0}) where id IN (select parent_id from test_plan_collection where id = #{0})
</select> </select>
<select id="selectByItemParentId" resultType="io.metersphere.plan.domain.TestPlanCollection">
select *
from test_plan_collection
where parent_id = (select parent_id from test_plan_collection where id = #{0})
ORDER BY pos
</select>
</mapper> </mapper>

View File

@ -50,4 +50,6 @@ public interface ExtTestPlanReportApiCaseMapper {
List<TestPlanReportDetailCollectionResponse> listCollection(@Param("request") TestPlanReportDetailPageRequest request); List<TestPlanReportDetailCollectionResponse> listCollection(@Param("request") TestPlanReportDetailPageRequest request);
List<String> getIdsByReportIdAndCollectionId(@Param("testPlanReportId") String testPlanReportId, @Param("collectionId") String collectionId); List<String> getIdsByReportIdAndCollectionId(@Param("testPlanReportId") String testPlanReportId, @Param("collectionId") String collectionId);
List<String> selectExecResultByReportIdAndCollectionId(@Param("collectionId") String collectionId, @Param("reportId") String prepareReportId);
} }

View File

@ -78,6 +78,12 @@
and atc.deleted = false and atc.deleted = false
order by tpac.pos desc order by tpac.pos desc
</select> </select>
<select id="selectExecResultByReportIdAndCollectionId" resultType="java.lang.String">
select distinct api_case_execute_result
from test_plan_report_api_case
where test_plan_collection_id = #{collectionId}
AND test_plan_report_id = #{reportId};
</select>
<sql id="filter"> <sql id="filter">
<if test="request.filter != null and request.filter.size() > 0"> <if test="request.filter != null and request.filter.size() > 0">

View File

@ -49,4 +49,6 @@ public interface ExtTestPlanReportApiScenarioMapper {
* @return 关联的测试集集合 * @return 关联的测试集集合
*/ */
List<TestPlanReportDetailCollectionResponse> listCollection(@Param("request") TestPlanReportDetailPageRequest request); List<TestPlanReportDetailCollectionResponse> listCollection(@Param("request") TestPlanReportDetailPageRequest request);
List<String> selectExecResultByReportIdAndCollectionId(@Param("collectionId") String collectionId, @Param("reportId") String prepareReportId);
} }

View File

@ -71,6 +71,12 @@
and aso.deleted = false and aso.deleted = false
order by tpas.pos desc order by tpas.pos desc
</select> </select>
<select id="selectExecResultByReportIdAndCollectionId" resultType="java.lang.String">
select distinct api_scenario_execute_result
from test_plan_report_api_scenario
where test_plan_collection_id = #{collectionId}
AND test_plan_report_id = #{reportId};
</select>
<sql id="filter"> <sql id="filter">
<if test="request.filter != null and request.filter.size() > 0"> <if test="request.filter != null and request.filter.size() > 0">

View File

@ -5,6 +5,7 @@ import io.metersphere.api.domain.ApiReportRelateTask;
import io.metersphere.api.mapper.ApiReportRelateTaskMapper; import io.metersphere.api.mapper.ApiReportRelateTaskMapper;
import io.metersphere.api.service.ApiBatchRunBaseService; import io.metersphere.api.service.ApiBatchRunBaseService;
import io.metersphere.api.service.ApiCommonService; import io.metersphere.api.service.ApiCommonService;
import io.metersphere.functional.constants.AssociateCaseType;
import io.metersphere.plan.domain.*; import io.metersphere.plan.domain.*;
import io.metersphere.plan.dto.request.TestPlanBatchExecuteRequest; import io.metersphere.plan.dto.request.TestPlanBatchExecuteRequest;
import io.metersphere.plan.dto.request.TestPlanExecuteRequest; import io.metersphere.plan.dto.request.TestPlanExecuteRequest;
@ -30,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import static io.metersphere.plan.service.TestPlanExecuteSupportService.*; import static io.metersphere.plan.service.TestPlanExecuteSupportService.*;
@ -56,6 +58,10 @@ public class TestPlanExecuteService {
@Resource @Resource
private TestPlanApiBatchRunBaseService testPlanApiBatchRunBaseService; private TestPlanApiBatchRunBaseService testPlanApiBatchRunBaseService;
@Resource
private ExtTestPlanReportApiCaseMapper extTestPlanReportApiCaseMapper;
@Resource
private ExtTestPlanReportApiScenarioMapper extTestPlanReportApiScenarioMapper;
@Resource @Resource
private TestPlanReportMapper testPlanReportMapper; private TestPlanReportMapper testPlanReportMapper;
@Resource @Resource
@ -642,7 +648,7 @@ public class TestPlanExecuteService {
this.executeByTestPlanCollection(queue); this.executeByTestPlanCollection(queue);
} else if (StringUtils.equalsIgnoreCase(queue.getQueueType(), QUEUE_PREFIX_TEST_PLAN_COLLECTION)) { } else if (StringUtils.equalsIgnoreCase(queue.getQueueType(), QUEUE_PREFIX_TEST_PLAN_COLLECTION)) {
// 判断是否是失败停止 如果是失败停止要检测父类是否也同样配置了失败停止是的话不再执行 // 判断是否是失败停止 如果是失败停止要检测父类是否也同样配置了失败停止是的话不再执行
if (this.isCaseTypeExecuteStop(queue.getSourceID(), isStopOnFailure)) { if (this.isCaseTypeExecuteStop(queue.getSourceID(), queue.getPrepareReportId(), isStopOnFailure)) {
this.collectionExecuteQueueFinish(queue.getQueueId(), isStopOnFailure); this.collectionExecuteQueueFinish(queue.getQueueId(), isStopOnFailure);
} else { } else {
this.executeCase(queue); this.executeCase(queue);
@ -650,10 +656,31 @@ public class TestPlanExecuteService {
} }
} }
private boolean isCaseTypeExecuteStop(String collectionId, boolean isStopOnFailure) { private boolean isCaseTypeExecuteStop(String collectionId, String prepareReportId, boolean isStopOnFailure) {
boolean caseTypeStopOnFailure = extTestPlanCollectionMapper.getParentStopOnFailure(collectionId); boolean caseTypeStopOnFailure = extTestPlanCollectionMapper.getParentStopOnFailure(collectionId);
// 如果测试集是失败停止触发的通过父类的配置决定是否执行结束
if (isStopOnFailure) { if (isStopOnFailure) {
return caseTypeStopOnFailure; return caseTypeStopOnFailure;
} else if (caseTypeStopOnFailure) {
// 如果是正常执行结束的并且配置了失败停止则根据该测试集的执行结果是否全部成功来决定是否继续执行
//首先拿到执行结束的测试集
List<TestPlanCollection> testPlanCollectionList = extTestPlanCollectionMapper.selectByItemParentId(collectionId);
TestPlanCollection lastCollection = null;
for (TestPlanCollection item : testPlanCollectionList) {
if (StringUtils.equalsIgnoreCase(item.getId(), collectionId)) {
break;
}
lastCollection = item;
}
List<String> execResult = null;
if (AssociateCaseType.API.equals(lastCollection.getType())) {
execResult = extTestPlanReportApiCaseMapper.selectExecResultByReportIdAndCollectionId(lastCollection.getId(), prepareReportId);
} else if (AssociateCaseType.SCENARIO.equals(lastCollection.getType())) {
execResult = extTestPlanReportApiScenarioMapper.selectExecResultByReportIdAndCollectionId(lastCollection.getId(), prepareReportId);
}
return CollectionUtils.size(execResult) != 1 || !StringUtils.equalsIgnoreCase(Objects.requireNonNull(execResult).getFirst(), ResultStatus.SUCCESS.name());
} else { } else {
return false; return false;
} }

View File

@ -257,11 +257,6 @@ export enum ScenarioExecuteStatus {
FAKE_ERROR = 'FAKE_ERROR', FAKE_ERROR = 'FAKE_ERROR',
} }
// 场景导出配置
export enum ScenarioExportType {
SIMPLE = 'METERSPHERE_SIMPLE',
ALL = 'METERSPHERE_ALL_DATA',
}
// 场景步骤类型 // 场景步骤类型
export enum ScenarioStepType { export enum ScenarioStepType {
API_CASE = 'API_CASE', // 接口用例 API_CASE = 'API_CASE', // 接口用例

View File

@ -548,5 +548,6 @@ export interface ImportScenarioParams {
// 导出场景参数 // 导出场景参数
export interface ExportScenarioParams extends BatchActionQueryParams { export interface ExportScenarioParams extends BatchActionQueryParams {
apiScenarioId: string; apiScenarioId: string;
exportAllRelatedData: boolean;
fileId: string; fileId: string;
} }

View File

@ -6,18 +6,20 @@
class="ms-modal-upload ms-modal-medium" class="ms-modal-upload ms-modal-medium"
:width="400" :width="400"
> >
<a-radio-group v-model:model-value="exportTypeRadio"> <div class="mb-[16px] flex items-center gap-[8px]">
<a-radio :value="ScenarioExportType.SIMPLE" <a-switch v-model:model-value="exportTypeRadio" type="line" size="small"></a-switch>
>{{ t('apiScenario.export.type.simple') }} {{ t('apiScenario.export.type.all') }}
<a-tooltip :content="t('apiScenario.export.simple.tooltip')" position="tl"> <a-tooltip :content="t('apiScenario.export.simple.tooltip')" position="tl">
<icon-question-circle <icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16" size="16"
/> />
<template #content>
<div>{{ t('apiScenario.export.simple.tooltip1') }}</div>
<div>{{ t('apiScenario.export.simple.tooltip2') }}</div>
</template>
</a-tooltip> </a-tooltip>
</a-radio> </div>
<a-radio :value="ScenarioExportType.ALL">{{ t('apiScenario.export.type.all') }}</a-radio>
</a-radio-group>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
@ -45,8 +47,6 @@
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { downloadByteFile, getGenerateId } from '@/utils'; import { downloadByteFile, getGenerateId } from '@/utils';
import { ScenarioExportType } from '@/enums/apiEnum';
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
@ -60,7 +60,7 @@
const visible = defineModel<boolean>('visible', { required: true }); const visible = defineModel<boolean>('visible', { required: true });
const exportLoading = ref(false); const exportLoading = ref(false);
const exportTypeRadio = ref(ScenarioExportType.SIMPLE); const exportTypeRadio = ref(false);
function cancelExport() { function cancelExport() {
visible.value = false; visible.value = false;
@ -184,6 +184,7 @@
const { selectedIds, selectAll, excludeIds } = props.batchParams; const { selectedIds, selectAll, excludeIds } = props.batchParams;
const res = await exportScenario( const res = await exportScenario(
{ {
exportAllRelatedData: exportTypeRadio.value,
selectIds: selectedIds || [], selectIds: selectedIds || [],
selectAll: !!selectAll, selectAll: !!selectAll,
excludeIds: excludeIds || [], excludeIds: excludeIds || [],
@ -191,7 +192,7 @@
sort: props.sorter || {}, sort: props.sorter || {},
fileId: reportId.value, fileId: reportId.value,
}, },
exportTypeRadio.value 'METERSPHERE'
); );
showExportingMessage(res); showExportingMessage(res);
visible.value = false; visible.value = false;

View File

@ -288,5 +288,6 @@ export default {
'apiScenario.csvFileNotNull': 'CSV file cannot be empty', 'apiScenario.csvFileNotNull': 'CSV file cannot be empty',
'apiScenario.export.type.simple': 'Simple', 'apiScenario.export.type.simple': 'Simple',
'apiScenario.export.type.all': 'All data', 'apiScenario.export.type.all': 'All data',
'apiScenario.export.simple.tooltip': 'Process referenced or copied request steps as custom requests', 'apiScenario.export.simple.tooltip1': 'Close: Export referenced and copied steps as custom requests',
'apiScenario.export.simple.tooltip2': 'Open: Export referenced and copied steps as native data',
}; };

View File

@ -282,5 +282,6 @@ export default {
'apiScenario.csvFileNotNull': 'CSV 文件不能为空', 'apiScenario.csvFileNotNull': 'CSV 文件不能为空',
'apiScenario.export.type.simple': '普通导出', 'apiScenario.export.type.simple': '普通导出',
'apiScenario.export.type.all': '保留引用关系', 'apiScenario.export.type.all': '保留引用关系',
'apiScenario.export.simple.tooltip': '将引用或复制的请求步骤处理为自定义请求', 'apiScenario.export.simple.tooltip1': '关闭:导出引用和复制的步骤处理为自定义请求',
'apiScenario.export.simple.tooltip2': '开启:导出引用和复制的步骤,保留接口、用例、场景的引用关系',
}; };