Merge branch 'scenario'

This commit is contained in:
Captain.B 2021-03-03 17:35:02 +08:00
commit f4dbf03580
19 changed files with 681 additions and 232 deletions

View File

@ -2,6 +2,7 @@ package io.metersphere.base.mapper.ext;
import io.metersphere.base.domain.LoadTest;
import io.metersphere.dto.LoadTestDTO;
import io.metersphere.dto.LoadTestFileDTO;
import io.metersphere.track.request.testplan.QueryTestPlanRequest;
import org.apache.ibatis.annotations.Param;
@ -16,4 +17,7 @@ public interface ExtLoadTestMapper {
int checkLoadTestOwner(@Param("testId") String testId, @Param("workspaceIds") Set<String> workspaceIds);
LoadTest getNextNum(@Param("projectId") String projectId);
List<LoadTestFileDTO> getProjectFiles(@Param("projectId") String projectId, @Param("loadTypes") List<String> loadType);
}

View File

@ -131,4 +131,16 @@
<select id="getNextNum" resultType="io.metersphere.base.domain.LoadTest">
select * from load_test lt where lt.project_id = #{projectId} ORDER BY num DESC LIMIT 1;
</select>
<select id="getProjectFiles" resultType="io.metersphere.dto.LoadTestFileDTO">
SELECT file_metadata.*, load_test.id as testId, load_test.name as testName
FROM load_test
JOIN load_test_file ON load_test.id = load_test_file.test_id
JOIN file_metadata ON load_test_file.file_id = file_metadata.id
WHERE file_metadata.type IN
<foreach collection="loadTypes" item="id" separator="," open="(" close=")">
#{id}
</foreach>
AND project_id = #{projectId,jdbcType=VARCHAR}
</select>
</mapper>

View File

@ -1,20 +1,24 @@
package io.metersphere.commons.utils;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
public class UrlTestUtils {
public static boolean testUrlWithTimeOut(String urlString, int timeOutMillSeconds) {
public static boolean testUrlWithTimeOut(String address, int timeOutMillSeconds) {
try {
URL url = new URL(urlString);
URLConnection co = url.openConnection();
co.setConnectTimeout(timeOutMillSeconds);
co.connect();
URL urlObj = new URL(address);
HttpURLConnection oc = (HttpURLConnection) urlObj.openConnection();
oc.setUseCaches(false);
oc.setConnectTimeout(timeOutMillSeconds); // 设置超时时间
int status = oc.getResponseCode();// 请求状态
if (200 == status) {
return true;
}
} catch (Exception e) {
LogUtil.error(e);
e.printStackTrace();
return false;
}
return false;
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.dto;
import io.metersphere.base.domain.FileMetadata;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LoadTestFileDTO extends FileMetadata {
private String testId;
private String testName;
}

View File

@ -12,6 +12,7 @@ import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.controller.request.QueryScheduleRequest;
import io.metersphere.dto.DashboardTestDTO;
import io.metersphere.dto.LoadTestDTO;
import io.metersphere.dto.LoadTestFileDTO;
import io.metersphere.dto.ScheduleDao;
import io.metersphere.performance.service.PerformanceTestService;
import io.metersphere.service.CheckPermissionService;
@ -112,6 +113,14 @@ public class PerformanceTestController {
return performanceTestService.getJmxContent(testId);
}
@GetMapping("/project/{loadType}/{projectId}/{goPage}/{pageSize}")
public Pager<List<LoadTestFileDTO>> getProjectFiles(@PathVariable String projectId, @PathVariable String loadType,
@PathVariable int goPage, @PathVariable int pageSize) {
checkPermissionService.checkProjectOwner(projectId);
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, performanceTestService.getProjectFiles(projectId, loadType));
}
@PostMapping("/delete")
public void delete(@RequestBody DeleteTestPlanRequest request) {
checkPermissionService.checkPerformanceTestOwner(request.getId());

View File

@ -9,6 +9,7 @@ import io.metersphere.config.KafkaProperties;
import io.metersphere.i18n.Translator;
import io.metersphere.performance.engine.EngineContext;
import io.metersphere.performance.parse.xml.reader.DocumentParser;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@ -639,6 +640,25 @@ public class JmeterDocumentParser implements DocumentParser {
((List<?>) holds).remove(0);
hold = o.toString();
}
Object deleteds = context.getProperty("deleted");
String deleted = "false";
if (deleteds instanceof List) {
Object o = ((List<?>) deleteds).get(0);
((List<?>) deleteds).remove(0);
deleted = o.toString();
}
Object enableds = context.getProperty("enabled");
String enabled = "true";
if (enableds instanceof List) {
Object o = ((List<?>) enableds).get(0);
((List<?>) enableds).remove(0);
enabled = o.toString();
}
threadGroup.setAttribute("enabled", enabled);
if (BooleanUtils.toBoolean(deleted)) {
threadGroup.setAttribute("enabled", "false");
}
Element elementProp = document.createElement("elementProp");
elementProp.setAttribute("name", "ThreadGroup.main_controller");
elementProp.setAttribute("elementType", "com.blazemeter.jmeter.control.VirtualUserController");

View File

@ -16,6 +16,7 @@ import io.metersphere.controller.request.OrderRequest;
import io.metersphere.controller.request.QueryScheduleRequest;
import io.metersphere.dto.DashboardTestDTO;
import io.metersphere.dto.LoadTestDTO;
import io.metersphere.dto.LoadTestFileDTO;
import io.metersphere.dto.ScheduleDao;
import io.metersphere.i18n.Translator;
import io.metersphere.job.sechedule.PerformanceTestJob;
@ -38,6 +39,7 @@ import javax.annotation.Resource;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@ -126,21 +128,54 @@ public class PerformanceTestService {
}
public String save(SaveTestPlanRequest request, List<MultipartFile> files) {
if (files == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
checkQuota(request, true);
final LoadTestWithBLOBs loadTest = saveLoadTest(request);
files.forEach(file -> {
final FileMetadata fileMetadata = fileService.saveFile(file);
LoadTestWithBLOBs loadTest = saveLoadTest(request);
// 新选择了一个文件删除原来的文件
List<FileMetadata> importFiles = request.getUpdatedFileList();
List<String> importFileIds = importFiles.stream().map(FileMetadata::getId).collect(Collectors.toList());
// 导入项目里其他的文件
this.importFiles(importFileIds, loadTest.getId());
// 保存上传的问题件
this.saveUploadFiles(files, loadTest.getId());
// 直接上传了jmx用于API导入的场景
String jmx = request.getJmx();
if (StringUtils.isNotBlank(jmx)) {
byte[] bytes = jmx.getBytes(StandardCharsets.UTF_8);
FileMetadata fileMetadata = fileService.saveFile(bytes, request.getName() + ".jmx", (long) bytes.length);
LoadTestFile loadTestFile = new LoadTestFile();
loadTestFile.setTestId(loadTest.getId());
loadTestFile.setFileId(fileMetadata.getId());
loadTestFileMapper.insert(loadTestFile);
});
}
return loadTest.getId();
}
private void saveUploadFiles(List<MultipartFile> files, String testId) {
if (files != null) {
files.forEach(file -> {
final FileMetadata fileMetadata = fileService.saveFile(file);
LoadTestFile loadTestFile = new LoadTestFile();
loadTestFile.setTestId(testId);
loadTestFile.setFileId(fileMetadata.getId());
loadTestFileMapper.insert(loadTestFile);
});
}
}
private void importFiles(List<String> importFileIds, String testId) {
importFileIds.forEach(fileId -> {
if (StringUtils.isBlank(fileId)) {
return;
}
FileMetadata fileMetadata = fileService.copyFile(fileId);
LoadTestFile loadTestFile = new LoadTestFile();
loadTestFile.setTestId(testId);
loadTestFile.setFileId(fileMetadata.getId());
loadTestFileMapper.insert(loadTestFile);
});
}
private LoadTestWithBLOBs saveLoadTest(SaveTestPlanRequest request) {
LoadTestExample example = new LoadTestExample();
@ -183,15 +218,19 @@ public class PerformanceTestService {
// 相减
List<String> deleteFileIds = ListUtils.subtract(originFileIds, updatedFileIds);
fileService.deleteFileByIds(deleteFileIds);
if (files != null) {
files.forEach(file -> {
final FileMetadata fileMetadata = fileService.saveFile(file);
// 导入项目里其他的文件
List<String> addFileIds = ListUtils.subtract(updatedFileIds, originFileIds);
this.importFiles(addFileIds, request.getId());
this.saveUploadFiles(files, request.getId());
// 直接上传了jmx用于API导入的场景
String jmx = request.getJmx();
if (StringUtils.isNotBlank(jmx)) {
byte[] bytes = jmx.getBytes(StandardCharsets.UTF_8);
FileMetadata fileMetadata = fileService.saveFile(bytes, request.getName() + ".jmx", (long) bytes.length);
LoadTestFile loadTestFile = new LoadTestFile();
loadTestFile.setTestId(request.getId());
loadTestFile.setFileId(fileMetadata.getId());
loadTestFileMapper.insert(loadTestFile);
});
}
loadTest.setName(request.getName());
@ -508,4 +547,14 @@ public class PerformanceTestService {
return Optional.of(loadTest.getNum() + 1).orElse(100001);
}
}
public List<LoadTestFileDTO> getProjectFiles(String projectId, String loadType) {
List<String> loadTypes = new ArrayList<>();
loadTypes.add(StringUtils.upperCase(loadType));
if (StringUtils.equalsIgnoreCase(loadType, "resource")) {
loadTypes.add(FileType.CSV.name());
loadTypes.add(FileType.JAR.name());
}
return extLoadTestMapper.getProjectFiles(projectId, loadTypes);
}
}

View File

@ -169,4 +169,10 @@ public class FileService {
public FileMetadata getFileMetadataById(String fileId) {
return fileMetadataMapper.selectByPrimaryKey(fileId);
}
public List<FileMetadata> getProjectJMXs(String projectId) {
FileMetadataExample example = new FileMetadataExample();
fileMetadataMapper.selectByExample(example);
return null;
}
}

View File

@ -1,4 +1,14 @@
package io.metersphere.track.request.testplan;
import io.metersphere.base.domain.FileMetadata;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Setter
@Getter
public class SaveTestPlanRequest extends TestPlanRequest {
private List<FileMetadata> updatedFileList;
}

View File

@ -16,7 +16,7 @@ public class TestPlanRequest {
private String description;
private String scenarioDefinition;
private String jmx;
private Long createTime;

View File

@ -8,7 +8,7 @@
<el-row>
<el-collapse v-model="activeNames">
<el-collapse-item :title="threadGroup.attributes.testname" :name="index"
v-for="(threadGroup, index) in threadGroups"
v-for="(threadGroup, index) in threadGroups.filter(th=>th.enabled === 'true' && th.deleted=='false')"
:key="index">
<el-col :span="10">
<el-form :inline="true">
@ -126,6 +126,8 @@ const RPS_LIMIT_ENABLE = "rpsLimitEnable";
const THREAD_TYPE = "threadType";
const ITERATE_NUM = "iterateNum";
const ITERATE_RAMP_UP = "iterateRampUpTime";
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)) + ','
@ -163,7 +165,6 @@ export default {
calculateLoadConfiguration: function (data) {
for (let i = 0; i < data.length; i++) {
let d = data[i];
if (d instanceof Array) {
d.forEach(item => {
switch (item.key) {
case TARGET_LEVEL:
@ -197,49 +198,18 @@ export default {
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;
default:
break;
}
})
this.calculateChart(this.threadGroups[i]);
} else {
switch (d.key) {
case TARGET_LEVEL:
this.threadGroups[0].threadNumber = d.value;
break;
case RAMP_UP:
this.threadGroups[0].rampUpTime = d.value;
break;
case ITERATE_RAMP_UP:
this.threadGroups[0].iterateRampUp = d.value;
break;
case DURATION:
if (d.unit) {
this.threadGroups[0].duration = d.value;
} else {
this.threadGroups[0].duration = d.value * 60;
}
break;
case STEPS:
this.threadGroups[0].step = d.value;
break;
case RPS_LIMIT:
this.threadGroups[0].rpsLimit = d.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[0].rpsLimitEnable = d.value;
break;
case THREAD_TYPE:
this.threadGroups[0].threadType = d.value;
break;
case ITERATE_NUM:
this.threadGroups[0].iterateNum = d.value;
break;
default:
break;
}
this.calculateChart(this.threadGroups[0]);
}
}
},
getLoadConfig() {
@ -307,6 +277,9 @@ export default {
for (let i = 0; i < handler.threadGroups.length; i++) {
if (handler.threadGroups[i].enabled === 'false' || handler.threadGroups[i].deleted === 'true') {
continue;
}
let seriesData = {
name: handler.threadGroups[i].attributes.testname,
data: [],

View File

@ -14,7 +14,7 @@
<el-card shadow="always" class="ms-card-index-2">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{ avgTransactions }}</span>
<span class="ms-card-data-unit"> Transactions/s</span>
<span class="ms-card-data-unit"> TPS</span>
</span>
<span class="ms-card-desc">Avg.Transactions</span>
</el-card>

View File

@ -8,7 +8,7 @@
class="input-with-select"
maxlength="30" show-word-limit
>
<template slot="prepend">测试名称</template>
<template slot="prepend">{{ $t('load_test.name') }}</template>
</el-input>
</el-col>
<el-col :span="12" :offset="2">
@ -32,6 +32,7 @@
</el-tab-pane>
<el-tab-pane :label="$t('load_test.pressure_config')">
<performance-pressure-config :is-read-only="isReadOnly" :test="test" :test-id="testId"
@fileChange="fileChange"
ref="pressureConfig" @changeActive="changeTabActive"/>
</el-tab-pane>
<el-tab-pane :label="$t('load_test.advanced_config')" class="advanced-config">
@ -305,6 +306,9 @@ export default {
this.$set(handler, "threadGroups", threadGroups);
this.$refs.basicConfig.threadGroups = threadGroups;
this.$refs.pressureConfig.threadGroups = threadGroups;
threadGroups.forEach(tg => {
handler.calculateChart(tg);
})
@ -317,7 +321,6 @@ export default {
.testplan-config {
margin-top: 15px;
text-align: center;
}
.el-select {

View File

@ -1,25 +1,89 @@
<template>
<div v-loading="result.loading">
<el-row type="flex" justify="space-between" align="middle">
<h4>{{ $t('load_test.scenario_list') }}</h4>
</el-row>
<el-row type="flex" justify="start" align="middle">
<el-upload
accept=".jmx,.csv,.jar"
drag
style="padding-right: 10px;"
accept=".jmx"
action=""
:limit="fileNumLimit"
multiple
:show-file-list="false"
:before-upload="beforeUpload"
:before-upload="beforeUploadJmx"
:http-request="handleUpload"
:on-exceed="handleExceed"
:disabled="isReadOnly"
:file-list="fileList">
<i class="el-icon-upload"/>
<div class="el-upload__text" v-html="$t('load_test.upload_tips')"></div>
<template v-slot:tip>
<div class="el-upload__tip">{{ $t('load_test.upload_type') }}</div>
<ms-table-button :is-tester-permission="true" icon="el-icon-upload2"
:content="$t('load_test.upload_jmx')"/>
</el-upload>
<ms-table-button :is-tester-permission="true" icon="el-icon-circle-plus-outline"
:content="$t('load_test.load_exist_jmx')" @click="loadJMX()"/>
<ms-table-button :is-tester-permission="true" icon="el-icon-share"
@click="loadApiAutomation()"
:content="$t('load_test.load_api_automation_jmx')"/>
</el-row>
<el-table class="basic-config" :data="threadGroups.filter(tg=>tg.deleted=='false')">
<el-table-column
:label="$t('load_test.scenario_name')">
<template v-slot:default="{row}">
{{ row.attributes.testname }}
</template>
</el-table-column>
<el-table-column
label="Enable/Disable">
<template v-slot:default="{row}">
<el-switch v-model="row.enabled"
inactive-color="#DCDFE6"
active-value="true"
inactive-value="false"
:disabled="threadGroupDisable(row)"
/>
</template>
</el-table-column>
<el-table-column
label="ThreadGroup">
<template v-slot:default="{row}">
{{ row.name.substring(row.name.lastIndexOf(".") + 1) }}
</template>
</el-table-column>
<el-table-column
:label="$t('commons.operating')">
<template v-slot:default="{row}">
<el-button :disabled="isReadOnly || threadGroupDisable(row)"
@click="handleDeleteThreadGroup(row)"
type="danger"
icon="el-icon-delete" size="mini"
circle/>
</template>
</el-table-column>
</el-table>
<el-row type="flex" justify="space-between" align="middle">
<h4>{{ $t('load_test.other_resource') }}</h4>
</el-row>
<el-row type="flex" justify="start" align="middle">
<el-upload
style="padding-right: 10px;"
accept=".jar,.csv"
action=""
:limit="fileNumLimit"
multiple
:show-file-list="false"
:before-upload="beforeUploadFile"
:http-request="handleUpload"
:on-exceed="handleExceed"
:disabled="isReadOnly"
:file-list="fileList">
<ms-table-button :is-tester-permission="true" icon="el-icon-upload2"
:content="$t('load_test.upload_file')"/>
</el-upload>
<el-table class="basic-config" :data="tableData">
<ms-table-button :is-tester-permission="true" icon="el-icon-circle-plus-outline"
:content="$t('load_test.load_exist_file')" @click="loadFile()"/>
</el-row>
<el-table class="basic-config" :data="tableData.filter(f => !f.name.toUpperCase().endsWith('.JMX'))">
<el-table-column
prop="name"
:label="$t('load_test.file_name')">
@ -51,15 +115,80 @@
</template>
</el-table-column>
</el-table>
<el-dialog :title="$t('load_test.exist_jmx')" width="70%" :visible.sync="loadFileVisible">
<el-table class="basic-config" :data="existFiles" v-loading="projectLoadingResult.loading">
<el-table-column
prop="testName"
:label="$t('load_test.test')">
</el-table-column>
<el-table-column
prop="name"
:label="$t('load_test.file_name')">
</el-table-column>
<el-table-column
prop="type"
:label="$t('load_test.file_type')">
</el-table-column>
<el-table-column
:label="$t('load_test.last_modify_time')">
<template v-slot:default="scope">
<i class="el-icon-time"/>
<span class="last-modified">{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column
:label="$t('commons.operating')">
<template v-slot:default="scope">
<ms-table-operator-button :is-tester-permission="true"
:tip="$t('api_test.api_import.label')"
icon="el-icon-upload"
@exec="handleImport(scope.row)"/>
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="getProjectFiles" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-dialog>
<el-dialog :title="$t('load_test.scenario_list')" width="60%" :visible.sync="loadApiAutomationVisible">
<el-table class="basic-config" :data="apiScenarios" v-loading="projectLoadingResult.loading">
<el-table-column
prop="num"
label="ID">
</el-table-column>
<el-table-column
prop="name"
:label="$t('load_test.scenario_name')">
</el-table-column>
<el-table-column
:label="$t('commons.operating')">
<template v-slot:default="scope">
<ms-table-operator-button :is-tester-permission="true"
:tip="$t('api_test.api_import.label')"
icon="el-icon-upload"
@exec="handleImportApi(scope.row)"/>
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="getProjectFiles" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-dialog>
</div>
</template>
<script>
import {Message} from "element-ui";
import {findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
import {findTestPlan, findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
import MsTableButton from "@/business/components/common/components/MsTableButton";
import {getCurrentProjectID} from "@/common/js/utils";
import MsTablePagination from "@/business/components/common/pagination/TablePagination";
import MsTableOperatorButton from "@/business/components/common/components/MsTableOperatorButton";
export default {
name: "PerformanceBasicConfig",
components: {MsTableOperatorButton, MsTablePagination, MsTableButton},
props: {
test: {
type: Object
@ -72,6 +201,7 @@ export default {
data() {
return {
result: {},
projectLoadingResult: {},
getFileMetadataPath: "/performance/file/metadata",
jmxDownloadPath: '/performance/file/download',
jmxDeletePath: '/performance/file/delete',
@ -79,6 +209,15 @@ export default {
tableData: [],
uploadList: [],
fileNumLimit: 10,
threadGroups: [],
loadFileVisible: false,
currentPage: 1,
pageSize: 5,
total: 0,
existFiles: [],
loadType: 'jmx',
apiScenarios: [],
loadApiAutomationVisible: false,
};
},
created() {
@ -98,9 +237,9 @@ export default {
if (fileList.length > 0) {
let file = fileList[0];
let jmxReader = new FileReader();
jmxReader.onload = function (event) {
let threadGroups = findThreadGroup(event.target.result);
self.$emit('fileChange', threadGroups);
jmxReader.onload = (event) => {
self.threadGroups = findThreadGroup(event.target.result);
self.$emit('fileChange', self.threadGroups);
};
jmxReader.readAsText(file);
}
@ -113,11 +252,11 @@ export default {
this.uploadList = [];
this.result = this.$get(this.getFileMetadataPath + "/" + test.id, response => {
let files = response.data;
if (!files) {
Message.error({message: this.$t('load_test.related_file_not_found'), showClose: true});
return;
}
console.log(files);
// deep copy
this.fileList = JSON.parse(JSON.stringify(files));
this.tableData = JSON.parse(JSON.stringify(files));
@ -126,12 +265,40 @@ export default {
});
})
},
beforeUpload(file) {
deleteExistJmx: function () {
// jmx
let jmxs = this.tableData.filter(f => {
let type = f.name.substring(f.name.lastIndexOf(".") + 1);
return type.toUpperCase() === 'JMX';
});
for (let i = 0; i < jmxs.length; i++) {
let index = this.tableData.indexOf(jmxs[i]);
if (index > -1) {
this.tableData.splice(index, 1);
}
let index2 = this.uploadList.indexOf(jmxs[i]);
if (index2 > -1) {
this.uploadList.splice(index2, 1);
}
}
jmxs = this.fileList.filter(f => {
let type = f.name.substring(f.name.lastIndexOf(".") + 1);
return type.toUpperCase() === 'JMX';
});
for (let i = 0; i < jmxs.length; i++) {
let index3 = this.fileList.indexOf(jmxs[i]);
if (index3 > -1) {
this.fileList.splice(index3, 1);
}
}
},
beforeUploadJmx(file) {
if (!this.fileValidator(file)) {
/// todo:
return false;
}
this.deleteExistJmx();
if (this.tableData.filter(f => f.name === file.name).length > 0) {
this.$error(this.$t('load_test.delete_file'));
return false;
@ -141,7 +308,28 @@ export default {
this.tableData.push({
name: file.name,
size: file.size + ' Bytes', /// todo: ByteKBMB
size: (file.size / 1024).toFixed(2) + ' KB',
type: type.toUpperCase(),
updateTime: file.lastModified,
});
return true;
},
beforeUploadFile(file) {
if (!this.fileValidator(file)) {
/// todo:
return false;
}
if (this.tableData.filter(f => f.name === file.name).length > 0) {
this.$error(this.$t('load_test.delete_file'));
return false;
}
let type = file.name.substring(file.name.lastIndexOf(".") + 1);
this.tableData.push({
name: file.name,
size: (file.size / 1024).toFixed(2) + ' KB',
type: type.toUpperCase(),
updateTime: file.lastModified,
});
@ -181,25 +369,116 @@ export default {
Message.error({message: e.message, showClose: true});
});
},
handleDelete(file, index) {
handleImport(row) {
if (this.tableData.filter(f => f.name === row.name).length > 0) {
this.$error(this.$t('load_test.delete_file'));
return;
}
if (this.loadType === 'resource') {
this.fileList.push(row);
this.tableData.push(row);
this.$success(this.$t('test_track.case.import.success'));
this.loadFileVisible = false;
return;
}
this.result = this.$get('/performance/get-jmx-content/' + row.testId, (response) => {
if (response.data) {
let testPlan = findTestPlan(response.data);
testPlan.elements.forEach(e => {
if (e.attributes.name === 'TestPlan.serialize_threadgroups') {
this.serializeThreadgroups = Boolean(e.elements[0].text);
}
});
this.threadGroups = findThreadGroup(response.data);
this.threadGroups.forEach(tg => {
tg.options = {};
});
this.$emit('fileChange', this.threadGroups);
}
this.deleteExistJmx();
this.fileList.push(row);
this.tableData.push(row);
this.$success(this.$t('test_track.case.import.success'));
this.loadFileVisible = false;
});
},
countStrToBit(str) {
let count = 0
const arr = str.split('')
arr.forEach(item => {
count += Math.ceil(item.charCodeAt().toString(2).length / 8)
})
return count
},
handleImportApi(row) {
let condition = {
projectId: getCurrentProjectID(),
ids: [row.id]
};
this.projectLoadingResult = this.$post('api/automation/export/jmx', condition, response => {
let data = response.data[0];
this.threadGroups = findThreadGroup(data.jmx);
this.threadGroups.forEach(tg => {
tg.options = {};
});
this.$emit('fileChange', this.threadGroups);
this.deleteExistJmx();
let bytes = this.countStrToBit(data.jmx);
this.fileList.push({
name: this.test.name + ".jmx",
size: bytes,
type: "JMX",
updateTime: new Date().getTime(),
});
this.tableData.push({
name: this.test.name + ".jmx",
size: (bytes / 1024).toFixed(2) + ' KB',
type: "JMX",
updateTime: new Date().getTime(),
});
this.test.jmx = data.jmx;
this.$success(this.$t('test_track.case.import.success'));
this.loadApiAutomationVisible = false;
})
},
handleDelete(file) {
this.$alert(this.$t('load_test.delete_file_confirm') + file.name + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this._handleDelete(file, index);
this._handleDelete(file);
}
}
});
},
_handleDelete(file, index) {
_handleDelete(file) {
let index = this.fileList.findIndex(f => f.name === file.name);
if (index > -1) {
this.fileList.splice(index, 1);
}
index = this.tableData.findIndex(f => f.name === file.name);
if (index > -1) {
this.tableData.splice(index, 1);
}
//
let i = this.uploadList.findIndex(upLoadFile => upLoadFile.name === file.name);
if (i > -1) {
this.uploadList.splice(i, 1);
}
},
handleDeleteThreadGroup(tg) {
this.$alert(this.$t('load_test.delete_threadgroup_confirm') + tg.attributes.testname + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
tg.deleted = 'true';
}
}
});
},
threadGroupDisable(row) {
return this.threadGroups.filter(tg => tg.enabled == 'true').length === 1 && row.enabled == 'true';
},
handleExceed() {
this.$error(this.$t('load_test.file_size_limit'));
},
@ -210,6 +489,39 @@ export default {
updatedFileList() {
return this.fileList;//
},
loadJMX() {
this.loadFileVisible = true;
this.loadType = "jmx";
this.getProjectFiles();
},
loadFile() {
this.loadFileVisible = true;
this.loadType = "resource";
this.getProjectFiles();
},
loadApiAutomation() {
this.loadApiAutomationVisible = true;
this.getProjectScenarios();
},
getProjectFiles() {
this.projectLoadingResult = this.$get('/performance/project/' + this.loadType + '/' + getCurrentProjectID() + "/" + this.currentPage + "/" + this.pageSize, res => {
let data = res.data;
this.total = data.itemCount;
this.existFiles = data.listObject;
})
},
getProjectScenarios() {
let condition = {
projectId: getCurrentProjectID(),
filters: {status: ["Prepare", "Underway", "Completed"]}
}
this.projectLoadingResult = this.$post('/api/automation/list/' + this.currentPage + "/" + this.pageSize, condition, res => {
let data = res.data;
this.total = data.itemCount;
this.apiScenarios = data.listObject;
})
},
validConfig() {
let newJmxNum = 0, oldJmxNum = 0, newCsvNum = 0, oldCsvNum = 0, newJarNum = 0, oldJarNum = 0;
if (this.uploadList.length > 0) {
@ -246,6 +558,10 @@ export default {
this.$error(this.$t('load_test.jmx_is_null'));
return false;
}
if (this.threadGroups.filter(tg => tg.attributes.enabled == 'true').length === 0) {
this.$error(this.$t('load_test.threadgroup_at_least_one'));
return false;
}
return true;
}
},
@ -260,4 +576,8 @@ export default {
.last-modified {
margin-left: 5px;
}
.el-dialog >>> .el-dialog__body {
padding: 10px 20px;
}
</style>

View File

@ -20,7 +20,7 @@
<el-row>
<el-collapse v-model="activeNames">
<el-collapse-item :title="threadGroup.attributes.testname" :name="index"
v-for="(threadGroup, index) in threadGroups"
v-for="(threadGroup, index) in threadGroups.filter(th=>th.enabled === 'true' && th.deleted=='false')"
:key="index">
<el-col :span="10">
<el-form :inline="true">
@ -140,6 +140,8 @@ 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)) + ','
@ -221,7 +223,6 @@ export default {
let data = JSON.parse(response.data);
for (let i = 0; i < data.length; i++) {
let d = data[i];
if (d instanceof Array) {
d.forEach(item => {
switch (item.key) {
case TARGET_LEVEL:
@ -255,6 +256,12 @@ export default {
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;
default:
break;
}
@ -262,49 +269,11 @@ export default {
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.calculateChart(this.threadGroups[i]);
} else {
switch (d.key) {
case TARGET_LEVEL:
this.threadGroups[0].threadNumber = d.value;
break;
case RAMP_UP:
this.threadGroups[0].rampUpTime = d.value;
break;
case ITERATE_RAMP_UP:
this.threadGroups[0].iterateRampUp = d.value;
break;
case DURATION:
if (d.unit) {
this.threadGroups[0].duration = d.value;
} else {
this.threadGroups[0].duration = d.value * 60;
}
break;
case STEPS:
this.threadGroups[0].step = d.value;
break;
case RPS_LIMIT:
this.threadGroups[0].rpsLimit = d.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[0].rpsLimitEnable = d.value;
break;
case THREAD_TYPE:
this.threadGroups[0].threadType = d.value;
break;
case ITERATE_NUM:
this.threadGroups[0].iterateNum = d.value;
break;
default:
break;
}
this.$set(this.threadGroups[0], "threadType", this.threadGroups[0].threadType || 'DURATION');
this.$set(this.threadGroups[0], "iterateNum", this.threadGroups[0].iterateNum || 1);
this.$set(this.threadGroups[0], "iterateRampUp", this.threadGroups[0].iterateRampUp || 10);
this.calculateChart(this.threadGroups[0]);
}
}
this.calculateTotalChart();
}
@ -324,6 +293,7 @@ export default {
this.threadGroups.forEach(tg => {
tg.options = {};
});
this.$emit('fileChange', this.threadGroups);
this.getLoadConfig();
}
});
@ -355,6 +325,9 @@ export default {
};
for (let i = 0; i < handler.threadGroups.length; i++) {
if (handler.threadGroups[i].enabled === 'false' || handler.threadGroups[i].deleted === 'true') {
continue;
}
let seriesData = {
name: handler.threadGroups[i].attributes.testname,
data: [],
@ -560,6 +533,8 @@ export default {
{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},
]);
}
return result;

View File

@ -5,8 +5,17 @@ let travel = function (elements, threadGroups) {
return;
}
for (let element of elements) {
if (element.name === 'ThreadGroup') {
switch (element.name) {
case "ThreadGroup":
case "kg.apc.jmeter.threads.UltimateThreadGroup":
case "com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup":
case "com.blazemeter.jmeter.threads.arrivals.FreeFormArrivalsThreadGroup":
case "com.blazemeter.jmeter.threads.arrivals.ArrivalsThreadGroup":
case "com.octoperf.jmeter.OctoPerfThreadGroup":
threadGroups.push(element);
break;
default:
break;
}
travel(element.elements, threadGroups)
}
@ -16,6 +25,10 @@ export function findThreadGroup(jmxContent) {
let jmxJson = JSON.parse(xml2json(jmxContent));
let threadGroups = [];
travel(jmxJson.elements, threadGroups);
threadGroups.forEach(tg => {
tg.deleted = 'false';
tg.enabled = tg.attributes.enabled;
})
return threadGroups;
}

View File

@ -423,6 +423,8 @@ export default {
delete_batch_confirm: 'Confirm batch delete report',
},
load_test: {
test: 'Test',
name: 'Test Name',
same_project_test: 'Only tests within the same project can be run',
run: 'One click operation',
operating: 'Operating',
@ -491,7 +493,18 @@ export default {
user_name: 'Creator',
special_characters_are_not_supported: 'Test name does not support special characters',
pressure_config_params_is_empty: 'Pressure configuration parameters cannot be empty!',
schedule_tip: 'The interval must not be less than the pressure measuring time'
schedule_tip: 'The interval must not be less than the pressure measuring time',
delete_threadgroup_confirm: 'Confirm delete scenario: ',
scenario_list: 'Scenario List',
scenario_name: 'Scenario Name',
upload_jmx: 'Upload JMX',
exist_jmx: 'Existed Files',
other_resource: 'Other Files',
upload_file: 'Upload Files',
load_exist_file: 'Load Project Files',
load_exist_jmx: 'Load Project JMX',
threadgroup_at_least_one: 'At least one ThreadGroup is enabled',
load_api_automation_jmx: 'Import API automation scenario',
},
api_test: {
creator: "Creator",
@ -608,7 +621,7 @@ export default {
edit_time_Reverse_order: "From back to front by update time",
request_method: "Request method",
request_interface: "Request interface",
search_by_api_name : "Search by api name",
search_by_api_name: "Search by api name",
request_info: "Request info",
request_head: "Request head",
request_param: "Param",
@ -618,7 +631,7 @@ export default {
response_head: "Response head",
response_body: "Response body",
response_code: "Response code",
table_coloum:{
table_coloum: {
name: "name",
value: "value",
is_required: "Is it required",

View File

@ -421,6 +421,8 @@ export default {
delete_batch_confirm: '确认批量删除报告',
},
load_test: {
test: '测试',
name: '测试名称',
same_project_test: '只能运行同一项目内的测试',
already_exists: '测试名称不能重复',
operating: '操作',
@ -490,7 +492,18 @@ export default {
user_name: '创建人',
special_characters_are_not_supported: '测试名称不支持特殊字符',
pressure_config_params_is_empty: '压力配置参数不能为空!',
schedule_tip: '间隔时间不能小于压测时长'
schedule_tip: '间隔时间不能小于压测时长',
delete_threadgroup_confirm: '确认删除场景',
scenario_list: '场景列表',
scenario_name: "场景名称",
upload_jmx: '上传 JMX 文件',
exist_jmx: '已存在的文件',
other_resource: '其他资源',
upload_file: '上传新文件',
load_exist_file: '加载已有文件',
load_exist_jmx: '加载已有 JMX 文件',
threadgroup_at_least_one: '至少启用一个线程组',
load_api_automation_jmx: '引用接口自动化场景',
},
api_test: {
creator: "创建人",
@ -609,7 +622,7 @@ export default {
edit_time_Reverse_order: "按更新时间从后到前",
request_method: "请求方式",
request_interface: "请求接口",
search_by_api_name : "名称搜索",
search_by_api_name: "名称搜索",
request_info: "请求信息",
request_head: "请求头",
request_param: "参数",
@ -619,7 +632,7 @@ export default {
response_head: "响应头",
response_body: "响应体",
response_code: "响应码",
table_coloum:{
table_coloum: {
name: "名称",
value: "值",
is_required: "是否必填",

View File

@ -421,6 +421,8 @@ export default {
delete_batch_confirm: '確認批量刪除報告',
},
load_test: {
test: '測試',
name: '測試名稱',
same_project_test: '只能運行同壹項目內的測試',
already_exists: '測試名稱不能重復',
operating: '操作',
@ -490,7 +492,18 @@ export default {
user_name: '創建人',
special_characters_are_not_supported: '測試名稱不支持特殊字符',
pressure_config_params_is_empty: '壓力配置參數不能為空!',
schedule_tip: '間隔時間不能小於壓測時長'
schedule_tip: '間隔時間不能小於壓測時長',
delete_threadgroup_confirm: '確認刪除場景: ',
scenario_list: '場景列表',
scenario_name: '場景名稱',
upload_jmx: '上傳 JMX文件',
exist_jmx: '已存在的文件',
other_resource: '其他資源',
upload_file: '上傳新文件',
load_exist_file: '加載已有文件',
load_exist_jmx: '加載已有 JMX 文件',
threadgroup_at_least_one: '至少啟用一個線程組',
load_api_automation_jmx: '引用接口自動化場景',
},
api_test: {
creator: "創建人",
@ -608,7 +621,7 @@ export default {
edit_time_Reverse_order: "按更新時間從後到前",
request_method: "請求方式",
request_interface: "請求接口e",
search_by_api_name : "API名稱搜索",
search_by_api_name: "API名稱搜索",
request_info: "請求信息",
request_head: "請求頭",
request_param: "參數",
@ -618,7 +631,7 @@ export default {
response_head: "響應頭",
response_body: "響應體",
response_code: "響應碼",
table_coloum:{
table_coloum: {
name: "名稱",
value: "值",
is_required: "是否必填",