refactor: 脚本中可添加自定义函数

This commit is contained in:
shiziyuan9527 2021-09-13 17:24:26 +08:00 committed by 刘瑞斌
parent 2d7d360ec4
commit ce8de01585
7 changed files with 316 additions and 78 deletions

View File

@ -78,7 +78,14 @@ public class CustomFunctionService {
projectId = SessionUtils.getCurrentProjectId(); projectId = SessionUtils.getCurrentProjectId();
} }
CustomFunctionExample example = new CustomFunctionExample(); CustomFunctionExample example = new CustomFunctionExample();
example.createCriteria().andProjectIdEqualTo(projectId); CustomFunctionExample.Criteria criteria = example.createCriteria();
criteria.andProjectIdEqualTo(projectId);
if (StringUtils.isNotBlank(request.getType())) {
criteria.andTypeEqualTo(request.getType());
}
if (StringUtils.isNotBlank(request.getName())) {
criteria.andNameEqualTo(request.getName());
}
return customFunctionMapper.selectByExample(example); return customFunctionMapper.selectByExample(example);
} }

View File

@ -45,3 +45,25 @@ ALTER TABLE test_plan_api_case ADD `order` bigint(20) NOT NULL COMMENT '自定
ALTER TABLE test_plan_api_scenario ADD `order` bigint(20) NOT NULL COMMENT '自定义排序间隔5000'; ALTER TABLE test_plan_api_scenario ADD `order` bigint(20) NOT NULL COMMENT '自定义排序间隔5000';
ALTER TABLE test_plan_load_case ADD `order` bigint(20) NOT NULL COMMENT '自定义排序间隔5000'; ALTER TABLE test_plan_load_case ADD `order` bigint(20) NOT NULL COMMENT '自定义排序间隔5000';
create table if not exists custom_function
(
id varchar(50) not null
primary key,
name varchar(255) null comment '函数名',
tags varchar(1000) null comment '标签',
description varchar(1000) null comment '函数描述',
type varchar(255) null comment '脚本语言类型',
params longtext null comment '参数列表',
script longtext null comment '函数体',
result longtext null comment '执行结果',
create_user varchar(100) null comment '创建人',
create_time bigint(13) null comment '创建时间',
update_time bigint(13) null comment '更新时间',
project_id varchar(50) null comment '所属项目ID'
)
ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE utf8mb4_general_ci;

View File

@ -14,10 +14,15 @@
<div v-for="(template, index) in codeTemplates" :key="index" class="code-template"> <div v-for="(template, index) in codeTemplates" :key="index" class="code-template">
<el-link :disabled="template.disabled" @click="addTemplate(template)">{{ template.title }}</el-link> <el-link :disabled="template.disabled" @click="addTemplate(template)">{{ template.title }}</el-link>
</div> </div>
<div v-for="funcLink in funcLinks" :key="funcLink.index" class="code-template">
<el-link :disabled="funcLink.disabled" @click="doFuncLink(funcLink)">{{ funcLink.title }}</el-link>
</div>
<el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor" <el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor"
target="componentReferenceDoc" style="margin-top: 10px" target="componentReferenceDoc" style="margin-top: 10px"
type="primary">{{ $t('commons.reference_documentation') }} type="primary">{{ $t('commons.reference_documentation') }}
</el-link> </el-link>
<custom-function-relate ref="customFunctionRelate" @addCustomFuncScript="addCustomFuncScript"/>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@ -26,9 +31,10 @@
<script> <script>
import MsCodeEdit from "../../../definition/components/MsCodeEdit"; import MsCodeEdit from "../../../definition/components/MsCodeEdit";
import MsDropdown from "../../../../common/components/MsDropdown"; import MsDropdown from "../../../../common/components/MsDropdown";
import CustomFunctionRelate from "@/business/components/settings/project/function/CustomFunctionRelate";
export default { export default {
name: "Jsr233ProcessorContent", name: "Jsr233ProcessorContent",
components: {MsDropdown, MsCodeEdit}, components: {MsDropdown, MsCodeEdit, CustomFunctionRelate},
data() { data() {
return { return {
jsr223ProcessorData: {}, jsr223ProcessorData: {},
@ -83,8 +89,20 @@
' }\n' + ' }\n' +
'}', '}',
disabled: this.isPreProcessor disabled: this.isPreProcessor
},
{
title: "终止测试",
value: 'ctx.getEngine().stopThreadNow(ctx.getThread().getThreadName())'
} }
], ],
funcLinks: [
{
title: "插入自定义函数",
command: "custom_function",
index: "custom_function"
}
],
isCodeEditAlive: true, isCodeEditAlive: true,
languages: [ languages: [
'beanshell', "python", "groovy", "nashornScript", "rhinoScript" 'beanshell', "python", "groovy", "nashornScript", "rhinoScript"
@ -142,6 +160,16 @@
this.jsr223ProcessorData.scriptLanguage = language; this.jsr223ProcessorData.scriptLanguage = language;
this.$emit("languageChange"); this.$emit("languageChange");
}, },
addCustomFuncScript(script) {
this.jsr223ProcessorData.script = this.jsr223ProcessorData.script ?
this.jsr223ProcessorData.script + '\n\n' + script : script;
this.reload();
},
doFuncLink(funcLink) {
if (funcLink.command === 'custom_function') {
this.$refs.customFunctionRelate.open(this.jsr223ProcessorData.scriptLanguage);
}
},
} }
} }
</script> </script>

View File

@ -110,9 +110,6 @@ export default {
if (item.tags && item.tags.length > 0) { if (item.tags && item.tags.length > 0) {
item.tags = JSON.parse(item.tags); item.tags = JSON.parse(item.tags);
} }
if (item.params && item.params.length > 0) {
item.params = JSON.parse(item.params);
}
}) })
}); });
}, },

View File

@ -0,0 +1,137 @@
<template>
<el-dialog :close-on-click-modal="false" :title="'自定义函数'" :visible.sync="visible" :destroy-on-close="true"
@close="close" width="60%" top="10vh" v-loading="result.loading" append-to-body class="customFunc">
<div>
<el-alert
title="在 系统设置->项目->自定义函数 菜单中创建函数"
type="info"
style="width: 350px;float: left;"
:closable="false" show-icon>
</el-alert>
<ms-table-search-bar :condition.sync="condition" @change="init" class="search-bar" :tip="'根据名称搜索'"/>
<el-table border class="adjust-table" :data="data" style="width: 100%" ref="table"
highlight-current-row @current-change="handleCurrentChange">
<el-table-column prop="name" :label="$t('commons.name')" show-overflow-tooltip/>
<el-table-column prop="description" :label="$t('commons.description')" show-overflow-tooltip>
<template v-slot:default="scope">
<pre>{{ scope.row.description }}</pre>
</template>
</el-table-column>
<el-table-column prop="tags" :label="$t('api_test.automation.tag')">
<template v-slot:default="scope">
<ms-tag v-for="(itemName,index) in scope.row.tags" :key="index" type="success" effect="plain"
:content="itemName" style="margin-left: 0; margin-right: 2px">
</ms-tag>
<span></span>
</template>
</el-table-column>
<el-table-column prop="type" :label="'脚本语言'" show-overflow-tooltip/>
<el-table-column prop="createTime"
:label="$t('commons.create_time')"
show-overflow-tooltip>
<template v-slot:default="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="init" :current-page.sync="currentPage" :page-size.sync="pageSize" :total="total"/>
</div>
<template v-slot:footer>
<el-button @click="close" size="medium">{{ $t('commons.cancel') }}</el-button>
<el-button type="primary" @click="submit" size="medium" style="margin-left: 10px;">
{{ $t('commons.confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script>
import MsTablePagination from "@/business/components/common/pagination/TablePagination";
import MsTag from "@/business/components/common/components/MsTag";
import MsTableOperator from "@/business/components/common/components/MsTableOperator";
import MsTableOperatorButton from "@/business/components/common/components/MsTableOperatorButton";
import {getCurrentProjectID} from "@/common/js/utils";
import MsTableSearchBar from "@/business/components/common/components/MsTableSearchBar";
export default {
name: "CustomFunctionRelate",
components: {
MsTablePagination,
MsTag,
MsTableOperator,
MsTableOperatorButton,
MsTableSearchBar
},
data() {
return {
visible: false,
result: {},
condition: {},
data: [],
currentPage: 1,
pageSize: 10,
total: 0,
screenHeight: 'calc(100vh - 195px)',
currentRow: {}
}
},
methods: {
init(language) {
if (language) {
this.condition.type = language;
}
this.condition.projectId = getCurrentProjectID();
this.result = this.$post("/custom/func/list/" + this.currentPage + "/" + this.pageSize, this.condition, res => {
let tableData = res.data;
const {itemCount, listObject} = tableData;
this.total = itemCount;
this.data = listObject;
this.data.forEach(item => {
if (item.tags && item.tags.length > 0) {
item.tags = JSON.parse(item.tags);
}
})
});
},
open(language) {
this.visible = true;
this.init(language);
},
close() {
this.visible = false;
},
handleCurrentChange(val) {
this.currentRow = val;
},
submit() {
if (!this.currentRow) {
this.$warning("请选择自定义函数!");
return;
}
this.result = this.$get("/custom/func/get/" + this.currentRow.id, res => {
if (!res.data) {
this.$warning("函数为空!")
}
let {script} = res.data;
this.$emit("addCustomFuncScript", script);
this.close();
});
},
}
}
</script>
<style scoped>
pre {
margin: 0 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial, sans-serif;
}
.search-bar {
width: 300px;
float: right;
}
.customFunc >>> .el-dialog__body {
padding: 0 20px;
}
</style>

View File

@ -28,11 +28,11 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row> <!-- <el-row>-->
<el-form-item :label="'参数列表'" prop=""> <!-- <el-form-item :label="'参数列表'" prop="">-->
<function-params :items="form.params"/> <!-- <function-params :items="form.params"/>-->
</el-form-item> <!-- </el-form-item>-->
</el-row> <!-- </el-row>-->
<el-row style="margin-right: 10px;"> <el-row style="margin-right: 10px;">
<el-col :span="20"> <el-col :span="20">
<el-form-item> <el-form-item>
@ -42,7 +42,7 @@
<ms-code-edit <ms-code-edit
v-if="isCodeEditAlive" v-if="isCodeEditAlive"
:mode="codeEditModeMap[form.type]" :mode="codeEditModeMap[form.type]"
height="330px" height="380px"
:data.sync="form.script" :data.sync="form.script"
theme="eclipse" theme="eclipse"
:modes="modes" :modes="modes"
@ -67,6 +67,9 @@
<div v-for="(template, index) in codeTemplates" :key="index" class="code-template"> <div v-for="(template, index) in codeTemplates" :key="index" class="code-template">
<el-link :disabled="template.disabled" @click="addTemplate(template)">{{ template.title }}</el-link> <el-link :disabled="template.disabled" @click="addTemplate(template)">{{ template.title }}</el-link>
</div> </div>
<div v-for="funcLink in funcLinks" :key="funcLink.index" class="code-template">
<el-link :disabled="funcLink.disabled" @click="doFuncLink(funcLink)">{{ funcLink.title }}</el-link>
</div>
<el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor" <el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor"
target="componentReferenceDoc" style="margin-top: 10px" target="componentReferenceDoc" style="margin-top: 10px"
type="primary">{{ $t('commons.reference_documentation') }} type="primary">{{ $t('commons.reference_documentation') }}
@ -77,6 +80,7 @@
</el-form> </el-form>
<!-- 执行组件 --> <!-- 执行组件 -->
<function-run :report-id="reportId" :run-data="runData" @runRefresh="runRefresh" @errorRefresh="errorRefresh"/> <function-run :report-id="reportId" :run-data="runData" @runRefresh="runRefresh" @errorRefresh="errorRefresh"/>
<custom-function-relate ref="customFunctionRelate" @addCustomFuncScript="addCustomFuncScript"/>
</div> </div>
<template v-slot:footer> <template v-slot:footer>
<el-button @click="close" size="medium">{{ $t('commons.cancel') }}</el-button> <el-button @click="close" size="medium">{{ $t('commons.cancel') }}</el-button>
@ -92,15 +96,17 @@ import MsInputTag from "@/business/components/api/automation/scenario/MsInputTag
import FunctionParams from "@/business/components/settings/project/function/FunctionParams"; import FunctionParams from "@/business/components/settings/project/function/FunctionParams";
import MsCodeEdit from "@/business/components/common/components/MsCodeEdit"; import MsCodeEdit from "@/business/components/common/components/MsCodeEdit";
import MsDropdown from "@/business/components/common/components/MsDropdown"; import MsDropdown from "@/business/components/common/components/MsDropdown";
import {splicingCustomFunc} from "@/business/components/settings/project/function/custom_function"; import {FUNC_TEMPLATE, splicingCustomFunc} from "@/business/components/settings/project/function/custom_function";
import MsRun from "@/business/components/api/automation/scenario/DebugRun"; import MsRun from "@/business/components/api/automation/scenario/DebugRun";
import {getUUID} from "@/common/js/utils"; import {getUUID} from "@/common/js/utils";
import {JSR223Processor} from "@/business/components/api/definition/model/ApiTestModel"; import {JSR223Processor} from "@/business/components/api/definition/model/ApiTestModel";
import FunctionRun from "@/business/components/settings/project/function/FunctionRun"; import FunctionRun from "@/business/components/settings/project/function/FunctionRun";
import CustomFunctionRelate from "@/business/components/settings/project/function/CustomFunctionRelate";
export default { export default {
name: "EditFunction", name: "EditFunction",
components: { components: {
CustomFunctionRelate,
FunctionRun, FunctionRun,
MsCodeEdit, MsCodeEdit,
FunctionParams, FunctionParams,
@ -139,7 +145,11 @@ export default {
}, },
modes: ['java', 'python'], modes: ['java', 'python'],
languages: [ languages: [
'beanshell', "python", "groovy", "nashornScript", "rhinoScript" 'beanshell',
"python",
"groovy",
"nashornScript",
"rhinoScript"
], ],
codeEditModeMap: { codeEditModeMap: {
beanshell: 'java', beanshell: 'java',
@ -199,48 +209,58 @@ export default {
' }\n' + ' }\n' +
'}', '}',
disabled: this.isPreProcessor disabled: this.isPreProcessor
},
{
title: "终止测试",
value: 'ctx.getEngine().stopThreadNow(ctx.getThread().getThreadName())'
} }
], ],
funcLinks: [
{
title: "插入自定义函数",
command: "custom_function",
index: "custom_function"
}
],
response: {}, response: {},
request: {}, request: {},
debug: true, debug: true,
console: "无执行结果" console: "无执行结果"
} }
}, },
watch: { // watch: {
'form.name'() { // 'form.name'() {
this.splicingFunc(); // this.splicingFunc();
}, // },
'form.params': { // 'form.params': {
handler() { // handler() {
this.splicingFunc(); // this.splicingFunc();
}, // },
deep: true // deep: true
} // }
}, // },
methods: { methods: {
_parseFuncParam(funcObj) { // _parseFuncParam(funcObj) {
let params = undefined; // let params = undefined;
if (funcObj.params) { // if (funcObj.params) {
params = funcObj.params.map(p => p.name); // params = funcObj.params.map(p => p.name);
if (params.length > 0) { // if (params.length > 0) {
params = params.filter(p => { // params = params.filter(p => {
return p !== undefined // return p !== undefined
}); // });
params.join(",\s"); // params.join(",\s");
} // }
} // }
// todo // // todo
return params; // return params;
}, // },
splicingFunc() { // splicingFunc() {
let funcObj = this.form; // let funcObj = this.form;
let funcName = funcObj.name; // let funcParams = this._parseFuncParam(funcObj);
let funcLanguage = this.form.type || "beanshell"; // this.form.script = splicingCustomFunc(funcObj, funcParams);
let funcParams = this._parseFuncParam(funcObj); // this.reloadCodeEdit();
this.form.script = splicingCustomFunc(funcLanguage, this.form.script, funcName, funcParams); // },
this.reloadCodeEdit();
},
open(data) { open(data) {
this.activeName = "code"; this.activeName = "code";
this.visible = true; this.visible = true;
@ -250,6 +270,8 @@ export default {
this.initFunc(data.id); this.initFunc(data.id);
this.dialogTitle = this.dialogUpdateTitle; this.dialogTitle = this.dialogUpdateTitle;
} else { } else {
this.form.script = FUNC_TEMPLATE[this.form.type];
this.reloadCodeEdit();
this.form.tags = []; this.form.tags = [];
this.form.params = [{}]; this.form.params = [{}];
this.dialogTitle = this.dialogCreateTitle; this.dialogTitle = this.dialogCreateTitle;
@ -263,11 +285,11 @@ export default {
} else { } else {
this.form.tags = JSON.parse(this.form.tags); this.form.tags = JSON.parse(this.form.tags);
} }
if (!this.form.params) { // if (!this.form.params) {
this.form.params = []; // this.form.params = [];
} else { // } else {
this.form.params = JSON.parse(this.form.params); // this.form.params = JSON.parse(this.form.params);
} // }
this.reload(); this.reload();
}) })
}, },
@ -281,7 +303,10 @@ export default {
}, },
languageChange(language) { languageChange(language) {
this.form.type = language; this.form.type = language;
this.$emit("languageChange"); if (!this.form.script) {
this.form.script = FUNC_TEMPLATE[language];
this.reloadCodeEdit();
}
}, },
addTemplate(template) { addTemplate(template) {
if (!this.form.script) { if (!this.form.script) {
@ -293,6 +318,11 @@ export default {
} }
this.reloadCodeEdit(); this.reloadCodeEdit();
}, },
doFuncLink(funcLink) {
if (funcLink.command === 'custom_function') {
this.$refs.customFunctionRelate.open(this.form.type);
}
},
reload() { reload() {
this.isFormAlive = false; this.isFormAlive = false;
this.$nextTick(() => { this.$nextTick(() => {
@ -337,8 +367,8 @@ export default {
this.console = "无执行结果"; this.console = "无执行结果";
this.reloadResult(); this.reloadResult();
this.runResult.loading = true; this.runResult.loading = true;
let jSR223Processor = new JSR223Processor({ let jSR223Processor = new JSR223Processor({
scriptLanguage: this.form.type,
script: this.form.script script: this.form.script
}); });
jSR223Processor.id = getUUID().substring(0, 8); jSR223Processor.id = getUUID().substring(0, 8);
@ -354,6 +384,10 @@ export default {
}, },
errorRefresh() { errorRefresh() {
this.runResult.loading = false; this.runResult.loading = false;
},
addCustomFuncScript(script) {
this.form.script = this.form.script + '\n\n' + script;
this.reloadCodeEdit();
} }
} }
} }

View File

@ -1,36 +1,49 @@
export function splicingCustomFunc(funcLanguage, funcObjScript, funcName, funcParams) { export const FUNC_TEMPLATE = {
let funcFirstLine = generateFuncFirstLine(funcLanguage, funcName, funcParams); beanshell: "public static void test() {\n\n\n}",
if (!funcObjScript) { groovy: "public static void test() {\n\n\n}",
funcObjScript = funcFirstLine + "\n\n\n}"; python: "def test():\n",
nashornScript: "function test() {\n\n\n}",
rhinoScript: "function test() {\n\n\n}"
} }
// 拼接函数
export function splicingCustomFunc(funcObj, funcParams) {
let funcLanguage = funcObj.type || "beanshell";
let funcObjScript = funcObj.script;
let funcName = funcObj.name;
let funcFirstLine = generateFuncFirstLine(funcLanguage, funcName, funcParams);
if (!funcObjScript && funcName) {
funcObjScript = funcLanguage === "python" ? funcFirstLine : funcFirstLine + "\n\n\n}";
}
if (funcObjScript) {
funcObjScript = funcObjScript.replace(regex[funcLanguage], funcFirstLine); funcObjScript = funcObjScript.replace(regex[funcLanguage], funcFirstLine);
}
return funcObjScript; return funcObjScript;
} }
export function generateFuncFirstLine(funcLanguage, funcName, funcParams) { export function generateFuncFirstLine(funcLanguage, funcName, funcParams) {
let funcFirstLine = ""; let funcEnd = funcLanguage === "python" ? ":" : "{";
switch (funcLanguage) { return scriptFuncDefinition[funcLanguage] + " " + funcName + "(" + funcParams + ") " + funcEnd;
case "beanshell":
funcFirstLine = "public static void " + funcName + "(" + funcParams + ") " + "{";
break;
case "python":
break;
case "groovy":
break;
case "nashornScript":
break;
case "rhinoScript":
break;
default:
}
return funcFirstLine;
} }
const scriptFuncDefinition = {
beanshell: "public static void",
python: "def",
groovy: "public static void",
// nashornScript: "",
// rhinoScript: "",
}
const firstFuncRegex = RegExp(".*\(.*\)\\s\{\\r?");
const regex = { const regex = {
beanshell: /^public static void\s.*\(.*\)\s\{/, beanshell: calcRegex(scriptFuncDefinition.beanshell),
python: /^function\s.*\(.*\)\s\{/, python: RegExp("^def\\s.*\(.*\)\\s\:"),
groovy: /^function\s.*\(.*\)\s\{/, groovy: calcRegex(scriptFuncDefinition.groovy),
nashornScript: /^function\s.*\(.*\)\s\{/, // nashornScript: calcRegex(scriptFuncDefinition.nashornScript),
rhinoScript: /^function\s.*\(.*\)\s\{/, // rhinoScript: calcRegex(scriptFuncDefinition.rhinoScript),
} }
function calcRegex(str) {
return RegExp("^" + str + "\\s.*\(.*\)\\s\{");
}