feat(性能测试): 性能测试高级配置增加选项

--story=1013936 --user=宋天阳 【性能测试】高级配置增加可配置项
https://www.tapd.cn/55049933/s/1443207
This commit is contained in:
song-tianyang 2023-12-13 12:12:52 +08:00 committed by 刘瑞斌
parent 6235a20af8
commit c30dac746f
14 changed files with 301 additions and 22 deletions

View File

@ -1327,6 +1327,14 @@ const message = {
"The file already exists in the project, please import it directly",
project_file_update_type_error: "Updated file types must be consistent",
csv_has_header: "Contains Title",
csv_file_end_recycle: "Recycle when file end",
csv_file_end_stop_thread: "Stop thread when file end",
csv_file_end_stop_thread_option: {
all: "All thread",
group: "Thread group",
thread: "Thread",
},
thread_share: "Thread share",
csv_split: "CSV Split",
on_sample_error: "Sampler error",
continue: "Continue",

View File

@ -1284,6 +1284,14 @@ const message = {
load_api_automation_jmx: "引用接口自动化场景",
project_file_exist: "项目中已存在该文件,请直接引用",
csv_has_header: "包含表头",
csv_file_end_recycle: "遇到文件结束符再次循环",
csv_file_end_stop_thread: "遇到文件结束符停止线程",
csv_file_end_stop_thread_option: {
all: "所有线程",
group: "当前线程组",
thread: "当前线程",
},
thread_share: "线程共享模式",
csv_split: "CSV分割",
on_sample_error: "取样器错误后",
continue: "继续",

View File

@ -1283,6 +1283,14 @@ const message = {
load_api_automation_jmx: "引用接口自動化場景",
project_file_exist: "項目中已存在該文件,請直接引用",
csv_has_header: "包含表頭",
csv_file_end_recycle: "遇到文件結束符再次循環",
csv_file_end_stop_thread: "遇到文件結束符停止線程",
csv_file_end_stop_thread_option: {
all: "所有線程",
group: "當前線程組",
thread: "當前線程",
},
thread_share: "線程共享模式",
csv_split: "CSV分割",
on_sample_error: "取樣器錯誤後",
continue: "繼續",

View File

@ -10,11 +10,13 @@ import io.metersphere.jmeter.utils.ScriptEngineUtils;
import io.metersphere.parse.EngineSourceParser;
import io.metersphere.parse.EngineSourceParserFactory;
import io.metersphere.service.BaseTestResourcePoolService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.tree.BaseElement;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@ -41,11 +43,24 @@ public class JmeterDocumentParser implements EngineSourceParser {
private final static String HTTP_SAMPLER_PROXY = "HTTPSamplerProxy";
private final static String CSV_DATA_SET = "CSVDataSet";
private final static String THREAD_GROUP_AUTO_STOP = "io.metersphere.jmeter.reporters.ThreadGroupAutoStop";
private final static List<String> SCRIPTS = new ArrayList<>() {{
add("JSR223PreProcessor");
add("JSR223PostProcessor");
add("JSR223Sampler");
add("JSR223Assertion");
add("JSR223Timer");
add("JSR223Listener");
}};
private EngineContext context;
private boolean cacheScript = false;
@Override
public byte[] parse(EngineContext context, InputStream source) throws Exception {
this.context = context;
Object cacheScriptObj = context.getProperty("cacheScript");
if (cacheScriptObj != null) {
cacheScript = BooleanUtils.toBoolean(cacheScriptObj.toString());
}
Document document = EngineSourceParserFactory.getDocument(source);
@ -56,7 +71,6 @@ public class JmeterDocumentParser implements EngineSourceParser {
// jmeterTestPlan的子元素肯定是<hashTree></hashTree>
parseHashTree(ele);
}
return EngineSourceParserFactory.getBytes(document);
}
@ -110,12 +124,31 @@ public class JmeterDocumentParser implements EngineSourceParser {
} else if (ele.getName().endsWith(RESULT_COLLECTOR)) {
// 处理结果收集器性能测试不需要这些
processResultCollector(ele);
} else if (CollectionUtils.containsAny(SCRIPTS, ele.getName())) {
this.elementCacheScript(ele);
}
}
}
}
private void elementCacheScript(Element element) {
List<Element> childNodes = element.elements();
Element cacheKeyNode = null;
for (Element item : childNodes) {
if (nodeNameEquals(item, STRING_PROP) && StringUtils.equalsIgnoreCase(item.attributeValue("name"), "cacheKey")) {
cacheKeyNode = item;
break;
}
}
if (cacheKeyNode == null) {
cacheKeyNode = new BaseElement(STRING_PROP);
cacheKeyNode.addAttribute("name", "cacheKey");
element.add(cacheKeyNode);
}
cacheKeyNode.setText(cacheScript ? "true" : "false");
}
private void processThreadType(Element ele) {
Object threadType = context.getProperty("threadType");
if (threadType instanceof List) {
@ -238,20 +271,101 @@ public class JmeterDocumentParser implements EngineSourceParser {
private void processCsvDataSet(Element element) {
List<Element> childNodes = element.elements();
String fileName = null;
for (Element item : childNodes) {
if (nodeNameEquals(item, STRING_PROP)) {
String filenameTag = item.attributeValue("name");
if (StringUtils.equals(filenameTag, "filename")) {
// 截取文件名
handleFilename(item);
// 切割CSV文件
splitCsvFile(item);
if (StringUtils.equals(item.attributeValue("name"), "filename")) {
fileName = item.getText();
String separator = "/";
if (!StringUtils.contains(fileName, "/")) {
separator = "\\";
}
fileName = fileName.substring(fileName.lastIndexOf(separator) + 1);
break;
}
}
}
Element recycleNode = null;
Element stopThreadNode = null;
Element shareModeNode = null;
for (Element item : childNodes) {
String filenameTag = item.attributeValue("name");
if (nodeNameEquals(item, STRING_PROP)) {
if (StringUtils.equals(filenameTag, "filename")) {
// 截取文件名
handleFilename(item);
splitCsvFile(item);
// 切割CSV文件
} else if (StringUtils.equalsIgnoreCase(filenameTag, "shareMode")) {
shareModeNode = item;
}
} else if (nodeNameEquals(item, BOOL_PROP)) {
if (StringUtils.equalsIgnoreCase(filenameTag, "recycle")) {
recycleNode = item;
} else if (StringUtils.equalsIgnoreCase(filenameTag, "stopThread")) {
stopThreadNode = item;
}
}
}
if (stopThreadNode == null) {
stopThreadNode = new BaseElement(BOOL_PROP);
stopThreadNode.addAttribute("name", "stopThread");
element.add(stopThreadNode);
}
if (recycleNode == null) {
recycleNode = new BaseElement(BOOL_PROP);
recycleNode.addAttribute("name", "recycle");
element.add(recycleNode);
}
if (shareModeNode == null) {
shareModeNode = new BaseElement(STRING_PROP);
shareModeNode.addAttribute("name", "shareMode");
element.add(shareModeNode);
}
setShareMode(shareModeNode, fileName);
setRecycle(recycleNode, fileName);
setStopThread(stopThreadNode, fileName);
}
private void setShareMode(Node item, String fileName) {
Object csvConfig = context.getProperty("csvConfig");
if (csvConfig == null) {
return;
}
Object config = ((Map) csvConfig).get(fileName);
if (config != null) {
String shareMode = String.valueOf(((Map) (config)).get("shareMode"));
item.setText(shareMode);
}
}
private void setRecycle(Node item, String fileName) {
Object csvConfig = context.getProperty("csvConfig");
if (csvConfig == null) {
return;
}
Object config = ((Map) csvConfig).get(fileName);
if (config != null) {
Boolean recycle = (Boolean) ((Map) (config)).get("recycle");
item.setText(String.valueOf(BooleanUtils.toBoolean(recycle)));
}
}
private void setStopThread(Node item, String fileName) {
Object csvConfig = context.getProperty("csvConfig");
if (csvConfig == null) {
return;
}
Object config = ((Map) csvConfig).get(fileName);
if (config != null) {
Boolean recycle = (Boolean) ((Map) (config)).get("stopThread");
item.setText(String.valueOf(BooleanUtils.toBoolean(recycle)));
}
}
private void splitCsvFile(Node item) {
String filename = item.getText();
filename = StringUtils.removeStart(filename, "/test/");

View File

@ -37,7 +37,14 @@ export default {
});
let csvFiles = [];
for (const f of csvSet) {
csvFiles.push({name: f, csvSplit: false, csvHasHeader: true});
csvFiles.push({
name: f,
csvSplit: false,
csvHasHeader: true,
recycle: true,
stopThread: false,
shareMode: "shareMode.thread"
});
}
this.$refs.advancedConfig.csvFiles = csvFiles;

View File

@ -162,7 +162,14 @@ export default {
});
let csvFiles = [];
for (const f of csvSet) {
csvFiles.push({name: f, csvSplit: false, csvHasHeader: true});
csvFiles.push({
name: f,
csvSplit: false,
csvHasHeader: true,
recycle: true,
stopThread: false,
shareMode: "shareMode.thread"
});
}
this.$set(handler, "threadGroups", threadGroups);
@ -196,7 +203,14 @@ export default {
});
let csvFiles = [];
for (const f of csvSet) {
csvFiles.push({name: f, csvSplit: false, csvHasHeader: true});
csvFiles.push({
name: f,
csvSplit: false,
csvHasHeader: true,
recycle: true,
stopThread: false,
shareMode: "shareMode.thread"
});
}
this.$set(handler, "threadGroups", threadGroups);

View File

@ -673,7 +673,14 @@ export default {
});
let csvFiles = [];
for (const f of csvSet) {
csvFiles.push({name: f, csvSplit: false, csvHasHeader: true});
csvFiles.push({
name: f,
csvSplit: false,
csvHasHeader: true,
recycle: true,
stopThread: false,
shareMode: "shareMode.thread"
});
}
this.$set(handler, "threadGroups", threadGroups);
@ -681,7 +688,6 @@ export default {
this.$refs.basicConfig.threadGroups = threadGroups;
this.$refs.pressureConfig.threadGroups = threadGroups;
this.$refs.advancedConfig.csvFiles = csvFiles;
this.$refs.pressureConfig.resourcePoolChange();
handler.calculateTotalChart();
},

View File

@ -2,7 +2,7 @@
<div>
<!-- 基本配置 -->
<el-row>
<el-col :span="6">
<el-col :span="5">
<el-form :inline="true" :disabled="isReadOnly">
<el-form-item>
<div>{{ $t('load_test.connect_timeout') }}</div>
@ -18,7 +18,7 @@
</el-form-item>
</el-form>
</el-col>
<el-col :span="6">
<el-col :span="5">
<el-form :inline="true" :disabled="isReadOnly">
<el-form-item>
<div>{{ $t('load_test.response_timeout') }}</div>
@ -34,7 +34,7 @@
</el-form-item>
</el-form>
</el-col>
<el-col :span="6">
<el-col :span="5">
<el-form :inline="true" :disabled="isReadOnly">
<el-form-item>
<div>
@ -64,7 +64,7 @@
</el-form-item>
</el-form>
</el-col>
<el-col :span="6">
<el-col :span="5">
<el-form :inline="true" :disabled="isReadOnly">
<el-form-item>
<div>{{ $t('load_test.custom_http_code') }}</div>
@ -77,6 +77,13 @@
</el-form-item>
</el-form>
</el-col>
<el-col :span="4">
<el-form :disabled="isReadOnly" :inline="true">
<el-form-item :label="$t('performance_test.cache_script')">
<el-checkbox v-model="cacheScript"/>
</el-form-item>
</el-form>
</el-col>
</el-row>
<!-- DNS -->
@ -177,6 +184,30 @@
<el-switch :disabled="isReadOnly || !row.csvSplit" v-model="row.csvHasHeader"/>
</template>
</el-table-column>
<el-table-column :label="$t('load_test.csv_file_end_recycle')" align="center" prop="recycle">
<template v-slot:default="{row}">
<el-switch v-model="row.recycle" :disabled="isReadOnly"/>
</template>
</el-table-column>
<el-table-column :label="$t('load_test.csv_file_end_stop_thread')" align="center" prop="stopThread">
<template v-slot:default="{row}">
<el-switch v-model="row.stopThread" :disabled="isReadOnly"/>
</template>
</el-table-column>
<el-table-column :label="$t('load_test.thread_share')" align="center" prop="shareMode">
<template v-slot:default="{row}">
<el-select v-model="row.shareMode" :disabled="isReadOnly">
<el-option key="shareMode.all" :label="$t('load_test.csv_file_end_stop_thread_option.all')"
value="shareMode.all"></el-option>
<el-option key="shareMode.group" :label="$t('load_test.csv_file_end_stop_thread_option.group')"
value="shareMode.group"></el-option>
<el-option key="shareMode.thread" :label="$t('load_test.csv_file_end_stop_thread_option.thread')"
value="shareMode.thread"></el-option>
</el-select>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
@ -481,6 +512,7 @@ export default {
csvFiles: [],
csvConfig: [],
statusCodeStr: '',
cacheScript: false,
granularity: undefined,
granularityData: [
{start: 0, end: 100, granularity: 1},
@ -523,7 +555,11 @@ export default {
}
},
csvFiles() {
this.refreshCsv();
if (this.testId) {
this.getAdvancedConfig();
} else if (this.reportId) {
this.getAdvancedConfig('report');
}
}
},
methods: {
@ -538,6 +574,7 @@ export default {
this.domains = data.domains || [];
this.params = data.params || [];
this.granularity = data.granularity;
this.cacheScript = data.cacheScript;
this.monitorParams = data.monitorParams || [];
this.properties = data.properties || [];
this.systemProperties = data.systemProperties || [];
@ -548,8 +585,23 @@ export default {
refreshCsv() {
if (this.csvConfig && this.csvFiles) {
this.csvFiles.forEach(f => {
f.csvSplit = this.csvConfig[f.name]?.csvSplit;
f.csvSplit = this.csvConfig[f.name]?.csvSplit
f.csvHasHeader = this.csvConfig[f.name]?.csvHasHeader;
f.recycle = this.csvConfig[f.name]?.recycle;
f.stopThread = this.csvConfig[f.name]?.stopThread;
f.shareMode = this.csvConfig[f.name]?.shareMode;
if (f.csvHasHeader === undefined) {
f.csvHasHeader = true;
}
if (f.stopThread === undefined) {
f.stopThread = false;
}
if (f.recycle === undefined) {
f.recycle = true;
}
if (f.shareMode === undefined) {
f.shareMode = "shareMode.thread";
}
});
}
},
@ -661,11 +713,18 @@ export default {
properties: this.properties,
systemProperties: this.systemProperties,
csvConfig: this.csvFiles.reduce((result, curr) => {
result[curr.name] = {csvHasHeader: curr.csvHasHeader, csvSplit: curr.csvSplit};
result[curr.name] = {
csvHasHeader: curr.csvHasHeader,
csvSplit: curr.csvSplit,
recycle: curr.recycle,
stopThread: curr.stopThread,
shareMode: curr.shareMode
};
return result;
}, {}),
domains: this.domains,
granularity: this.granularity,
cacheScript: this.cacheScript,
monitorParams: this.monitorParams
};
},

View File

@ -15,6 +15,7 @@ const message = {
error_samples: 'Error samples',
all_samples: 'All samples',
response_3_samples: 'The first three pieces of data',
cache_script: 'Cache script',
}
}
export default {

View File

@ -15,6 +15,7 @@ const message = {
error_samples: '错误请求',
all_samples: '所有请求',
response_3_samples: '默认抽样前3个请求的响应数据',
cache_script: '缓存编译脚本',
}
}

View File

@ -15,6 +15,7 @@ const message = {
error_samples: '錯誤請求',
all_samples: '所有請求',
response_3_samples: '默认抽样前3个请求的响应数据',
cache_script: '緩存編譯腳本',
}
}

View File

@ -37,7 +37,14 @@ export default {
});
let csvFiles = [];
for (const f of csvSet) {
csvFiles.push({name: f, csvSplit: false, csvHasHeader: true});
csvFiles.push({
name: f,
csvSplit: false,
csvHasHeader: true,
recycle: true,
stopThread: false,
shareMode: "shareMode.thread"
});
}
this.$refs.advancedConfig.csvFiles = csvFiles;

View File

@ -175,6 +175,30 @@
<el-switch :disabled="readOnly || !row.csvSplit" v-model="row.csvHasHeader"/>
</template>
</el-table-column>
<el-table-column :label="$t('load_test.csv_file_end_recycle')" align="center" prop="recycle">
<template v-slot:default="{row}">
<el-switch v-model="row.recycle" :disabled="isReadOnly"/>
</template>
</el-table-column>
<el-table-column :label="$t('load_test.csv_file_end_stop_thread')" align="center" prop="stopThread">
<template v-slot:default="{row}">
<el-switch v-model="row.stopThread" :disabled="isReadOnly"/>
</template>
</el-table-column>
<el-table-column :label="$t('load_test.thread_share')" align="center" prop="shareMode">
<template v-slot:default="{row}">
<el-select v-model="row.shareMode" :disabled="isReadOnly">
<el-option key="shareMode.all" :label="$t('load_test.csv_file_end_stop_thread_option.all')"
value="shareMode.all"></el-option>
<el-option key="shareMode.group" :label="$t('load_test.csv_file_end_stop_thread_option.group')"
value="shareMode.group"></el-option>
<el-option key="shareMode.thread" :label="$t('load_test.csv_file_end_stop_thread_option.thread')"
value="shareMode.thread"></el-option>
</el-select>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
@ -439,6 +463,21 @@ export default {
this.csvFiles.forEach(f => {
f.csvSplit = this.csvConfig[f.name]?.csvSplit;
f.csvHasHeader = this.csvConfig[f.name]?.csvHasHeader;
f.recycle = this.csvConfig[f.name]?.recycle;
f.stopThread = this.csvConfig[f.name]?.stopThread;
f.shareMode = this.csvConfig[f.name]?.shareMode;
if (f.csvHasHeader === undefined) {
f.csvHasHeader = true;
}
if (f.stopThread === undefined) {
f.stopThread = false;
}
if (f.recycle === undefined) {
f.recycle = true;
}
if (f.shareMode === undefined) {
f.shareMode = "shareMode.thread";
}
});
}
}
@ -561,7 +600,13 @@ export default {
params: this.params,
properties: this.properties,
csvConfig: this.csvFiles.reduce((result, curr) => {
result[curr.name] = {csvHasHeader: curr.csvHasHeader, csvSplit: curr.csvSplit};
result[curr.name] = {
csvHasHeader: curr.csvHasHeader,
csvSplit: curr.csvSplit,
recycle: curr.recycle,
stopThread: curr.stopThread,
shareMode: curr.shareMode
};
return result;
}, {}),
domains: this.domains,

View File

@ -63,7 +63,7 @@ export default {
});
let csvFiles = [];
for (const f of csvSet) {
csvFiles.push({ name: f, csvSplit: false, csvHasHeader: true });
csvFiles.push({ name: f, csvSplit: false, csvHasHeader: true, recycle: true, stopThread: false, shareMode: "shareMode.thread" });
}
if (this.$refs.advancedConfig) {
this.$refs.advancedConfig.csvFiles = csvFiles;