基于百度腦圖的用例增量保存 + diff 展示整體設(shè)計(jì)

背景

現(xiàn)在所在公司用上了滴滴出品的 agileTC ,整體上非常好用。但有個(gè)功能大家都在呼喚:支持測(cè)試任務(wù)中修改用例集內(nèi)容蚌吸,并同步修改到完整用例集中。所以有了這篇文章砌庄。

先說(shuō)明下具體使用的場(chǎng)景羹唠,讓大家更理解為什么要做這個(gè)功能,目的是什么:

首先娄昆,測(cè)試集佩微、測(cè)試任務(wù)這塊是本身 agileTC 的已有設(shè)計(jì),測(cè)試集用于存儲(chǔ)測(cè)試用例萌焰,測(cè)試任務(wù)用于執(zhí)行用例哺眯。測(cè)試任務(wù)可以在用例集中篩選/全選用例,進(jìn)行每個(gè)用例的測(cè)試結(jié)果登記扒俯。但測(cè)試用例不能修改用例奶卓,要修改用例必須到用例集編輯界面(此界面無(wú)篩選功能)

在我司的實(shí)際項(xiàng)目使用中,常見(jiàn)用法是:

1撼玄、項(xiàng)目前期主要是少數(shù) 1-2 人參與夺姑,他們根據(jù)需求及技術(shù)方案,完成初版的完整用例掌猛,并在里面標(biāo)記開(kāi)發(fā)自測(cè)用例盏浙。用例評(píng)審用的也是這一版。

2、提測(cè)前開(kāi)發(fā)自測(cè)時(shí)废膘,測(cè)試會(huì)創(chuàng)建對(duì)應(yīng)的開(kāi)發(fā)自測(cè)用例并給開(kāi)發(fā)參照?qǐng)?zhí)行竹海、登記結(jié)果。

3丐黄、提測(cè)后斋配,用例會(huì)分給多人執(zhí)行(人數(shù)可能不止前期的 1-2 人),會(huì)通過(guò)自定義標(biāo)簽形式記錄這批用例是誰(shuí)負(fù)責(zé)灌闺。此時(shí)會(huì)通過(guò)測(cè)試任務(wù)的篩選條件來(lái)篩選到只剩下此人負(fù)責(zé)的用例许起,單獨(dú)進(jìn)行執(zhí)行和登記。

實(shí)際執(zhí)行中菩鲜,可能會(huì)由于需求變更园细、部分需求細(xì)節(jié)需要補(bǔ)充等原因,在 2接校、3 步經(jīng)常需要更新用例集猛频。雖然也可以到測(cè)試集進(jìn)行更新,但測(cè)試集合計(jì)會(huì)有過(guò)千個(gè)用例蛛勉,而測(cè)試任務(wù)則只有數(shù)百個(gè)鹿寻,因此在測(cè)試任務(wù)中就進(jìn)行更新,相對(duì)而言會(huì)更為方便诽凌。

由于總用例集需要用于進(jìn)行一些數(shù)據(jù)統(tǒng)計(jì)和二輪測(cè)試用毡熏,所以此時(shí)也需要更新÷滤校基于此痢法,所以做這個(gè)增量保存的功能。

這塊功能之前自己其實(shí)也有大致想過(guò)杜顺,但一直沒(méi)有想得特別透徹财搁,這次做的過(guò)程中也是一開(kāi)始想著直接用現(xiàn)成的 json-patch 直接就可以滿足,但自己隨機(jī)測(cè)試一下就出現(xiàn)用例被覆蓋且沒(méi)有任何沖突提示的問(wèn)題躬络,所以后期干脆徹底梳理了一遍整體設(shè)計(jì)思路尖奔,再重新寫代碼、加單測(cè)穷当。

這塊可能其他有做基于百度腦圖的 xmind 用例管理平臺(tái)相關(guān)的同學(xué)也有遇到提茁,所以在此分享一下,也期望大家有更好的思路馁菜,可以下面評(píng)論交流下茴扁。

全文較長(zhǎng),建議可以先看目錄了解大概火邓,再具體看內(nèi)容

設(shè)計(jì)方案思考

現(xiàn)有技術(shù)方案是這樣的:

1丹弱、現(xiàn)在用例集和測(cè)試任務(wù),涉及 2 個(gè)表铲咨,test_case 和 exec_record躲胳。

2、test_case 表存儲(chǔ)每個(gè)完整的用例集 json 內(nèi)容纤勒,包括節(jié)點(diǎn)坯苹、標(biāo)簽、優(yōu)先級(jí)摇天。這個(gè)完整的 json 可以直接被腦圖組件完整加載和展示粹湃。

3、exec_record 表存儲(chǔ)每個(gè)測(cè)試任務(wù)的信息泉坐,包括篩選條件为鳄、各節(jié)點(diǎn)的執(zhí)行結(jié)果。其中各節(jié)點(diǎn)執(zhí)行結(jié)果存儲(chǔ)方式是節(jié)點(diǎn) id+ 測(cè)試結(jié)果腕让,示例:

{"bv8nxhi3c800":"9","c8tws927cpc0":"9","c8tws7dgbm80":"9"}

4孤钦、在前端界面展示的測(cè)試任務(wù)內(nèi)容沪曙,實(shí)際是經(jīng)過(guò) 用例集表 json 根據(jù)篩選條件篩選節(jié)點(diǎn)->篩選后節(jié)點(diǎn) json 和測(cè)試任務(wù)的測(cè)試結(jié)果進(jìn)行合并 兩個(gè)步驟得出拣技。這個(gè)合并和篩選是實(shí)時(shí)的,每次刷新加載測(cè)試任務(wù)的腦圖編輯界面寡痰,都會(huì)做一遍觉鼻。

5俊扭、對(duì)于用例集的多人協(xié)作,滴滴本身也自帶多人同時(shí)編輯用例的功能(集成在前端編輯器中坠陈,通過(guò) websocket 實(shí)時(shí)存儲(chǔ) diff 和更新)萨惑,但因?yàn)橹笆褂脮r(shí)發(fā)現(xiàn)會(huì)出現(xiàn)用例丟失、用例重復(fù)之類的問(wèn)題仇矾,原因猜測(cè)和一些網(wǎng)絡(luò)不穩(wěn)定導(dǎo)致同步可能不夠?qū)崟r(shí)有關(guān)咒钟。由于前端編輯器沒(méi)有開(kāi)源,無(wú)法真正尋找到根源及修復(fù)若未,加上一些對(duì)腦圖編輯器二次調(diào)整的需要朱嘴,所以改用了另一個(gè)基于 kityminder + vue 改的腦圖組件,也因此無(wú)法使用這個(gè)自帶的多人同時(shí)編輯用例功能(這個(gè)功能要求編輯器實(shí)時(shí)上報(bào)用戶的每一次操作改動(dòng)粗合,這個(gè)功能只有 agileTC 的腦圖編輯器組件才具備)萍嬉。

以前用過(guò)的另一個(gè)用例管理平臺(tái),模型會(huì)簡(jiǎn)單很多:

1隙疚、不區(qū)分用例和任務(wù)壤追,用例本身就帶有登記測(cè)試結(jié)果功能,數(shù)據(jù)庫(kù)只需要存用例內(nèi)容供屉。

2行冰、保存時(shí)溺蕉,會(huì)自動(dòng)根據(jù)服務(wù)端內(nèi)容計(jì)算出本次保存和打開(kāi)界面時(shí)版本的 diff ,然后把這個(gè) diff 和最新用例內(nèi)容進(jìn)行自動(dòng)合并存儲(chǔ)悼做。若合并發(fā)現(xiàn)沖突疯特,則反饋沖突內(nèi)容,讓用戶在前端界面手動(dòng)解決沖突后存儲(chǔ)肛走。

在實(shí)際實(shí)踐中漓雅,會(huì)出現(xiàn)合并沖突的情況極少,絕大部分情況都是可以直接自動(dòng)合并存儲(chǔ)的朽色。所以大家的實(shí)際用法邻吞,也基本都是主測(cè)先創(chuàng)建一個(gè)簡(jiǎn)單的 xmind 并分好每個(gè)人負(fù)責(zé)的一級(jí)節(jié)點(diǎn),然后各負(fù)責(zé)人員再去往這個(gè)一級(jí)下面擴(kuò)展具體的用例內(nèi)容葫男。

基于上面的這些歷史經(jīng)驗(yàn)和方案抱冷,整體設(shè)計(jì)方案有兩個(gè)大方向:

方向一:最小改動(dòng)原則。用例集每次保存都是增量保存(包括任務(wù)中編輯梢褐、用例集中編輯)徘层,由服務(wù)端自動(dòng)通過(guò) base 版本和保存版本得出 diff ,再應(yīng)用到最新的用例集中利职。

方向二:簡(jiǎn)化整體模型原則趣效。直接去掉測(cè)試任務(wù)概念,回歸之前用過(guò)的直接用例集保存測(cè)試結(jié)果猪贪,然后在此基礎(chǔ)上跷敬,再應(yīng)用增量保存功能。

考慮到目前大家已經(jīng)有測(cè)試任務(wù)的使用習(xí)慣热押,且提出任務(wù)可以改用例需求的組西傀,也比較認(rèn)可測(cè)試任務(wù)這個(gè)工具。因此決定桶癣,采用方向一拥褂。

整體方案設(shè)計(jì)

整體方案看起來(lái)比較簡(jiǎn)單,改動(dòng)點(diǎn)主要有:

1牙寞、保存從全量保存變?yōu)樵隽勘4?/p>

2饺鹃、保存時(shí)可以檢測(cè)沖突

3、類似 git 间雀,不沖突的部分可以直接保存悔详,沖突部分再單獨(dú)引導(dǎo)用戶手動(dòng)處理。

但這里面的增量保存惹挟、沖突檢測(cè)茄螃、diff 展示,都是一些技術(shù)難點(diǎn)连锯。

技術(shù)難點(diǎn)及解決

難點(diǎn)一:如何檢測(cè)生成兩個(gè)版本 json 的 diff?

分析

針對(duì) json 的增量保存归苍,剛好業(yè)內(nèi)也有其他業(yè)務(wù)場(chǎng)景用到(通過(guò)增量同步 json 變更用狱,減少網(wǎng)絡(luò)帶寬占用),目前已有兩種官方正式協(xié)議:RFC 6902 (JSON Patch)RFC 7396 (JSON Merge Patch)

json-patch 格式說(shuō)明:https://atbug.com/json-patch/?拼弃、https://datatracker.ietf.org/doc/html/rfc6902(官方協(xié)議定義文檔)

json-merge-patch 格式說(shuō)明:https://datatracker.ietf.org/doc/html/rfc7386(官方協(xié)議定義文檔)

兩者對(duì)比:https://erosb.github.io/post/json-patch-vs-merge-patch/

相關(guān) java 實(shí)現(xiàn)庫(kù):

https://github.com/flipkart-incubator/zjsonpatch?——僅支持 json-patch 格式

https://nicedoc.io/java-json-tools/json-patch?——支持 json-patch + json-merge-patch 格式

簡(jiǎn)單總結(jié)下夏伊,兩者的區(qū)別:

json-patch :生成兩個(gè) json 間的變化,并把每個(gè)變化點(diǎn)通過(guò)操作記錄的方式來(lái)記錄肴敛。如:

{ "op": "replace", "path": "/baz/1", "value": "boo" }

{ "op": "move", "from": "/biscuits", "path": "/cookies" }

op 代表操作。支持 add吗购、remove医男、replace、copy捻勉、move镀梭、test 共 6 種操作。其中 test 僅作為校驗(yàn)用踱启,不表達(dá) json 變化报账。

針對(duì) add、replace埠偿、test透罢,會(huì)帶上 path 和 value 字段。示例:{ "op": "replace", "path": "/baz", "value": "boo" }

其中 path 內(nèi)容遵循另一個(gè)叫做 json-pointer 的規(guī)范冠蒋。這個(gè)規(guī)范簡(jiǎn)單的說(shuō)羽圃,就是所在對(duì)象為 object 的用 key 定位,為 array 的用下標(biāo)定位抖剿,父子之間用 / 間隔朽寞。舉例:"/biscuits"?、"/biscuits/0/name" 斩郎、""(代表整個(gè) json )

value 字段則直接就是對(duì)應(yīng)的 value 脑融,可以是單個(gè)值、json object 或者 json array 缩宜。

針對(duì) remove 肘迎,只有 path ,沒(méi)有 value?

針對(duì) copy锻煌、move膜宋,用 from 指代源位置,path 指代目標(biāo)位置炼幔。示例:

{ "op": "move", "from": "/biscuits", "path": "/cookies" }

json-merge-patch:直接指示新的 json 中秋茫,各個(gè) key 對(duì)應(yīng) value 變成的結(jié)果。無(wú)變化的不出現(xiàn)乃秀。如:

// 這個(gè) patch 會(huì)把根節(jié)點(diǎn)下 key 為 a 的值替換為 z 肛著,再把 c 下面的 f 刪掉

{

? ? "a":"z",

? ? "c": {

? ? ? ? "f": null

? ? }

}

key 代表要應(yīng)用的位置圆兵。如果有嵌套,則 patch 內(nèi)也要對(duì)應(yīng)嵌套枢贿。

value 代表要改為的新值殉农。其中 null 表示刪除,非 null 表示要改的值

如果遇到某個(gè)對(duì)象是 array 局荚,由于 key 不具備指代 array 中單個(gè)元素的能力超凳,所以 patch 中必須完整地把新的 array 完整記錄進(jìn)來(lái),直接進(jìn)行完整的替換耀态。

解決方案

json-merge-patch

特點(diǎn)一:不會(huì)出現(xiàn)沖突轮傍,因?yàn)橹复木褪且某墒裁礃恿?/p>

特點(diǎn)二: array 需要完整記錄,腦圖的 children 節(jié)點(diǎn)是 array 類型的首装,而且很可能很龐大创夜,用這個(gè)基本相當(dāng)于把一級(jí)節(jié)點(diǎn)外的所有其他節(jié)點(diǎn)都全量更新了,不符合場(chǎng)景需要仙逻。

json-patch

特點(diǎn)一:原子化驰吓,每個(gè)改動(dòng)對(duì)應(yīng)一個(gè) op

特點(diǎn)二:對(duì) array 也可以支持(難點(diǎn)二會(huì)提到,實(shí)際還是要廢掉這個(gè)支持系奉,篩選后腦圖 json 的下標(biāo)和原始下標(biāo)會(huì)有很大差異)

因此檬贰,最終選擇 json-patch ,選用?zjsonpatch?這個(gè)庫(kù)缺亮。

關(guān)鍵代碼如下:

ObjectMapper mapper = new ObjectMapper();

String convertedBaseContent = convertChildrenArrayToObject(baseContent);

String convertedTargetContent = convertChildrenArrayToObject(targetContent);

JsonNode base = mapper.readTree(convertedBaseContent);

JsonNode result = mapper.readTree(convertedTargetContent);

// OMIT_COPY_OPERATION: 每個(gè)節(jié)點(diǎn)的 id 都是不一樣的偎蘸,界面上的 copy 到 json-patch 應(yīng)該是 add ,不應(yīng)該出現(xiàn) copy 操作瞬内。

// ADD_ORIGINAL_VALUE_ON_REPLACE: replace 中加一個(gè) fromValue 表達(dá)原來(lái)的值

// 去掉了默認(rèn)自帶的 OMIT_VALUE_ON_REMOVE 迷雪,這樣所有 remove 會(huì)帶上原始值,在 value 字段中

EnumSet<DiffFlags> flags = EnumSet.of(OMIT_COPY_OPERATION, ADD_ORIGINAL_VALUE_ON_REPLACE);

JsonNode originPatch = JsonDiff.asJson(base, result, flags);

難點(diǎn)二:如何在應(yīng)用 diff 時(shí)發(fā)現(xiàn)沖突虫蝶,并盡可能應(yīng)用無(wú)沖突的部分

分析

針對(duì)難點(diǎn)一使用了 json-patch 章咧,意味著每個(gè)改動(dòng)點(diǎn)都會(huì)有一個(gè)原子的 patch 進(jìn)行記錄,整體改動(dòng)會(huì)是一個(gè)數(shù)組模式能真,每個(gè)元素對(duì)應(yīng)一次原子改動(dòng)

解決方案

那這個(gè)方案就變得比較簡(jiǎn)單了:一次只應(yīng)用整體 patch 數(shù)組中的一次改動(dòng)赁严,如果出錯(cuò),則跳過(guò)并記錄為沖突粉铐,不出錯(cuò)疼约,則應(yīng)用并更新用例內(nèi)容

關(guān)鍵代碼:

/**

? ? * 逐個(gè)應(yīng)用 patch 到目標(biāo) json 中,并自動(dòng)跳過(guò)無(wú)法應(yīng)用的 patch 蝙泼。

? ? * @param patch patch json

? ? * @param baseContent 需要應(yīng)用到的 json

? ? * @param flags EnumSet程剥,每個(gè)元素為 ApplyPatchFlagEnum 枚舉值。用于指代應(yīng)用 patch 過(guò)程中一些特殊操作

? ? * @return ApplyPatchResultDto 對(duì)象汤踏,包含應(yīng)用后的 json 织鲸、應(yīng)用成功的 patch 和跳過(guò)的 patch

? ? * @throws IOException json 解析錯(cuò)誤時(shí)舔腾,拋出此異常

? ? */

? ? public static ApplyPatchResultDto batchApplyPatch(String patch, String baseContent, EnumSet<ApplyPatchFlagEnum> flags) throws IOException {

? ? ? ? baseContent = convertChildrenArrayToObject(baseContent);

? ? ? ? ApplyPatchResultDto applyPatchResultDto = new ApplyPatchResultDto();

? ? ? ? ObjectMapper mapper = new ObjectMapper();

? ? ? ? JsonNode patchJson = mapper.readTree(patch);

? ? ? ? JsonNode afterPatchJson = mapper.readTree(baseContent);

? ? ? ? List<String> conflictPatch = new ArrayList<>();

? ? ? ? List<String> applyPatch = new ArrayList<>();

? ? ? ? for (JsonNode onePatchOperation : patchJson) {

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? if (onePatchOperation.isArray()) {

? ? ? ? ? ? ? ? ? ? afterPatchJson = JsonPatch.apply(onePatchOperation, afterPatchJson);

? ? ? ? ? ? ? ? } else { // 外面包一個(gè) array

? ? ? ? ? ? ? ? ? ? afterPatchJson = JsonPatch.apply(mapper.createArrayNode().add(onePatchOperation), afterPatchJson);

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? applyPatch.add(mapper.writeValueAsString(onePatchOperation));

? ? ? ? ? ? } catch (JsonPatchApplicationException e) {

? ? ? ? ? ? ? ? conflictPatch.add(mapper.writeValueAsString(onePatchOperation));

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? String afterPatch = mapper.writeValueAsString(afterPatchJson);

? ? ? ? afterPatch = convertChildrenObjectToArray(afterPatch);

? ? ? ? applyPatchResultDto.setJsonAfterPatch(afterPatch);

? ? ? ? applyPatchResultDto.setConflictPatch(conflictPatch);

? ? ? ? applyPatchResultDto.setApplyPatch(applyPatch);

? ? ? ? return applyPatchResultDto;

? ? }

難點(diǎn)三:怎么保障無(wú)沖突的部分應(yīng)用后正確

分析

某個(gè)角度來(lái)說(shuō),這個(gè)才是最難的搂擦。沖突的有人工兜底稳诚,沒(méi)沖突就真的純靠系統(tǒng)識(shí)別了,等到人工發(fā)現(xiàn)可能已經(jīng)過(guò)了好多個(gè)版本瀑踢,不好追溯和恢復(fù)扳还。

需要先盡可能窮舉所有可能的改動(dòng)場(chǎng)景,并一一分析是否有問(wèn)題橱夭。

首先氨距,每次改動(dòng),從原子操作角度徘钥,可能產(chǎn)生的情況有:

增加新節(jié)點(diǎn)(包括從零編輯和通過(guò)復(fù)制粘貼得到的衔蹲,因 id 值會(huì)不一樣肢娘,從 json object 角度都認(rèn)為是新增節(jié)點(diǎn))

修改已有節(jié)點(diǎn)內(nèi)容(文字呈础、標(biāo)簽、優(yōu)先級(jí)等屬性)橱健、

刪除已有節(jié)點(diǎn)

移動(dòng)已有節(jié)點(diǎn)(節(jié)點(diǎn)的 id 不變而钞,只是位置變了)

四種場(chǎng)景。對(duì)應(yīng) json-patch 里面的 op :add拘荡、replace臼节、remove、move(特別留意珊皿,這里也有暗坑网缝,實(shí)際實(shí)現(xiàn)庫(kù)有可能用 remove + add 來(lái)取代 move 操作,這樣 add 就帶上了內(nèi)容的絕對(duì)值且無(wú)法比對(duì)是否和數(shù)據(jù)庫(kù)一致)

考慮到多人協(xié)作蟋定,有可能 base 版本實(shí)際非數(shù)據(jù)庫(kù)中實(shí)際最新版粉臊,因此每個(gè)原子操作進(jìn)行分析時(shí),增加 path 或 value 的 base 值驶兜,和數(shù)據(jù)庫(kù)當(dāng)前最新版一致/不一致的場(chǎng)景

add

影響因素:path + value

path:

和數(shù)據(jù)庫(kù)不一致:直接提示沖突扼仲,沒(méi)問(wèn)題。

和數(shù)據(jù)庫(kù)一致(問(wèn)題一):object 時(shí) key 都是唯一的抄淑,如果被其他人刪掉導(dǎo)致無(wú) key 會(huì)直接產(chǎn)生沖突屠凶,沒(méi)問(wèn)題。但 array 時(shí)根據(jù)下標(biāo)定位肆资,測(cè)試任務(wù)篩選條件可能會(huì)導(dǎo)致 children 節(jié)點(diǎn)在完整用例里有 3 個(gè)矗愧,任務(wù)里只有 1 個(gè)(實(shí)際對(duì)應(yīng)完整用例第三個(gè)),引起下標(biāo)指向錯(cuò)誤郑原。

value:

和數(shù)據(jù)庫(kù)不一致:value 只會(huì)是此用戶修改得出贱枣,數(shù)據(jù)庫(kù)原來(lái)無(wú)值监署,此場(chǎng)景不存在。

和數(shù)據(jù)庫(kù)一致:只有單人修改纽哥,value 屬于獨(dú)占內(nèi)容钠乏,不會(huì)缺失或受其他人影響,可直接應(yīng)用春塌。無(wú)問(wèn)題晓避。

replace

影響因素:path + value

path(問(wèn)題一):同 add 中的 path

value(問(wèn)題二):

和數(shù)據(jù)庫(kù)一致:無(wú)從知道是否和數(shù)據(jù)庫(kù)一致,無(wú)原有值記錄

和數(shù)據(jù)庫(kù)不一致:可修改的有 text(文字)只壳、priority(優(yōu)先級(jí))俏拱、resource(自定義標(biāo)簽)等,本質(zhì)上都是節(jié)點(diǎn) object 下 data 字段的子屬性吼句。因?yàn)?replace 并不會(huì)記錄原值锅必,所以可能存在 replace 后的新值覆蓋了中途某人修改過(guò)的值,且不產(chǎn)生沖突的問(wèn)題惕艳。

remove

影響因素:path + value(原 json-path 不考慮 value 搞隐,但為了保障刪除內(nèi)容和刪除者意愿一致,需要校驗(yàn)一下)

path(問(wèn)題一):同 add 的問(wèn)題远搪。

value:

和數(shù)據(jù)庫(kù)一致:無(wú)問(wèn)題劣纲,直接刪除即可。

和數(shù)據(jù)庫(kù)不一致(問(wèn)題二):可能原因是別人有改動(dòng)過(guò)且先保存谁鳍,或者處于測(cè)試任務(wù)篩選條件導(dǎo)致內(nèi)容和用例全集不一致癞季。此時(shí)可能出現(xiàn)錯(cuò)刪除了作者見(jiàn)不到,但實(shí)際存在的子節(jié)點(diǎn)倘潜。

move

影響因素:from绷柒、path。value 因?yàn)楸旧碇皇窍氡磉_(dá)移動(dòng)節(jié)點(diǎn)意愿涮因,可以無(wú)需校驗(yàn)废睦。

from(問(wèn)題一):同 add 的問(wèn)題。

path(問(wèn)題一):同 add 的問(wèn)題蕊退。

總結(jié)起來(lái)郊楣,存在兩個(gè)問(wèn)題:

問(wèn)題一:path 描述 array 時(shí),數(shù)組下標(biāo)由于用例可能被篩選過(guò)瓤荔,只是子集净蚤,很可能不準(zhǔn)

問(wèn)題二:replace 及 remove 時(shí),并沒(méi)有記錄原來(lái)的值输硝,而是直接操作今瀑。有可能出現(xiàn)其實(shí)作者改動(dòng)的源值和實(shí)際數(shù)據(jù)庫(kù)最新值不一致的問(wèn)題。

補(bǔ)充一個(gè)測(cè)試 java 庫(kù)自動(dòng)生成 patch 的規(guī)則時(shí)發(fā)現(xiàn)的問(wèn)題:

問(wèn)題三:自動(dòng)生成的 patch ,可能會(huì)使用 remove + add 取代 move 橘荠。此時(shí) add 帶有的絕對(duì)值屿附,可能會(huì)出現(xiàn)類似問(wèn)題二的直接覆蓋導(dǎo)致缺失問(wèn)題。

解決方案

問(wèn)題一:path 描述 array 時(shí)哥童,數(shù)組下標(biāo)由于用例可能被篩選過(guò)挺份,只是子集,很可能不準(zhǔn)

生成 patch 時(shí)贮懈,把 array 改為 object 匀泊,object 中每個(gè)子元素的 key 都為這個(gè)節(jié)點(diǎn)本身的 id 屬性(腦圖中每個(gè)節(jié)點(diǎn)的 id 屬性會(huì)保證整個(gè) json 全部節(jié)點(diǎn)中絕對(duì)的唯一)。生成完 patch 再改回來(lái)朵你。

示例:

// 原腦圖格式:

? {"root": {"data": {"id": "nodeA"}, "children": [{"data": {"id": "nodeAa"}, "children": []}, {"data": {"id": "nodeAb"}, "children": []}]}}

// 把 array 改為 object 后格式:

{"root": {"data": {"id": "nodeA"}, "childrenObject": {"nodeAa": {"data": {"id": "nodeAa"}, "childrenObject": {}, "order": 0}}, {"nodeAb": {"data": {"id": "nodeAb"}, "childrenObject": {}, "order": 1}}}}

關(guān)鍵代碼:

/**

? ? * 把 children 從 array 改為 object (array中每個(gè)元素外面多加一個(gè) key 各聘,key 的值為元素中的 data.id ),解決 json-pointer 針對(duì)數(shù)組用下標(biāo)定位抡医,會(huì)不準(zhǔn)確問(wèn)題

? ? * 示例:

? ? * 轉(zhuǎn)換前:? {"root": {"data": {"id": "nodeA"}, "children": [{"data": {"id": "nodeAa"}, "children": []}, {"data": {"id": "nodeAb"}, "children": []}]}}

? ? * 轉(zhuǎn)換后:? ? {"root": {"data": {"id": "nodeA"}, "childrenObject": {"nodeAa": {"data": {"id": "nodeAa"}, "childrenObject": {}, "order": 0}}, {"nodeAb": {"data": {"id": "nodeAb"}, "childrenObject": {}, "order": 1}}}}

? ? * @param caseContent 完整用例 json 躲因,需包含 root 節(jié)點(diǎn)數(shù)據(jù)

? ? * @return 轉(zhuǎn)換后 children 都不是 array 的新完整用例 json

? ? */

? ? public static String convertChildrenArrayToObject(String caseContent) {

? ? ? ? return convertChildrenArrayToObject(caseContent, true);

? ? }

? ? private static String convertChildrenArrayToObject(String caseContent, Boolean withOrder) {

? ? ? ? JSONObject caseContentJson = JSON.parseObject(caseContent);

? ? ? ? JSONObject rootData = caseContentJson.getJSONObject("root");

? ? ? ? rootData.put("childrenObject", convertArrayToObject(rootData.getJSONArray("children"), withOrder));

? ? ? ? // 把舊數(shù)據(jù)直接刪掉,換成新數(shù)據(jù)

? ? ? ? rootData.remove("children");

? ? ? ? return JSON.toJSONString(caseContentJson);

? ? }

? ? // 遞歸把 array 改為 object 忌傻,key 為原來(lái)子元素的 id

? ? private static JSONObject convertArrayToObject(JSONArray childrenArray, Boolean withOrder) {

? ? ? ? // 把 children 這個(gè) array 換成 Object

? ? ? ? JSONObject childrenObject = new JSONObject();

? ? ? ? // children 中每個(gè)子元素都變?yōu)?object

? ? ? ? for (int i=0; i<childrenArray.size(); i++) {

? ? ? ? ? ? JSONObject child = childrenArray.getJSONObject(i);

? ? ? ? ? ? String childId = child.getJSONObject("data").getString("id");

? ? ? ? ? ? if (withOrder) {

? ? ? ? ? ? ? ? // 加一個(gè) order 字段大脉,用于轉(zhuǎn)回 array 時(shí)保證內(nèi)部順序一致。

? ? ? ? ? ? ? ? child.put("order", i);

? ? ? ? ? ? }

? ? ? ? ? ? childrenObject.put(childId, child);

? ? ? ? ? ? // 對(duì) child 進(jìn)行遞歸芯勘,把它的 children 再變成 object

? ? ? ? ? ? JSONArray childrenArrayInChild = child.getJSONArray("children");

? ? ? ? ? ? child.put("childrenObject", convertArrayToObject(childrenArrayInChild, withOrder));

? ? ? ? ? ? // 刪掉已經(jīng)不需要的 children 字段

? ? ? ? ? ? child.remove("children");

? ? ? ? }

? ? ? ? return childrenObject;

? ? }

問(wèn)題二:replace 及 remove 時(shí)箱靴,并沒(méi)有記錄原來(lái)的值腺逛,而是直接操作荷愕。有可能出現(xiàn)其實(shí)作者改動(dòng)的源值和實(shí)際數(shù)據(jù)庫(kù)最新值不一致的問(wèn)題。

解決思路:

patch 中增加原值校驗(yàn)相關(guān)字段棍矛。原值一致才允許應(yīng)用安疗,原值不一致則認(rèn)為沖突不允許應(yīng)用。

考慮到改動(dòng) json-patch 的實(shí)現(xiàn)庫(kù)比較麻煩且容易埋坑够委,改為使用 test 這個(gè) op 字段來(lái)進(jìn)行校驗(yàn)荐类,即原來(lái)單純的 replace/remove 變?yōu)?test + replace/remove ,test 用于校驗(yàn)原有字段值茁帽。至于 test 原字段值玉罐,則通過(guò)生成的 patch 內(nèi)容拿

相關(guān)代碼:

/**

* 給所有 replace 或 remove 的 patch ,能校驗(yàn)原始值的潘拨,都加上 test

* @param allPatch ArrayNode 形式的所有 patch 內(nèi)容

* @return 添加完 test 后的所有 patch 內(nèi)容

*/

private static ArrayNode addTestToAllReplaceAndRemove(ArrayNode allPatch) {

? ? ObjectMapper mapper = new ObjectMapper();

? ? ArrayNode result = mapper.createArrayNode();

? ? for (JsonNode onePatch : allPatch) {

? ? ? ? // 實(shí)際應(yīng)用 patch 時(shí)吊输,不會(huì)管 replace 本身的 fromValue 字段。得手動(dòng)前面加一個(gè) test 的校驗(yàn)應(yīng)用前的原內(nèi)容是否一致铁追,并在外面再用一個(gè) array 包起來(lái)季蚂。

? ? ? ? // 即 [.., {op: replace, fromValue: .., path: .., value: ..}] 改為 [.., [{op: test, path: .., value: <fromValue>}, {op: replace, path: .., value: <value>}]]

? ? ? ? // 如果沒(méi)有 fromValue 字段,那無(wú)法校驗(yàn),直接按原來(lái)樣子記錄即可

? ? ? ? if ("replace".equals(onePatch.get("op").asText()) && onePatch.get("fromValue") != null) {

? ? ? ? ? ? ArrayNode testAndReplaceArray = mapper.createArrayNode();

? ? ? ? ? ? ObjectNode testPatch = mapper.createObjectNode();

? ? ? ? ? ? testPatch.put("op", "test");

? ? ? ? ? ? testPatch.put("path", onePatch.get("path").asText());

? ? ? ? ? ? testPatch.set("value", onePatch.get("fromValue"));

? ? ? ? ? ? testAndReplaceArray.add(testPatch);

? ? ? ? ? ? testAndReplaceArray.add(onePatch);

? ? ? ? ? ? result.add(testAndReplaceArray);

? ? ? ? ? ? continue;

? ? ? ? }

? ? ? ? // remove 同理扭屁,有 value 的前面都加一個(gè) test

? ? ? ? if ("remove".equals(onePatch.get("op").asText()) && onePatch.get("value") != null) {

? ? ? ? ? ? ArrayNode testAndRemoveArray = mapper.createArrayNode();

? ? ? ? ? ? ObjectNode testPatch = mapper.createObjectNode();

? ? ? ? ? ? testPatch.put("op", "test");

? ? ? ? ? ? testPatch.put("path", onePatch.get("path").asText());

? ? ? ? ? ? testPatch.set("value", onePatch.get("value"));

? ? ? ? ? ? testAndRemoveArray.add(testPatch);

? ? ? ? ? ? testAndRemoveArray.add(onePatch);

? ? ? ? ? ? result.add(testAndRemoveArray);

? ? ? ? ? ? continue;

? ? ? ? }

? ? ? ? result.add(onePatch);

? ? }

? ? return result;

}

問(wèn)題三:自動(dòng)生成的 patch 算谈,可能會(huì)使用 remove + add 取代 move 。此時(shí) add 帶有的絕對(duì)值料滥,可能會(huì)出現(xiàn)類似問(wèn)題二的直接覆蓋導(dǎo)致缺失問(wèn)題然眼。

經(jīng)過(guò)查看 zjsonpatch 庫(kù)里 move 的實(shí)現(xiàn),原理還是確認(rèn) add 和 remove 的 value 是否有完全一樣葵腹,如果有罪治,則兩者合并成 move 。

之所以會(huì)無(wú)法合并礁蔗,原因是前面的 array 轉(zhuǎn) object 里面加入的 order 字段會(huì)變化觉义。

所以,可以做一次不帶有 order 字段的轉(zhuǎn)換浴井,先得出 move 字段晒骇。然后再把帶 order 字段轉(zhuǎn)換中 path 和 move 的 from 或者 path 重合的去掉。

衍生問(wèn)題:order 位置未被更新(比如原來(lái)位置 order 為 5 磺浙,新位置 order 為 3 洪囤,但因?yàn)?move 是原版直接挪,所以 move 完內(nèi)容的 order 還是 5)撕氧。放到問(wèn)題四單獨(dú)分析解決

相關(guān)代碼:

// OMIT_COPY_OPERATION: 每個(gè)節(jié)點(diǎn)的 id 都是不一樣的瘤缩,界面上的 copy 到 json-patch 應(yīng)該是 add ,不應(yīng)該出現(xiàn) copy 操作伦泥。

// ADD_ORIGINAL_VALUE_ON_REPLACE: replace 中加一個(gè) fromValue 表達(dá)原來(lái)的值

// OMIT_MOVE_OPERATION: 所有 move 操作剥啤,都還是維持原來(lái) add + remove 的狀態(tài),避免一些類似 priority 屬性值的一增一減被認(rèn)為是 move 不脯。

// 去掉了默認(rèn)自帶的 OMIT_VALUE_ON_REMOVE 府怯,這樣所有 remove 會(huì)在 value 字段中帶上原始值

JsonNode originPatch = JsonDiff.asJson(base, result,

? ? ? ? EnumSet.of(OMIT_COPY_OPERATION, ADD_ORIGINAL_VALUE_ON_REPLACE, OMIT_MOVE_OPERATION));

// 借助去掉 order 的內(nèi)容,正確生成 move 操作

JsonNode baseWithoutOrder = mapper.readTree(convertChildrenArrayToObject(baseContent, false));

JsonNode targetWithoutOrder = mapper.readTree(convertChildrenArrayToObject(targetContent, false));

List<String> allFromPath = new ArrayList<>();

List<String> allToPath = new ArrayList<>();

List<JsonNode> allMoveOprations = new ArrayList<>();

// 需要生成 move 操作,去掉原有 flags 里面的忽略 move 標(biāo)記

JsonNode noOrderPatch = JsonDiff.asJson(baseWithoutOrder, targetWithoutOrder,

? ? ? ? EnumSet.of(OMIT_COPY_OPERATION, ADD_ORIGINAL_VALUE_ON_REPLACE));

for (JsonNode oneNoOrderPatch: noOrderPatch) {

? ? if ("move".equals(oneNoOrderPatch.get("op").asText())) {

? ? ? ? allFromPath.add(oneNoOrderPatch.get("from").asText());

? ? ? ? allToPath.add(oneNoOrderPatch.get("path").asText());

? ? ? ? allMoveOprations.add(oneNoOrderPatch);

? ? }

}

ArrayNode finalPatch = mapper.createArrayNode();

// 先把所有 move 加進(jìn)這個(gè)最終的 patch 中

for (JsonNode movePatch : allMoveOprations) {

? ? finalPatch.add(movePatch);

}

for (JsonNode onePatch : originPatch) {

? ? // 和 move 匹配的 add 中,根節(jié)點(diǎn) order 字段需要變?yōu)?replace 存下來(lái)午绳,避免丟失順序

? ? if ("add".equals(onePatch.get("op").asText()) && allToPath.contains(onePatch.get("path").asText())) {

? ? ? ? // 獲取 add 中 value 第一層的 order 值。此時(shí) value 實(shí)際是移動(dòng)的整體 object 冲簿,order 就在第一層

? ? ? ? int newOrder = onePatch.get("value").get("order").asInt();

? ? ? ? ObjectNode replaceOrderPatch = mapper.createObjectNode();

? ? ? ? replaceOrderPatch.put("op", "replace");

? ? ? ? replaceOrderPatch.put("path", onePatch.get("path").asText() + "/order");

? ? ? ? replaceOrderPatch.put("value", newOrder);

? ? ? ? // 這種情況下就不用管 replace 的原來(lái)值是什么了,所以不設(shè)定 fromValue

? ? ? ? finalPatch.add(replaceOrderPatch);

? ? ? ? // 這個(gè) add 的作用已經(jīng)被 move + replace 達(dá)成了亿昏,所以不需要記錄這個(gè) add

? ? ? ? continue;

? ? }

? ? // move 的源節(jié)點(diǎn)刪除操作峦剔,需要忽略,因?yàn)?move 已經(jīng)起到相應(yīng)的作用了

? ? if ("remove".equals(onePatch.get("op").asText()) && allFromPath.contains(onePatch.get("path").asText())) {

? ? ? ? continue;

? ? }

? ? // 如果 order 沒(méi)變龙优,那不去除 order 的 patch 有可能也有 move 羊异。這個(gè)時(shí)候這個(gè) move 需要去掉事秀,避免重復(fù)

? ? if ("move".equals(onePatch.get("op").asText()) && allMoveOprations.contains(onePatch)) {

? ? ? ? continue;

? ? }

? ? // 其他不需要調(diào)整的,直接加進(jìn)去就可以了

? ? finalPatch.add(onePatch);

}

問(wèn)題三解決方案的衍生問(wèn)題四:move 操作的元素野舶,因?yàn)槭钦w內(nèi)容挪過(guò)來(lái)的易迹,會(huì)導(dǎo)致 order 位置未被更新(比如原來(lái)位置 order 為 5 ,新位置 order 為 3 平道,但因?yàn)?move 是原版直接挪睹欲,所以 move 完內(nèi)容的 order 還是 5)。

如果不用 move 操作一屋,則會(huì)出現(xiàn) add + replace(如果 order 有變更)+ remove 窘疮。

所以,解決方法只需要重新應(yīng)用 replace 操作即可冀墨,并且要保障 replace 放在 move 后闸衫,避免節(jié)點(diǎn)已經(jīng)被 move 應(yīng)用失敗。

由于生成的 replace 操作有可能作用在原有位置诽嘉,因此匹配的 path 需要改為新位置蔚出。

相關(guān)代碼:

... 前面是問(wèn)題三中生成了 move patch 的相關(guān)邏輯,其中 allToPath 指代所有 move 中的 path 路徑虫腋,即移動(dòng)到的新位置 path

for (JsonNode onePatch : originPatch) {

? ? // 和 move 匹配的 add 中骄酗,根節(jié)點(diǎn) order 字段需要變?yōu)?replace 存下來(lái),避免丟失順序

? ? if ("add".equals(onePatch.get("op").asText()) && allToPath.contains(onePatch.get("path").asText())) {

? ? ? ? // 獲取 add 中 value 第一層的 order 值悦冀。此時(shí) value 實(shí)際是移動(dòng)的整體 object 趋翻,order 就在第一層

? ? ? ? int newOrder = onePatch.get("value").get("order").asInt();

? ? ? ? ObjectNode replaceOrderPatch = mapper.createObjectNode();

? ? ? ? replaceOrderPatch.put("op", "replace");

? ? ? ? replaceOrderPatch.put("path", onePatch.get("path").asText() + "/order");

? ? ? ? replaceOrderPatch.put("value", newOrder);

? ? ? ? // 這種情況下就不用管 replace 的原來(lái)值是什么了,所以不設(shè)定 fromValue

? ? ? ? finalPatch.add(replaceOrderPatch);

? ? ? ? continue;

? ? }

? ? // move 的源節(jié)點(diǎn)刪除操作盒蟆,可以忽略

? ? if ("remove".equals(onePatch.get("op").asText()) && allFromPath.contains(onePatch.get("path").asText())) {

? ? ? ? continue;

? ? }

? ? // 其他不需要調(diào)整的踏烙,直接加進(jìn)去就可以了

? ? finalPatch.add(onePatch);

}

難點(diǎn)四:測(cè)試任務(wù)帶有篩選條件,有可能只是完整用例集的子集茁影。對(duì)子集的修改應(yīng)用到全集時(shí)宙帝,可能部分內(nèi)容會(huì)對(duì)不上引起沖突丧凤。

分析

首先募闲,篩選條件目前只有兩類:優(yōu)先級(jí)/自定義標(biāo)簽。篩選的的子集和全集相比愿待,在節(jié)點(diǎn) data 內(nèi)容層面不會(huì)有任何不同浩螺,只有在節(jié)點(diǎn) children 這個(gè)數(shù)組層面會(huì)減少內(nèi)容(數(shù)量上的減少,子元素內(nèi)容不會(huì)少)仍侥。

內(nèi)容減少要出,只會(huì)引起數(shù)組下標(biāo)的變化,即上一個(gè)問(wèn)題解決方案中 childrenObject 子元素的 order 值不正確农渊,進(jìn)而引起如果增量改動(dòng)里有改動(dòng) order 會(huì)引起沖突(子集的原始值和全集里的原始值不一致)患蹂。

舉例:

全集:root 節(jié)點(diǎn)下一級(jí),依次有 A、B传于、C 節(jié)點(diǎn)囱挑。只有 A、C 符合篩選條件

子集:root 節(jié)點(diǎn)下一級(jí)沼溜,只有 A平挑、C 兩個(gè)節(jié)點(diǎn)

操作 1:在 C 后面增加節(jié)點(diǎn)。新節(jié)點(diǎn)會(huì)以 add 操作增加到 root 下面的 children 中系草,order 會(huì)為 3 甚至更大的值通熄。因?yàn)槭切略龅模粫?huì)有沖突找都,但因?yàn)?order 可能大于原有 array 的 size 唇辨,只需要轉(zhuǎn)換回 array 時(shí)只要把沒(méi)應(yīng)用上的都在后面補(bǔ)回去即可。

操作 2:在 A能耻、C 之間增加節(jié)點(diǎn)助泽。新節(jié)點(diǎn) add 和操作 1,但會(huì)引起 C 節(jié)點(diǎn)的 replace 嚎京,order 從 2 變 3 嗡贺。由于全集里 C 的 order 其實(shí)是 3,這個(gè) replace 會(huì)在驗(yàn)證原始值時(shí)失敗認(rèn)為沖突鞍帝。這個(gè)沖突其實(shí)無(wú)關(guān)緊要诫睬,加一個(gè)忽略即可。

解決方案

1帕涌、操作 1:在 C 后面增加節(jié)點(diǎn)摄凡。新節(jié)點(diǎn)會(huì)以 add 操作增加到 root 下面的 children 中,order 會(huì)為 3 甚至更大的值蚓曼。因?yàn)槭切略龅那自瑁粫?huì)有沖突,但因?yàn)?order 可能大于原有 array 的 size 纫版,只需要轉(zhuǎn)換回 array 時(shí)只要把沒(méi)應(yīng)用上的都在后面補(bǔ)回去即可床绪。

相關(guān)代碼:

// 遞歸把每個(gè) object 改回 array ,去掉 object 中第一層的 key

private static JSONArray convertObjectToArray(JSONObject childrenObject, Boolean withOrder) {

? ? JSONArray childrenArray = new JSONArray();

? ? List<String> keyMoved = new ArrayList<>();

? ? // object 中每個(gè)子元素其弊,重新放回到 array 中

? ? for (int i=0; i<childrenObject.keySet().size(); i++) {

? ? ? ? for (String key : childrenObject.keySet()) {

? ? ? ? ? ? JSONObject child = childrenObject.getJSONObject(key);

? ? ? ? ? ? if (withOrder) {

? ? ? ? ? ? ? ? // 需要根據(jù) order 判定原來(lái)的順序癞己,按順序加進(jìn)去,避免順序錯(cuò)誤

? ? ? ? ? ? ? ? if (Integer.valueOf(i).equals(child.getInteger("order"))) {

? ? ? ? ? ? ? ? ? ? childrenArray.add(child);

? ? ? ? ? ? ? ? ? ? keyMoved.add(key);

? ? ? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? ? ? continue;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? // 不用管 order 梭伐,直接一個(gè)一個(gè) key 加進(jìn)去就是了

? ? ? ? ? ? ? ? childrenArray.add(child);

? ? ? ? ? ? ? ? keyMoved.add(key);

? ? ? ? ? ? }

? ? ? ? ? ? // 對(duì)添加的 child 進(jìn)行遞歸痹雅,把它的 childrenObject 再變回 array

? ? ? ? ? ? JSONObject childrenObjectInChild = child.getJSONObject("childrenObject");

? ? ? ? ? ? child.put("children", convertObjectToArray(childrenObjectInChild, withOrder));

? ? ? ? ? ? if (withOrder) {

? ? ? ? ? ? ? ? // 去掉排序用的臨時(shí)字段

? ? ? ? ? ? ? ? child.remove("order");

? ? ? ? ? ? }

? ? ? ? ? ? child.remove("childrenObject");

? ? ? ? }

? ? }

? ? // ** 重點(diǎn):有可能通過(guò) move 過(guò)來(lái)的 order 值很大,最后要把剩余的 childrenObject 元素繼續(xù)放到 array 里面

? ? for (String key : childrenObject.keySet()) {

? ? ? ? if (!keyMoved.contains(key)) {

? ? ? ? ? ? childrenArray.add(childrenObject.getJSONObject(key));

? ? ? ? }

? ? }

? ? return childrenArray;

}

2糊识、操作 2:在 A绩社、C 之間增加節(jié)點(diǎn)摔蓝。新節(jié)點(diǎn) add 和操作 1,但會(huì)引起 C 節(jié)點(diǎn)的 replace 愉耙,order 從 2 變 3 项鬼。由于全集里 C 的 order 其實(shí)是 3,這個(gè) replace 會(huì)在驗(yàn)證原始值時(shí)失敗認(rèn)為沖突劲阎。這個(gè)沖突其實(shí)無(wú)關(guān)緊要绘盟,加一個(gè)忽略即可。

相關(guān)代碼:

for (JsonNode onePatchOperation : patchJson) {

? ? try {

? ? ? ? if (onePatchOperation.isArray()) {

? ? ? ? ? ? afterPatchJson = JsonPatch.apply(onePatchOperation, afterPatchJson);

? ? ? ? } else { // 外面包一個(gè) array

? ? ? ? ? ? afterPatchJson = JsonPatch.apply(mapper.createArrayNode().add(onePatchOperation), afterPatchJson);

? ? ? ? }

? ? ? ? applyPatch.add(mapper.writeValueAsString(onePatchOperation));

? ? } catch (JsonPatchApplicationException e) {

? ? ? ? // 檢查是否是對(duì) order 的操作悯仙。如果是龄毡,那就忽略這個(gè)沖突

? ? ? ? if (flags.contains(IGNORE_REPLACE_ORDER_CONFLICT) &&

? ? ? ? ? ? ? ? onePatchOperation.isArray() &&

? ? ? ? ? ? ? ? onePatchOperation.get(0).get("path").asText().endsWith("/order")) {

? ? ? ? ? ? continue;

? ? ? ? }

? ? ? ? conflictPatch.add(mapper.writeValueAsString(onePatchOperation));

? ? }

}

難點(diǎn)五:如何在出現(xiàn)沖突后進(jìn)行友好標(biāo)識(shí),提高解決沖突效率

分析

首先锡垄,需要存儲(chǔ)存在沖突的變更沦零。從難點(diǎn)二的解決可知,只要從沖突 patch 列表就可以得到货岭。只要備份里增加這個(gè)字段即可路操。

然后,就是怎么根據(jù)這個(gè) patch 列表千贯,以及沖突副本完整腦圖內(nèi)容屯仗,呈現(xiàn)變更了。

git 標(biāo)記 diff 的方法搔谴,是給增加的內(nèi)容(+)加上綠色底色魁袜,刪除的內(nèi)容(-)加上紅色底色,重命名或移動(dòng)文件則直接通過(guò)文件名位置敦第, 以 old -> new 的格式標(biāo)識(shí)峰弹。修改內(nèi)容(replace)從底層上就直接是 刪除 + 增加 來(lái)表示。

同樣的方式放到腦圖芜果,增加沒(méi)問(wèn)題鞠呈,刪除只要把被刪除內(nèi)容加回來(lái)也沒(méi)問(wèn)題。沒(méi)有重命名或移動(dòng)文件機(jī)制右钾,但有修改節(jié)點(diǎn)內(nèi)容及移動(dòng)節(jié)點(diǎn)機(jī)制蚁吝。

由于腦圖非純文本文件,而是以 json 形式記錄數(shù)據(jù)霹粥,腦圖編輯器呈現(xiàn)數(shù)據(jù)的形式灭将。diff 內(nèi)容基本是 path + value 的形式記錄,通過(guò) path 不好直觀看出改動(dòng)位置后控,因此需要直接在沖突副本上通過(guò)添加標(biāo)記的方式進(jìn)行展示。

按照相對(duì)直覺(jué)的方式空镜,設(shè)定如下標(biāo)識(shí):

1浩淘、增加的節(jié)點(diǎn):加上綠色底色

2捌朴、刪除的節(jié)點(diǎn):加上紅色底色

3、修改的節(jié)點(diǎn)(包括移動(dòng)節(jié)點(diǎn)张抄、修改節(jié)點(diǎn)自身的文字砂蔽、優(yōu)先級(jí)、自定義標(biāo)簽等):加上藍(lán)色底色

解決方案

由于實(shí)際 json-patch 的操作署惯,并不會(huì)認(rèn)識(shí) “節(jié)點(diǎn)” 這個(gè)概念左驾,只知道 json 里的 object 及 array 。

所以极谊,需要先判斷 patch 的操作對(duì)象诡右,是一個(gè)節(jié)點(diǎn)還是非節(jié)點(diǎn)。判斷條件為操作的 path 屬性轻猖。如果是節(jié)點(diǎn)帆吻,一定會(huì)以類似 /childrenObject/xxx 的形式結(jié)尾

相關(guān)代碼:

/**

? ? * 根據(jù) jsonPatch 內(nèi)容,在腦圖中標(biāo)記變更咙边。以節(jié)點(diǎn)為單位猜煮,增加的加綠色背景,刪除的加紅色背景败许,修改的加藍(lán)色背景王带。

? ? * 特別注意,移動(dòng)節(jié)點(diǎn)(move)因?yàn)閷?shí)際節(jié)點(diǎn) id 未有變化市殷,所以也會(huì)被標(biāo)記為修改

? ? *

? ? * @param minderContent

? ? * @param jsonPatch

? ? * @return

? ? */

? ? public static String markJsonPatchOnMinderContent(String jsonPatch, String minderContent) throws IOException, IllegalArgumentException {

? ? ? ? String green = "#67c23a";

? ? ? ? String blue = "#409eff";

? ? ? ? String red = "#f56c6c";

? ? ? ? ObjectMapper objectMapper = new ObjectMapper();

? ? ? ? // 因?yàn)?jsonPatch 是針對(duì)已經(jīng)把 children 數(shù)組變?yōu)閷?duì)象的 json 格式辫秧,所以要先轉(zhuǎn)換下

? ? ? ? ObjectNode convertedMinderContentJson = objectMapper.readTree(convertChildrenArrayToObject(minderContent)).deepCopy();

? ? ? ? ArrayNode jsonPatchArray = (ArrayNode) objectMapper.readTree(jsonPatch);

? ? ? ? for (JsonNode onePatch : jsonPatchArray) {

? ? ? ? ? ? JsonNode operation;

? ? ? ? ? ? if (onePatch.isArray() && onePatch.size() == 2) {

? ? ? ? ? ? ? ? // 只可能是 replace 或 remove 的。前面多加了 test 被丧,會(huì)是一個(gè)帶有兩個(gè)子元素的 array 盟戏。第二個(gè)才是 replace 或 remove

? ? ? ? ? ? ? ? operation = onePatch.get(1);

? ? ? ? ? ? ? ? if (!("replace".equals(operation.get("op").asText()) || "remove".equals(operation.get("op").asText()))) {

? ? ? ? ? ? ? ? ? ? throw new IllegalArgumentException(String.format("此單個(gè) patch 格式不正常," +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "正常格式在雙元素 array 的第二個(gè)甥桂,應(yīng)該是 replace 或 remove 操作" +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "不符合的 patch 內(nèi)容: %s",

? ? ? ? ? ? ? ? ? ? ? ? ? ? objectMapper.writeValueAsString(onePatch)));

? ? ? ? ? ? ? ? }

? ? ? ? ? ? } else if (onePatch.isObject()) {

? ? ? ? ? ? ? ? operation = onePatch;

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? // 目前不會(huì)生成不符合這兩種格式的 patch 柿究,拋異常

? ? ? ? ? ? ? ? throw new IllegalArgumentException(String.format("此單個(gè) patch 格式不正常,正常格式應(yīng)該是雙元素array或單個(gè)object" +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "請(qǐng)確認(rèn) patch 內(nèi)容是通過(guò)此工具類提供的獲取 patch 方法生成黄选。不符合的 patch 內(nèi)容: %s",

? ? ? ? ? ? ? ? ? ? ? ? objectMapper.writeValueAsString(onePatch)));

? ? ? ? ? ? }

? ? ? ? ? ? // 先判定是否為整個(gè)節(jié)點(diǎn)的內(nèi)容變更

? ? ? ? ? ? if (isNodePath(operation.get("path").asText())) {

? ? ? ? ? ? ? ? // 節(jié)點(diǎn)級(jí)別蝇摸,只支持 add 、 remove 办陷、move 貌夕。因?yàn)?replace 只改值不改key,不可能在節(jié)點(diǎn)級(jí)別產(chǎn)生 replace 操作

? ? ? ? ? ? ? ? switch (operation.get("op").asText()) {

? ? ? ? ? ? ? ? ? ? case "add":

? ? ? ? ? ? ? ? ? ? ? ? addAddNodeMark(convertedMinderContentJson, operation, green);

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? case "move":

? ? ? ? ? ? ? ? ? ? ? ? addMoveNodeMark(convertedMinderContentJson, operation, blue);

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? case "remove":

? ? ? ? ? ? ? ? ? ? ? ? addRemoveNodeMark(convertedMinderContentJson, operation, red);

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? default:

? ? ? ? ? ? ? ? ? ? ? ? throw new IllegalArgumentException(String.format("此單個(gè) patch 格式不正常民镜," +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "正常的節(jié)點(diǎn)級(jí)別 patch 啡专,op 應(yīng)該是 add、move制圈、remove 其中一個(gè)" +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "不符合的 patch 內(nèi)容: %s",

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? objectMapper.writeValueAsString(operation)));

? ? ? ? ? ? ? ? }

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? // 非節(jié)點(diǎn)級(jí)別變更们童,都將它標(biāo)記為 修改內(nèi)容 即可畔况。不應(yīng)該出現(xiàn) move 節(jié)點(diǎn)屬性的動(dòng)作

? ? ? ? ? ? ? ? switch (operation.get("op").asText()) {

? ? ? ? ? ? ? ? ? ? case "add":

? ? ? ? ? ? ? ? ? ? ? ? addAddAttrMark(convertedMinderContentJson, operation, blue);

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? case "replace":

? ? ? ? ? ? ? ? ? ? ? ? addReplaceAttrMark(convertedMinderContentJson, operation, blue);

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? case "remove":

? ? ? ? ? ? ? ? ? ? ? ? addRemoveAttrMark(convertedMinderContentJson, operation, blue);

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? default:

? ? ? ? ? ? ? ? ? ? ? ? throw new IllegalArgumentException(String.format("此單個(gè) patch 格式不正常," +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "正常的非節(jié)點(diǎn)級(jí)別 patch 慧库,op 應(yīng)該是 add跷跪、replace、remove 四個(gè)其中一個(gè)" +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "不符合的 patch 內(nèi)容: %s",

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? objectMapper.writeValueAsString(operation)));

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return convertChildrenObjectToArray(objectMapper.writeValueAsString(convertedMinderContentJson));

? ? }

總結(jié)

由于篇幅所限齐板,其實(shí)里面有些小的問(wèn)題解決并沒(méi)有列在上面的技術(shù)難點(diǎn)里面(比如應(yīng)用變更時(shí)吵瞻,如果 replace order 操作出錯(cuò),可以忽略)甘磨。整體改動(dòng)大概花了 4 人天左右橡羞,而且中途也寫了不少單測(cè)代碼來(lái)保障每次改動(dòng)都不會(huì)影響已有功能(行覆蓋率達(dá)到 94%,只有少量格式不對(duì)拋異常的邏輯沒(méi)有覆蓋)宽档。

此次場(chǎng)景比較復(fù)雜尉姨,已經(jīng)盡自己所能,用相對(duì)靠譜的分析方法列舉出所有可能的場(chǎng)景吗冤,并進(jìn)行對(duì)應(yīng)處理又厉。但是否靠譜還需要靠實(shí)踐檢驗(yàn),預(yù)計(jì)節(jié)后會(huì)上線此功能椎瘟,屆時(shí)再看看實(shí)際使用的效果覆致。

如果有其它同學(xué)也做過(guò)類似的功能,有更好的算法或者思路肺蔚,也歡迎直接評(píng)論分享交流下

開(kāi)源

目前服務(wù)端相關(guān)的代碼改動(dòng)及配套單測(cè)煌妈,已提交 PR 給官方。地址:https://github.com/didi/AgileTC/pull/93

增量生成宣羊、應(yīng)用璧诵、標(biāo)記的邏輯全部在 case-server/src/main/java/com/xiaoju/framework/util/MinderJsonPatchUtil.java 這個(gè)工具類

配套單測(cè)在 case-server/src/test/java/com/xiaoju/framework/util/MinderJsonPatchUtilTest.java

如果有需要的,可以按需自取哈仇冯。


由陳恒捷首發(fā)于TesterHome社區(qū)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末之宿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子苛坚,更是在濱河造成了極大的恐慌比被,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泼舱,死亡現(xiàn)場(chǎng)離奇詭異等缀,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)娇昙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門尺迂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事枪狂∥:恚” “怎么了宋渔?”我有些...
    開(kāi)封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵州疾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我皇拣,道長(zhǎng)严蓖,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任氧急,我火速辦了婚禮颗胡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吩坝。我一直安慰自己毒姨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布钉寝。 她就那樣靜靜地躺著弧呐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嵌纲。 梳的紋絲不亂的頭發(fā)上俘枫,一...
    開(kāi)封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音逮走,去河邊找鬼鸠蚪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛师溅,可吹牛的內(nèi)容都是我干的茅信。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼墓臭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蘸鲸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起起便,我...
    開(kāi)封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤棚贾,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后榆综,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體妙痹,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年鼻疮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怯伊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡判沟,死狀恐怖耿芹,靈堂內(nèi)的尸體忽然破棺而出崭篡,到底是詐尸還是另有隱情,我是刑警寧澤吧秕,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布琉闪,位于F島的核電站,受9級(jí)特大地震影響砸彬,放射性物質(zhì)發(fā)生泄漏颠毙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一砂碉、第九天 我趴在偏房一處隱蔽的房頂上張望蛀蜜。 院中可真熱鬧,春花似錦增蹭、人聲如沸滴某。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)霎奢。三九已至,卻和暖如春杀怠,著一層夾襖步出監(jiān)牢的瞬間椰憋,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工赔退, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留橙依,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓硕旗,卻偏偏與公主長(zhǎng)得像窗骑,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子漆枚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理创译,服務(wù)發(fā)現(xiàn),斷路器墙基,智...
    卡卡羅2017閱讀 134,662評(píng)論 18 139
  • 原文地址https://tools.ietf.org/html/rfc6902 1.簡(jiǎn)介 JavaScript O...
    勇赴閱讀 3,946評(píng)論 0 4
  • Correctness AdapterViewChildren Summary: AdapterViews can...
    MarcusMa閱讀 8,867評(píng)論 0 6
  • 面試必背 會(huì)舍棄软族、總結(jié)概括——根據(jù)我這些年面試和看面試題搜集過(guò)來(lái)的知識(shí)點(diǎn)匯總而來(lái) 建議根據(jù)我的寫的面試應(yīng)對(duì)思路中的...
    luoyangzk閱讀 6,756評(píng)論 6 173
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,938評(píng)論 2 89