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")
private Map<@Valid @Pattern(regexp = "^[A-Za-z]+$") String, @Valid @NotBlank String> sort;
private boolean exportAllRelatedData;
public String getSortString() {
if (sort == null || sort.isEmpty()) {
return null;

View File

@ -1,7 +1,6 @@
package io.metersphere.api.service;
import io.metersphere.api.constants.ApiDefinitionStatus;
import io.metersphere.api.constants.ApiScenarioExportType;
import io.metersphere.api.constants.ApiScenarioStepType;
import io.metersphere.api.domain.*;
import io.metersphere.api.dto.ApiFile;
@ -76,6 +75,7 @@ import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
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));
String fileFolder = tmpDir.getPath() + File.separatorChar + request.getFileId();
int fileIndex = 1;
AtomicInteger fileIndex = new AtomicInteger(1);
SubListUtils.dealForSubList(ids, 500, subList -> {
request.setSelectIds(subList);
ApiScenarioExportResponse exportResponse = this.genMetersphereExportResponse(request, moduleMap, exportType, userId);
TempFileUtils.writeExportFile(fileFolder + File.separatorChar + "scenario_export_" + fileIndex + ".ms", exportResponse);
ApiScenarioExportResponse exportResponse = this.genMetersphereExportResponse(request, moduleMap);
TempFileUtils.writeExportFile(fileFolder + File.separatorChar + "scenario_" + fileIndex.getAndIncrement() + ".ms", exportResponse);
});
File zipFile = MsFileUtils.zipFile(tmpDir.getPath(), request.getFileId());
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());
MetersphereApiScenarioExportResponse response = apiScenarioService.selectAndSortScenarioDetailWithIds(request.getSelectIds(), moduleMap);
response.setProjectId(project.getId());
response.setOrganizationId(project.getOrganizationId());
if (StringUtils.equalsIgnoreCase(ApiScenarioExportType.METERSPHERE_ALL_DATA.name(), exportType)) {
if (request.isExportAllRelatedData()) {
// 全量导出导出引用的apiapiCase
List<String> apiDefinitionIdList = new ArrayList<>();
List<String> apiCaseIdList = new ArrayList<>();

View File

@ -39,6 +39,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@ -133,11 +134,12 @@ public class ApiDefinitionExportService {
Map<String, String> moduleMap = this.buildModuleIdPathMap(request.getProjectId());
String fileFolder = tmpDir.getPath() + File.separatorChar + request.getFileId();
int fileIndex = 1;
AtomicInteger fileIndex = new AtomicInteger(1);
SubListUtils.dealForSubList(ids, 1000, subList -> {
request.setSelectIds(subList);
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());
if (zipFile == null) {

View File

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

View File

@ -64,4 +64,10 @@
from test_plan_collection
where id IN (select parent_id from test_plan_collection where id = #{0})
</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>

View File

@ -50,4 +50,6 @@ public interface ExtTestPlanReportApiCaseMapper {
List<TestPlanReportDetailCollectionResponse> listCollection(@Param("request") TestPlanReportDetailPageRequest request);
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
order by tpac.pos desc
</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">
<if test="request.filter != null and request.filter.size() > 0">

View File

@ -49,4 +49,6 @@ public interface ExtTestPlanReportApiScenarioMapper {
* @return 关联的测试集集合
*/
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
order by tpas.pos desc
</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">
<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.service.ApiBatchRunBaseService;
import io.metersphere.api.service.ApiCommonService;
import io.metersphere.functional.constants.AssociateCaseType;
import io.metersphere.plan.domain.*;
import io.metersphere.plan.dto.request.TestPlanBatchExecuteRequest;
import io.metersphere.plan.dto.request.TestPlanExecuteRequest;
@ -30,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static io.metersphere.plan.service.TestPlanExecuteSupportService.*;
@ -56,6 +58,10 @@ public class TestPlanExecuteService {
@Resource
private TestPlanApiBatchRunBaseService testPlanApiBatchRunBaseService;
@Resource
private ExtTestPlanReportApiCaseMapper extTestPlanReportApiCaseMapper;
@Resource
private ExtTestPlanReportApiScenarioMapper extTestPlanReportApiScenarioMapper;
@Resource
private TestPlanReportMapper testPlanReportMapper;
@Resource
@ -642,7 +648,7 @@ public class TestPlanExecuteService {
this.executeByTestPlanCollection(queue);
} 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);
} else {
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);
// 如果测试集是失败停止触发的通过父类的配置决定是否执行结束
if (isStopOnFailure) {
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 {
return false;
}

View File

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

View File

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

View File

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

View File

@ -288,5 +288,6 @@ export default {
'apiScenario.csvFileNotNull': 'CSV file cannot be empty',
'apiScenario.export.type.simple': 'Simple',
'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.export.type.simple': '普通导出',
'apiScenario.export.type.all': '保留引用关系',
'apiScenario.export.simple.tooltip': '将引用或复制的请求步骤处理为自定义请求',
'apiScenario.export.simple.tooltip1': '关闭:导出引用和复制的步骤处理为自定义请求',
'apiScenario.export.simple.tooltip2': '开启:导出引用和复制的步骤,保留接口、用例、场景的引用关系',
};