refactor(测试计划): 接口用例批量执行可选环境

This commit is contained in:
shiziyuan9527 2021-08-11 17:12:54 +08:00 committed by 刘瑞斌
parent e9493f9925
commit 61469c1e8a
8 changed files with 562 additions and 3 deletions

View File

@ -2,6 +2,8 @@ package io.metersphere.api.dto.automation;
import lombok.Data;
import java.util.Map;
@Data
public class RunModeConfig {
private String mode;
@ -10,4 +12,9 @@ public class RunModeConfig {
private String reportId;
private boolean onSampleError;
private String resourcePoolId;
/**
* 运行环境
*/
private Map<String, String> envMap;
}

View File

@ -27,6 +27,7 @@ import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RequestMapping("/test/plan")
@ -167,6 +168,11 @@ public class TestPlanController {
return testPlanService.copy(id);
}
@PostMapping("/api/case/env")
public Map<String, List<String>> getApiCaseEnv(@RequestBody List<String> caseIds) {
return testPlanService.getApiCaseEnv(caseIds);
}
@GetMapping("/report/export/{planId}")
public void exportHtmlReport(@PathVariable String planId, HttpServletResponse response) throws UnsupportedEncodingException {
testPlanService.exportPlanReport(planId, response);

View File

@ -475,11 +475,50 @@ public class TestPlanApiCaseService {
MSException.throwException("并发数量过大,请重新选择!");
}
}
Map<String, String> envMap = request.getConfig().getEnvMap();
if (!envMap.isEmpty()) {
setApiCaseEnv(request.getPlanIds(), envMap);
}
return this.modeRun(request);
}
return request.getId();
}
private void setApiCaseEnv(List<String> planIds, Map<String, String> map) {
if (CollectionUtils.isEmpty(planIds)) {
return;
}
TestPlanApiCaseExample caseExample = new TestPlanApiCaseExample();
caseExample.createCriteria().andIdIn(planIds);
List<TestPlanApiCase> testPlanApiCases = testPlanApiCaseMapper.selectByExample(caseExample);
List<String> apiCaseIds = testPlanApiCases.stream().map(TestPlanApiCase::getApiCaseId).collect(Collectors.toList());
if (CollectionUtils.isEmpty(apiCaseIds)) {
return;
}
ApiTestCaseExample example = new ApiTestCaseExample();
example.createCriteria().andIdIn(apiCaseIds);
List<ApiTestCase> apiTestCases = apiTestCaseMapper.selectByExample(example);
Map<String, String> projectCaseIdMap = new HashMap<>(16);
apiTestCases.forEach(c -> projectCaseIdMap.put(c.getId(), c.getProjectId()));
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
TestPlanApiCaseMapper mapper = sqlSession.getMapper(TestPlanApiCaseMapper.class);
testPlanApiCases.forEach(testPlanApiCase -> {
String caseId = testPlanApiCase.getApiCaseId();
String projectId = projectCaseIdMap.get(caseId);
String envId = map.get(projectId);
if (StringUtils.isNotBlank(envId)) {
testPlanApiCase.setEnvironmentId(envId);
mapper.updateByPrimaryKey(testPlanApiCase);
}
});
sqlSession.flushStatements();
}
public Boolean hasFailCase(String planId, List<String> apiCaseIds) {
if (CollectionUtils.isEmpty(apiCaseIds)) {
return false;

View File

@ -1262,6 +1262,46 @@ public class TestPlanService {
}
}
public Map<String, List<String>> getApiCaseEnv(List<String> planApiCaseIds) {
Map<String, List<String>> envMap = new HashMap<>();
if (CollectionUtils.isEmpty(planApiCaseIds)) {
return envMap;
}
TestPlanApiCaseExample caseExample = new TestPlanApiCaseExample();
caseExample.createCriteria().andIdIn(planApiCaseIds);
List<TestPlanApiCase> testPlanApiCases = testPlanApiCaseMapper.selectByExample(caseExample);
List<String> apiCaseIds = testPlanApiCases.stream().map(TestPlanApiCase::getApiCaseId).collect(Collectors.toList());
if (CollectionUtils.isEmpty(apiCaseIds)) {
return envMap;
}
ApiTestCaseExample example = new ApiTestCaseExample();
example.createCriteria().andIdIn(apiCaseIds);
List<ApiTestCase> apiTestCases = apiTestCaseMapper.selectByExample(example);
Map<String, String> projectCaseIdMap = new HashMap<>(16);
apiTestCases.forEach(c -> projectCaseIdMap.put(c.getId(), c.getProjectId()));
testPlanApiCases.forEach(testPlanApiCase -> {
String caseId = testPlanApiCase.getApiCaseId();
String envId = testPlanApiCase.getEnvironmentId();
String projectId = projectCaseIdMap.get(caseId);
if (StringUtils.isNotBlank(projectId)) {
if (envMap.containsKey(projectId)) {
List<String> list = envMap.get(projectId);
if (!list.contains(envId)) {
list.add(envId);
}
} else {
List<String> envs = new ArrayList<>();
envs.add(envId);
envMap.put(projectId, envs);
}
}
});
return envMap;
}
public void exportPlanReport(String planId, HttpServletResponse response) throws UnsupportedEncodingException {
TestPlan testPlan = getTestPlan(planId);
if (StringUtils.isBlank(testPlan.getReportId())) {

View File

@ -0,0 +1,86 @@
<template>
<el-popover
v-model="visible"
placement="bottom-start"
width="400"
:disabled="isReadOnly"
@show="showPopover"
trigger="click">
<env-select :project-ids="projectIds"
:result="result"
:project-env-map="projectEnvMap"
:project-list="projectList"
:show-config-button-with-out-permission="showConfigButtonWithOutPermission"
@close="visible = false"
ref="envSelect"
@setProjectEnvMap="setProjectEnvMap"/>
<el-button type="primary" slot="reference" size="mini" style="margin-top: 2px;">
{{ $t('api_test.definition.request.run_env') }}
<i class="el-icon-caret-bottom el-icon--right"></i>
</el-button>
</el-popover>
</template>
<script>
import EnvSelect from "@/business/components/track/plan/common/EnvSelect";
export default {
name: "EnvPopover",
components: {EnvSelect},
props: {
projectIds: Set,
projectList: Array,
showConfigButtonWithOutPermission: {
type: Boolean,
default() {
return true;
}
},
isReadOnly: {
type: Boolean,
default() {
return false;
}
},
result: {
type: Object,
default() {
return {loading: false}
}
},
projectEnvMap: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
visible: false
}
},
methods: {
showPopover() {
this.$emit("showPopover");
},
openEnvSelect() {
return this.$refs.envSelect.open();
},
setProjectEnvMap(map) {
this.$emit("setProjectEnvMap", map);
},
initEnv() {
return this.$refs.envSelect.initEnv();
},
checkEnv(data) {
return this.$refs.envSelect.checkEnv(data);
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,211 @@
<template>
<div v-loading="result.loading">
<div v-for="pe in data" :key="pe.id" style="margin-left: 20px;">
<el-select v-model="pe['selectEnv']"
placeholder="请选择环境"
style="margin-top: 8px;width: 200px;"
size="small"
clearable>
<el-option v-for="(environment, index) in pe.envs"
:key="index"
:label="environment.name"
:value="environment.id"/>
<el-button v-if="isShowConfirmButton(pe.id)"
@click="openEnvironmentConfig(pe.id, pe['selectEnv'])"
class="ms-scenario-button"
size="mini"
type="primary">
{{ $t('api_test.environment.environment_config') }}
</el-button>
<template v-slot:empty>
<div v-if="isShowConfirmButton(pe.id)" class="empty-environment">
<el-button class="ms-scenario-button" size="mini" type="primary"
@click="openEnvironmentConfig(pe.id, pe['selectEnv'])">
{{ $t('api_test.environment.environment_config') }}
</el-button>
</div>
</template>
</el-select>
<span class="project-name" :title="getProjectName(pe.id)">
{{ getProjectName(pe.id) }}
<el-tooltip class="item"
effect="light"
:content="'存在多个环境' + '(' + pe.conflictEnv + ')'"
placement="top-end">
<i class="el-icon-warning-outline"
v-show="pe.selectEnv === '' && pe.conflictEnv !== ''"/>
</el-tooltip>
</span>
</div>
<el-button type="primary" @click="handleConfirm" size="small" class="env-confirm"> </el-button>
<!-- 环境配置 -->
<api-environment-config ref="environmentConfig" @close="environmentConfigClose"/>
</div>
</template>
<script>
import {parseEnvironment} from "@/business/components/api/test/model/EnvironmentModel";
import ApiEnvironmentConfig from "@/business/components/api/test/components/ApiEnvironmentConfig";
export default {
name: "EnvironmentSelect",
components: {ApiEnvironmentConfig},
props: {
projectIds: Set,
projectList: Array,
showConfigButtonWithOutPermission: {
type: Boolean,
default() {
return true;
}
},
result: {
type: Object,
default() {
return {loading: false}
}
},
projectEnvMap: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
data: [],
permissionProjectIds: [],
dialogVisible: false,
envMap: new Map()
}
},
methods: {
isShowConfirmButton(projectId) {
if (this.showConfigButtonWithOutPermission === true) {
return true;
} else {
if (this.permissionProjectIds) {
if (this.permissionProjectIds.indexOf(projectId) < 0) {
return false;
} else {
return true;
}
} else {
return false;
}
}
},
init() {
//ID
if (this.permissionProjectIds.length === 0) {
this.getUserPermissionProjectIds();
}
let arr = [];
this.projectIds.forEach(projectId => {
const project = this.projectList.find(p => p.id === projectId);
if (project) {
let item = {id: projectId, envs: [], selectEnv: "", conflictEnv: ""};
this.data.push(item);
let p = new Promise(resolve => {
this.$get('/api/environment/list/' + projectId, res => {
let envs = res.data;
//
envs.forEach(environment => {
parseEnvironment(environment);
});
//
let temp = this.data.find(dt => dt.id === projectId);
temp.envs = envs;
let envList = [];
// projectEnvMap {"projectId": {"env1", "env2"}}
for (let pid in this.projectEnvMap) {
if (projectId === pid) {
envList = this.projectEnvMap[pid];
break;
}
}
//
if (envList.length <= 1) {
//
temp.selectEnv = envs.filter(e => e.id === envList[0]).length === 0 ? null : envList[0];
} else {
//
envList.forEach(env => {
const index = envs.findIndex(e => e.id === env);
if (index !== -1) {
item.conflictEnv = item.conflictEnv + " " + envs[index].name;
}
});
}
resolve();
})
});
arr.push(p);
}
})
return arr;
},
getUserPermissionProjectIds() {
this.$get('/project/getOwnerProjectIds/', res => {
this.permissionProjectIds = res.data;
})
},
open() {
this.data = [];
if (this.projectIds.size > 0) {
this.init();
}
},
initEnv() {
this.data = [];
return Promise.all(this.init());
},
getProjectName(id) {
const project = this.projectList.find(p => p.id === id);
return project ? project.name : "";
},
openEnvironmentConfig(projectId, envId) {
if (!projectId) {
this.$error(this.$t('api_test.select_project'));
return;
}
this.$refs.environmentConfig.open(projectId, envId);
},
handleConfirm() {
let map = new Map();
this.data.forEach(dt => {
map.set(dt.id, dt.selectEnv);
})
this.$emit('setProjectEnvMap', map);
this.$emit('close');
},
environmentConfigClose() {
// todo
}
}
}
</script>
<style scoped>
.ms-scenario-button {
margin-left: 20px;
}
.env-confirm {
margin-left: 20px;
width: 360px;
margin-top: 10px;
}
.project-name {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 150px;
margin-left: 8px;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<el-dialog
destroy-on-close
:title="$t('load_test.runtime_config')"
width="550px"
:visible.sync="runModeVisible"
>
<div style="margin-bottom: 10px;">
<span class="ms-mode-span">{{ $t("commons.environment") }}</span>
<env-popover :project-ids="projectIds" :project-list="projectList" :project-env-map="projectEnvListMap"
@setProjectEnvMap="setProjectEnvMap" @showPopover="showPopover"
ref="envPopover" class="env-popover"/>
</div>
<div>
<span class="ms-mode-span">{{ $t("run_mode.title") }}</span>
<el-radio-group v-model="runConfig.mode" @change="changeMode">
<el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio>
<el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio>
</el-radio-group>
</div>
<div class="ms-mode-div" v-if="runConfig.mode === 'serial'">
<el-row>
<el-col :span="6">
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}</span>
</el-col>
<el-col :span="18">
<div>
<el-checkbox v-model="runConfig.onSampleError">失败停止</el-checkbox>
</div>
<div v-if="testType === 'API'" style="padding-top: 10px">
<el-checkbox v-model="runConfig.runWithinResourcePool" style="padding-right: 10px;">
{{ $t('run_mode.run_with_resource_pool') }}
</el-checkbox>
<el-select :disabled="!runConfig.runWithinResourcePool" v-model="runConfig.resourcePoolId" size="mini">
<el-option
v-for="item in resourcePools"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</div>
</el-col>
</el-row>
</div>
<div class="ms-mode-div" v-if="runConfig.mode === 'parallel'">
<el-row>
<el-col :span="6">
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}</span>
</el-col>
<el-col :span="18">
<div v-if="testType === 'API'">
<el-checkbox v-model="runConfig.runWithinResourcePool" style="padding-right: 10px;">
{{ $t('run_mode.run_with_resource_pool') }}
</el-checkbox>
<el-select :disabled="!runConfig.runWithinResourcePool" v-model="runConfig.resourcePoolId" size="mini">
<el-option
v-for="item in resourcePools"
:key="item.id"
:label="item.name"
:disabled="!item.api"
:value="item.id">
</el-option>
</el-select>
</div>
</el-col>
</el-row>
</div>
<template v-slot:footer>
<ms-dialog-footer @cancel="close" @confirm="handleRunBatch"/>
</template>
</el-dialog>
</template>
<script>
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
import EnvPopover from "@/business/components/track/plan/common/EnvPopover";
import {strMapToObj} from "@/common/js/utils";
export default {
name: "MsPlanRunModeWithEnv",
components: {EnvPopover, MsDialogFooter},
data() {
return {
runModeVisible: false,
testType: null,
resourcePools: [],
runConfig: {
mode: "serial",
reportType: "iddReport",
onSampleError: false,
runWithinResourcePool: false,
resourcePoolId: null,
envMap: new Map()
},
projectEnvListMap: {},
projectList: [],
projectIds: new Set(),
};
},
props: ['planCaseIds'],
methods: {
open(testType) {
this.runModeVisible = true;
this.testType = testType;
this.getResourcePools();
this.getWsProjects();
},
changeMode() {
this.runConfig.onSampleError = false;
this.runConfig.runWithinResourcePool = false;
this.runConfig.resourcePoolId = null;
},
close() {
this.runConfig = {
mode: "serial",
reportType: "iddReport",
onSampleError: false,
runWithinResourcePool: false,
resourcePoolId: null,
envMap: new Map()
};
this.runModeVisible = false;
},
handleRunBatch() {
this.$emit("handleRunBatch", this.runConfig);
this.close();
},
getResourcePools() {
this.result = this.$get('/testresourcepool/list/quota/valid', response => {
this.resourcePools = response.data;
});
},
setProjectEnvMap(projectEnvMap) {
this.runConfig.envMap = strMapToObj(projectEnvMap);
},
getWsProjects() {
this.$get("/project/listAll", res => {
this.projectList = res.data;
})
},
showPopover() {
this.projectIds.clear();
this.$post('/test/plan/api/case/env', this.planCaseIds, res => {
let data = res.data;
if (data) {
this.projectEnvListMap = data;
for (let d in data) {
this.projectIds.add(d);
}
}
this.$refs.envPopover.openEnvSelect();
})
}
},
};
</script>
<style scoped>
.ms-mode-span {
margin-right: 10px;
}
.ms-mode-div {
margin-top: 20px;
}
</style>

View File

@ -145,7 +145,7 @@
<batch-edit :dialog-title="$t('test_track.case.batch_edit_case')" :type-arr="typeArr" :value-arr="valueArr"
:select-row="$refs.table ? $refs.table.selectRows : new Set()" ref="batchEdit" @batchEdit="batchEdit"/>
<ms-plan-run-mode @handleRunBatch="handleRunBatch" ref="runMode"/>
<ms-plan-run-mode @handleRunBatch="handleRunBatch" ref="runMode" :plan-case-ids="testPlanCaseIds"/>
</el-card>
<ms-task-center ref="taskCenter"/>
</div>
@ -179,7 +179,7 @@ import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOpe
import MsTaskCenter from "../../../../../task/TaskCenter";
import MsTable from "@/business/components/common/components/table/MsTable";
import MsTableColumn from "@/business/components/common/components/table/MsTableColumn";
import MsPlanRunMode from "@/business/components/track/plan/common/PlanRunMode";
import MsPlanRunMode from "@/business/components/track/plan/common/PlanRunModeWithEnv";
import MsUpdateTimeColumn from "@/business/components/common/components/table/MsUpdateTimeColumn";
import MsCreateTimeColumn from "@/business/components/common/components/table/MsCreateTimeColumn";
@ -579,8 +579,10 @@ export default {
this.$post("/test/plan/api/case/run", obj, response => {
this.$message(this.$t('commons.run_message'));
this.$refs.taskCenter.open();
this.search();
}, () => {
this.search();
});
this.search();
},
autoCheckStatus() { //
if (!this.planId) {