feat(测试计划): 性能测试用例可以调整压力配置#1001972

--story=1001972 --user=lyh 性能测试添加到测试计划后,可以重新调整压力配置并不影响原有的配置
https://www.tapd.cn/55049933/s/1044891
This commit is contained in:
shiziyuan9527 2021-09-08 18:34:50 +08:00 committed by 刘瑞斌
parent a148886e06
commit 98ddf35ef7
14 changed files with 1123 additions and 17 deletions

View File

@ -1,8 +1,7 @@
package io.metersphere.base.domain;
import lombok.Data;
import java.io.Serializable;
import lombok.Data;
@Data
public class TestPlanLoadCase implements Serializable {
@ -22,5 +21,9 @@ public class TestPlanLoadCase implements Serializable {
private String createUser;
private String testResourcePoolId;
private String loadConfiguration;
private static final long serialVersionUID = 1L;
}

View File

@ -643,6 +643,76 @@ public class TestPlanLoadCaseExample {
addCriterion("create_user not between", value1, value2, "createUser");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdIsNull() {
addCriterion("test_resource_pool_id is null");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdIsNotNull() {
addCriterion("test_resource_pool_id is not null");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdEqualTo(String value) {
addCriterion("test_resource_pool_id =", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdNotEqualTo(String value) {
addCriterion("test_resource_pool_id <>", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdGreaterThan(String value) {
addCriterion("test_resource_pool_id >", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdGreaterThanOrEqualTo(String value) {
addCriterion("test_resource_pool_id >=", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdLessThan(String value) {
addCriterion("test_resource_pool_id <", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdLessThanOrEqualTo(String value) {
addCriterion("test_resource_pool_id <=", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdLike(String value) {
addCriterion("test_resource_pool_id like", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdNotLike(String value) {
addCriterion("test_resource_pool_id not like", value, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdIn(List<String> values) {
addCriterion("test_resource_pool_id in", values, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdNotIn(List<String> values) {
addCriterion("test_resource_pool_id not in", values, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdBetween(String value1, String value2) {
addCriterion("test_resource_pool_id between", value1, value2, "testResourcePoolId");
return (Criteria) this;
}
public Criteria andTestResourcePoolIdNotBetween(String value1, String value2) {
addCriterion("test_resource_pool_id not between", value1, value2, "testResourcePoolId");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {

View File

@ -16,15 +16,21 @@ public interface TestPlanLoadCaseMapper {
int insertSelective(TestPlanLoadCase record);
List<TestPlanLoadCase> selectByExampleWithBLOBs(TestPlanLoadCaseExample example);
List<TestPlanLoadCase> selectByExample(TestPlanLoadCaseExample example);
TestPlanLoadCase selectByPrimaryKey(String id);
int updateByExampleSelective(@Param("record") TestPlanLoadCase record, @Param("example") TestPlanLoadCaseExample example);
int updateByExampleWithBLOBs(@Param("record") TestPlanLoadCase record, @Param("example") TestPlanLoadCaseExample example);
int updateByExample(@Param("record") TestPlanLoadCase record, @Param("example") TestPlanLoadCaseExample example);
int updateByPrimaryKeySelective(TestPlanLoadCase record);
int updateByPrimaryKeyWithBLOBs(TestPlanLoadCase record);
int updateByPrimaryKey(TestPlanLoadCase record);
}

View File

@ -10,6 +10,10 @@
<result column="create_time" jdbcType="BIGINT" property="createTime" />
<result column="update_time" jdbcType="BIGINT" property="updateTime" />
<result column="create_user" jdbcType="VARCHAR" property="createUser" />
<result column="test_resource_pool_id" jdbcType="VARCHAR" property="testResourcePoolId" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.TestPlanLoadCase">
<result column="load_configuration" jdbcType="LONGVARCHAR" property="loadConfiguration" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
@ -71,8 +75,27 @@
</sql>
<sql id="Base_Column_List">
id, test_plan_id, load_case_id, load_report_id, `status`, create_time, update_time,
create_user
create_user, test_resource_pool_id
</sql>
<sql id="Blob_Column_List">
load_configuration
</sql>
<select id="selectByExampleWithBLOBs" parameterType="io.metersphere.base.domain.TestPlanLoadCaseExample" resultMap="ResultMapWithBLOBs">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from test_plan_load_case
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
<select id="selectByExample" parameterType="io.metersphere.base.domain.TestPlanLoadCaseExample" resultMap="BaseResultMap">
select
<if test="distinct">
@ -87,9 +110,11 @@
order by ${orderByClause}
</if>
</select>
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="ResultMapWithBLOBs">
select
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from test_plan_load_case
where id = #{id,jdbcType=VARCHAR}
</select>
@ -106,10 +131,12 @@
<insert id="insert" parameterType="io.metersphere.base.domain.TestPlanLoadCase">
insert into test_plan_load_case (id, test_plan_id, load_case_id,
load_report_id, `status`, create_time,
update_time, create_user)
update_time, create_user, test_resource_pool_id,
load_configuration)
values (#{id,jdbcType=VARCHAR}, #{testPlanId,jdbcType=VARCHAR}, #{loadCaseId,jdbcType=VARCHAR},
#{loadReportId,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR}, #{createTime,jdbcType=BIGINT},
#{updateTime,jdbcType=BIGINT}, #{createUser,jdbcType=VARCHAR})
#{updateTime,jdbcType=BIGINT}, #{createUser,jdbcType=VARCHAR}, #{testResourcePoolId,jdbcType=VARCHAR},
#{loadConfiguration,jdbcType=LONGVARCHAR})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.TestPlanLoadCase">
insert into test_plan_load_case
@ -138,6 +165,12 @@
<if test="createUser != null">
create_user,
</if>
<if test="testResourcePoolId != null">
test_resource_pool_id,
</if>
<if test="loadConfiguration != null">
load_configuration,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
@ -164,6 +197,12 @@
<if test="createUser != null">
#{createUser,jdbcType=VARCHAR},
</if>
<if test="testResourcePoolId != null">
#{testResourcePoolId,jdbcType=VARCHAR},
</if>
<if test="loadConfiguration != null">
#{loadConfiguration,jdbcType=LONGVARCHAR},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.base.domain.TestPlanLoadCaseExample" resultType="java.lang.Long">
@ -199,11 +238,33 @@
<if test="record.createUser != null">
create_user = #{record.createUser,jdbcType=VARCHAR},
</if>
<if test="record.testResourcePoolId != null">
test_resource_pool_id = #{record.testResourcePoolId,jdbcType=VARCHAR},
</if>
<if test="record.loadConfiguration != null">
load_configuration = #{record.loadConfiguration,jdbcType=LONGVARCHAR},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExampleWithBLOBs" parameterType="map">
update test_plan_load_case
set id = #{record.id,jdbcType=VARCHAR},
test_plan_id = #{record.testPlanId,jdbcType=VARCHAR},
load_case_id = #{record.loadCaseId,jdbcType=VARCHAR},
load_report_id = #{record.loadReportId,jdbcType=VARCHAR},
`status` = #{record.status,jdbcType=VARCHAR},
create_time = #{record.createTime,jdbcType=BIGINT},
update_time = #{record.updateTime,jdbcType=BIGINT},
create_user = #{record.createUser,jdbcType=VARCHAR},
test_resource_pool_id = #{record.testResourcePoolId,jdbcType=VARCHAR},
load_configuration = #{record.loadConfiguration,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExample" parameterType="map">
update test_plan_load_case
set id = #{record.id,jdbcType=VARCHAR},
@ -213,7 +274,8 @@
`status` = #{record.status,jdbcType=VARCHAR},
create_time = #{record.createTime,jdbcType=BIGINT},
update_time = #{record.updateTime,jdbcType=BIGINT},
create_user = #{record.createUser,jdbcType=VARCHAR}
create_user = #{record.createUser,jdbcType=VARCHAR},
test_resource_pool_id = #{record.testResourcePoolId,jdbcType=VARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
@ -242,9 +304,28 @@
<if test="createUser != null">
create_user = #{createUser,jdbcType=VARCHAR},
</if>
<if test="testResourcePoolId != null">
test_resource_pool_id = #{testResourcePoolId,jdbcType=VARCHAR},
</if>
<if test="loadConfiguration != null">
load_configuration = #{loadConfiguration,jdbcType=LONGVARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.base.domain.TestPlanLoadCase">
update test_plan_load_case
set test_plan_id = #{testPlanId,jdbcType=VARCHAR},
load_case_id = #{loadCaseId,jdbcType=VARCHAR},
load_report_id = #{loadReportId,jdbcType=VARCHAR},
`status` = #{status,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT},
create_user = #{createUser,jdbcType=VARCHAR},
test_resource_pool_id = #{testResourcePoolId,jdbcType=VARCHAR},
load_configuration = #{loadConfiguration,jdbcType=LONGVARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.TestPlanLoadCase">
update test_plan_load_case
set test_plan_id = #{testPlanId,jdbcType=VARCHAR},
@ -253,7 +334,8 @@
`status` = #{status,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT},
create_user = #{createUser,jdbcType=VARCHAR}
create_user = #{createUser,jdbcType=VARCHAR},
test_resource_pool_id = #{testResourcePoolId,jdbcType=VARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
</mapper>

View File

@ -8,5 +8,16 @@ import lombok.Setter;
public class RunTestPlanRequest extends TestPlanRequest {
private String userId;
private String triggerMode;
/**
* 测试计划用例跑性能测试时需要设置此值
*/
private String testPlanLoadId;
/**
* 压力配置信息
*/
private String ownLoadConfiguration;
/**
* 资源池ID
*/
private String ownTestPoolId;
}

View File

@ -4,12 +4,15 @@ package io.metersphere.performance.service;
import com.alibaba.excel.util.CollectionUtils;
import io.metersphere.base.domain.LoadTestReportWithBLOBs;
import io.metersphere.base.domain.LoadTestWithBLOBs;
import io.metersphere.base.domain.TestPlanLoadCase;
import io.metersphere.base.mapper.LoadTestMapper;
import io.metersphere.base.mapper.TestPlanLoadCaseMapper;
import io.metersphere.base.mapper.ext.ExtLoadTestReportMapper;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.performance.engine.EngineContext;
import io.metersphere.performance.engine.EngineFactory;
import org.codehaus.plexus.util.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -30,11 +33,35 @@ public class JmeterFileService {
private LoadTestMapper loadTestMapper;
@Resource
private ExtLoadTestReportMapper extLoadTestReportMapper;
@Resource
private TestPlanLoadCaseMapper testPlanLoadCaseMapper;
public byte[] downloadZip(String testId, double[] ratios, String reportId, int resourceIndex) {
try {
LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(testId);
TestPlanLoadCase testPlanLoadCase = null;
if (loadTest == null) {
// 通过测试计划执行性能用例时testId为测试计划性能用例表的主键ID根据ID查询用例自身的压力配置
testPlanLoadCase = testPlanLoadCaseMapper.selectByPrimaryKey(testId);
if (testPlanLoadCase != null) {
loadTest = loadTestMapper.selectByPrimaryKey(testPlanLoadCase.getLoadCaseId());
if (loadTest != null) {
// 用例自身设置了资源池ID
if (StringUtils.isNotBlank(testPlanLoadCase.getTestResourcePoolId())) {
loadTest.setTestResourcePoolId(testPlanLoadCase.getTestResourcePoolId());
}
// 用例自身设置了压力配置
if (StringUtils.isNotBlank(testPlanLoadCase.getLoadConfiguration())) {
loadTest.setLoadConfiguration(testPlanLoadCase.getLoadConfiguration());
}
}
}
}
EngineContext context = EngineFactory.createContext(loadTest, ratios, reportId, resourceIndex);
if (testPlanLoadCase != null) {
// ID
context.setTestId(testPlanLoadCase.getId());
}
return zipFilesToByteArray(context);
} catch (MSException e) {
LogUtil.error(e.getMessage(), e);

View File

@ -41,6 +41,7 @@ import io.metersphere.service.QuotaService;
import io.metersphere.service.ScheduleService;
import io.metersphere.track.service.TestCaseService;
import io.metersphere.track.service.TestPlanLoadCaseService;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.ExecutorType;
@ -109,6 +110,8 @@ public class PerformanceTestService {
@Lazy
@Resource
private TestPlanLoadCaseService testPlanLoadCaseService;
@Resource
private TestPlanLoadCaseMapper testPlanLoadCaseMapper;
public List<LoadTestDTO> list(QueryTestPlanRequest request) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
@ -318,8 +321,21 @@ public class PerformanceTestService {
checkKafka();
LogUtil.info("Load test started " + loadTest.getName());
LoadTestWithBLOBs copyTest = new LoadTestWithBLOBs();
BeanUtils.copyBean(copyTest, loadTest);
// 如果是执行测试计划用例把EngineFactory.createEngine参数对象的id 设置为测试计划用例id
// 设置用例id目的是当 JmeterFileService 下载zip拼装 jmx 文件时如果用例自身带有压力配置使用用例自身压力配置拼装 jmx
String testPlanLoadId = request.getTestPlanLoadId();
if (StringUtils.isNotBlank(testPlanLoadId)) {
copyTest.setId(testPlanLoadId);
// 设置本次报告中的压力配置信息
TestPlanLoadCase testPlanLoadCase = testPlanLoadCaseMapper.selectByPrimaryKey(testPlanLoadId);
if (testPlanLoadCase != null && StringUtils.isNotBlank(testPlanLoadCase.getLoadConfiguration())) {
loadTest.setLoadConfiguration(testPlanLoadCase.getLoadConfiguration());
}
}
// engine type (NODE)
final Engine engine = EngineFactory.createEngine(loadTest);
final Engine engine = EngineFactory.createEngine(copyTest);
if (engine == null) {
MSException.throwException(String.format("Test cannot be runtest ID%s", request.getId()));
}
@ -363,6 +379,9 @@ public class PerformanceTestService {
} else {
testReport.setUserId(SessionUtils.getUser().getId());
}
// 只更新已设置的属性其它未设置属性不更新
LoadTestWithBLOBs updateTest = new LoadTestWithBLOBs();
updateTest.setId(loadTest.getId());
// 启动测试
try {
// 启动插入 report
@ -376,8 +395,8 @@ public class PerformanceTestService {
engine.start();
// 启动正常修改状态 starting
loadTest.setStatus(PerformanceTestStatus.Starting.name());
loadTestMapper.updateByPrimaryKeySelective(loadTest);
updateTest.setStatus(PerformanceTestStatus.Starting.name());
loadTestMapper.updateByPrimaryKeySelective(updateTest);
LoadTestReportDetail reportDetail = new LoadTestReportDetail();
reportDetail.setContent(HEADERS);
@ -397,9 +416,9 @@ public class PerformanceTestService {
// 启动失败之后清理任务
engine.stop();
LogUtil.error(e.getMessage(), e);
loadTest.setStatus(PerformanceTestStatus.Error.name());
loadTest.setDescription(e.getMessage());
loadTestMapper.updateByPrimaryKeySelective(loadTest);
updateTest.setStatus(PerformanceTestStatus.Error.name());
updateTest.setDescription(e.getMessage());
loadTestMapper.updateByPrimaryKeySelective(updateTest);
loadTestReportMapper.deleteByPrimaryKey(testReport.getId());
throw e;
}

View File

@ -100,4 +100,14 @@ public class TestPlanLoadCaseController {
public List<TestPlanLoadCaseDTO> getAllCases(@PathVariable String planId) {
return testPlanLoadCaseService.getAllCases(planId);
}
@GetMapping("/get-load-config/{loadCaseId}")
public String getPlanLoadCaseConfig(@PathVariable String loadCaseId) {
return testPlanLoadCaseService.getPlanLoadCaseConfig(loadCaseId);
}
@GetMapping("/get/{loadCaseId}")
public TestPlanLoadCase getTestPlanLoadCase(@PathVariable String loadCaseId) {
return testPlanLoadCaseService.getTestPlanLoadCase(loadCaseId);
}
}

View File

@ -396,4 +396,22 @@ public class TestPlanLoadCaseService {
});
return cases;
}
public String getPlanLoadCaseConfig(String loadCaseId) {
if (StringUtils.isBlank(loadCaseId)) {
return "";
}
TestPlanLoadCase testPlanLoadCase = testPlanLoadCaseMapper.selectByPrimaryKey(loadCaseId);
if (testPlanLoadCase != null) {
return testPlanLoadCase.getLoadConfiguration();
}
return "";
}
public TestPlanLoadCase getTestPlanLoadCase(String loadCaseId) {
if (StringUtils.isBlank(loadCaseId)) {
return new TestPlanLoadCase();
}
return testPlanLoadCaseMapper.selectByPrimaryKey(loadCaseId);
}
}

View File

@ -1046,7 +1046,7 @@ public class TestPlanService {
String caseID = entry.getValue();
RunTestPlanRequest performanceRequest = new RunTestPlanRequest();
performanceRequest.setId(caseID);
performanceRequest.setTestPlanLoadId(caseID);
performanceRequest.setTestPlanLoadId(id);
if (StringUtils.equals(ReportTriggerMode.API.name(), triggerMode)) {
performanceRequest.setTriggerMode(ReportTriggerMode.TEST_PLAN_API.name());
} else if (StringUtils.equals(ReportTriggerMode.MANUAL.name(), triggerMode)) {

View File

@ -17,6 +17,13 @@ CREATE TABLE `plugin` (
DEFAULT CHARSET = utf8mb4
COLLATE utf8mb4_general_ci;
alter table test_plan_load_case
add test_resource_pool_id varchar(50) null;
alter table test_plan_load_case
add load_configuration longtext null;
update test_case_node set name = '未规划用例' where name = '默认模块' and `level` = 1;
update test_case set node_path = replace (`node_path`,'/默认模块','/未规划用例') where node_path like '/默认模块%';

View File

@ -0,0 +1,92 @@
<template>
<el-dialog :close-on-click-modal="false" :visible.sync="visible" :title="$t('load_test.pressure_config')"
width="61%" top="8vh" @close="close" v-loading="result.loading" :destroy-on-close="true">
<performance-load-config :test-id="loadTestId" :load-case-id="loadCaseId" :resource-pool="poolId"
@fileChange="fileChange" ref="pressureConfig" style="height: 50vh; overflow-y: auto;"/>
<template v-slot:footer>
<el-button @click="close" size="medium">{{ $t('commons.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit" size="medium" style="margin-left: 10px;">
{{ $t('commons.confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script>
import PerformanceLoadConfig from "@/business/components/track/plan/view/comonents/load/PerformanceLoadConfig";
export default {
name: "LoadCaseConfig",
components: {
PerformanceLoadConfig
},
data() {
return {
visible: false,
loadTestId: "",
result: {},
loadCaseId: "",
poolId: ""
}
},
methods: {
open(loadTestId, loadCaseId) {
this.visible = true;
this.loadTestId = loadTestId;
this.loadCaseId = loadCaseId;
this.result = this.$get("/test/plan/load/case/get/" + loadCaseId, res => {
this.poolId = res.data ? res.data.testResourcePoolId : "";
})
},
close() {
this.visible = false;
},
fileChange(threadGroups) {
let handler = this.$refs.pressureConfig;
let csvSet = new Set;
threadGroups.forEach(tg => {
tg.threadNumber = tg.threadNumber || 10;
tg.duration = tg.duration || 10;
tg.rampUpTime = tg.rampUpTime || 5;
tg.step = tg.step || 5;
tg.rpsLimit = tg.rpsLimit || 10;
tg.threadType = tg.threadType || 'DURATION';
tg.iterateNum = tg.iterateNum || 1;
tg.iterateRampUp = tg.iterateRampUp || 10;
if (tg.csvFiles) {
tg.csvFiles.map(item => csvSet.add(item));
}
});
this.$set(handler, "threadGroups", threadGroups);
this.$refs.pressureConfig.threadGroups = threadGroups;
handler.calculateTotalChart();
},
onSubmit() {
if (!this.$refs.pressureConfig.validConfig()) {
return false;
}
//
let loadConfiguration = JSON.stringify(this.$refs.pressureConfig.convertProperty());
let testResourcePoolId = this.$refs.pressureConfig.resourcePool;
let params = {
id: this.loadCaseId,
loadConfiguration,
testResourcePoolId
}
this.result = this.$post("/test/plan/load/case/update", params, () => {
this.visible = false;
this.$success(this.$t("commons.modify_success"));
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,747 @@
<template>
<div v-loading="result.loading" class="pressure-config-container">
<el-row>
<el-col>
<el-form :inline="true">
<el-form-item :label="$t('load_test.select_resource_pool')">
<el-select v-model="resourcePool" :disabled="isReadOnly" size="mini" @change="resourcePoolChange">
<el-option
v-for="item in resourcePools"
:key="item.id"
:label="item.name"
:disabled="!item.performance"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('load_test.serialize_threadgroups')">
<el-switch v-model="serializeThreadGroups"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.autostop_threadgroups')">
<el-switch v-model="autoStop"/>
</el-form-item>
<el-form-item :label="$t('load_test.reaches_duration')">
<el-input-number
:disabled="isReadOnly || !autoStop"
v-model="autoStopDelay"
:min="1"
:max="9999"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.autostop_delay')"/>
</el-form>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-collapse v-model="activeNames" accordion>
<el-collapse-item :name="index"
v-for="(threadGroup, index) in threadGroups.filter(th=>th.enabled === 'true' && th.deleted=='false')"
:key="index">
<template slot="title">
<el-row>
<el-col :span="14">
<el-tooltip :content="threadGroup.attributes.testname" placement="top">
<div style="padding-right:20px; font-size: 16px;" class="wordwrap">
{{ threadGroup.attributes.testname }}
</div>
</el-tooltip>
</el-col>
<el-col :span="10">
<el-tag type="primary" size="mini" v-if="threadGroup.threadType === 'DURATION'">
{{ $t('load_test.thread_num') }}{{ threadGroup.threadNumber }},
{{ $t('load_test.duration') }}: {{ threadGroup.duration }} {{ getUnitLabel(threadGroup) }}
</el-tag>
<el-tag type="primary" size="mini" v-if="threadGroup.threadType === 'ITERATION'">
{{ $t('load_test.thread_num') }} {{ threadGroup.threadNumber }},
{{ $t('load_test.iterate_num') }} {{ threadGroup.iterateNum }}
</el-tag>
</el-col>
</el-row>
</template>
<el-form :inline="true">
<el-form-item :label="$t('load_test.thread_num')">
<el-input-number
:disabled="isReadOnly"
v-model="threadGroup.threadNumber"
@change="calculateTotalChart()"
:min="1"
:max="maxThreadNumbers"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.on_sample_error')">
<el-select v-model="threadGroup.onSampleError" :disabled="isReadOnly" size="mini">
<el-option
v-for="item in onSampleErrors"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<br>
<el-form-item>
<el-radio-group v-model="threadGroup.threadType" @change="calculateTotalChart()">
<el-radio label="DURATION">{{ $t('load_test.by_duration') }}</el-radio>
<el-radio label="ITERATION">{{ $t('load_test.by_iteration') }}</el-radio>
</el-radio-group>
</el-form-item>
<br>
<div v-if="threadGroup.threadType === 'DURATION'">
<el-form-item :label="$t('load_test.duration')">
<el-input-number
:disabled="isReadOnly"
v-model="threadGroup.duration"
:min="1"
:max="9999"
@change="calculateTotalChart()"
size="mini"/>
</el-form-item>
<el-form-item>
<el-radio-group v-model="threadGroup.unit" @change="changeUnit(threadGroup)">
<el-radio label="S">{{ $t('schedule.cron.seconds') }}</el-radio>
<el-radio label="M">{{ $t('schedule.cron.minutes') }}</el-radio>
<el-radio label="H">{{ $t('schedule.cron.hours') }}</el-radio>
</el-radio-group>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.rps_limit')">
<el-switch v-model="threadGroup.rpsLimitEnable" @change="calculateTotalChart()"/>
&nbsp;
<el-input-number
:disabled="isReadOnly || !threadGroup.rpsLimitEnable"
v-model="threadGroup.rpsLimit"
@change="calculateTotalChart()"
:min="1"
:max="99999"
size="mini"/>
</el-form-item>
<br>
<div v-if="threadGroup.tgType === 'com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup'">
<el-form-item :label="$t('load_test.ramp_up_time_within')">
<el-input-number
:disabled="isReadOnly"
:min="1"
v-if="rampUpTimeVisible"
:max="getDuration(threadGroup)"
v-model="threadGroup.rampUpTime"
@change="calculateTotalChart()"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_minutes', [getUnitLabel(threadGroup)])">
<el-input-number
:disabled="isReadOnly"
:min="1"
:max="Math.min(threadGroup.threadNumber, threadGroup.rampUpTime)"
v-model="threadGroup.step"
@change="calculateTotalChart()"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_times')"/>
</div>
<div v-if="threadGroup.tgType === 'ThreadGroup'">
<el-form-item :label="$t('load_test.ramp_up_time_within')">
<el-input-number
:disabled="isReadOnly"
v-if="rampUpTimeVisible"
:min="1"
:max="getDuration(threadGroup)"
v-model="threadGroup.rampUpTime"
@change="calculateTotalChart()"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_seconds', [getUnitLabel(threadGroup)])"/>
</div>
</div>
<div v-if="threadGroup.threadType === 'ITERATION'">
<el-form-item :label="$t('load_test.iterate_num')">
<el-input-number
:disabled="isReadOnly"
v-model="threadGroup.iterateNum"
:min="1"
:max="9999999"
@change="calculateTotalChart()"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.rps_limit')">
<el-switch v-model="threadGroup.rpsLimitEnable" @change="calculateTotalChart()"/>
&nbsp;
<el-input-number
:disabled="isReadOnly || !threadGroup.rpsLimitEnable"
v-model="threadGroup.rpsLimit"
:min="1"
:max="99999"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.ramp_up_time_within')">
<el-input-number
:disabled="isReadOnly"
:min="1"
v-model="threadGroup.iterateRampUp"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_seconds', [getUnitLabel(threadGroup)])"/>
</div>
</el-form>
</el-collapse-item>
</el-collapse>
</el-col>
<el-col :span="12">
<div class="title">{{ $t('load_test.pressure_prediction_chart') }}</div>
<ms-chart class="chart-container" ref="chart1" :options="options" :autoresize="true"></ms-chart>
</el-col>
</el-row>
</div>
</template>
<script>
import echarts from "echarts";
import MsChart from "@/business/components/common/chart/MsChart";
import {findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
import {hasPermission} from "@/common/js/utils";
const HANDLER = "handler";
const THREAD_GROUP_TYPE = "tgType";
const ON_SAMPLE_ERROR = "onSampleError";
const SERIALIZE_THREAD_GROUPS = "serializeThreadGroups";
const AUTO_STOP = "autoStop";
const AUTO_STOP_DELAY = "autoStopDelay";
const TARGET_LEVEL = "TargetLevel";
const RAMP_UP = "RampUp";
const ITERATE_RAMP_UP = "iterateRampUpTime";
const STEPS = "Steps";
const DURATION = "duration";
const UNIT = "unit";
const RPS_LIMIT = "rpsLimit";
const RPS_LIMIT_ENABLE = "rpsLimitEnable";
const HOLD = "Hold";
const THREAD_TYPE = "threadType";
const ITERATE_NUM = "iterateNum";
const ENABLED = "enabled";
const DELETED = "deleted";
const hexToRgba = function (hex, opacity) {
return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ','
+ parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')';
};
const hexToRgb = function (hex) {
return 'rgb(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5))
+ ',' + parseInt('0x' + hex.slice(5, 7)) + ')';
};
export default {
name: "PerformanceLoadConfig",
components: {MsChart},
props: {
test: {
type: Object
},
testId: {
type: String
},
loadCaseId: {
type: String
},
resourcePool: {
type: String
}
},
data() {
return {
result: {},
threadNumber: 0,
duration: 0,
rampUpTime: 0,
step: 0,
rpsLimit: 0,
rpsLimitEnable: false,
options: {},
resourcePools: [],
activeNames: ["0"],
threadGroups: [],
maxThreadNumbers: 5000,
serializeThreadGroups: false,
autoStop: false,
autoStopDelay: 30,
isReadOnly: false,
rampUpTimeVisible: true,
};
},
computed: {
onSampleErrors() {
return [
{value: 'continue', label: this.$t('load_test.continue')},
{value: 'startnextloop', label: this.$t('load_test.startnextloop')},
{value: 'stopthread', label: this.$t('load_test.stopthread')},
{value: 'stoptest', label: this.$t('load_test.stoptest')},
{value: 'stoptestnow', label: this.$t('load_test.stoptestnow')},
];
}
},
mounted() {
if (this.testId) {
this.getJmxContent();
} else {
this.calculateTotalChart();
}
this.getResourcePools();
this.isReadOnly = !hasPermission('PROJECT_PERFORMANCE_TEST:READ+EDIT');
},
watch: {
testId() {
if (this.testId) {
this.getJmxContent();
} else {
this.calculateTotalChart();
}
this.getResourcePools();
},
},
methods: {
getResourcePools() {
this.result = this.$get('/testresourcepool/list/quota/valid', response => {
this.resourcePools = response.data;
// null
if (response.data.filter(p => p.id === this.resourcePool).length === 0) {
this.resourcePool = null;
}
this.resourcePoolChange();
});
},
getLoadConfig() {
this.$get('/test/plan/load/case/get-load-config/' + this.loadCaseId, (response) => {
if (response.data) {
let data = JSON.parse(response.data);
let oldVersion;
for (let i = 0; i < data.length; i++) {
let d = data[i];
d.forEach(item => {
switch (item.key) {
case TARGET_LEVEL:
this.threadGroups[i].threadNumber = item.value;
break;
case RAMP_UP:
this.threadGroups[i].rampUpTime = item.value;
break;
case ITERATE_RAMP_UP:
this.threadGroups[i].iterateRampUp = item.value;
break;
case DURATION:
this.threadGroups[i].duration = item.value;
oldVersion = item.unit;
break;
case UNIT:
this.threadGroups[i].unit = item.value;
break;
case STEPS:
this.threadGroups[i].step = item.value;
break;
case RPS_LIMIT:
this.threadGroups[i].rpsLimit = item.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[i].rpsLimitEnable = item.value;
break;
case THREAD_TYPE:
this.threadGroups[i].threadType = item.value;
break;
case ITERATE_NUM:
this.threadGroups[i].iterateNum = item.value;
break;
case ENABLED:
this.threadGroups[i].enabled = item.value;
break;
case DELETED:
this.threadGroups[i].deleted = item.value;
break;
case HANDLER:
this.threadGroups[i].handler = item.value;
break;
case THREAD_GROUP_TYPE:
this.threadGroups[i].tgType = item.value;
break;
case ON_SAMPLE_ERROR:
this.threadGroups[i].onSampleError = item.value;
break;
case SERIALIZE_THREAD_GROUPS:
this.serializeThreadGroups = item.value;// 线
break;
case AUTO_STOP:
this.autoStop = item.value;// 线
break;
case AUTO_STOP_DELAY:
this.autoStopDelay = item.value;// 线
break;
default:
break;
}
//
this.$set(this.threadGroups[i], "unit", this.threadGroups[i].unit || 'S');
this.$set(this.threadGroups[i], "threadType", this.threadGroups[i].threadType || 'DURATION');
this.$set(this.threadGroups[i], "iterateNum", this.threadGroups[i].iterateNum || 1);
this.$set(this.threadGroups[i], "iterateRampUp", this.threadGroups[i].iterateRampUp || 10);
this.$set(this.threadGroups[i], "enabled", this.threadGroups[i].enabled || 'true');
this.$set(this.threadGroups[i], "deleted", this.threadGroups[i].deleted || 'false');
this.$set(this.threadGroups[i], "onSampleError", this.threadGroups[i].onSampleError || 'continue');
});
}
for (let i = 0; i < this.threadGroups.length; i++) {
//
if (oldVersion) {
break;
}
//
switch (this.threadGroups[i].unit) {
case 'M':
this.threadGroups[i].duration = this.threadGroups[i].duration / 60;
break;
case 'H':
this.threadGroups[i].duration = this.threadGroups[i].duration / 60 / 60;
break;
default:
break;
}
}
this.calculateTotalChart();
}
});
},
getJmxContent() {
if (this.testId) {
let threadGroups = [];
this.$get('/performance/get-jmx-content/' + this.testId, (response) => {
response.data.forEach(d => {
threadGroups = threadGroups.concat(findThreadGroup(d.jmx, d.name));
threadGroups.forEach(tg => {
tg.options = {};
});
});
this.threadGroups = threadGroups;
this.$emit('fileChange', threadGroups);
this.getLoadConfig();
});
}
},
resourcePoolChange() {
let result = this.resourcePools.filter(p => p.id === this.resourcePool);
if (result.length === 1) {
let threadNumber = 0;
result[0].resources.forEach(resource => {
threadNumber += JSON.parse(resource.configuration).maxConcurrency;
});
this.$set(this, 'maxThreadNumbers', threadNumber);
this.threadGroups.forEach(tg => {
if (tg.threadNumber > threadNumber) {
this.$set(tg, "threadNumber", threadNumber);
}
});
this.calculateTotalChart();
}
},
calculateTotalChart() {
this.rampUpTimeVisible = false;
this.$nextTick(() => {
this.rampUpTimeVisible = true;
this._calculateTotalChart();
});
},
_calculateTotalChart() {
let handler = this;
let color = ['#60acfc', '#32d3eb', '#5bc49f', '#feb64d', '#ff7c7c', '#9287e7', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'];
handler.options = {
color: color,
xAxis: {
type: 'category',
boundaryGap: false,
data: []
},
yAxis: {
type: 'value'
},
tooltip: {
trigger: 'axis',
},
series: []
};
for (let i = 0; i < handler.threadGroups.length; i++) {
let tg = handler.threadGroups[i];
if (tg.enabled === 'false' ||
tg.deleted === 'true' ||
tg.threadType === 'ITERATION') {
continue;
}
if (this.getDuration(tg) < tg.rampUpTime) {
tg.rampUpTime = tg.duration;
}
if (tg.rampUpTime < tg.step) {
tg.step = tg.rampUpTime;
}
let seriesData = {
name: tg.attributes.testname,
data: [],
type: 'line',
smooth: false,
symbolSize: 5,
showSymbol: false,
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: hexToRgba(color[i % color.length], 0.3),
}, {
offset: 0.8,
color: hexToRgba(color[i % color.length], 0),
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: hexToRgb(color[i % color.length]),
borderColor: 'rgba(137,189,2,0.27)',
borderWidth: 12
}
},
};
let timePeriod = Math.floor(tg.rampUpTime / tg.step);
let timeInc = timePeriod;
let threadPeriod = Math.floor(tg.threadNumber / tg.step);
let threadInc1 = Math.floor(tg.threadNumber / tg.step);
let threadInc2 = Math.ceil(tg.threadNumber / tg.step);
let inc2count = tg.threadNumber - tg.step * threadInc1;
let times = 1;
switch (tg.unit) {
case 'M':
times *= 60;
break;
case 'H':
times *= 3600;
break;
default:
break;
}
let duration = tg.duration * times;
for (let j = 0; j <= duration; j++) {
// x
let xAxis = handler.options.xAxis.data;
if (xAxis.indexOf(j) < 0) {
xAxis.push(j);
}
if (tg.tgType === 'ThreadGroup') {
seriesData.step = undefined;
if (j === 0) {
seriesData.data.push(['0', 0]);
}
if (j >= tg.rampUpTime) {
if (xAxis.indexOf(duration) < 0) {
xAxis.push(duration);
}
seriesData.data.push([j + '', tg.threadNumber]);
seriesData.data.push([duration + '', tg.threadNumber]);
break;
}
} else {
seriesData.step = 'start';
if (j > timePeriod) {
timePeriod += timeInc;
if (inc2count > 0) {
threadPeriod = threadPeriod + threadInc2;
inc2count--;
} else {
threadPeriod = threadPeriod + threadInc1;
}
if (threadPeriod > tg.threadNumber) {
threadPeriod = tg.threadNumber;
//
if (xAxis.indexOf(duration) < 0) {
xAxis.push(duration);
}
seriesData.data.push([j + '', threadPeriod]);
seriesData.data.push([duration + '', threadPeriod]);
break;
}
}
seriesData.data.push([j + '', threadPeriod]);
}
}
// x
handler.options.xAxis.data = handler.options.xAxis.data.sort((a, b) => a - b);
handler.options.series.push(seriesData);
}
// console.log(JSON.stringify(handler.options));
},
validConfig() {
if (!this.resourcePool) {
this.$warning(this.$t('load_test.resource_pool_is_null'));
// Tabname
this.$emit('changeActive', '1');
return false;
}
for (let i = 0; i < this.threadGroups.length; i++) {
if (!this.threadGroups[i].threadNumber || !this.threadGroups[i].duration
|| !this.threadGroups[i].rampUpTime || !this.threadGroups[i].step || !this.threadGroups[i].iterateNum) {
this.$warning(this.$t('load_test.pressure_config_params_is_empty'));
this.$emit('changeActive', '1');
return false;
}
if (this.threadGroups[i].rpsLimitEnable && !this.threadGroups[i].rpsLimit) {
this.$warning(this.$t('load_test.pressure_config_params_is_empty'));
this.$emit('changeActive', '1');
return false;
}
}
return true;
},
getHold(tg) {
if (tg.unit === 'S') {
return tg.duration - tg.rampUpTime;
}
if (tg.unit === 'M') {
return tg.duration * 60 - tg.rampUpTime;
}
if (tg.unit === 'H') {
return tg.duration * 60 * 60 - tg.rampUpTime;
}
return tg.duration - tg.rampUpTime;
},
getDuration(tg) {
if (tg.unit === 'S') {
return tg.duration;
}
if (tg.unit === 'M') {
return tg.duration * 60;
}
if (tg.unit === 'H') {
return tg.duration * 60 * 60;
}
return tg.duration;
},
changeUnit(tg) {
this.rampUpTimeVisible = false;
this.$nextTick(() => {
this.rampUpTimeVisible = true;
this.calculateTotalChart();
});
},
getUnitLabel(tg) {
if (tg.unit === 'S') {
return this.$t('schedule.cron.seconds');
}
if (tg.unit === 'M') {
return this.$t('schedule.cron.minutes');
}
if (tg.unit === 'H') {
return this.$t('schedule.cron.hours');
}
return this.$t('schedule.cron.seconds');
},
convertProperty() {
/// todo4jmeter ConcurrencyThreadGroup plugin
let result = [];
//
for (let i = 0; i < this.threadGroups.length; i++) {
result.push([
{key: HANDLER, value: this.threadGroups[i].handler},
{key: TARGET_LEVEL, value: this.threadGroups[i].threadNumber},
{key: RAMP_UP, value: this.threadGroups[i].rampUpTime},
{key: STEPS, value: this.threadGroups[i].step},
{key: DURATION, value: this.getDuration(this.threadGroups[i])},
{key: UNIT, value: this.threadGroups[i].unit},
{key: RPS_LIMIT, value: this.threadGroups[i].rpsLimit},
{key: RPS_LIMIT_ENABLE, value: this.threadGroups[i].rpsLimitEnable},
{key: HOLD, value: this.getHold(this.threadGroups[i])},
{key: THREAD_TYPE, value: this.threadGroups[i].threadType},
{key: ITERATE_NUM, value: this.threadGroups[i].iterateNum},
{key: ITERATE_RAMP_UP, value: this.threadGroups[i].iterateRampUp},
{key: ENABLED, value: this.threadGroups[i].enabled},
{key: DELETED, value: this.threadGroups[i].deleted},
{key: ON_SAMPLE_ERROR, value: this.threadGroups[i].onSampleError},
{key: THREAD_GROUP_TYPE, value: this.threadGroups[i].tgType},
{key: SERIALIZE_THREAD_GROUPS, value: this.serializeThreadGroups},
{key: AUTO_STOP, value: this.autoStop},
{key: AUTO_STOP_DELAY, value: this.autoStopDelay},
]);
}
return result;
}
}
};
</script>
<style scoped>
.pressure-config-container .el-input {
width: 130px;
}
.pressure-config-container .config-form-label {
width: 130px;
}
.pressure-config-container .input-bottom-border input {
border: 0;
border-bottom: 1px solid #DCDFE6;
}
/deep/ .el-collapse-item__content {
padding-left: 10px;
padding-bottom: 5px;
border-left-width: 8px;
border-left-style: solid;
border-left-color: #F5F7FA;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.chart-container {
width: 100%;
height: 300px;
}
.el-form-item {
margin-top: 5px;
margin-bottom: 5px;
}
.el-col .el-form {
margin-top: 5px;
text-align: left;
}
.el-col {
text-align: left;
}
.title {
margin-left: 60px;
}
.wordwrap {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -111,6 +111,7 @@
<ms-plan-run-mode @handleRunBatch="runBatch" ref="runMode"/>
<load-case-report :report-id="reportId" ref="loadCaseReport" @refresh="initTable"/>
<load-case-config ref="loadCaseConfig"/>
</div>
</template>
@ -132,6 +133,7 @@ import MsTable from "@/business/components/common/components/table/MsTable";
import MsTableColumn from "@/business/components/common/components/table/MsTableColumn";
import MsCreateTimeColumn from "@/business/components/common/components/table/MsCreateTimeColumn";
import MsUpdateTimeColumn from "@/business/components/common/components/table/MsUpdateTimeColumn";
import LoadCaseConfig from "@/business/components/track/plan/view/comonents/load/LoadCaseConfig";
export default {
name: "TestPlanLoadCaseList",
@ -145,7 +147,8 @@ export default {
TestPlanLoadCaseListHeader,
MsTablePagination,
MsPerformanceTestStatus,
MsPlanRunMode
MsPlanRunMode,
LoadCaseConfig
},
data() {
return {
@ -170,13 +173,20 @@ export default {
isDisable: this.isReadOnly,
permissions: ['PROJECT_TRACK_PLAN:READ+RUN']
},
{
tip: '修改压力配置',
icon: "el-icon-setting",
exec: this.changeLoadConfig,
type: 'danger',
isDisable: this.isReadOnly,
},
{
tip: this.$t('test_track.plan_view.cancel_relevance'), icon: "el-icon-unlock",
exec: this.handleDelete,
type: 'danger',
isDisable: this.isReadOnly,
permissions: ['PROJECT_TRACK_PLAN:READ+RELEVANCE_OR_CANCEL']
}
},
],
buttons: [
{
@ -405,6 +415,10 @@ export default {
this.initTable();
});
},
changeLoadConfig(loadCase) {
const {loadCaseId, id} = loadCase;
this.$refs.loadCaseConfig.open(loadCaseId, id);
},
getReport(data) {
const {loadReportId} = data;
this.reportId = loadReportId;