feat(接口测试): 增加逻辑控制器

This commit is contained in:
q4speed 2020-09-17 18:04:51 +08:00
parent 84c891419f
commit 4e9a2f084f
16 changed files with 452 additions and 28 deletions

View File

@ -0,0 +1,13 @@
package io.metersphere.api.dto.scenario.controller;
import lombok.Data;
@Data
public class IfController {
private String type;
private String id;
private Boolean enable;
private String variable;
private String operator;
private String value;
}

View File

@ -5,6 +5,7 @@ import com.alibaba.fastjson.annotation.JSONType;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.controller.IfController;
import io.metersphere.api.dto.scenario.extract.Extract;
import io.metersphere.api.dto.scenario.processor.BeanShellPostProcessor;
import io.metersphere.api.dto.scenario.processor.BeanShellPreProcessor;
@ -13,6 +14,7 @@ import io.metersphere.api.dto.scenario.processor.JSR223PreProcessor;
import io.metersphere.api.dto.scenario.request.dubbo.ConfigCenter;
import io.metersphere.api.dto.scenario.request.dubbo.ConsumerAndService;
import io.metersphere.api.dto.scenario.request.dubbo.RegistryCenter;
import io.metersphere.api.dto.scenario.timer.ConstantTimer;
import lombok.Data;
import java.util.List;
@ -59,4 +61,8 @@ public class DubboRequest implements Request {
private JSR223PreProcessor jsr223PreProcessor;
@JSONField(ordinal = 16)
private JSR223PostProcessor jsr223PostProcessor;
@JSONField(ordinal = 17)
private IfController controller;
@JSONField(ordinal = 18)
private ConstantTimer timer;
}

View File

@ -5,11 +5,13 @@ import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.scenario.Body;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.controller.IfController;
import io.metersphere.api.dto.scenario.extract.Extract;
import io.metersphere.api.dto.scenario.processor.BeanShellPostProcessor;
import io.metersphere.api.dto.scenario.processor.BeanShellPreProcessor;
import io.metersphere.api.dto.scenario.processor.JSR223PostProcessor;
import io.metersphere.api.dto.scenario.processor.JSR223PreProcessor;
import io.metersphere.api.dto.scenario.timer.ConstantTimer;
import lombok.Data;
import java.util.List;
@ -57,4 +59,8 @@ public class HttpRequest implements Request {
private JSR223PreProcessor jsr223PreProcessor;
@JSONField(ordinal = 18)
private JSR223PostProcessor jsr223PostProcessor;
@JSONField(ordinal = 19)
private IfController controller;
@JSONField(ordinal = 20)
private ConstantTimer timer;
}

View File

@ -0,0 +1,11 @@
package io.metersphere.api.dto.scenario.timer;
import lombok.Data;
@Data
public class ConstantTimer {
private String type;
private String id;
private Boolean enable;
private String delay;
}

@ -1 +1 @@
Subproject commit d5b4969642fd8d10cc2f949d7377e0a0e5217a3a
Subproject commit b72b002a16d72cb3674d4d4dc4e48027693fdf7e

View File

@ -292,7 +292,8 @@ export default {
});
},
cancel() {
this.$router.push('/api/test/list/all');
console.log(this.test.toJMX().xml)
// this.$router.push('/api/test/list/all');
},
handleCommand(command) {
switch (command) {

View File

@ -4,6 +4,7 @@
:disabled="isReference">
<div class="request-item" v-for="(request, index) in this.scenario.requests" :key="index" @click="select(request)"
:class="{'selected': isSelected(request), 'disable-request': !request.enable || !scenario.enable}">
<ms-condition-label :request="request"/>
<el-row type="flex" align="middle">
<div class="request-type">
{{ request.showType() }}
@ -32,6 +33,12 @@
:command="{type: 'enable', index: index}">
{{ $t('api_test.scenario.enable') }}
</el-dropdown-item>
<el-dropdown-item :disabled="isReadOnly" :command="{type: 'controller', index: index}">
{{ $t('api_test.request.condition') }}
</el-dropdown-item>
<el-dropdown-item :disabled="isReadOnly" :command="{type: 'wait', index: index}">
{{ $t('api_test.request.wait') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
@ -46,18 +53,22 @@
<el-button slot="reference" :disabled="isReadOnly"
class="request-create" type="primary" size="mini" icon="el-icon-plus" plain/>
</el-popover>
<ms-if-controller ref="controller"/>
<ms-constant-timer ref="timer"/>
</div>
</template>
<script>
import {RequestFactory} from "../../model/ScenarioModel";
import draggable from 'vuedraggable';
import MsIfController from "@/business/components/api/test/components/request/condition/IfController";
import MsConstantTimer from "@/business/components/api/test/components/request/condition/ConstantTimer";
import MsConditionLabel from "@/business/components/api/test/components/request/condition/ConditionLabel";
export default {
name: "MsApiRequestConfig",
components: {draggable},
components: {MsConditionLabel, MsConstantTimer, MsIfController, draggable},
props: {
scenario: Object,
@ -88,7 +99,7 @@ export default {
},
methods: {
createRequest: function (type) {
createRequest(type) {
let request = new RequestFactory({type: type});
if (this.scenario.environmentId) {
request.useEnvironment = true;
@ -97,23 +108,31 @@ export default {
this.type = "";
this.visible = false;
},
copyRequest: function (index) {
copyRequest(index) {
let request = this.scenario.requests[index];
this.scenario.requests.push(new RequestFactory(request));
},
disableRequest: function (index) {
disableRequest(index) {
this.scenario.requests[index].enable = false;
},
enableRequest: function (index) {
enableRequest(index) {
this.scenario.requests[index].enable = true;
},
deleteRequest: function (index) {
deleteRequest(index) {
this.scenario.requests.splice(index, 1);
if (this.scenario.requests.length === 0) {
this.createRequest();
}
},
handleCommand: function (command) {
addController(index) {
let request = this.scenario.requests[index];
this.$refs.controller.open(request);
},
addTimer(index) {
let request = this.scenario.requests[index];
this.$refs.timer.open(request);
},
handleCommand(command) {
switch (command.type) {
case "copy":
this.copyRequest(command.index);
@ -127,9 +146,15 @@ export default {
case "enable":
this.enableRequest(command.index);
break;
case "controller":
this.addController(command.index);
break;
case "wait":
this.addTimer(command.index);
break;
}
},
select: function (request) {
select(request) {
request.environment = this.scenario.environment;
if (!request.useEnvironment) {
request.useEnvironment = false;
@ -149,7 +174,6 @@ export default {
<style scoped>
.request-item {
border-left: 5px solid #1E90FF;
max-height: 40px;
border-top: 1px solid #EBEEF5;
cursor: pointer;
}

View File

@ -0,0 +1,75 @@
<template>
<div>
<div>
<el-button size="mini" @click="openController"
class="condition"
:class="controllerClass"
v-if="request.controller.isValid()">
<el-row type="flex" align="middle">
<font-awesome-icon :icon="['fas', 'random']"/>
<div class="condition-label">{{ request.controller.label() }}</div>
</el-row>
</el-button>
</div>
<div>
<el-button size="mini" @click="openTimer"
class="condition"
:class="timerClass"
v-if="request.timer.isValid()">
<el-row type="flex" align="middle">
<font-awesome-icon :icon="['fas', 'clock']"/>
<div class="condition-label">{{ request.timer.label() }}</div>
</el-row>
</el-button>
</div>
<ms-if-controller ref="controller"/>
<ms-constant-timer ref="timer"/>
</div>
</template>
<script>
import {Request} from "@/business/components/api/test/model/ScenarioModel";
import MsIfController from "@/business/components/api/test/components/request/condition/IfController";
import MsConstantTimer from "@/business/components/api/test/components/request/condition/ConstantTimer";
export default {
name: "MsConditionLabel",
components: {MsConstantTimer, MsIfController},
props: {
request: Request
},
methods: {
openController() {
this.$refs.controller.open(this.request);
},
openTimer() {
this.$refs.timer.open(this.request);
}
},
computed: {
controllerClass() {
let disabled = this.request.controller.enable === false;
return {'is-disabled': disabled, 'click-cursor': disabled}
},
timerClass() {
let disabled = this.request.timer.enable === false;
return {'is-disabled': disabled, 'click-cursor': disabled}
},
}
}
</script>
<style scoped>
.condition {
width: 100%;
}
.condition-label {
padding-left: 5px;
}
.click-cursor {
cursor: pointer !important;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<el-dialog :visible.sync="visible" width="600px" @close="close">
<el-row :gutter="10" type="flex" align="middle">
<el-col :span="2">{{ $t('api_test.request.wait') }}</el-col>
<el-col :span="10">
<el-input-number class="width-100" size="small" v-model="timer.delay" :min="0" :step="1000"/>
</el-col>
<el-col :span="2">ms</el-col>
<el-col :span="8">
<el-switch v-model="timer.enable" :inactive-text="$t('api_test.scenario.enable_disable')"/>
</el-col>
<el-col :span="2">
<el-button size="mini" type="danger" icon="el-icon-delete" circle @click="remove"/>
</el-col>
</el-row>
</el-dialog>
</template>
<script>
import {ConstantTimer} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsConstantTimer",
data() {
return {
request: {},
timer: new ConstantTimer(),
visible: false,
}
},
methods: {
open(request) {
this.request = request;
this.timer = new ConstantTimer(request.timer);
this.visible = true;
},
close() {
this.request.timer = this.timer
this.visible = false;
},
remove() {
this.timer = new ConstantTimer();
this.visible = false;
}
}
}
</script>
<style scoped>
.width-100 {
width: 100%
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<el-dialog :visible.sync="visible" width="800px" @close="close">
<el-row :gutter="10" type="flex" align="middle">
<el-col :span="1">If</el-col>
<el-col :span="6">
<el-input size="small" v-model="controller.variable" :placeholder="$t('api_test.request.condition_variable')"/>
</el-col>
<el-col :span="5">
<el-select v-model="controller.operator" :placeholder="$t('commons.please_select')" size="small">
<el-option v-for="o in operators" :key="o.value" :label="$t(o.label)" :value="o.value"/>
</el-select>
</el-col>
<el-col :span="6">
<el-input size="small" v-model="controller.value" :placeholder="$t('api_test.value')"/>
</el-col>
<el-col :span="4">
<el-switch v-model="controller.enable" :inactive-text="$t('api_test.scenario.enable_disable')"/>
</el-col>
<el-col :span="2">
<el-button size="mini" type="danger" icon="el-icon-delete" circle @click="remove"/>
</el-col>
</el-row>
</el-dialog>
</template>
<script>
import {IfController} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsIfController",
data() {
return {
request: {},
controller: new IfController(),
visible: false,
operators: {
EQ: {
label: "commons.adv_search.operators.equals",
value: "=="
},
NE: {
label: "commons.adv_search.operators.not_equals",
value: "!="
},
LIKE: {
label: "commons.adv_search.operators.like",
value: "=~"
},
NOT_LIKE: {
label: "commons.adv_search.operators.not_like",
value: "!~"
},
GT: {
label: "commons.adv_search.operators.gt",
value: ">"
},
LT: {
label: "commons.adv_search.operators.lt",
value: "<"
}
}
}
},
methods: {
open(request) {
this.request = request;
this.controller = new IfController(request.controller);
if (!this.controller.operator) {
this.controller.operator = this.operators.EQ.value;
}
this.visible = true;
},
close() {
this.request.controller = this.controller
this.visible = false;
},
remove() {
this.controller = new IfController();
this.visible = false;
}
}
}
</script>
<style scoped>
</style>

View File

@ -481,6 +481,25 @@ export class BeanShellPostProcessor extends BeanShellProcessor {
}
}
export class IfController extends DefaultTestElement {
constructor(testName, controller = {}) {
super('IfController', 'IfControllerPanel', 'IfController', testName);
this.stringProp('IfController.comments', controller.comments);
this.stringProp('IfController.condition', controller.condition);
this.boolProp('IfController.evaluateAll', controller.evaluateAll, false);
this.boolProp('IfController.useExpression', controller.useExpression, true);
}
}
export class ConstantTimer extends DefaultTestElement {
constructor(testName, timer = {}) {
super('ConstantTimer', 'ConstantTimerGui', 'ConstantTimer', testName);
this.stringProp('ConstantTimer.delay', timer.delay);
}
}
export class HeaderManager extends DefaultTestElement {
constructor(testName, headers) {
super('HeaderManager', 'HeaderPanel', 'HeaderManager', testName);

View File

@ -1,5 +1,5 @@
import {
Arguments, BeanShellPostProcessor, BeanShellPreProcessor,
Arguments,
CookieManager,
DNSCacheManager,
DubboSample,
@ -7,10 +7,13 @@ import {
Element,
HashTree,
HeaderManager,
HTTPSamplerArguments, HTTPsamplerFiles,
HTTPSamplerArguments,
HTTPsamplerFiles,
HTTPSamplerProxy,
JSONPathAssertion,
JSONPostProcessor, JSR223PostProcessor, JSR223PreProcessor,
JSONPostProcessor,
JSR223PostProcessor,
JSR223PreProcessor,
RegexExtractor,
ResponseCodeAssertion,
ResponseDataAssertion,
@ -19,6 +22,8 @@ import {
TestPlan,
ThreadGroup,
XPath2Extractor,
IfController as JMXIfController,
ConstantTimer as JMXConstantTimer,
} from "./JMX";
import Mock from "mockjs";
import {funcFilters} from "@/common/js/func-filter";
@ -214,7 +219,12 @@ export class Scenario extends BaseConfig {
this.databaseConfigs = undefined;
this.set(options);
this.sets({variables: KeyValue, headers: KeyValue, requests: RequestFactory, databaseConfigs: DatabaseConfig}, options);
this.sets({
variables: KeyValue,
headers: KeyValue,
requests: RequestFactory,
databaseConfigs: DatabaseConfig
}, options);
}
initOptions(options = {}) {
@ -287,9 +297,12 @@ export class RequestFactory {
}
export class Request extends BaseConfig {
constructor(type) {
constructor(type, options = {}) {
super();
this.type = type;
options.id = options.id || uuid();
options.timer = new ConstantTimer(options.timer);
options.controller = new IfController(options.controller);
}
showType() {
@ -303,8 +316,7 @@ export class Request extends BaseConfig {
export class HttpRequest extends Request {
constructor(options) {
super(RequestFactory.TYPES.HTTP);
this.id = undefined;
super(RequestFactory.TYPES.HTTP, options);
this.name = undefined;
this.url = undefined;
this.path = undefined;
@ -331,7 +343,6 @@ export class HttpRequest extends Request {
}
initOptions(options = {}) {
options.id = options.id || uuid();
options.method = options.method || "GET";
options.body = new Body(options.body);
options.assertions = new Assertions(options.assertions);
@ -389,8 +400,7 @@ export class DubboRequest extends Request {
}
constructor(options = {}) {
super(RequestFactory.TYPES.DUBBO);
this.id = options.id || uuid();
super(RequestFactory.TYPES.DUBBO, options);
this.name = options.name;
this.protocol = options.protocol || DubboRequest.PROTOCOLS.DUBBO;
this.interface = options.interface;
@ -502,6 +512,7 @@ export class DatabaseConfig extends BaseConfig {
// options.id = options.id || uuid();
return options;
}
// <JDBCDataSource guiclass="TestBeanGUI" testclass="JDBCDataSource" testname="JDBC Connection Configurationqqq" enabled="true">
// <boolProp name="autocommit">true</boolProp>
// <stringProp name="checkQuery"></stringProp>
@ -794,6 +805,77 @@ export class ExtractXPath extends ExtractCommon {
}
}
export class Controller extends BaseConfig {
static TYPES = {
IF_CONTROLLER: "If Controller",
}
constructor(type, options = {}) {
super();
this.type = type
options.id = options.id || uuid();
options.enable = options.enable || true;
}
}
export class IfController extends Controller {
constructor(options = {}) {
super(Controller.TYPES.IF_CONTROLLER, options);
this.variable;
this.operator;
this.value;
this.set(options);
}
isValid() {
return !!this.variable && !!this.operator && !!this.value;
}
label() {
if (this.isValid()) {
let label = this.variable;
label += " " + this.operator;
label += " " + this.value;
return label;
}
return "";
}
}
export class Timer extends BaseConfig {
static TYPES = {
CONSTANT_TIMER: "Constant Timer",
}
constructor(type, options = {}) {
super();
this.type = type;
options.id = options.id || uuid();
options.enable = options.enable || true;
}
}
export class ConstantTimer extends Timer {
constructor(options = {}) {
super(Timer.TYPES.CONSTANT_TIMER, options);
this.delay;
this.set(options);
}
isValid() {
return this.delay > 0;
}
label() {
if (this.isValid()) {
return this.delay + " ms";
}
return "";
}
}
/** ------------------------------------------------------------------------ **/
const JMX_ASSERTION_CONDITION = {
MATCH: 1,
@ -954,10 +1036,18 @@ class JMXGenerator {
this.addJSR223PreProcessor(sampler, request);
threadGroup.put(sampler);
this.addConstantsTimer(sampler, request);
if (request.controller.isValid() && request.controller.enable) {
if (request.controller instanceof IfController) {
let controller = this.getController(sampler, request);
threadGroup.put(controller);
}
} else {
threadGroup.put(sampler);
}
}
})
testPlan.put(threadGroup);
}
@ -1040,6 +1130,31 @@ class JMXGenerator {
}
}
addConstantsTimer(sampler, request) {
if (request.timer.isValid() && request.timer.enable) {
sampler.put(new JMXConstantTimer(request.timer.label(), request.timer));
}
}
getController(sampler, request) {
if (request.controller.isValid() && request.controller.enable) {
if (request.controller instanceof IfController) {
let name = request.controller.label();
let variable = request.controller.variable;
let operator = request.controller.operator;
let value = request.controller.value;
if (operator === "=~" || operator === "!~") {
value = "\".*" + value + ".*\"";
}
let condition = "${__jexl3(" + variable + operator + value + ")}";
let controller = new JMXIfController(name, {condition: condition});
controller.put(sampler);
return controller;
}
}
}
addBodyFormat(request) {
let bodyFormat = request.body.format;
if (!request.body.isKV() && bodyFormat) {

@ -1 +1 @@
Subproject commit 0a375848d034d20eaf05caf11769e1c75c39235c
Subproject commit 2d5807501b026efc48c309daab988ee0719356d4

View File

@ -149,12 +149,13 @@ export default {
lt: "<",
le: "<=",
equals: "=",
not_equals: "!=",
between: "Between",
current_user: "Current user"
}
}
},
license:{
license: {
title: 'Authorization management',
corporation: 'corporation',
time: 'Authorization time',
@ -439,7 +440,7 @@ export default {
test_name: "Test Name",
reference: "Reference",
clone: "Copy",
cant_reference:'Historical test files, can be referenced after re-saving'
cant_reference: 'Historical test files, can be referenced after re-saving'
},
request: {
debug: "Debug",
@ -481,6 +482,9 @@ export default {
response_timeout: "Response Timeout",
follow_redirects: "Follow Redirects",
body_upload_limit_size: "The file size does not exceed 500 MB",
condition: "condition",
condition_variable: "Variable, e.g: ${var}",
wait: "wait",
assertions: {
label: "Assertion",
text: "Text",

View File

@ -149,12 +149,13 @@ export default {
lt: "小于",
le: "小于等于",
equals: "等于",
not_equals: "不等于",
between: "之间",
current_user: "是当前用户"
}
}
},
license:{
license: {
title: '授权管理',
corporation: '客户名称',
time: '授权时间',
@ -482,6 +483,9 @@ export default {
response_timeout: "响应超时",
follow_redirects: "跟随重定向",
body_upload_limit_size: "上传文件大小不能超过 500 MB!",
condition: "条件",
condition_variable: "变量,例如: ${var}",
wait: "等待",
assertions: {
label: "断言",
text: "文本",

View File

@ -146,6 +146,7 @@ export default {
ge: "大於等於",
lt: "小於",
le: "小於等於",
not_equals: "不等於",
equals: "等於",
between: "之間",
current_user: "是當前用戶"
@ -479,6 +480,9 @@ export default {
connect_timeout: "連接超時",
response_timeout: "響應超時",
body_upload_limit_size: "上傳文件大小不能超過 500 MB!",
condition: "條件",
condition_variable: "變量,例如: ${var}",
wait: "等待",
assertions: {
label: "斷言",
text: "文字",