feat(接口自动化): 单个报告执行和调试统一 实时接收结果

This commit is contained in:
fit2-zhao 2021-07-16 18:39:41 +08:00 committed by fit2-zhao
parent 12243e7a83
commit 0ff5adf993
8 changed files with 536 additions and 24 deletions

View File

@ -126,7 +126,7 @@ public class JMeterService {
init();
FixedTask.tasks.put(testId, System.currentTimeMillis());
addBackendListener(testId, debugReportId, runMode, testPlan);
if (ExecuteType.Debug.name().equals(debugReportId)) {
if (ExecuteType.Debug.name().equals(debugReportId) || ApiRunMode.SCENARIO.name().equals(runMode)) {
addResultCollector(testId, testPlan);
}
LocalRunner runner = new LocalRunner(testPlan);

View File

@ -1,5 +1,6 @@
package io.metersphere.api.service;
import com.alibaba.fastjson.JSON;
import io.metersphere.api.dto.scenario.request.RequestType;
import io.metersphere.api.jmeter.*;
import io.metersphere.commons.utils.LogUtil;
@ -34,6 +35,9 @@ public class MsResultService {
}
public void setCache(String key, SampleResult result) {
if (key.startsWith("[") && key.endsWith("]")) {
key = JSON.parseArray(key).get(0).toString();
}
TestResult testResult = this.getResult(key);
if (testResult == null) {
testResult = new TestResult();
@ -98,9 +102,16 @@ public class MsResultService {
public String getJmeterLogger(String testId, boolean removed) {
Long startTime = FixedTask.tasks.get(testId);
if (startTime == null) {
startTime = FixedTask.tasks.get("[" + testId + "]");
}
if (startTime == null) {
startTime = System.currentTimeMillis();
}
Long endTime = System.currentTimeMillis();
Long finalStartTime = startTime;
String logMessage = JmeterLoggerAppender.logger.entrySet().stream()
.filter(map -> map.getKey() > startTime && map.getKey() < endTime)
.filter(map -> map.getKey() > finalStartTime && map.getKey() < endTime)
.map(map -> map.getValue()).collect(Collectors.joining());
if (removed) {
FixedTask.tasks.remove(testId);

View File

@ -0,0 +1,480 @@
<template>
<ms-container v-loading="loading">
<ms-main-container>
<el-card>
<section class="report-container">
<ms-api-report-view-header :debug="debug" :report="report" @reportExport="handleExport"/>
<main>
<ms-metric-chart :content="content" :totalTime="totalTime" v-if="!loading"/>
<div>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane :label="$t('api_report.total')" name="total">
<ms-scenario-results :treeData="fullTreeNodes" :default-expand="true" :console="content.console" v-on:requestResult="requestResult"/>
</el-tab-pane>
<el-tab-pane name="fail">
<template slot="label">
<span class="fail">{{ $t('api_report.fail') }}</span>
</template>
<ms-scenario-results v-on:requestResult="requestResult" :console="content.console" :treeData="failsTreeNodes"/>
</el-tab-pane>
</el-tabs>
</div>
<ms-api-report-export v-if="reportExportVisible" id="apiTestReport" :title="report.testName"
:content="content" :total-time="totalTime"/>
</main>
</section>
</el-card>
</ms-main-container>
</ms-container>
</template>
<script>
import MsRequestResult from "./components/RequestResult";
import MsRequestResultTail from "./components/RequestResultTail";
import MsScenarioResult from "./components/ScenarioResult";
import MsMetricChart from "./components/MetricChart";
import MsScenarioResults from "./components/ScenarioResults";
import MsContainer from "@/business/components/common/components/MsContainer";
import MsMainContainer from "@/business/components/common/components/MsMainContainer";
import MsApiReportExport from "./ApiReportExport";
import MsApiReportViewHeader from "./ApiReportViewHeader";
import {RequestFactory} from "../../definition/model/ApiTestModel";
import {windowPrint, getCurrentProjectID} from "@/common/js/utils";
import {ELEMENTS} from "../scenario/Setting";
export default {
name: "SysnApiReportDetail",
components: {
MsApiReportViewHeader,
MsApiReportExport,
MsMainContainer,
MsContainer, MsScenarioResults, MsRequestResultTail, MsMetricChart, MsScenarioResult, MsRequestResult
},
data() {
return {
activeName: "total",
content: {total: 0, scenarioTotal: 1},
report: {},
loading: false,
fails: [],
failsTreeNodes: [],
totalTime: 0,
isRequestResult: false,
request: {},
isActive: false,
scenarioName: null,
reportExportVisible: false,
requestType: undefined,
fullTreeNodes: [],
debugResult: new Map,
scenarioMap: new Map,
}
},
activated() {
this.isRequestResult = false;
},
created() {
if (this.scenarioId) {
this.getApiScenario();
} else {
if (this.scenario && this.scenario.scenarioDefinition) {
this.content.scenarioStepTotal = this.scenario.scenarioDefinition.hashTree.length;
this.initTree();
this.initWebSocket();
this.clearDebug();
}
}
},
props: {
reportId: String,
currentProjectId: String,
infoDb: Boolean,
debug: Boolean,
scenario: {},
scenarioId: String
},
methods: {
getApiScenario() {
this.loading = true;
this.result = this.$get("/api/automation/getApiScenario/" + this.scenarioId, response => {
if (response.data) {
this.path = "/api/automation/update";
if (response.data.scenarioDefinition != null) {
let obj = JSON.parse(response.data.scenarioDefinition);
this.scenario.scenarioDefinition = obj;
this.scenario.name = response.data.name;
this.content.scenarioStepTotal = obj.hashTree.length;
this.initTree();
this.initWebSocket();
this.clearDebug();
this.loading = false;
}
}
})
},
initTree() {
this.fullTreeNodes = [];
let obj = {index: 1, label: this.scenario.name, value: {responseResult: {}, unexecute: true}, children: [], unsolicited: true};
this.formatContent(this.scenario.scenarioDefinition.hashTree, obj);
this.fullTreeNodes.push(obj);
},
setTreeValue(arr) {
arr.forEach(item => {
if (this.debugResult && this.debugResult.get(item.resId)) {
let arrValue = this.debugResult.get(item.resId);
if (arrValue.length > 1) {
for (let i = 0; i < arrValue.length; i++) {
let obj = {resId: item.resId, index: i, label: item.resId, value: arrValue[i], children: []};
let isAdd = true;
arr.forEach(obj => {
if (obj.value.name === arrValue[i].name) {
isAdd = false;
}
if (obj.value.name.indexOf("循环-") === -1) {
arr.splice(0, 1);
}
})
if (isAdd) {
arr.push(obj);
}
}
} else {
item.value = arrValue[0];
}
}
if (item.children && item.children.length > 0) {
this.setTreeValue(item.children);
}
})
},
formatContent(hashTree, tree) {
if (hashTree) {
hashTree.forEach(item => {
if (item.enable) {
let key = item.id + item.name;
let name = item.name ? item.name : item.type;
let obj = {resId: key, index: Number(item.index), label: name, value: {name: name, responseResult: {}, unexecute: true}, children: [], unsolicited: true};
tree.children.push(obj);
if (ELEMENTS.get("AllSamplerProxy").indexOf(item.type) != -1) {
obj.unsolicited = false;
obj.type = item.type;
} else if (item.type === 'scenario') {
this.content.scenarioTotal += 1;
}
if (item.hashTree && item.hashTree.length > 0 && ELEMENTS.get("AllSamplerProxy").indexOf(item.type) === -1) {
this.formatContent(item.hashTree, obj);
}
}
})
}
},
handleExport() {
this.reportExportVisible = true;
let reset = this.exportReportReset;
this.$nextTick(() => {
windowPrint('apiTestReport', 0.57);
reset();
});
},
handleClick(tab, event) {
this.isRequestResult = false
},
exportReportReset() {
this.$router.go(0);
},
requestResult(requestResult) {
this.active();
this.isRequestResult = false;
this.requestType = undefined;
if (requestResult.request.body.indexOf('[Callable Statement]') > -1) {
this.requestType = RequestFactory.TYPES.SQL;
}
this.$nextTick(function () {
this.isRequestResult = true;
this.request = requestResult.request;
this.scenarioName = requestResult.scenarioName;
});
},
clearDebug() {
this.totalTime = 0;
this.content.total = 0;
this.content.error = 0;
this.content.success = 0;
this.content.passAssertions = 0;
this.content.totalAssertions = 0;
this.content.scenarioSuccess = 0;
this.content.scenarioError = 0;
},
initWebSocket() {
let protocol = "ws://";
if (window.location.protocol === 'https:') {
protocol = "wss://";
}
const uri = protocol + window.location.host + "/api/scenario/report/get/real/" + this.reportId;
this.websocket = new WebSocket(uri);
this.websocket.onmessage = this.onMessage;
this.websocket.onopen = this.onOpen;
this.websocket.onerror = this.onError;
this.websocket.onclose = this.onClose;
},
onOpen() {
},
onError(e) {
window.console.error(e)
},
onMessage(e) {
if (e.data) {
let data = JSON.parse(e.data);
this.formatResult(data);
if (data.end) {
this.removeReport();
this.getReport();
}
}
},
onClose(e) {
if (e.code === 1005) {
return;
}
},
removeReport() {
let url = "/api/scenario/report/remove/real/" + this.reportId;
this.$get(url, response => {
this.$success(this.$t('schedule.event_success'));
});
},
formatResult(res) {
let resMap = new Map;
let startTime = 99991611737506593;
let endTime = 0;
this.clearDebug();
if (res && res.scenarios) {
res.scenarios.forEach(item => {
this.content.total += item.requestResults.length;
this.content.passAssertions += item.passAssertions
this.content.totalAssertions += item.totalAssertions;
if (item && item.requestResults) {
item.requestResults.forEach(req => {
req.responseResult.console = res.console;
let name = req.name.split('<->')[0];
let key = req.id + name;
if (resMap.get(key)) {
if (resMap.get(key).indexOf(req) === -1) {
resMap.get(key).push(req);
}
} else {
resMap.set(key, [req]);
}
if (req.success) {
this.content.success++;
} else {
this.content.error++;
}
if (req.startTime && Number(req.startTime) < startTime) {
startTime = req.startTime;
}
if (req.endTime && Number(req.endTime) > endTime) {
endTime = req.endTime;
}
})
}
})
}
if (startTime < endTime) {
this.totalTime = endTime - startTime + 100;
}
this.debugResult = resMap;
this.setTreeValue(this.fullTreeNodes);
this.reload();
},
reload() {
this.loading = true
this.$nextTick(() => {
this.loading = false
});
},
getReport() {
let url = "/api/scenario/report/get/" + this.reportId;
this.$get(url, response => {
this.report = response.data || {};
if (response.data) {
try {
this.content = JSON.parse(this.report.content);
if (!this.content) {
this.content = {scenarios: []};
}
} catch (e) {
throw e;
}
this.getFails();
}
});
},
getFails() {
this.fails = [];
let array = [];
if (this.content.scenarios) {
this.content.scenarios.forEach((scenario) => {
let failScenario = Object.assign({}, scenario);
if (scenario.error > 0) {
this.fails.push(failScenario);
failScenario.requestResults = [];
scenario.requestResults.forEach((request) => {
if (!request.success) {
let failRequest = Object.assign({}, request);
failScenario.requestResults.push(failRequest);
array.push(request);
}
})
}
})
this.formatTree(array, this.failsTreeNodes);
this.recursiveSorting(this.failsTreeNodes);
}
},
formatTree(array, tree) {
array.map((item) => {
let key = item.name;
let nodeArray = key.split('^@~@^');
let children = tree;
let scenarioId = "";
let scenarioName = "";
if (item.scenario) {
let scenarioArr = JSON.parse(item.scenario);
if (scenarioArr.length > 1) {
let scenarioIdArr = scenarioArr[0].split("_");
scenarioId = scenarioIdArr[0];
scenarioName = scenarioIdArr[1];
}
}
//
for (let i = 0; i < nodeArray.length; i++) {
if (!nodeArray[i]) {
continue;
}
let node = {
label: nodeArray[i],
value: item,
};
if (i !== nodeArray.length) {
node.children = [];
}
if (children.length === 0) {
children.push(node);
}
let isExist = false;
for (let j in children) {
if (children[j].label === node.label) {
let idIsPath = true;
//ID
//
if (i === nodeArray.length - 2) {
idIsPath = false;
let childId = "";
let childName = "";
if (children[j].value && children[j].value.scenario) {
let scenarioArr = JSON.parse(children[j].value.scenario);
if (scenarioArr.length > 1) {
let childArr = scenarioArr[0].split("_");
childId = childArr[0];
if (childArr.length > 1) {
childName = childArr[1];
}
}
}
if (scenarioId === "") {
idIsPath = true;
} else if (scenarioId === childId) {
idIsPath = true;
} else if (scenarioName !== childName) {
//ID
idIsPath = true;
}
}
if (idIsPath) {
if (i !== nodeArray.length - 1 && !children[j].children) {
children[j].children = [];
}
children = (i === nodeArray.length - 1 ? children : children[j].children);
isExist = true;
break;
}
}
}
if (!isExist) {
children.push(node);
if (i !== nodeArray.length - 1 && !children[children.length - 1].children) {
children[children.length - 1].children = [];
}
children = (i === nodeArray.length - 1 ? children : children[children.length - 1].children);
}
}
})
},
recursiveSorting(arr) {
for (let i in arr) {
if (arr[i]) {
arr[i].index = Number(i) + 1;
if (arr[i].children && arr[i].children.length > 0) {
this.recursiveSorting(arr[i].children);
}
}
}
},
},
computed: {
projectId() {
return getCurrentProjectID();
},
}
}
</script>
<style>
.report-container .el-tabs__header {
margin-bottom: 1px;
}
</style>
<style scoped>
.report-container {
height: calc(100vh - 155px);
min-height: 600px;
overflow-y: auto;
}
.report-header {
font-size: 15px;
}
.report-header a {
text-decoration: none;
}
.report-header .time {
color: #909399;
margin-left: 10px;
}
.report-container .fail {
color: #F56C6C;
}
.report-container .is-active .fail {
color: inherit;
}
.export-button {
float: right;
}
.scenario-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -34,19 +34,16 @@
</el-col>
<el-col :span="2">
<div>
<el-tag size="mini" type="success" v-if="request.success">
{{ $t('api_report.success') }}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{ $t('api_report.fail') }}
</el-tag>
<el-tag size="mini" v-if="request.unexecute">{{ $t('api_test.home_page.detail_card.unexecute') }}</el-tag>
<el-tag size="mini" type="success" v-else-if="request.success"> {{ $t('api_report.success') }}</el-tag>
<el-tag size="mini" type="danger" v-else> {{ $t('api_report.fail') }}</el-tag>
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive" style="width: 99%">
<div v-show="isActive && !request.unexecute" style="width: 99%">
<ms-request-result-tail :scenario-name="scenarioName"
:request-type="requestType"
:request="request"
@ -100,18 +97,29 @@ export default {
},
methods: {
active() {
this.isActive = !this.isActive;
if (this.request.unexecute) {
this.isActive = false;
} else {
this.isActive = !this.isActive;
}
},
getName(name) {
if (name && name.indexOf("<->") !== -1) {
return name.split("<->")[0];
}
if (name && name.indexOf("^@~@^") !== -1) {
let arr = name.split("^@~@^");
if (arr[arr.length - 1].indexOf("UUID=")) {
return arr[arr.length - 1].split("UUID=")[0];
let value = arr[arr.length - 1];
if (value.indexOf("UUID=") !== -1) {
return value.split("UUID=")[0];
}
if (arr[arr.length - 1] && arr[arr.length - 1].startsWith("UUID=")) {
if (value && value.startsWith("UUID=")) {
return "";
}
return arr[arr.length - 1];
if (value && value.indexOf("<->") !== -1) {
return value.split("<->")[0];
}
return value;
}
if (name && name.startsWith("UUID=")) {
return "";
@ -209,6 +217,7 @@ export default {
.icon.is-active {
transform: rotate(90deg);
}
.ms-req-name {
display: inline-block;
margin: 0 5px;

View File

@ -1,6 +1,6 @@
<template>
<div class="scenario-result">
<div v-if="node.children && node.children.length >0 ">
<div v-if="(node.children && node.children.length >0) || node.unsolicited">
<el-card class="ms-card">
<div class="el-step__icon is-text ms-api-col">
<div class="el-step__icon-inner"> {{ node.index }}</div>

View File

@ -2,8 +2,9 @@
<el-card class="scenario-results">
<el-tree :data="treeData"
:expand-on-click-node="false"
:default-expand-all="defaultExpand"
highlight-current
class="ms-tree ms-report-tree">
class="ms-tree ms-report-tree" ref="resultsTree">
<span slot-scope="{ node, data}" style="width: 99%" @click="nodeClick(node)">
<ms-scenario-result :node="data" :console="console" v-on:requestResult="requestResult"/>
</span>
@ -21,6 +22,15 @@ export default {
scenarios: Array,
treeData: Array,
console: String,
defaultExpand: {
default: false,
type: Boolean,
}
},
created() {
if (this.$refs.resultsTree && this.$refs.resultsTree.root) {
this.$refs.resultsTree.root.expanded = true;
}
},
methods: {
requestResult(requestResult) {

View File

@ -190,7 +190,7 @@
<!-- 执行结果 -->
<el-drawer :visible.sync="runVisible" :destroy-on-close="true" direction="ltr" :withHeader="true" :modal="false"
size="90%">
<ms-api-report-detail @refresh="search" :infoDb="infoDb" :report-id="reportId" :currentProjectId="projectId"/>
<ms-api-report-detail @refresh="search" :debug="true" :scenario="currentScenario" :scenarioId="scenarioId" :infoDb="infoDb" :report-id="reportId" :currentProjectId="projectId"/>
</el-drawer>
<!--测试计划-->
<el-drawer :visible.sync="planVisible" :destroy-on-close="true" direction="ltr" :withHeader="false"
@ -214,8 +214,8 @@ import MsTableHeader from "@/business/components/common/components/MsTableHeader
import MsTablePagination from "@/business/components/common/pagination/TablePagination";
import ShowMoreBtn from "@/business/components/track/case/components/ShowMoreBtn";
import MsTag from "../../../common/components/MsTag";
import {downloadFile, getCurrentProjectID, getUUID, strMapToObj} from "@/common/js/utils";
import MsApiReportDetail from "../report/ApiReportDetail";
import {downloadFile, getCurrentProjectID, getUUID, objToStrMap, strMapToObj} from "@/common/js/utils";
import MsApiReportDetail from "../report/SysnApiReportDetail";
import MsTableMoreBtn from "./TableMoreBtn";
import MsScenarioExtendButtons from "@/business/components/api/automation/scenario/ScenarioExtendBtns";
import MsTestPlanList from "./testplan/TestPlanList";
@ -305,7 +305,7 @@ export default {
default: false
},
initApiTableOpretion: String,
isRelate: Boolean
isRelate: Boolean,
},
data() {
return {
@ -319,6 +319,7 @@ export default {
condition: {
components: API_SCENARIO_CONFIGS
},
scenarioId: "",
currentScenario: {},
schedule: {},
tableData: [],
@ -466,7 +467,7 @@ export default {
if (!this.projectName || this.projectName === "") {
this.getProjectName();
}
if(!this.isReferenceTable){
if (!this.isReferenceTable) {
this.operators = this.unTrashOperators;
this.buttons = this.unTrashButtons;
}
@ -775,6 +776,7 @@ export default {
execute(row) {
this.infoDb = false;
this.scenarioId = row.id;
let url = "/api/automation/run";
let run = {};
let scenarioIds = [];
@ -783,7 +785,6 @@ export default {
run.projectId = this.projectId;
run.ids = scenarioIds;
this.$post(url, run, response => {
let data = response.data;
this.runVisible = true;
this.reportId = run.id;
});

View File

@ -245,7 +245,7 @@
<!-- 调试结果 -->
<el-drawer v-if="type!=='detail'" :visible.sync="debugVisible" :destroy-on-close="true" direction="ltr"
:withHeader="true" :modal="false" size="90%">
<ms-api-report-detail :report-id="reportId" :debug="true" :currentProjectId="projectId" @refresh="detailRefresh"/>
<ms-api-report-detail :scenario="currentScenario" :report-id="reportId" :debug="true" :currentProjectId="projectId" @refresh="detailRefresh"/>
</el-drawer>
<!--场景公共参数-->
@ -296,7 +296,7 @@ import {
import ApiEnvironmentConfig from "@/business/components/api/test/components/ApiEnvironmentConfig";
import MsInputTag from "./MsInputTag";
import MsRun from "./DebugRun";
import MsApiReportDetail from "../report/ApiReportDetail";
import MsApiReportDetail from "../report/SysnApiReportDetail";
import MsVariableList from "./variable/VariableList";
import ApiImport from "../../definition/components/import/ApiImport";
import "@/common/css/material-icons.css"
@ -571,6 +571,7 @@ export default {
promise.then(() => {
let sign = this.$refs.envPopover.checkEnv(this.isFullUrl);
if (!sign) {
this.debugLoading = false;
return;
}
this.editScenario().then(() => {