修改多个脚本并行运行的处理方式

This commit is contained in:
q4speed 2020-05-22 16:45:08 +08:00
parent 4896115c1c
commit a5bc32ff57
8 changed files with 111 additions and 143 deletions

View File

@ -20,64 +20,59 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public class APIBackendListenerClient extends AbstractBackendListenerClient implements Serializable { public class APIBackendListenerClient extends AbstractBackendListenerClient implements Serializable {
// 与前端JMXGenerator的SPLIT对应用于获取 测试名称 测试ID private final static String THREAD_SPLIT = " ";
private final static String SPLIT = "@@:";
// 测试ID作为key private final static String ID_SPLIT = "-";
private final Map<String, List<SampleResult>> queue = new ConcurrentHashMap<>();
private final static String TEST_ID = "id";
private final List<SampleResult> queue = new ArrayList<>();
private APITestService apiTestService;
private APIReportService apiReportService;
// 测试ID
private String id;
@Override
public void setupTest(BackendListenerContext context) throws Exception {
this.id = context.getParameter(TEST_ID);
apiTestService = CommonBeanFactory.getBean(APITestService.class);
if (apiTestService == null) {
LogUtil.error("apiTestService is required");
}
apiReportService = CommonBeanFactory.getBean(APIReportService.class);
if (apiReportService == null) {
LogUtil.error("apiReportService is required");
}
}
@Override @Override
public void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context) { public void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context) {
sampleResults.forEach(result -> { queue.addAll(sampleResults);
// 将不同的测试脚本按测试ID分开
String label = result.getSampleLabel();
if (!label.contains(SPLIT)) {
LogUtil.error("request name format is invalid, name: " + label);
return;
}
String name = label.split(SPLIT)[0];
String testId = label.split(SPLIT)[1];
if (!queue.containsKey(testId)) {
List<SampleResult> testResults = new ArrayList<>();
queue.put(testId, testResults);
}
result.setSampleLabel(name);
queue.get(testId).add(result);
});
} }
@Override @Override
public void teardownTest(BackendListenerContext context) throws Exception { public void teardownTest(BackendListenerContext context) throws Exception {
APITestService apiTestService = CommonBeanFactory.getBean(APITestService.class);
if (apiTestService == null) {
LogUtil.error("apiTestService is required");
return;
}
APIReportService apiReportService = CommonBeanFactory.getBean(APIReportService.class);
if (apiReportService == null) {
LogUtil.error("apiReportService is required");
return;
}
queue.forEach((id, sampleResults) -> {
TestResult testResult = new TestResult(); TestResult testResult = new TestResult();
testResult.setTestId(id); testResult.setTestId(id);
testResult.setTotal(sampleResults.size()); testResult.setTotal(queue.size());
// key: 场景Id // 一个脚本里可能包含多个场景(ThreadGroup)所以要区分开key: 场景Id
final Map<String, ScenarioResult> scenarios = new LinkedHashMap<>(); final Map<String, ScenarioResult> scenarios = new LinkedHashMap<>();
sampleResults.forEach(result -> { queue.forEach(result -> {
String thread = StringUtils.substringBeforeLast(result.getThreadName(), " "); // 线程名称: <场景名> <场景Index>-<请求Index>, 例如Scenario 2-1
String order = StringUtils.substringAfterLast(result.getThreadName(), " "); String scenarioName = StringUtils.substringBeforeLast(result.getThreadName(), THREAD_SPLIT);
String scenarioName = StringUtils.substringBefore(thread, SPLIT); String index = StringUtils.substringAfterLast(result.getThreadName(), THREAD_SPLIT);
String scenarioId = StringUtils.substringAfter(thread, SPLIT); String scenarioId = StringUtils.substringBefore(index, ID_SPLIT);
ScenarioResult scenarioResult; ScenarioResult scenarioResult;
if (!scenarios.containsKey(scenarioId)) { if (!scenarios.containsKey(scenarioId)) {
scenarioResult = new ScenarioResult(); scenarioResult = new ScenarioResult();
scenarioResult.setId(scenarioId); scenarioResult.setId(scenarioId);
scenarioResult.setName(scenarioName); scenarioResult.setName(scenarioName);
scenarioResult.setOrder(StringUtils.substringBefore(order, "-"));
scenarios.put(scenarioId, scenarioResult); scenarios.put(scenarioId, scenarioResult);
} else { } else {
scenarioResult = scenarios.get(scenarioId); scenarioResult = scenarios.get(scenarioId);
@ -101,11 +96,12 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
scenarioResult.addPassAssertions(requestResult.getPassAssertions()); scenarioResult.addPassAssertions(requestResult.getPassAssertions());
scenarioResult.addTotalAssertions(requestResult.getTotalAssertions()); scenarioResult.addTotalAssertions(requestResult.getTotalAssertions());
}); });
testResult.getScenarios().addAll(scenarios.values()); testResult.getScenarios().addAll(scenarios.values());
testResult.getScenarios().sort(Comparator.comparing(ScenarioResult::getOrder)); testResult.getScenarios().sort(Comparator.comparing(ScenarioResult::getId));
apiTestService.changeStatus(id, APITestStatus.Completed); apiTestService.changeStatus(id, APITestStatus.Completed);
apiReportService.complete(testResult); apiReportService.complete(testResult);
});
queue.clear(); queue.clear();
super.teardownTest(context); super.teardownTest(context);
} }

View File

@ -12,8 +12,6 @@ public class ScenarioResult {
private String name; private String name;
private String order;
private long responseTime; private long responseTime;
private int error = 0; private int error = 0;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="container" v-loading="result.loading"> <div class="container" v-loading="loading">
<div class="main-content"> <div class="main-content">
<el-card> <el-card>
<section class="report-container" v-if="this.report.testId"> <section class="report-container" v-if="this.report.testId">
@ -53,20 +53,23 @@
return { return {
content: {}, content: {},
report: {}, report: {},
result: {}, loading: true
} }
}, },
methods: { methods: {
getReport() { getReport() {
this.loading = true;
this.report = {}; this.report = {};
this.content = {}; this.content = {};
if (this.reportId) { if (this.reportId) {
let url = "/api/report/get/" + this.reportId; let url = "/api/report/get/" + this.reportId;
this.result = this.$get(url, response => { this.$get(url, response => {
this.report = response.data || {}; this.report = response.data || {};
if (this.isCompleted) { if (this.isCompleted) {
this.content = JSON.parse(this.report.content); this.content = JSON.parse(this.report.content);
this.loading = false;
} else { } else {
setTimeout(this.getReport, 2000) setTimeout(this.getReport, 2000)
} }

View File

@ -114,9 +114,11 @@
saveTest: function () { saveTest: function () {
this.save(() => { this.save(() => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t('commons.save_success'));
if (this.create) {
this.$router.push({ this.$router.push({
path: '/api/test/edit?id=' + this.test.id path: '/api/test/edit?id=' + this.test.id
}) })
}
}) })
}, },
runTest: function () { runTest: function () {

View File

@ -65,7 +65,7 @@
}, },
copyRequest: function (index) { copyRequest: function (index) {
let request = this.requests[index]; let request = this.requests[index];
this.requests.push(request.clone()); this.requests.push(new Request(request));
}, },
deleteRequest: function (index) { deleteRequest: function (index) {
this.requests.splice(index, 1); this.requests.splice(index, 1);

View File

@ -81,7 +81,7 @@
}, },
copyScenario: function (index) { copyScenario: function (index) {
let scenario = this.scenarios[index]; let scenario = this.scenarios[index];
this.scenarios.push(scenario.clone()); this.scenarios.push(new Scenario(scenario));
}, },
deleteScenario: function (index) { deleteScenario: function (index) {
this.scenarios.splice(index, 1); this.scenarios.splice(index, 1);

View File

@ -163,7 +163,7 @@ export class DefaultTestElement extends TestElement {
super(tag, { super(tag, {
guiclass: guiclass, guiclass: guiclass,
testclass: testclass, testclass: testclass,
testname: testname || tag + ' Name', testname: testname === undefined ? tag + ' Name' : testname,
enabled: enabled || true enabled: enabled || true
}); });
} }
@ -171,7 +171,7 @@ export class DefaultTestElement extends TestElement {
export class TestPlan extends DefaultTestElement { export class TestPlan extends DefaultTestElement {
constructor(testName, props) { constructor(testName, props) {
super('TestPlan', 'TestPlanGui', 'TestPlan', testName || 'TestPlan'); super('TestPlan', 'TestPlanGui', 'TestPlan', testName);
props = props || {}; props = props || {};
this.boolProp("TestPlan.functional_mode", props.mode || false); this.boolProp("TestPlan.functional_mode", props.mode || false);
@ -212,7 +212,7 @@ export class ThreadGroup extends DefaultTestElement {
export class PostThreadGroup extends DefaultTestElement { export class PostThreadGroup extends DefaultTestElement {
constructor(testName, props) { constructor(testName, props) {
super('PostThreadGroup', 'PostThreadGroupGui', 'PostThreadGroup', testName || 'tearDown Thread Group'); super('PostThreadGroup', 'PostThreadGroupGui', 'PostThreadGroup', testName);
props = props || {}; props = props || {};
this.intProp("ThreadGroup.num_threads", props.threads || 1); this.intProp("ThreadGroup.num_threads", props.threads || 1);
@ -238,7 +238,7 @@ export class PostThreadGroup extends DefaultTestElement {
export class DebugSampler extends DefaultTestElement { export class DebugSampler extends DefaultTestElement {
constructor(testName) { constructor(testName) {
super('DebugSampler', 'TestBeanGUI', 'DebugSampler', testName || 'Debug Sampler'); super('DebugSampler', 'TestBeanGUI', 'DebugSampler', testName);
this.boolProp("displayJMeterProperties", false); this.boolProp("displayJMeterProperties", false);
this.boolProp("displayJMeterVariables", true); this.boolProp("displayJMeterVariables", true);
@ -248,7 +248,7 @@ export class DebugSampler extends DefaultTestElement {
export class HTTPSamplerProxy extends DefaultTestElement { export class HTTPSamplerProxy extends DefaultTestElement {
constructor(testName, request) { constructor(testName, request) {
super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName || 'HTTP Request'); super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName);
this.request = request || {}; this.request = request || {};
this.stringProp("HTTPSampler.domain", this.request.hostname); this.stringProp("HTTPSampler.domain", this.request.hostname);
@ -292,7 +292,7 @@ export class HTTPSamplerArguments extends Element {
export class DurationAssertion extends DefaultTestElement { export class DurationAssertion extends DefaultTestElement {
constructor(testName, duration) { constructor(testName, duration) {
super('DurationAssertion', 'DurationAssertionGui', 'DurationAssertion', testName || 'Duration Assertion'); super('DurationAssertion', 'DurationAssertionGui', 'DurationAssertion', testName);
this.duration = duration || 0; this.duration = duration || 0;
this.stringProp('DurationAssertion.duration', this.duration); this.stringProp('DurationAssertion.duration', this.duration);
} }
@ -300,7 +300,7 @@ export class DurationAssertion extends DefaultTestElement {
export class ResponseAssertion extends DefaultTestElement { export class ResponseAssertion extends DefaultTestElement {
constructor(testName, assertion) { constructor(testName, assertion) {
super('ResponseAssertion', 'AssertionGui', 'ResponseAssertion', testName || 'Response Assertion'); super('ResponseAssertion', 'AssertionGui', 'ResponseAssertion', testName);
this.assertion = assertion || {}; this.assertion = assertion || {};
this.stringProp('Assertion.test_field', this.assertion.field); this.stringProp('Assertion.test_field', this.assertion.field);
@ -352,7 +352,7 @@ export class ResponseHeadersAssertion extends ResponseAssertion {
export class HeaderManager extends DefaultTestElement { export class HeaderManager extends DefaultTestElement {
constructor(testName, headers) { constructor(testName, headers) {
super('HeaderManager', 'HeaderPanel', 'HeaderManager', testName || 'HTTP Header manager'); super('HeaderManager', 'HeaderPanel', 'HeaderManager', testName);
this.headers = headers || []; this.headers = headers || [];
let collectionProp = this.collectionProp('HeaderManager.headers'); let collectionProp = this.collectionProp('HeaderManager.headers');
@ -366,7 +366,7 @@ export class HeaderManager extends DefaultTestElement {
export class Arguments extends DefaultTestElement { export class Arguments extends DefaultTestElement {
constructor(testName, args) { constructor(testName, args) {
super('Arguments', 'ArgumentsPanel', 'Arguments', testName || 'User Defined Variables'); super('Arguments', 'ArgumentsPanel', 'Arguments', testName);
this.args = args || []; this.args = args || [];
let collectionProp = this.collectionProp('Arguments.arguments'); let collectionProp = this.collectionProp('Arguments.arguments');
@ -382,7 +382,7 @@ export class Arguments extends DefaultTestElement {
export class BackendListener extends DefaultTestElement { export class BackendListener extends DefaultTestElement {
constructor(testName, className, args) { constructor(testName, className, args) {
super('BackendListener', 'BackendListenerGui', 'BackendListener', testName || 'Backend Listener'); super('BackendListener', 'BackendListenerGui', 'BackendListener', testName);
this.stringProp('classname', className); this.stringProp('classname', className);
if (args && args.length > 0) { if (args && args.length > 0) {
this.add(new ElementArguments(args)); this.add(new ElementArguments(args));
@ -415,7 +415,7 @@ export class ElementArguments extends Element {
export class RegexExtractor extends DefaultTestElement { export class RegexExtractor extends DefaultTestElement {
constructor(testName, props) { constructor(testName, props) {
super('RegexExtractor', 'RegexExtractorGui', 'RegexExtractor', testName || 'Regular Expression Extractor'); super('RegexExtractor', 'RegexExtractorGui', 'RegexExtractor', testName);
this.props = props || {} this.props = props || {}
this.stringProp('RegexExtractor.useHeaders', props.headers); this.stringProp('RegexExtractor.useHeaders', props.headers);
this.stringProp('RegexExtractor.refname', props.name); this.stringProp('RegexExtractor.refname', props.name);
@ -428,7 +428,7 @@ export class RegexExtractor extends DefaultTestElement {
export class JSONPostProcessor extends DefaultTestElement { export class JSONPostProcessor extends DefaultTestElement {
constructor(testName, props) { constructor(testName, props) {
super('JSONPostProcessor', 'JSONPostProcessorGui', 'JSONPostProcessor', testName || 'JSON Extractor'); super('JSONPostProcessor', 'JSONPostProcessorGui', 'JSONPostProcessor', testName);
this.props = props || {} this.props = props || {}
this.stringProp('JSONPostProcessor.referenceNames', props.name); this.stringProp('JSONPostProcessor.referenceNames', props.name);
this.stringProp('JSONPostProcessor.jsonPathExprs', props.expression); this.stringProp('JSONPostProcessor.jsonPathExprs', props.expression);
@ -438,7 +438,7 @@ export class JSONPostProcessor extends DefaultTestElement {
export class XPath2Extractor extends DefaultTestElement { export class XPath2Extractor extends DefaultTestElement {
constructor(testName, props) { constructor(testName, props) {
super('XPath2Extractor', 'XPath2ExtractorGui', 'XPath2Extractor', testName || 'XPath2 Extractor'); super('XPath2Extractor', 'XPath2ExtractorGui', 'XPath2Extractor', testName);
this.props = props || {} this.props = props || {}
this.stringProp('XPathExtractor2.default', props.default); this.stringProp('XPathExtractor2.default', props.default);
this.stringProp('XPathExtractor2.refname', props.name); this.stringProp('XPathExtractor2.refname', props.name);

View File

@ -5,8 +5,6 @@ import {
TestElement, TestElement,
TestPlan, TestPlan,
ThreadGroup, ThreadGroup,
PostThreadGroup,
DebugSampler,
HeaderManager, HeaderManager,
HTTPSamplerArguments, HTTPSamplerArguments,
ResponseCodeAssertion, ResponseCodeAssertion,
@ -121,7 +119,6 @@ export class Test extends BaseConfig {
export class Scenario extends BaseConfig { export class Scenario extends BaseConfig {
constructor(options) { constructor(options) {
super(); super();
this.id = uuid();
this.name = undefined; this.name = undefined;
this.url = undefined; this.url = undefined;
this.variables = []; this.variables = [];
@ -137,22 +134,11 @@ export class Scenario extends BaseConfig {
options.requests = options.requests || [new Request()]; options.requests = options.requests || [new Request()];
return options; return options;
} }
clone() {
let scenario = new Scenario(this);
scenario.id = uuid();
scenario.requests.forEach(function (request) {
request.id = uuid();
});
return scenario;
}
} }
export class Request extends BaseConfig { export class Request extends BaseConfig {
constructor(options) { constructor(options) {
super(); super();
this.id = uuid();
this.name = undefined; this.name = undefined;
this.url = undefined; this.url = undefined;
this.method = undefined; this.method = undefined;
@ -178,12 +164,6 @@ export class Request extends BaseConfig {
isValid() { isValid() {
return !!this.url && !!this.method return !!this.url && !!this.method
} }
clone() {
let request = new Request(this);
request.id = uuid();
return request;
}
} }
export class Body extends BaseConfig { export class Body extends BaseConfig {
@ -402,17 +382,19 @@ class JMeterTestPlan extends Element {
class JMXGenerator { class JMXGenerator {
constructor(test) { constructor(test) {
if (!test || !(test instanceof Test)) return undefined; if (!test || !test.id || !(test instanceof Test)) return undefined;
if (!test.id) {
test.id = "#NULL_TEST_ID#";
}
const SPLIT = "@@:";
let testPlan = new TestPlan(test.name); let testPlan = new TestPlan(test.name);
test.scenarioDefinition.forEach(scenario => { this.addScenarios(testPlan, test.scenarioDefinition);
let testName = scenario.name ? scenario.name + SPLIT + scenario.id : SPLIT + scenario.id; this.addBackendListener(testPlan, test.id);
let threadGroup = new ThreadGroup(testName);
this.jmeterTestPlan = new JMeterTestPlan();
this.jmeterTestPlan.put(testPlan);
}
addScenarios(testPlan, scenarios) {
scenarios.forEach(scenario => {
let threadGroup = new ThreadGroup(scenario.name || "");
this.addScenarioVariables(threadGroup, scenario); this.addScenarioVariables(threadGroup, scenario);
@ -421,9 +403,7 @@ class JMXGenerator {
scenario.requests.forEach(request => { scenario.requests.forEach(request => {
if (!request.isValid()) return; if (!request.isValid()) return;
// test.id用于处理结果时区分属于哪个测试 let httpSamplerProxy = new HTTPSamplerProxy(request.name || "", new JMXRequest(request));
let name = request.name ? request.name + SPLIT + test.id : SPLIT + test.id;
let httpSamplerProxy = new HTTPSamplerProxy(name, new JMXRequest(request));
this.addRequestHeader(httpSamplerProxy, request); this.addRequestHeader(httpSamplerProxy, request);
@ -440,19 +420,14 @@ class JMXGenerator {
threadGroup.put(httpSamplerProxy); threadGroup.put(httpSamplerProxy);
}) })
this.addBackendListener(threadGroup);
testPlan.put(threadGroup); testPlan.put(threadGroup);
// 暂时不加
// let tearDownThreadGroup = new PostThreadGroup();
// tearDownThreadGroup.put(new DebugSampler(test.id));
// this.addBackendListener(tearDownThreadGroup);
//
// testPlan.put(tearDownThreadGroup);
}) })
}
this.jmeterTestPlan = new JMeterTestPlan(); addBackendListener(testPlan, testId) {
this.jmeterTestPlan.put(testPlan); let className = 'io.metersphere.api.jmeter.APIBackendListenerClient';
let args = [new KeyValue("id", testId)];
testPlan.put(new BackendListener(testId, className, args));
} }
addScenarioVariables(threadGroup, scenario) { addScenarioVariables(threadGroup, scenario) {
@ -567,12 +542,6 @@ class JMXGenerator {
} }
} }
addBackendListener(threadGroup) {
let testName = 'API Backend Listener';
let className = 'io.metersphere.api.jmeter.APIBackendListenerClient';
threadGroup.put(new BackendListener(testName, className));
}
filter(config) { filter(config) {
return config.isValid(); return config.isValid();
} }