Merge remote-tracking branch 'origin/master'

This commit is contained in:
wenyann 2020-12-15 15:07:52 +08:00
commit 0590e92890
21 changed files with 136 additions and 134 deletions

View File

@ -4,6 +4,7 @@ import io.metersphere.api.dto.definition.ApiTestCaseRequest;
import io.metersphere.api.dto.definition.ApiTestCaseResult; import io.metersphere.api.dto.definition.ApiTestCaseResult;
import io.metersphere.api.dto.definition.SaveApiTestCaseRequest; import io.metersphere.api.dto.definition.SaveApiTestCaseRequest;
import io.metersphere.api.service.ApiTestCaseService; import io.metersphere.api.service.ApiTestCaseService;
import io.metersphere.base.domain.ApiTestCaseWithBLOBs;
import io.metersphere.commons.constants.RoleConstants; import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.utils.SessionUtils; import io.metersphere.commons.utils.SessionUtils;
import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.Logical;
@ -43,4 +44,9 @@ public class ApiTestCaseController {
apiTestCaseService.delete(id); apiTestCaseService.delete(id);
} }
@GetMapping("/get/{id}")
public ApiTestCaseWithBLOBs get(@PathVariable String id) {
return apiTestCaseService.get(id);
}
} }

View File

@ -32,16 +32,16 @@ import java.util.List;
public class MsScenario extends MsTestElement { public class MsScenario extends MsTestElement {
private String type = "scenario"; private String type = "scenario";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private String name; private String name;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String referenced; private String referenced;
@JSONField(ordinal = 12) @JSONField(ordinal = 22)
private String environmentId; private String environmentId;
@JSONField(ordinal = 13) @JSONField(ordinal = 23)
private List<KeyValue> variables; private List<KeyValue> variables;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -76,8 +76,9 @@ public abstract class MsTestElement {
private String index; private String index;
@JSONField(ordinal = 8) @JSONField(ordinal = 8)
private boolean enable = true; private boolean enable = true;
@JSONField(ordinal = 9) @JSONField(ordinal = 9)
private String refType ;
@JSONField(ordinal = 10)
private LinkedList<MsTestElement> hashTree; private LinkedList<MsTestElement> hashTree;
// 公共环境逐层传递如果自身有环境 以自身引用环境为准否则以公共环境作为请求环境 // 公共环境逐层传递如果自身有环境 以自身引用环境为准否则以公共环境作为请求环境

View File

@ -24,31 +24,31 @@ import java.util.List;
@JSONType(typeName = "AuthManager") @JSONType(typeName = "AuthManager")
public class MsAuthManager extends MsTestElement { public class MsAuthManager extends MsTestElement {
private String type = "AuthManager"; private String type = "AuthManager";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private String username; private String username;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String password; private String password;
@JSONField(ordinal = 12) @JSONField(ordinal = 22)
private String url; private String url;
@JSONField(ordinal = 13) @JSONField(ordinal = 23)
private String realm; private String realm;
@JSONField(ordinal = 14) @JSONField(ordinal = 24)
private String verification; private String verification;
@JSONField(ordinal = 15) @JSONField(ordinal = 25)
private String mechanism; private String mechanism;
@JSONField(ordinal = 16) @JSONField(ordinal = 26)
private String encrypt; private String encrypt;
@JSONField(ordinal = 17) @JSONField(ordinal = 27)
private String domain; private String domain;
@JSONField(ordinal = 18) @JSONField(ordinal = 28)
private String environment; private String environment;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -22,7 +22,7 @@ import java.util.List;
public class MsHeaderManager extends MsTestElement { public class MsHeaderManager extends MsTestElement {
private String type = "HeaderManager"; private String type = "HeaderManager";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private List<KeyValue> headers; private List<KeyValue> headers;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -20,10 +20,10 @@ import java.util.List;
public class MsJSR223Processor extends MsTestElement { public class MsJSR223Processor extends MsTestElement {
private String type = "JSR223Processor"; private String type = "JSR223Processor";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private String script; private String script;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String scriptLanguage; private String scriptLanguage;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -20,10 +20,10 @@ import java.util.List;
public class MsJSR223PostProcessor extends MsTestElement { public class MsJSR223PostProcessor extends MsTestElement {
private String type = "JSR223PostProcessor"; private String type = "JSR223PostProcessor";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private String script; private String script;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String scriptLanguage; private String scriptLanguage;

View File

@ -20,10 +20,10 @@ import java.util.List;
public class MsJSR223PreProcessor extends MsTestElement { public class MsJSR223PreProcessor extends MsTestElement {
private String type = "JSR223PreProcessor"; private String type = "JSR223PreProcessor";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private String script; private String script;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String scriptLanguage; private String scriptLanguage;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -40,52 +40,52 @@ import java.util.regex.Pattern;
public class MsHTTPSamplerProxy extends MsTestElement { public class MsHTTPSamplerProxy extends MsTestElement {
private String type = "HTTPSamplerProxy"; private String type = "HTTPSamplerProxy";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private String protocol; private String protocol;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String domain; private String domain;
@JSONField(ordinal = 12) @JSONField(ordinal = 22)
private String port; private String port;
@JSONField(ordinal = 13) @JSONField(ordinal = 23)
private String method; private String method;
@JSONField(ordinal = 14) @JSONField(ordinal = 24)
private String path; private String path;
@JSONField(ordinal = 15) @JSONField(ordinal = 25)
private String connectTimeout; private String connectTimeout;
@JSONField(ordinal = 16) @JSONField(ordinal = 26)
private String responseTimeout; private String responseTimeout;
@JSONField(ordinal = 17) @JSONField(ordinal = 27)
private List<KeyValue> headers; private List<KeyValue> headers;
@JSONField(ordinal = 18) @JSONField(ordinal = 28)
private Body body; private Body body;
@JSONField(ordinal = 19) @JSONField(ordinal = 29)
private List<KeyValue> rest; private List<KeyValue> rest;
@JSONField(ordinal = 20) @JSONField(ordinal = 30)
private String url; private String url;
@JSONField(ordinal = 21) @JSONField(ordinal = 31)
private boolean followRedirects; private boolean followRedirects;
@JSONField(ordinal = 22) @JSONField(ordinal = 32)
private boolean doMultipartPost; private boolean doMultipartPost;
@JSONField(ordinal = 23) @JSONField(ordinal = 33)
private String useEnvironment; private String useEnvironment;
@JSONField(ordinal = 24) @JSONField(ordinal = 34)
private List<KeyValue> arguments; private List<KeyValue> arguments;
@JSONField(ordinal = 25) @JSONField(ordinal = 35)
private Object requestResult; private Object requestResult;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -24,21 +24,21 @@ import java.util.List;
public class MsJDBCSampler extends MsTestElement { public class MsJDBCSampler extends MsTestElement {
// type 必须放最前面以便能够转换正确的类 // type 必须放最前面以便能够转换正确的类
private String type = "JDBCSampler"; private String type = "JDBCSampler";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private DatabaseConfig dataSource; private DatabaseConfig dataSource;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String query; private String query;
@JSONField(ordinal = 12) @JSONField(ordinal = 22)
private long queryTimeout; private long queryTimeout;
@JSONField(ordinal = 13) @JSONField(ordinal = 23)
private String resultVariable; private String resultVariable;
@JSONField(ordinal = 14) @JSONField(ordinal = 24)
private String variableNames; private String variableNames;
@JSONField(ordinal = 15) @JSONField(ordinal = 25)
private List<KeyValue> variables; private List<KeyValue> variables;
@JSONField(ordinal = 16) @JSONField(ordinal = 26)
private String environmentId; private String environmentId;
@JSONField(ordinal = 17) @JSONField(ordinal = 27)
private Object requestResult; private Object requestResult;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -20,35 +20,35 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@JSONType(typeName = "TCPSampler") @JSONType(typeName = "TCPSampler")
public class MsTCPSampler extends MsTestElement { public class MsTCPSampler extends MsTestElement {
@JSONField(ordinal = 10)
private String type = "TCPSampler";
@JSONField(ordinal = 11)
private String classname = "";
@JSONField(ordinal = 12)
private String server = "";
@JSONField(ordinal = 13)
private String port = "";
@JSONField(ordinal = 14)
private String ctimeout = "";
@JSONField(ordinal = 15)
private String timeout = "";
@JSONField(ordinal = 16)
private boolean reUseConnection = true;
@JSONField(ordinal = 17)
private boolean nodelay;
@JSONField(ordinal = 18)
private boolean closeConnection;
@JSONField(ordinal = 19)
private String soLinger = "";
@JSONField(ordinal = 20) @JSONField(ordinal = 20)
private String eolByte = ""; private String type = "TCPSampler";
@JSONField(ordinal = 21) @JSONField(ordinal = 21)
private String username = ""; private String classname = "";
@JSONField(ordinal = 22) @JSONField(ordinal = 22)
private String password = ""; private String server = "";
@JSONField(ordinal = 23) @JSONField(ordinal = 23)
private String request; private String port = "";
@JSONField(ordinal = 24) @JSONField(ordinal = 24)
private String ctimeout = "";
@JSONField(ordinal = 25)
private String timeout = "";
@JSONField(ordinal = 26)
private boolean reUseConnection = true;
@JSONField(ordinal = 27)
private boolean nodelay;
@JSONField(ordinal = 28)
private boolean closeConnection;
@JSONField(ordinal = 29)
private String soLinger = "";
@JSONField(ordinal = 30)
private String eolByte = "";
@JSONField(ordinal = 31)
private String username = "";
@JSONField(ordinal = 32)
private String password = "";
@JSONField(ordinal = 33)
private String request;
@JSONField(ordinal = 34)
private Object requestResult; private Object requestResult;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -19,9 +19,9 @@ import java.util.List;
@JSONType(typeName = "ConstantTimer") @JSONType(typeName = "ConstantTimer")
public class MsConstantTimer extends MsTestElement { public class MsConstantTimer extends MsTestElement {
private String type = "ConstantTimer"; private String type = "ConstantTimer";
@JSONField(ordinal = 10) @JSONField(ordinal = 20)
private String id; private String id;
@JSONField(ordinal = 11) @JSONField(ordinal = 21)
private String delay; private String delay;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) { public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {

View File

@ -52,7 +52,7 @@ public class ApiTestCaseService {
return extApiTestCaseMapper.list(request); return extApiTestCaseMapper.list(request);
} }
public ApiTestCase get(String id) { public ApiTestCaseWithBLOBs get(String id) {
return apiTestCaseMapper.selectByPrimaryKey(id); return apiTestCaseMapper.selectByPrimaryKey(id);
} }

View File

@ -1,3 +1,3 @@
for file in ${TESTS_DIR}/*.jmx; do for file in ${TESTS_DIR}/*.jmx; do
jmeter -n -t ${file} -Jserver.rmi.ssl.disable=${SSL_DISABLED} -l ${TESTS_DIR}/${REPORT_ID}.jtl jmeter -n -t ${file} -Jserver.rmi.ssl.disable=${SSL_DISABLED}
done done

View File

@ -11,12 +11,12 @@
<div> <div>
<ms-scenario-results :scenarios="content.scenarios" v-on:requestResult="requestResult"/> <ms-scenario-results :scenarios="content.scenarios" v-on:requestResult="requestResult"/>
</div> </div>
<el-collapse-transition> <!--<el-collapse-transition>-->
<div v-show="isActive" style="width: 99%"> <!--<div v-show="isActive" style="width: 99%">-->
<ms-request-result-tail v-if="isRequestResult" :request-type="requestType" :request="request" <!--<ms-request-result-tail v-if="isRequestResult" :request-type="requestType" :request="request"-->
:scenario-name="scenarioName"/> <!--:scenario-name="scenarioName"/>-->
</div> <!--</div>-->
</el-collapse-transition> <!--</el-collapse-transition>-->
<ms-api-report-export v-if="reportExportVisible" id="apiTestReport" :title="report.testName" <ms-api-report-export v-if="reportExportVisible" id="apiTestReport" :title="report.testName"
:content="content" :total-time="totalTime"/> :content="content" :total-time="totalTime"/>
</main> </main>

View File

@ -36,6 +36,13 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<el-collapse-transition>
<div v-show="isActive" style="width: 99%">
<ms-request-result-tail v-if="isActive" :request-type="requestType" :request="request"
:scenario-name="scenarioName"/>
</div>
</el-collapse-transition>
</div> </div>
</template> </template>
@ -44,21 +51,23 @@
import MsAssertionResults from "./AssertionResults"; import MsAssertionResults from "./AssertionResults";
import MsRequestText from "./RequestText"; import MsRequestText from "./RequestText";
import MsResponseText from "./ResponseText"; import MsResponseText from "./ResponseText";
import MsRequestResultTail from "./RequestResultTail";
export default { export default {
name: "MsRequestResult", name: "MsRequestResult",
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric}, components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric, MsRequestResultTail},
props: { props: {
request: Object, request: Object,
scenarioName: String, scenarioName: String,
indexNumber: Number, indexNumber: Number,
}, },
data() { data() {
return {} return {isActive: false, requestType: undefined,}
}, },
methods: { methods: {
active() { active() {
this.$emit("requestResult", {request: this.request, scenarioName: this.scenarioName}); this.isActive = !this.isActive;
//this.$emit("requestResult", {request: this.request, scenarioName: this.scenarioName});
} }
}, },
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<el-dialog :close-on-click-modal="false" :title="$t('api_test.definition.request.title')" :visible.sync="visible" <el-dialog :close-on-click-modal="false" :title="$t('api_test.automation.create_tag')" :visible.sync="visible"
width="45%" width="45%"
:destroy-on-close="true"> :destroy-on-close="true">
<el-form :model="tagForm" label-position="right" label-width="80px" size="small" :rules="rule" ref="tagForm"> <el-form :model="tagForm" label-position="right" label-width="80px" size="small" :rules="rule" ref="tagForm">
@ -16,11 +16,11 @@
</el-form> </el-form>
<el-table :data="tagData" row-key="id"> <el-table :data="tagData" row-key="id">
<el-table-column prop="name" :label="$t('api_test.definition.api_name')" show-overflow-tooltip/> <el-table-column prop="name" :label="$t('commons.name')" show-overflow-tooltip/>
<el-table-column :label="$t('commons.operating')" min-width="130" align="center"> <el-table-column :label="$t('commons.operating')" min-width="130" align="center">
<template v-slot:default="scope"> <template v-slot:default="scope">
<el-button type="text" @click="editApi(scope.row)">编辑</el-button> <el-button type="text" @click="editApi(scope.row)">{{$t('commons.edit')}}</el-button>
<el-button type="text" @click="handleDelete(scope.row)" style="color: #F56C6C">删除</el-button> <el-button type="text" @click="handleDelete(scope.row)" style="color: #F56C6C">{{$t('commons.delete')}}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@ -115,7 +115,8 @@
getApiInfo() { getApiInfo() {
if (this.request.id && this.request.referenced === 'REF') { if (this.request.id && this.request.referenced === 'REF') {
let requestResult = this.request.requestResult; let requestResult = this.request.requestResult;
this.$get("/api/definition/get/" + this.request.id, response => { let url = this.request.refType && this.request.refType === 'CASE' ? "/api/testcase/get/" : "/api/definition/get/";
this.$get(url + this.request.id, response => {
if (response.data) { if (response.data) {
Object.assign(this.request, JSON.parse(response.data.request)); Object.assign(this.request, JSON.parse(response.data.request));
this.request.name = response.data.name; this.request.name = response.data.name;
@ -123,6 +124,7 @@
this.request.method = response.data.method; this.request.method = response.data.method;
this.request.url = response.data.path; this.request.url = response.data.path;
this.request.requestResult = requestResult; this.request.requestResult = requestResult;
this.request.id = response.data.id;
this.reload(); this.reload();
} else { } else {
this.request.referenced = "Deleted"; this.request.referenced = "Deleted";

View File

@ -238,8 +238,8 @@
<!--接口列表--> <!--接口列表-->
<el-drawer :visible.sync="apiListVisible" :destroy-on-close="true" direction="ltr" :withHeader="false" :title="$t('api_test.automation.api_list_import')" :modal="false" size="90%"> <el-drawer :visible.sync="apiListVisible" :destroy-on-close="true" direction="ltr" :withHeader="false" :title="$t('api_test.automation.api_list_import')" :modal="false" size="90%">
<ms-api-definition :visible="visibleRef" :currentRow="currentRow"/> <ms-api-definition :visible="visibleRef" :currentRow="currentRow"/>
<el-button style="float: right;margin: 0px 20px 0px" type="primary" @click="copyApi('REF')">{{$t('api_test.scenario.reference')}}</el-button> <el-button style="float: right;margin: 0px 20px 0px" type="primary" @click="pushApiOrCase('REF')">{{$t('api_test.scenario.reference')}}</el-button>
<el-button style="float: right;" type="primary" @click="copyApi('Copy')">{{ $t('commons.copy') }}</el-button> <el-button style="float: right;" type="primary" @click="pushApiOrCase('Copy')">{{ $t('commons.copy') }}</el-button>
</el-drawer> </el-drawer>
<!--自定义接口--> <!--自定义接口-->
@ -467,50 +467,39 @@
this.reload(); this.reload();
this.scenarioVisible = false; this.scenarioVisible = false;
}, },
copyApi(referenced) { setApiParameter(item, refType, referenced) {
let request = {};
if (Object.prototype.toString.call(item.request).indexOf("String") > 0) {
request = JSON.parse(item.request);
} else {
request = item.request;
}
request.id = item.id;
request.name = item.name;
request.refType = refType;
request.referenced = referenced;
request.enable === undefined ? request.enable = true : request.enable;
request.active = false;
request.resourceId = getUUID();
if (referenced === 'REF' || !request.hashTree) {
request.hashTree = [];
}
if (this.selectedTreeNode != undefined) {
this.selectedTreeNode.hashTree.push(request);
} else {
this.scenarioDefinition.push(request);
}
},
pushApiOrCase(referenced) {
if (this.currentRow.cases.length === 0 && this.currentRow.apis.length === 0) { if (this.currentRow.cases.length === 0 && this.currentRow.apis.length === 0) {
this.$warning(this.$t('api_test.automation.reference_info')); this.$warning(this.$t('api_test.automation.reference_info'));
return; return;
} }
this.currentRow.cases.forEach(item => { this.currentRow.cases.forEach(item => {
let request = {}; this.setApiParameter(item, "CASE", referenced);
if (Object.prototype.toString.call(item.request).indexOf("String") > 0) {
request = JSON.parse(item.request);
} else {
request = item.request;
}
request.referenced = referenced;
request.enable === undefined ? request.enable = true : request.enable;
request.active = false;
request.resourceId = getUUID();
if (referenced === 'REF' || !request.hashTree) {
request.hashTree = [];
}
if (this.selectedTreeNode != undefined) {
this.selectedTreeNode.hashTree.push(request);
} else {
this.scenarioDefinition.push(request);
}
}) })
this.currentRow.apis.forEach(item => { this.currentRow.apis.forEach(item => {
let request = {}; this.setApiParameter(item, "API", referenced);
if (Object.prototype.toString.call(item.request).indexOf("String") > 0) {
request = JSON.parse(item.request);
} else {
request = item.request;
}
request.referenced = referenced;
request.enable === undefined ? request.enable = true : request.enable;
request.active = false;
request.resourceId = getUUID();
if (referenced === 'REF' || !request.hashTree) {
request.hashTree = [];
}
if (this.selectedTreeNode != undefined) {
this.selectedTreeNode.hashTree.push(request);
} else {
this.scenarioDefinition.push(request);
}
}) })
this.apiListVisible = false; this.apiListVisible = false;
this.currentRow.cases = []; this.currentRow.cases = [];

View File

@ -104,11 +104,6 @@
threadGroup.hashTree = []; threadGroup.hashTree = [];
testPlan.hashTree = [threadGroup]; testPlan.hashTree = [threadGroup];
this.runData.forEach(item => { this.runData.forEach(item => {
if (!item.useEnvironment) {
this.$error(this.$t("api_test.environment.select_environment"));
this.$emit('runRefresh', {});
return;
}
threadGroup.hashTree.push(item); threadGroup.hashTree.push(item);
}) })
let reqObj = {id: this.reportId, testElement: testPlan}; let reqObj = {id: this.reportId, testElement: testPlan};

View File

@ -9,9 +9,9 @@
<ms-sql-result-table v-if="isSqlType" :body="response.responseResult.body"/> <ms-sql-result-table v-if="isSqlType" :body="response.responseResult.body"/>
<ms-code-edit v-if="!isSqlType" :mode="mode" :read-only="true" :modes="modes" :data.sync="response.responseResult.body" ref="codeEdit"/> <ms-code-edit v-if="!isSqlType" :mode="mode" :read-only="true" :modes="modes" :data.sync="response.responseResult.body" ref="codeEdit"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="Cookie" name="cookie" class="pane cookie"> <!--<el-tab-pane label="Cookie" name="cookie" class="pane cookie">-->
<pre>{{response.cookies}}</pre> <!--<pre>{{response.cookies}}</pre>-->
</el-tab-pane> <!--</el-tab-pane>-->
<el-tab-pane :label="$t('api_test.definition.request.console')" name="console" class="pane"> <el-tab-pane :label="$t('api_test.definition.request.console')" name="console" class="pane">
<pre>{{response.responseResult.console}}</pre> <pre>{{response.responseResult.console}}</pre>