From 74bca78188b52c9d938ce94628f36c0d4257633e Mon Sep 17 00:00:00 2001 From: metersphere-bot <78466014+metersphere-bot@users.noreply.github.com> Date: Wed, 29 Dec 2021 17:50:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=80=A7=E8=83=BD=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=AF=B9=E6=AF=94=20(#8993)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(性能测试): 版本对比 --user=郭雨琦 版本对比 Co-authored-by: guoyuqi --- .../common/components/MsTableHeader.vue | 5 +- .../performance/test/DiffVersion.vue | 226 ++++++++++++ .../performance/test/EditPerformanceTest.vue | 48 ++- .../components/performance/v_node_diff.js | 347 ++++++++++++++++++ .../business/components/performance/vnode.ts | 99 +++++ 5 files changed, 722 insertions(+), 3 deletions(-) create mode 100644 frontend/src/business/components/performance/test/DiffVersion.vue create mode 100644 frontend/src/business/components/performance/v_node_diff.js create mode 100644 frontend/src/business/components/performance/vnode.ts diff --git a/frontend/src/business/components/common/components/MsTableHeader.vue b/frontend/src/business/components/common/components/MsTableHeader.vue index b21893453a..554b033e4f 100644 --- a/frontend/src/business/components/common/components/MsTableHeader.vue +++ b/frontend/src/business/components/common/components/MsTableHeader.vue @@ -15,8 +15,8 @@ - + @@ -156,6 +156,7 @@ projectId() { return getCurrentProjectID(); }, + } } diff --git a/frontend/src/business/components/performance/test/DiffVersion.vue b/frontend/src/business/components/performance/test/DiffVersion.vue new file mode 100644 index 0000000000..ca588a44f8 --- /dev/null +++ b/frontend/src/business/components/performance/test/DiffVersion.vue @@ -0,0 +1,226 @@ + + + + diff --git a/frontend/src/business/components/performance/test/EditPerformanceTest.vue b/frontend/src/business/components/performance/test/EditPerformanceTest.vue index 40e5470f06..a41510fbf3 100644 --- a/frontend/src/business/components/performance/test/EditPerformanceTest.vue +++ b/frontend/src/business/components/performance/test/EditPerformanceTest.vue @@ -76,6 +76,18 @@ + + + + 取 消 + 确 定 + + + @@ -91,6 +103,7 @@ import MsScheduleConfig from "../../common/components/MsScheduleConfig"; import MsChangeHistory from "../../history/ChangeHistory"; import MsTableOperatorButton from "@/business/components/common/components/MsTableOperatorButton"; import MsTipButton from "@/business/components/common/components/MsTipButton"; +import DiffVersion from "@/business/components/performance/test/DiffVersion"; const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/); const versionHistory = requireComponent.keys().length > 0 ? requireComponent("./version/VersionHistory.vue") : {}; @@ -108,14 +121,19 @@ export default { MsMainContainer, MsChangeHistory, 'MsVersionHistory': versionHistory.default, + DiffVersion }, inject: [ 'reload' ], data() { return { + dialogVisible:false, result: {}, test: {schedule: {}, follows: []}, + oldData: {schedule: {}, follows: []}, + newData:{schedule: {}, follows: []}, + newShowFollow:false, savePath: "/performance/save", editPath: "/performance/edit", runPath: "/performance/run", @@ -183,7 +201,10 @@ export default { }, importAPITest() { let apiTest = this.$store.state.test; + console.log("输出vuex的test") + console.log(apiTest) if (apiTest && apiTest.name) { + console.log("set test name") this.$set(this.test, "name", apiTest.name); if (apiTest.jmx.scenarioId) { this.$refs.basicConfig.importScenario(apiTest.jmx.scenarioId); @@ -220,7 +241,10 @@ export default { this.$store.commit("clearTest"); } else { let scenarioJmxs = this.$store.state.scenarioJmxs; + console.log("输出vuex的scenarioJmxs") + console.log(scenarioJmxs) if (scenarioJmxs && scenarioJmxs.name) { + console.log("set scenarioJmxs name") this.$set(this.test, "name", scenarioJmxs.name); let relateApiList = []; if (scenarioJmxs.jmxs) { @@ -444,6 +468,7 @@ export default { }; }, fileChange(threadGroups) { + console.log("zou") let handler = this.$refs.pressureConfig; let csvSet = new Set; @@ -524,7 +549,27 @@ export default { }); }, compare(row) { - // console.log(row); + this.oldData = this.test; + this.$get('/performance/get/' + row.id+"/"+this.test.refId, response => { + this.$get('/performance/get/' + response.data.id, res => { + if (res.data) { + this.newData = res.data; + this.$get('/performance/test/follow/' + response.data.id, resp => { + if(resp.data&&resp.data.follows){ + for (let i = 0; i v.versionId === row.id)[0]; @@ -580,4 +625,5 @@ export default { margin-right: 25px; margin-top: 5px; } + diff --git a/frontend/src/business/components/performance/v_node_diff.js b/frontend/src/business/components/performance/v_node_diff.js new file mode 100644 index 0000000000..8feeed4d49 --- /dev/null +++ b/frontend/src/business/components/performance/v_node_diff.js @@ -0,0 +1,347 @@ + +function isUndef (v) { + return v === undefined || v === null +} + +function isDef (v){ + return v !== undefined && v !== null +} + +function isTrue (v){ + return v === true +} + +function makeMap (str, expectsLowerCase){ + const map = Object.create(null) + const list= str.split(',') + for (let i = 0; i < list.length; i++) { + map[list[i]] = true + } + return expectsLowerCase + ? val => map[val.toLowerCase()] + : val => map[val] +} + +const isTextInputType = makeMap('text,number,password,search,email,tel,url') + +/** + * 该比较方法认为 1,2 -> 2,1 版本是有改变的 + * @param oldDom 被比较的节点 + * @param newDom 比较的节点 + */ +export function diff (oldDom, newDom) { + let diffNode = { + oldNodeArray:[], + nodeArray:[], + } + //两个独立的节点传过来 首先单独比较一下他们最外层的 dom的_vnode 如果 _vnode 相同 则认为最外层节点相同,否则更改样式 return + let oldVnode = oldDom._vnode; + let newVnode = newDom._vnode; + + if (isUndef(newVnode)) { //如果新节点不存在 + //如果旧节点存在, 旧节点加背景颜色 + if (isDef(oldVnode)) { + diffNode.oldNodeArray.push(oldVnode.elm) + } + return + } + if (isUndef(oldVnode)) {//如果旧节点不存在 + //如果新节点存在, 新节点加背景颜色 + if (isDef(newVnode)) { + diffNode.nodeArray.push(newVnode.elm) + } + return + } + + if (sameVnode(oldVnode, newVnode)) { + //逐层比较节点 这里涉及到 children 的length 可能不同的情况 + diffDetail(oldVnode, newVnode,diffNode) + + } else { + diffNode.oldNodeArray.push(oldVnode.elm) + diffNode.nodeArray.push(newVnode.elm) + } + + changeStyle(diffNode); +} + +function changeStyle(diffNode){ + console.log("查看结果"); + console.log(diffNode.oldNodeArray); + console.log(diffNode.nodeArray); + for (let i = 0; i < diffNode.oldNodeArray.length; i++) { + if(diffNode.oldNodeArray[i]==='comment'||isUndef(diffNode.oldNodeArray[i].style)){ + continue + } + if(diffNode.oldNodeArray[i].className==='cell'){ + let rowVnodeElm = findRowVnodeElm(diffNode.oldNodeArray[i]); + rowVnodeElm.style.setProperty("background-color","rgb(241,200,196)",'important') + }else{ + changeStyleBySubset(diffNode.oldNodeArray[i],"rgb(241,200,196)"); + } + } + + for (let i = 0; i < diffNode.nodeArray.length; i++) { + if(diffNode.nodeArray[i]==='comment'||isUndef(diffNode.nodeArray[i].style)){ + continue + } + if(diffNode.nodeArray[i].className==='cell'){ + let rowVnodeElm = findRowVnodeElm(diffNode.nodeArray[i]); + rowVnodeElm.style.setProperty("background-color","rgb(215, 243, 215)",'important') + }else{ + changeStyleBySubset(diffNode.nodeArray[i],"rgb(215, 243, 215)"); + } + } +} + +function changeStyleBySubset(vnodeElm,color){ + if(isDef(vnodeElm.children)&&vnodeElm.children.length>0){ + if(isDef(vnodeElm.style)){ + vnodeElm.style.setProperty("background-color",color,'important') + } + for (let i = 0; i < vnodeElm.children.length; i++) { + changeStyleBySubset(vnodeElm.children[i],color); + } + }else { + if(isDef(vnodeElm.style)){ + vnodeElm.style.setProperty("background-color",color,'important') + }else { + vnodeElm.parentNode.style.setProperty("background-color",color,'important') + } + } +} + +/** + * + * @param oldChildren Array + * @param newChildren Array + * @param diffNode + * @param isCompareChildren + */ +function diffChildren(oldChildren,newChildren,diffNode,isCompareChildren){ + let oldLength = oldChildren.length; + let newLength = newChildren.length; + //如果isCompareChildren===true,证明是需要轮巡比较table内容是否有重复的,方法是比较每个元素的最子dom,tr的td的length是相等的; + if(isCompareChildren===true){ + let oldIndexArray = []; + let newIndexArray = []; + //现在oldChildren,newChildren是tbody的所有数据, + for (let i = 0; i < oldLength; i++) { + for (let j = 0; j < newLength; j++) { + let sameNode = { + nodeArray:[], + } + //找每行的数据,但是行是确定的,这里应该直接比较每个tr的 children + sameDetail(oldChildren[i],newChildren[j],sameNode) + if(sameNode.nodeArray.length>0){ + //证明oldChildren[i]与 newChildren[j] 是相同的 内容 + if(oldIndexArray.indexOf(i) === -1){ + oldIndexArray.push(i); + } + if(newIndexArray.indexOf(j) === -1){ + newIndexArray.push(j); + } + } + } + } + for (let j = 0; j < oldLength; j++) { + if(oldIndexArray.indexOf(j)===-1){ + diffNode.oldNodeArray.push(oldChildren[j].elm); + } + } + + for (let j = 0; j < newLength; j++) { + if(newIndexArray.indexOf(j)===-1){ + diffNode.nodeArray.push(newChildren[j].elm); + } + } + + }else { + //取二者公共长度 + let childrenLength = Math.min(oldLength, newLength); + for (let i = 0; i < childrenLength; i++) { + let oldVnode = oldChildren[i] + let newVnode = newChildren[i] + diffDetail(oldVnode,newVnode,diffNode) + } + for (let i = childrenLength; i <= (oldLength - childrenLength); i++) { + if(oldChildren[i]){ + diffNode.oldNodeArray.push(oldChildren[i].elm); + } + } + for (let i = childrenLength; i <= (newLength - childrenLength); i++) { + if(newChildren[i]){ + diffNode.nodeArray.push(newChildren[i].elm); + } + } + } +} +function sameChildren(oldChildren,newChildren,sameNode){ + let oldLength = oldChildren.length; + for (let i = 0; i < oldLength; i++) { + let oldVnode = oldChildren[i] + let newVnode = newChildren[i] + sameDetail(oldVnode,newVnode,sameNode) + if(sameNode.nodeArray.length===0){ + return; + } + } +} + +function sameDetail(oldVnode,newVnode,sameNode){ + if(isDef(oldVnode.child)&&isDef(newVnode.child)){ + let ovnode = oldVnode.child._vnode; + let nvnode = newVnode.child._vnode; + sameDetail(ovnode,nvnode,sameNode) + if(sameNode.nodeArray.length===0){ + return; + } + } + if(isDef(oldVnode.children)&&isDef(newVnode.children)){ + sameChildren(oldVnode.children,newVnode.children,sameNode) + } + //剩最后的子节点的时候,分类型做判断 + if(isUndef(oldVnode.child)&&isUndef(newVnode.child)&&isUndef(oldVnode.children)&&isUndef(newVnode.children)){ + if(isDef(oldVnode.text)&&isDef(newVnode.text)){ + if(oldVnode.text===newVnode.text){ + sameNode.nodeArray.push(newVnode.elm); + }else{ + sameNode.nodeArray = []; + } + }else if(isDef(oldVnode.tag)&&isDef(newVnode.tag)){ + if(oldVnode.tag==='input'&&newVnode.tag==='input'){ + if(oldVnode.elm.value===newVnode.elm.value){ + if(oldVnode.elm.checked!==undefined&&newVnode.elm.checked!==undefined){ + if(oldVnode.elm.checked===newVnode.elm.checked){ + sameNode.nodeArray.push(newVnode.elm); + }else { + sameNode.nodeArray = []; + } + }else { + sameNode.nodeArray.push(newVnode.elm); + } + }else{ + sameNode.nodeArray = []; + } + } + } + else { + if(sameVnode(oldVnode,newVnode)){ + sameNode.nodeArray.push(newVnode.elm); + }else{ + sameNode.nodeArray = []; + } + } + } + +} + + +function diffDetail(oldVnode,newVnode,diffNode){ + if(isDef(oldVnode.child)&&isUndef(newVnode.child)){ + diffNode.oldNodeArray.push(oldVnode.child._vnode.elm); + } + if(isDef(oldVnode.children)&&isUndef(newVnode.children)){ + diffNode.oldNodeArray.push(oldVnode.elm); + } + if(isUndef(oldVnode.child)&&isDef(newVnode.child)){ + diffNode.nodeArray.push(newVnode.child._vnode.elm); + } + if(isUndef(oldVnode.children)&&isDef(newVnode.children)){ + diffNode.nodeArray.push(newVnode.elm); + } + if(isDef(oldVnode.child)&&isDef(newVnode.child)){ + let ovnode = oldVnode.child._vnode; + let nvnode = newVnode.child._vnode; + diffDetail(ovnode,nvnode,diffNode) + } + if(isDef(oldVnode.children)&&isDef(newVnode.children)){ + //处理节点数据结构为table的情况 + let isCompareChildren = false; + if(oldVnode.tag==='tbody'&&newVnode.tag==='tbody'){ + isCompareChildren = true; + }else + if(isDef(oldVnode.elm.className)&&isDef(newVnode.elm.className)){ + if(oldVnode.elm.className==='el-collapse'&&newVnode.elm.className==='el-collapse'){ + isCompareChildren = true; + } + } + diffChildren(oldVnode.children,newVnode.children,diffNode,isCompareChildren) + } + //剩最后的子节点的时候,分类型做判断 + if(isUndef(oldVnode.child)&&isUndef(newVnode.child)&&isUndef(oldVnode.children)&&isUndef(newVnode.children)){ + + if(isDef(oldVnode.text)&&isDef(newVnode.text)){ + if(oldVnode.text!==newVnode.text){ + if(isDef(oldVnode.elm.style)){ + diffNode.oldNodeArray.push(oldVnode.elm); + }else { + diffNode.oldNodeArray.push(oldVnode.elm.parentNode); + } + if(isDef(newVnode.elm.style)){ + diffNode.nodeArray.push(newVnode.elm); + }else { + diffNode.nodeArray.push(newVnode.elm.parentNode); + } + } + }else if(isDef(oldVnode.tag)&&isDef(newVnode.tag)){ + if(oldVnode.tag==='input'&&newVnode.tag==='input'){ + if(oldVnode.elm.value!==newVnode.elm.value){ + diffNode.oldNodeArray.push(oldVnode.elm); + diffNode.nodeArray.push(newVnode.elm); + }else { + if(oldVnode.elm.checked!==undefined&&newVnode.elm.checked!==undefined){ + if(oldVnode.elm.checked!==newVnode.elm.checked){ + diffNode.oldNodeArray.push(oldVnode.elm); + diffNode.nodeArray.push(newVnode.elm); + } + } + } + } + } + else { + if(!sameVnode(oldVnode,newVnode)){ + diffNode.oldNodeArray.push(oldVnode.elm); + diffNode.nodeArray.push(newVnode.elm); + } + } + } + +} + +function sameVnode (a, b) { + return ( + a.key === b.key && + a.asyncFactory === b.asyncFactory && ( + ( + a.tag === b.tag && + a.isComment === b.isComment && + isDef(a.data) === isDef(b.data) && + sameInputType(a, b) + ) || ( + isTrue(a.isAsyncPlaceholder) && + isUndef(b.asyncFactory.error) + ) + ) + ) +} + +function findRowVnodeElm(nodeElm){ + if(nodeElm.localName==="td"||nodeElm.className==="cell"){ + return findRowVnodeElm(nodeElm.parentNode) + }else if(nodeElm.localName==="tr"){ + return nodeElm; + }else { + return nodeElm; + } +} + + +function sameInputType (a, b) { + if (a.tag !== 'input') return true + let i + const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type + const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type + return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) +} + diff --git a/frontend/src/business/components/performance/vnode.ts b/frontend/src/business/components/performance/vnode.ts new file mode 100644 index 0000000000..feb8ab3fdb --- /dev/null +++ b/frontend/src/business/components/performance/vnode.ts @@ -0,0 +1,99 @@ +/* @flow */ +import {Component, ComponentOptions, VNodeComponentOptions, VNodeData} from "vue"; + +export default class VNode { + tag: string | void; + data: VNodeData | void; + children: VNode[]; + text: string | void; + elm: Node | void; + ns: string | void; + context: Component | void; // rendered in this component's scope + key: string | number | void; + componentOptions: VNodeComponentOptions | void; + componentInstance: Component | void; // component instance + parent: VNode | void; // component placeholder node + + // strictly internal + raw: boolean; // contains raw HTML? (server only) + isStatic: boolean; // hoisted static node + isRootInsert: boolean; // necessary for enter transition check + isComment: boolean; // empty comment placeholder? + isCloned: boolean; // is a cloned node? + isOnce: boolean; // is a v-once node? + asyncFactory: Function | void; // async component factory function + asyncMeta: Object | void; + isAsyncPlaceholder: boolean; + ssrContext: Object | void; + fnContext: Component | void; // real context vm for functional nodes + fnOptions: any; // for SSR caching + devtoolsMeta: Object; // used to store functional render context for devtools + fnScopeId: string; // functional scope id support + + constructor( + tag?: string | void, + data?: VNodeData | void, + children?: VNode[], + text?: string | void, + elm?: Node | void, + context?: Component | void, + componentOptions?: VNodeComponentOptions | void, + asyncFactory?: Function | void + ) { + this.tag = tag + this.data = data + this.children = children + this.text = text + this.elm = elm + this.ns = undefined + this.context = context + this.fnContext = undefined + this.fnOptions = undefined + this.fnScopeId = undefined + this.key = data && data.key + this.componentOptions = componentOptions + this.componentInstance = undefined + this.parent = undefined + this.raw = false + this.isStatic = false + this.isRootInsert = true + this.isComment = false + this.isCloned = false + this.isOnce = false + this.asyncFactory = asyncFactory + this.asyncMeta = undefined + this.isAsyncPlaceholder = false + } + + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + get child (): Component | void { + return this.componentInstance + } +} + +export function cloneVNode (vnode: VNode): VNode { + const cloned = new VNode( + vnode.tag, + vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ) + cloned.ns = vnode.ns + cloned.isStatic = vnode.isStatic + cloned.key = vnode.key + cloned.isComment = vnode.isComment + cloned.fnContext = vnode.fnContext + cloned.fnOptions = vnode.fnOptions + cloned.fnScopeId = vnode.fnScopeId + cloned.asyncMeta = vnode.asyncMeta + cloned.isCloned = true + return cloned +}