基于 Jacoco 的二次開(kāi)發(fā)【解決不同版本 exec 數(shù)據(jù)合并問(wèn)題】

概述


對(duì)于 Jacoco 理想的使用場(chǎng)景:在測(cè)試階段,能夠?qū)崟r(shí)統(tǒng)計(jì)手工測(cè)試的代碼覆蓋率情況

了解了 jacoco 的一些基本使用方法后,發(fā)現(xiàn)要滿足這個(gè)使用場(chǎng)景,至少需要解決 2 個(gè)問(wèn)題。

類(lèi)修改萍倡,帶來(lái)的探針數(shù)據(jù)合并問(wèn)題

方法的修改,對(duì)探針數(shù)據(jù)造成的影響

下面講講具體問(wèn)題以及解決思路

重點(diǎn)說(shuō)明:以下實(shí)踐使用普通 java 類(lèi)測(cè)試辟汰,并且我所使用的是 JDK8列敲。大于 JDK8 版本的插樁邏輯并不相同阱佛,如果是 JDK9 及以上版本,可能并不適用

問(wèn)題一


類(lèi)修改戴而,帶來(lái)的探針數(shù)據(jù)合并問(wèn)題

圖 1

如圖 1 所示凑术,修改前的 Hello.java 文件,包含 3 個(gè)方法:A/B/C所意,修改后包含 4 個(gè)方法:A/B/C/D淮逊。

收集修改前 Hello 類(lèi)的探針數(shù)據(jù) (假設(shè) A/B 方法已執(zhí)行):dump1.exec

修改 Hello 中的 B 方法,然后重新收集探針數(shù)據(jù) (假設(shè) C/D 方法已執(zhí)行):dump2.exec

dump1.exec 和 dump2.exec 的數(shù)據(jù)合并扶踊,想要合并后的覆蓋率數(shù)據(jù)中包括:已被執(zhí)行方法【A/C/D】泄鹏,未被執(zhí)行方法:【B】

合并 exec 數(shù)據(jù)使用 jacoco 的 merge 指令,merge 對(duì)于同一個(gè)類(lèi)文件數(shù)據(jù)是否能合并的主要判斷邏輯代碼如下:

publicvoidassertCompatibility(finallongid,finalStringname,finalintprobecount)throwsIllegalStateException{/**

////這里是我加的注釋

同一個(gè)java文件秧耗,每次修改后對(duì)應(yīng)生成的classId都是不一致的备籽,

所以在這個(gè)地方就會(huì)被判斷不通過(guò),無(wú)法合并同一個(gè)java文件的統(tǒng)計(jì)數(shù)據(jù)

假設(shè)這里注釋掉id的判斷邏輯分井,繼續(xù)往下執(zhí)行

*/if(this.id!=id){thrownewIllegalStateException(format("Different ids (%016x and %016x).",Long.valueOf(this.id),Long.valueOf(id)));}if(!this.name.equals(name)){thrownewIllegalStateException(format("Different class names %s and %s for id %016x.",this.name,name,Long.valueOf(id)));}/**

////還是我加的注釋

如果上面的id判斷邏輯注釋掉车猬,在這里面探針數(shù)組長(zhǎng)度的時(shí)候還是會(huì)校驗(yàn)失敗,

Hello.java文件修改后尺锚,新增了D方法诈唬,導(dǎo)致Hello類(lèi)文件的探針數(shù)據(jù)長(zhǎng)度是發(fā)生了變化,這里長(zhǎng)度校驗(yàn)會(huì)失斔豸铩;

假設(shè)沒(méi)有新增D方法赡矢,同時(shí)假設(shè)數(shù)組長(zhǎng)度剛好一致能夠合并杭朱。但同時(shí)無(wú)法過(guò)濾掉修改前(dump1.exec)B方法的統(tǒng)計(jì)數(shù)據(jù)

所以?xún)H僅注釋掉id的判斷邏輯是行不通的

*/if(this.probes.length!=probecount){thrownewIllegalStateException(format("Incompatible execution data for class %s with id %016x.",name,Long.valueOf(id)));}}

通過(guò)上面的代碼注釋?zhuān)梢钥闯霈F(xiàn)有的 jacoco 合并邏輯無(wú)法滿足在測(cè)試環(huán)境數(shù)據(jù)合并的需求。

我的解決方案是針對(duì)同一個(gè) java 文件吹散,按照方法作為顆粒度弧械,切割類(lèi)對(duì)應(yīng)統(tǒng)計(jì)的探針數(shù)組,拿到各個(gè)方法的探針數(shù)據(jù)空民,再依次進(jìn)行對(duì)應(yīng)方法的數(shù)據(jù)合并刃唐。

圖 2

如圖 2 所示,只要切割拿到修改前后對(duì)應(yīng)方法的探針數(shù)據(jù)界轩,就能實(shí)現(xiàn)不同 class 版本收集的覆蓋率數(shù)據(jù)合并画饥。

關(guān)于如何切割,其實(shí)通過(guò)分析 jacoco-cli 工程中 report 指令浊猾,會(huì)發(fā)現(xiàn)按照方法切割很簡(jiǎn)單 (也可能是我考慮的太少....)

目前我還未完成這個(gè)合并功能抖甘,僅僅是找到了按照方法切割的思路,有興趣的可以動(dòng)手實(shí)踐一下葫慎。

下面貼一下簡(jiǎn)單的示例代碼圖片:

org/jacoco/core/internal/flow/ClassProbesAdapter.java

org/jacoco/core/internal/flow/MethodProbesAdapter.java

我的 demo 類(lèi)輸出(這是我之前測(cè)試的截圖衔彻,所以輸出的和上面說(shuō)的 Hello 文件不太一樣):

問(wèn)題二


方法的修改薇宠,對(duì)探針數(shù)據(jù)造成的影響

目前我考慮到 2 種比較常見(jiàn)的情況。

第一種情況:

圖 6

<問(wèn)題描述>

如圖 6 所示艰额,ApiController 類(lèi)中的 api 和 api2 方法都調(diào)用了 Services 類(lèi)的 print 方法澄港。

假設(shè)我們執(zhí)行了 api 方法,在收集 (dump1.exec) 的覆蓋率報(bào)告中柄沮,api 方法和 print 方法會(huì)顯示已被執(zhí)行回梧。

然后修改 ApiController 類(lèi)中的 api 方法,不執(zhí)行任何方法铡溪,直接收集覆蓋率數(shù)據(jù) (dump2.exe)漂辐。然后合并 dump1 和 dump2,

這時(shí)候查看覆蓋率報(bào)告棕硫,print 方法會(huì)顯示已被執(zhí)行髓涯。實(shí)際上,我認(rèn)為 print 方法不應(yīng)該被標(biāo)記為已被執(zhí)行哈扮。

<解決思路>

針對(duì)圖 6 所描述的問(wèn)題纬纪,利用函數(shù)調(diào)用鏈可以解決。api 調(diào)用了 print 方法滑肉,當(dāng) api 方法修改后包各,api 方法對(duì)應(yīng)的覆蓋率數(shù)據(jù)應(yīng)被舍棄,那么 api 方法設(shè)計(jì)的整個(gè)調(diào)用鏈的數(shù)據(jù)都應(yīng)該被舍棄

第二種情況:

圖 7

<問(wèn)題描述>

如圖 7 所示靶庙,api 方法會(huì)執(zhí)行 print 方法的 if 代碼塊以及” System.out.println(3);“輸出語(yǔ)句问畅。

api2 會(huì)執(zhí)行 print 方法的 else 代碼塊及” System.out.println(3);“輸出語(yǔ)句。

假設(shè)執(zhí)行 api 和 api2 方法六荒,收集覆蓋率數(shù)據(jù) (dump1.exec);

然后修改 api 方法护姆,不執(zhí)行任何方法,收集覆蓋率數(shù)據(jù) (dump2.exec);

按照函數(shù)調(diào)用鏈的解決思路合并 dump1 和 dump2掏击。

那么這時(shí)候 print 方法的覆蓋數(shù)據(jù)會(huì)丟失 (因?yàn)?print 方法被 api 調(diào)用卵皂,而 api 方法又被修改過(guò))。

我認(rèn)為較理想的合并結(jié)果是:api 方法被修改了所以覆蓋率數(shù)據(jù)舍棄砚亭;api2 方法未修改所以覆蓋率數(shù)據(jù)保留灯变;

print 方法中 if 代碼塊是被 api 方法調(diào)用,所以 if 代碼塊的覆蓋率數(shù)據(jù)舍棄捅膘。

else 代碼塊是被 api2 方法調(diào)用添祸,所以 else 代碼塊覆蓋率數(shù)據(jù)保留。

同時(shí)輸出語(yǔ)句” System.out.println(3);“被 api 和 api2 均調(diào)用篓跛,所以覆蓋率數(shù)據(jù)應(yīng)保留膝捞。

<解決思路>

從上述的問(wèn)題描述可以看出,僅僅是依賴(lài)函數(shù)調(diào)用鏈并不能達(dá)到我們想要的目的。

我們需要知道每個(gè)方法中蔬咬,每一個(gè)探針包含的代碼塊具體被哪個(gè)方法執(zhí)行過(guò)鲤遥。

這句話涉及 2 個(gè)動(dòng)作:調(diào)用者是誰(shuí)、并且記錄下來(lái)

想要的效果林艘,如下圖

總結(jié)一下盖奈,針對(duì)上述 2 種情況。我們需要實(shí)現(xiàn)函數(shù)調(diào)用鏈狐援,并且知道每個(gè)方法的調(diào)用者是誰(shuí)钢坦,

并在每個(gè)探針下面記錄調(diào)用者的 URI。有了解決思路啥酱,剩下的就是實(shí)現(xiàn)就好了爹凹。

1.先定義一個(gè)節(jié)點(diǎn)類(lèi)

publicclassChainNode{privateStringuri;privateChainNodepreNode;//鏈路上一級(jí)節(jié)點(diǎn)privateChainNodecalledNode;//調(diào)用者節(jié)點(diǎn)}

2.通過(guò) ASM 在每個(gè)方法開(kāi)始和結(jié)束,記錄節(jié)點(diǎn)信息镶殷,完成函數(shù)調(diào)用鏈的實(shí)現(xiàn)

publicstaticvoidaddChainNode(Stringuri){ChainNodecurrentNode=newChainNode();currentNode.setUri(uri);//set headNodeif(headNode.get()==null){headNode.set(currentNode);}//set preNodeif(tailNode.get()!=null){currentNode.setPreNode(tailNode.get());}if(calledNode.get()!=null){currentNode.setCalledNode(calledNode.get());}calledNode.set(currentNode);tailNode.set(currentNode);}publicstaticvoidsetCalledNode(Stringuri){if(uri.equals(headNode.get().getUri())){try{lock.lock();chainsSet.add(tailNode.get());headNode.set(null);//多線程情況下這個(gè)其實(shí)不用set為nulltailNode.set(null);calledNode.set(null);}finally{lock.unlock();}}else{calledNode.set(calledNode.get().getCalledNode());}}

3.在每個(gè)探針下面添加一個(gè) Set禾酱,用來(lái)存儲(chǔ)調(diào)用者的 URI 信息

privatevoidcreateSetInitMethod(finalClassVisitorcv,finalintprobeCount){MethodVisitormv=cv.visitMethod(InstrSupport.INITMETHOD_ACC,InstrSupport.INITSETMETHOD_NAME,InstrSupport.INITSETMETHOD_DESC,null,null);mv.visitCode();// [$jacocoSet_ref]mv.visitFieldInsn(Opcodes.GETSTATIC,className,InstrSupport.SET_DATA_FIELD_NAME,InstrSupport.SET_DATA_FIELD_DESC);// [$jacocoSet_ref, $jacocoSer_ref]mv.visitInsn(Opcodes.DUP);// [$jacocoSet_ref]finalLabelalreadyInitialized=newLabel();mv.visitJumpInsn(Opcodes.IFNONNULL,alreadyInitialized);mv.visitInsn(Opcodes.POP);// []// [data_ref]mv.visitFieldInsn(Opcodes.GETSTATIC,InstrSupport.CLASS_UNKONW_ERROR,"$jacocoAccess",InstrSupport.OBJECT_DESC);// [data_ref, 3]mv.visitInsn(Opcodes.ICONST_3);// [data_ref, array_ref]mv.visitTypeInsn(Opcodes.ANEWARRAY,"java/lang/Object");// set classIdmv.visitInsn(Opcodes.DUP);// [data_ref, array_ref, array_ref]mv.visitInsn(Opcodes.ICONST_0);mv.visitLdcInsn(Long.valueOf(classId));mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/Long","valueOf","(J)Ljava/lang/Long;",false);mv.visitInsn(Opcodes.AASTORE);// set classNamemv.visitInsn(Opcodes.DUP);mv.visitInsn(Opcodes.ICONST_1);mv.visitLdcInsn(className);mv.visitInsn(Opcodes.AASTORE);// set probeCountmv.visitInsn(Opcodes.DUP);mv.visitInsn(Opcodes.ICONST_2);InstrSupport.push(mv,probeCount);mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/Integer","valueOf","(I)Ljava/lang/Integer",false);mv.visitInsn(Opcodes.AASTORE);// [runtimeData_ref, array_ref]mv.visitInsn(Opcodes.DUP_X1);// [array_ref, int] mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,InstrSupport.CLASS_RUNTIME_DATA,"generateCalledSetArray","(Ljava/lang/Object;)Z",false);mv.visitInsn(Opcodes.POP);// [array_ref]// set array_ref = Set[]mv.visitInsn(Opcodes.ICONST_0);// [array_ref, 0]mv.visitInsn(Opcodes.AALOAD);// [obj_array_ref]//? [array_ref]mv.visitTypeInsn(Opcodes.CHECKCAST,"[Ljava/util/HashSet;");// [array_ref, array_ref]mv.visitInsn(Opcodes.DUP);// [array_ref]mv.visitFieldInsn(Opcodes.PUTSTATIC,className,InstrSupport.SET_DATA_FIELD_NAME,InstrSupport.SET_DATA_FIELD_DESC);// Return the class' probe array:if(withFrames){mv.visitFrame(Opcodes.F_NEW,0,FRAME_LOCALS_EMPTY,1,newObject[]{InstrSupport.SET_DATA_FIELD_DESC});}mv.visitLabel(alreadyInitialized);// []mv.visitInsn(Opcodes.ARETURN);mv.visitMaxs(Math.max(6,2),0);// Maximum local stack size is 2mv.visitEnd();}

最后看一下通過(guò)修改后的 jacoco 插樁后的 class 文件:

總結(jié)


通過(guò)上述解決思路绘趋,我認(rèn)為是可以解決在測(cè)試過(guò)程中覆蓋率數(shù)據(jù)的合并問(wèn)題颤陶。

截止到發(fā)帖,暫時(shí)還未完全實(shí)現(xiàn)整個(gè)功能陷遮。在這里僅提供解決思路滓走,如果大家感興趣,可以一起多多嘗試帽馋。

上述測(cè)試的主要是普通 class 文件搅方,對(duì) interface,enum,abstract 并未測(cè)試,并且 jacoco 的插樁策略和 jdk 版本有關(guān)的绽族。

不同的 jdk 版本腰懂,jacoco 插樁的策略不同,我目前嘗試基于 jdk8项秉。


===========2021-09-07 更新=============


測(cè)試項(xiàng)目代碼如下圖:

第一次提交代碼。發(fā)布應(yīng)用慷彤,執(zhí)行 test1娄蔼,2,3 方法底哗,第一次收集的覆蓋率報(bào)告

修改 test1 方法岁诉,第二次提交代碼。重新發(fā)布應(yīng)用跋选,執(zhí)行 test4 方法涕癣,第二次收集的覆蓋率報(bào)告

合并了上面 2 次不同版本代碼的探針數(shù)據(jù),生成的覆蓋率報(bào)告前标,如下圖

alwans首發(fā)于TesterHome社區(qū)「測(cè)試覆蓋率」節(jié)點(diǎn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末坠韩,一起剝皮案震驚了整個(gè)濱河市距潘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌只搁,老刑警劉巖音比,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異氢惋,居然都是意外死亡洞翩,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)焰望,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)骚亿,“玉大人,你說(shuō)我怎么就攤上這事熊赖±赐溃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵秫舌,是天一觀的道長(zhǎng)的妖。 經(jīng)常有香客問(wèn)我,道長(zhǎng)足陨,這世上最難降的妖魔是什么嫂粟? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮墨缘,結(jié)果婚禮上星虹,老公的妹妹穿的比我還像新娘。我一直安慰自己镊讼,他們只是感情好宽涌,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蝶棋,像睡著了一般卸亮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上玩裙,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天兼贸,我揣著相機(jī)與錄音,去河邊找鬼吃溅。 笑死溶诞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的决侈。 我是一名探鬼主播螺垢,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了枉圃?” 一聲冷哼從身側(cè)響起功茴,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讯蒲,沒(méi)想到半個(gè)月后痊土,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡墨林,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年赁酝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旭等。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酌呆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出搔耕,到底是詐尸還是另有隱情隙袁,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布弃榨,位于F島的核電站菩收,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏鲸睛。R本人自食惡果不足惜娜饵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望官辈。 院中可真熱鬧箱舞,春花似錦、人聲如沸拳亿。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肺魁。三九已至电湘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鹅经,已是汗流浹背胡桨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瞬雹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓刽虹,卻偏偏與公主長(zhǎng)得像酗捌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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