背景
現(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ū)