Jacoco詳解


1-Java代碼覆蓋率

Java Code Coverage

JaCoCo是一個開源的覆蓋率工具(官網地址:http://www.eclemma.org/JaCoCo/),它針對的開發語言是java,其使用方法很靈活,可以嵌入到Ant、Maven中;可以作為Eclipse插件,可以使用其JavaAgent技術監控Java程序等等。
很多第三方的工具提供了對JaCoCo的集成,如sonar、Jenkins等。其他語言也基本都有覆蓋率工具,例如python的coverage。

2-jacoco接入

2.1-maven工程接入jacoco

1-IDEA新建Maven工程

IDEA-File-New-Project-Maven直接Next

Groupid=cn.youzan.jacoco

Artifactid=jacoco

Version=默認

2-pom.xml設置

- 在<configuration>中配置具體生成的jacoco.exec的目錄和報告的目錄,設置includes/excludes;

 

- 在<rules>中配置對應的覆蓋率檢測規則;覆蓋率規則未達到時,mvn install會失敗;

<element>BUNDLE</element>


在check元素中,任何數量的rule元素可以被嵌套

屬性 描述 默認
element rule應用的element,可以取值:bundle,package,class,sourcefilemethod bundle
includes 應當被檢查的元素集合名 *
excludes 不需要被檢查的元素 empty
limits 用於檢查的limits none

 

<limit implementation="org.jacoco.report.check.Limit">
<counter>METHOD</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>

在rule元素中,任何數量的limit元素可以被嵌套

屬性 描述 默認
counter 被檢查的counter,可以是: INSTRUCTIONLINEBRANCHCOMPLEXITYMETHOD and CLASS. INSTRUCTION
value 需要被檢查的counter的值,可以是: TOTALCOUNTMISSEDCOUNTCOVEREDCOUNTMISSEDRATIO and COVEREDRATIO. COVEREDRATIO
minimum 期望的最小值。 none
maximum 期望的最大值。 none


- 在<executions>中配置執行步驟:
1)prepare-agent(即構建jacoco-unit.exec);
2)check(即根據在<rules>定義的規矩進行檢測);
3)package(生成覆蓋率報告,默認生成在target/site/index.html)

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.youzan.ycm</groupId>
    <artifactId>jacoco_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <compiler.source>1.8</compiler.source>
        <compiler.target>1.8</compiler.target>
        <junit.version>4.12</junit.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.7.9</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>freewill</finalName>
        <plugins>
            <plugin>
                <inherited>true</inherited>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${compiler.source}</source>
                    <target>${compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.7.9</version>
                <configuration>
                <!-- rules里面指定覆蓋規則 -->
                <rules>
                    <rule implementation="org.jacoco.maven.RuleConfiguration">
                        <element>BUNDLE</element>
                        <limits>  
                            <!-- 指定方法覆蓋最低 -->
                            <limit implementation="org.jacoco.report.check.Limit">
                                <counter>METHOD</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                            <!-- 指定分支覆蓋最低 -->
                            <limit implementation="org.jacoco.report.check.Limit">
                                <counter>BRANCH</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                            <!-- 指定類覆蓋到,最多Missed 0 -->
                            <limit implementation="org.jacoco.report.check.Limit">
                                <counter>CLASS</counter>
                                <value>MISSEDCOUNT</value>
                                <maximum>0</maximum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
                <executions>
                    <execution>
                        <id>prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>check</id>
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>target/jacoco.exec</dataFile>
                            <outputDirectory>target/jacoco-wl</outputDirectory>
                            <includes>
                                <include>**/Func1**</include>
								<include>**/Func2**</include>
                            </includes>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

3-新建類

PS:測試類的命名一定按照如下命名方式來

Func1.java

public class Func1 {
    public int add(int a, int b) {
        return a + b;
    }
    public int sub(int a, int b) {
        return a - b;
    }
}

 

Func2.java

Func3.java

2個內容一樣也可以

public class Func2 {
    public int multi(int a, int b) {
        return a*b;
    }

    public int div(int a, int b) {
        while(b!=0){
            return a/b;
        }
        if(b<0){
            return a/b;
        }else{
            return -a/b;
        }
    }
}

4-新建單測類

PS:測試類的命名一定按照如下命名方式來

ATest3.java

import org.junit.Assert;
import org.junit.Test;

public class ATest3 {
    private Func2 func2 = new Func2();

    @Test
    public void testAdd1() {
        int a = 10;
        int b = 20;
        int expected = 200;
        Assert.assertEquals(expected, func2.multi(a, b));
    }

    @Test
    public void testSub2() {
        int a = 40;
        int b = 20;
        int expected =2;
        Assert.assertEquals(expected, func2.div(a, b));
    }
}

 

BTest.java

import org.junit.Assert;
import org.junit.Test;

public class BTest {
    private Func2 func2 = new Func2();

    @Test
    public void testAdd1() {
        int a = 10;
        int b = 20;
        int expected = 200;
        Assert.assertEquals(expected, func2.multi(a, b));
    }

    @Test
    public void testSub2() {
        int a = 40;
        int b = 20;
        int expected =2;
        Assert.assertEquals(expected, func2.div(a, b));
    }
}

 

Test1.java

import org.junit.Assert;
import org.junit.Test;

public class Test1 {
    private Func1 func1 = new Func1();

    @Test
    public void testAdd() {
        int a = 10;
        int b = 20;
        int expected = 30;
        Assert.assertEquals(expected, func1.add(a, b));
    }

    @Test
    public void testSub() {
        int a = 10;
        int b = 20;
        int expected = -10;
        Assert.assertEquals(expected, func1.sub(a, b));
    }
}

工程的組織如下:

5-MVN Test

 

幾點說明:

說明1:測試類的命名規范

maven 的測試類需要遵循相應的規范命名,否則無法運行測試類,無法生成測試報告以及覆蓋率報告。
jacoco 使用的是 maven-surefire-plugin 插件,它的默認測試類名規范是:
Test*.java:以 Test 開頭的 Java 類;
*Test.java:以 Test 結尾的 Java 類;
*TestCase.java:以 TestCase 結尾的 Java 類;
或者可以在pom中自定義測試類:

 

說明2:includes/excludes設置

 

同理:excludes的設置,完全參考includes即可

 

說明3:rules指定覆蓋規則 

當設置了覆蓋率規則,但是實際結果未達標時,mvn test命令正常執行,但是mvn install 失敗:

mvn test 成功

 

mvn install失敗,覆蓋率規則不達標

例子2:現有開發工程接入情況:ycm-perfrom

舉例子

3-如何看懂jacoco報告

Jacoco報告層級:包>類>方法

Jacoco報告緯度:

字段 名稱 描述
Instructions 代碼指令 字節碼中的指令覆蓋:后面原理部分會詳解
Branches 分支 for,if,while,switch 語句代表分支,報告里面用菱形標識分支,Assert,Boolean也會被定義為分支,判斷語句都會被定義為分支
Cyclomatic Complexity 圈復雜度

Jacoco為每個非抽象方法計算圈復雜度,並也會計算每個類,包,組的復雜度。

圈復雜度可以理解為覆蓋所有的可能情況最少使用的測試用例數。后面詳解

Lines 綠色+紅色+黃色背景的行才是jacoco統計的行,else不統計行,else里面的代碼會計入
Methods/CLasses 方法/類 非抽象方法/類被執行1條指令,覆蓋率就統計

 

3.1-Instructions-代碼指令

紅色代表未覆蓋,綠色代表已覆蓋,Cov 為總體覆蓋率。
275/357 22%
未覆蓋275條指令/總357條指令  覆蓋率22%

Jacoco 計算的最小單位就是字節碼指令。指令覆蓋率表明了在所有的指令中,哪些被執行過以及哪些沒有被執行。

3.2-Branches-分支

for,if,while,switch 語句代表分支,Assert,Boolean也會被定義為分支,判斷語句都會被定義為分支

 

分支用菱形標示:

紅色菱形:無覆蓋,該分支指令均無執行。
黃色菱形:部分覆蓋,該分支部分指令被執行。
綠色菱形:全覆蓋,該分支所有指令被執行。

 

PS:指令全覆蓋,不代表分支全覆蓋!

Missed Instructions覆蓋率100%,但分支覆蓋率為75%; 原因:所有代碼行都覆蓋並不代表所有分支都覆蓋完整。

分析:urls!=null這個條件已覆蓋,但urls=null這個條件還沒有覆蓋 ;所有的代碼行都有覆蓋到、但分支還沒有覆蓋完整、所以Instructions的覆蓋率100%、Braches的覆蓋率75%。

3.3-Cyclomatic Complexity-圈復雜度

1-什么是圈復雜度

圈復雜度(Cyclomatic Complexity)是一種代碼復雜度的衡量標准,由 Thomas McCabe 於 1976年定義。它可以用來衡量一個模塊判定結構的復雜程度,數量上表現為獨立現行路徑條數,也可理解為覆蓋所有的可能情況最少使用的測試用例數。圈復雜度大說明程序代碼的判斷邏輯復雜,可能質量低且難於測試和維護。程序的可能錯誤和高的圈復雜度有着很大關系。

圈復雜度主要與分支語句(if、else、,switch 等)的個數成正相關。可以在圖1中看到常用到的幾種語句的控制流圖(表示程序執行流程的有向圖)。當一段代碼中含有較多的分支語句,其邏輯復雜程度就會增加。在計算圈復雜度時,可以通過程序控制流圖方便的計算出來。

2-采用圈復雜度去衡量代碼的好處
1.指出極復雜模塊或方法,這樣的模塊或方法也許可以進一步細化。

2.限制程序邏輯過長。
McCabe&Associates 公司建議盡可能使 V(G) <= 10。NIST(國家標准技術研究所)認為在一些特定情形下,模塊圈復雜度上限放寬到 15 會比較合適。
因此圈復雜度 V(G)與代碼質量的關系如下:
V(G) ∈ [ 0 , 10 ]:代碼質量不錯;
V(G) ∈ [ 11 , 15 ]:可能存在需要拆分的代碼,應當盡可能想措施重構;
V(G) ∈ [ 16 , ∞ ):必須進行重構;

3.方便做測試計划,確定測試重點。
許多研究指出一模塊及方法的圈復雜度和其中的缺陷個數有相關性,許多這類研究發現圈復雜度和模塊或者方法的缺陷個數有正相關的關系:圈復雜度最高的模塊及方法,其中的缺陷個數也最多,做測試時做重點測試。

3-計算圈復雜度的方法

通常使用的計算公式是V(G) = e – n + 2 , e 代表在控制流圖中的邊的數量(對應代碼中順序結構的部分),n 代表在控制流圖中的節點數量,包括起點和終點(1、所有終點只計算一次,即便有多個return或者throw;2、節點對應代碼中的分支語句)。

增加圈復雜度的語句:在代碼中的表現形式:在一段代碼中含有很多的 if / else 語句或者其他的判定語句(if / else , switch / case , for , while , | | , ? , …)。

代碼示例-控制流圖
根據公式 V(G) = e – n + 2 = 12 – 8 + 2 = 6 ,上圖的圈復雜段為6。
說明一下為什么n = 8,雖然圖上的真正節點有12個,但是其中有5個節點為throw、return,這樣的節點為end節點,只能記做一個。

4-Jacoco圈復雜度計算

Jacoco 基於下面的方程來計算復雜度,B是分支的數量,D是決策點的數量:
v(G) = B – D + 1
基於每個分支的被覆蓋情況,Jacoco也為每個方法計算覆蓋和缺失的復雜度。缺失的復雜度同樣表示測試案例沒有完全覆蓋到這個模塊。注意Jacoco不將異常處理作為分支,try/catch塊也同樣不增加復雜度。

 

例子1:

報告可以看出:圈=3,Missed=2(if,else)

同理可以計算:

multi方法的圈復雜度=0(無分支)-0(無決策點)+1

類的圈復雜度:2(2個方法)-2(2個決策點)+1

 

例子2:

圈復雜度=?

5-降低圈復雜度的重構技術

1.Extract Method(提煉函數)
2.Substitute Algorithm(替換你的算法)
3.Decompose Conditional(分解條件式)
4.Consolidate Conditional Expression(合並條件式)
5.Consolidate Duplicate Conditional Fragments(合並重復的條件片斷)
6.Remove Control Flag(移除控制標記)
7.Parameterize Method(令函數攜帶參數)
8.異常邏輯處理型重構方法
9.狀態處理型重構方法(1)
10.狀態處理型重構方法(2)
11.case語句重構方法(1)

參考文檔:https://blog.csdn.net/u010684134/article/details/94412483

3.4-Lines-行

綠色+紅色+黃色背景的行才是jacoco實際統計的代碼行,

紅色背景代表Missed行

黃色+綠色代表覆蓋行;

無背景的都不統計(變量的定義,引用,else定義等)

上面的行覆蓋結果:

 

3.5-方法/類

方法,類里面有一行指令被執行,代表覆蓋

4-Jacoco的原理

JaCoCo使用ASM技術修改字節碼方法,可以修改Jar文件、class文件字節碼文件。

1-ASM簡介

ASM是一個Java字節碼操縱框架,它能被用來動態生成類或者增強既有類的功能。ASM可以直接產生二進制class文件,也可以在類被加載入Java虛擬機之前動態改變類行為。Java class被存儲在嚴格格式定義的.class文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。簡單使用

2-插樁方式

覆蓋實現技術

上圖包含了幾種不同的收集覆蓋率信息的方法,每個方法的實現都不太一樣,這里主要關心字節碼注入這種方式(Byte Code)。Byte Code包含Offline和On-The-Fly兩種注入方式

On-the-fly更加方便獲取代碼覆蓋率
無需提前進行字節碼插樁
無需停機(Offline需要停機),可以實時獲取覆蓋率
Offline無需額外開啟代理

2.1-Offline

對Class文件進行插樁(探針),生成最終的目標文件,執行目標文件以后得到覆蓋執行結果,最終生成覆蓋率報告。

Offline使用場景(From Jacoco Documentation)
運行環境不支持Java Agent
部署環境不允許設置JVM參數
字節碼需要被轉換成其他虛擬機字節碼,如Android Dalvik Vm
動態修改字節碼文件和其他Agent沖突
無法自定義用戶加載類

【主要用於單測,集成測試等靜態場景】

offline大致流程:

2.2-On The Fly

JVM通過-javaagent參數指定特定的jar文件啟動Instrumentation代理程序,代理程序在裝載class文件前判斷是否已經轉換修改了該文件,若沒有,則將探針(統計代碼)插入class文件,最后在JVM執行測試代碼的過程中完成對覆蓋率的分析。

【主要用於服務化系統的代碼動態覆蓋率獲取】

JaCoCo代理收集執行信息並根據請求或在JVM退出時將其轉儲。有三種不同的執行數據輸出模式:
文件系統:在JVM終止時,執行數據被寫入本地文件。
TCP套接字服務器:外部工具可以連接到JVM,並通過套接字連接檢索執行數據。可以在VM退出時進行可選的執行數據重置和執行數據轉儲。
TCP套接字客戶端:啟動時,JaCoCo代理連接到給定的TCP端點。執行數據根據請求寫入套接字連接。可以在VM退出時進行可選的執行數據重置和執行數據轉儲。

該代理jacocoagent.jar是JaCoCo發行版的一部分,包括所有必需的依賴項。可以使用以下JVM選項激活Java代理:

-javaagent:[yourpath /] jacocoagent.jar = [option1] = [value1],[option2] = [value2]

通過這種方式進行服務的agent 啟動時,一般需要在容器的啟動腳本配置,下面是一個參考的配置:

 

#!/usr/bin/env bash
MAIN_CLASS="me.ele.marketing.hongbao.Application"
SCRIPTS_DIR=`dirname "$0"`
DIR_PROJECT=`cd $SCRIPTS_DIR && pwd`
DIR_TMP="${DIR_PROJECT}/tmp"
DIR_LOG="${DIR_PROJECT}/log"
DATETIME=`date +"%Y%m%d_%H%M%S"`
mkdir -p ${DIR_TMP} ${DIR_LOG}
if [ -z ${ELEAPPOS_OFFER_MEM+x} ]; then
echo "Cannot get system mem from system var, because mem var ELEAPPOS_OFFER_MEM not set."
MEM_OPTS="${JVM_MEMORY}"
else
echo "ELEAPPOS_OFFER_MEM is set: ${ELEAPPOS_OFFER_MEM}, so generate jvm memory by system mem var..."
JVM_MEM=$(($ELEAPPOS_OFFER_MEM*700))
MEM_OPTS="-Xms${JVM_MEM}m -Xmx${JVM_MEM}m"
echo "jvm mem is set to: ${MEM_OPTS}"
fi

#步驟1:下載包
wget -O jacocoagent.jar http://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/0.8.1/org.jacoco.agent-0.8.1-runtime.jar

#步驟2:確定本機IP
LOCAL_IP=$(/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v 172.17.0.1|grep -v inet6|awk '{print $2}'|tr -d "addr:"|head -1)

MEM_OPTS=${MEM_OPTS}
GC_OPTS="-XX:+UseG1GC"
GC_OPTS="${GC_OPTS} -XX:MaxGCPauseMillis=20"
GC_OPTS="${GC_OPTS} -XX:+UnlockExperimentalVMOptions"
GC_OPTS="${GC_OPTS} -XX:InitiatingHeapOccupancyPercent=56"
GC_OPTS="${GC_OPTS} -Xloggc:${DIR_LOG}/gc_${DATETIME}.log"
GC_OPTS="${GC_OPTS} -XX:+PrintGCDateStamps"
GC_OPTS="${GC_OPTS} -XX:+PrintGCDetails"
GC_OPTS="${GC_OPTS} -XX:+HeapDumpOnOutOfMemoryError"
GC_OPTS="${GC_OPTS} -XX:HeapDumpPath=${DIR_LOG}/heapdump_${DATETIME}.hprof"
PARAMS="-Dfile.encoding=UTF-8"
PARAMS="${PARAMS} -DAPPID=${APP_ID}"
PARAMS="${PARAMS} -Duser.dir=${DIR_PROJECT}"
PARAMS="${PARAMS} -Djava.io.tmpdir=${DIR_TMP}"
PARAMS="${PARAMS} -DTEAM=${APP_TEAM}"

#步驟3:添加啟動agent的參數,主要注意 IP+端口,因為這個是jacoco agent的通信接口(tcp)
PARAMS="${PARAMS} -javaagent:/data/marketing.hongbao/jacocoagent.jar=includes=me.ele.marketing.hongbao.*,output=tcpserver,address=${LOCAL_IP},port=8335,classdumpdir=/data/marketing.hongbao/eship/jacoco"

CLASS_PATH="$PROJECT_DIR/conf:"
CLASS_PATH="${CLASS_PATH}:$PROJECT_DIR/conf:"
CLASS_PATH="${CLASS_PATH}:$PROJECT_DIR/lib/*:"
CLASS_PATH="${CLASS_PATH}:/data/marketing.hongbao/marketing.hongbao/*"
#verify_codes
echo `pwd`
echo "##########################################################"
echo "exec java -server ${MEM_OPTS} ${GC_OPTS} ${PARAMS} -classpath ${CLASS_PATH} ${MAIN_CLASS}"
echo "##########################################################"
exec java -server ${MEM_OPTS} ${GC_OPTS} ${PARAMS} -classpath ${CLASS_PATH} ${MAIN_CLASS}


重點看下步驟3:

-javaagent:/data/marketing.test/jacocoagent.jar=includes=me.test.maketing.*,output=tcpserver,address=${LOCAL_IP},port=8335,classdumpdir=/data/marketing.hongbao/eship/jacoco"

 

當服務啟動的時候,容器的8335/默認6330端口會開啟TCP Server,如何生成覆蓋率結果:

// dump結果數據

java -jar jacococli.jar dump --port 6300 --destfile data/jacoco-it.exec

// 生成覆蓋率結果

java -jar jacococli.jar report data/jacoco-it.exec --classfiles ***/classes --html html

就可以獲取覆蓋率的結果數據,這種方法獲取的是全量的代碼覆蓋率。

 

遠程代理控制的安全注意事項
在tcpserver和 tcpclient模式下打開的端口和連接以及JMX接口不提供任何身份驗證機制。如果在生產系統上運行JaCoCo,請確保沒有不受信任的源可以訪問TCP服務器端口,或者JaCoCo TCP客戶端僅連接到受信任的目標。否則,可能會泄露應用程序的內部信息,或者可能發生DOS攻擊。

3-增量覆蓋率

增量覆蓋率的思想:

1. 獲取測試完成后的 exec 文件(二進制文件,里面有探針的覆蓋執行信息);
2. 獲取基線提交與被測提交之間的差異代碼;
3. 對差異代碼進行解析,切割為更小的顆粒度,我們選擇方法作為最小緯度;
4. 改造 JaCoCo ,使它支持僅對差異代碼生成覆蓋率報告;

3.1-獲取exec數據

參考On The Fly方式獲取dump數據,或者通過JaCoCo 開放出來的 API 進行 exec 文件獲取:

public void dumpData(String localRepoDir, List<IcovRequest> icovRequestList) throws IOException {  
icovRequestList.forEach(req -> req.validate());
icovRequestList.parallelStream().map(icovRequest -> {
String destFileDir = ...;
String address = icovRequest.getAddress();
try {
final FileOutputStream localFile = new FileOutputStream(destFileDir + "/" + DEST_FILE_NAME);
final ExecutionDataWriter localWriter = new ExecutionDataWriter(localFile);
final Socket socket = new Socket(InetAddress.getByName(address), PORT);
final RemoteControlWriter writer = new RemoteControlWriter(socket.getOutputStream());
final RemoteControlReader reader = new RemoteControlReader(socket.getInputStream());
reader.setSessionInfoVisitor(localWriter);
reader.setExecutionDataVisitor(localWriter);
writer.visitDumpCommand(true, false);
if (!reader.read()) {
throw new IOException("Socket closed unexpectedly.");
}
...
} ...
return null;
}).count();
}

 

3.2-獲取代碼差異

JGit 是一個用 Java 寫成的功能比較健全的 Git 的實現,它在 Java 社區中被廣泛使用。在這一步的主要流程是獲取基線提交與被測提交之間的差異代碼,然后過濾一些需要排除的文件(比如非 Java 文件、測試文件等等),對剩余文件進行解析,將變更代碼解析到方法緯度,部分代碼片段如下:

 

private List<AnalyzeRequest> findDiffClasses(IcovRequest request) throws GitAPIException, IOException {  
String gitAppName = DiffService.extractAppNameFrom(request.getRepoURL());
String gitDir = workDirFor(localRepoDir,request) + File.separator + gitAppName;
DiffService.cloneBranch(request.getRepoURL(),gitDir,branchName);
String masterCommit = DiffService.getCommitId(gitDir);
List<DiffEntry> diffs = diffService.diffList(request.getRepoURL(),gitDir,request.getNowCommit(),masterCommit);
List<AnalyzeRequest> diffClasses = new ArrayList<>();
String classPath;
for (DiffEntry diff : diffs) {
if(diff.getChangeType() == DiffEntry.ChangeType.DELETE){
continue;
}
AnalyzeRequest analyzeRequest = new AnalyzeRequest();
if(diff.getChangeType() == DiffEntry.ChangeType.ADD){
...
}else {
HashSet<String> changedMethods = MethodDiff.methodDiffInClass(oldPath, newPath);
analyzeRequest.setMethodnames(changedMethods);
}
classPath = gitDir + File.separator + diff.getNewPath().replace("src/main/java","target/classes").replace(".java",".class");
analyzeRequest.setClassesPath(classPath);
diffClasses.add(analyzeRequest);
}
return diffClasses;
}

3.3-差異代碼解析

JaCoCo默認的注入方式為全量注入。通過閱讀源碼,發現注入的邏輯主要在ClassProbesAdapter中。ASM在遍歷字節碼時,每次訪問一個方法定義,都會回調這個類的visitMethod方法 ,在visitMethod方法中再調用ClassProbeVisitor的visitMethod方法,並最終調用MethodInstrumenter完成注入。部分代碼片段如下:

 

@Override
public final MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature, final String[] exceptions) {
final MethodProbesVisitor methodProbes;
final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
signature, exceptions);
if (mv == null) {
methodProbes = EMPTY_METHOD_PROBES_VISITOR;
} else {
methodProbes = mv;
}
return new MethodSanitizer(null, access, name, desc, signature,
exceptions) {
@Override
public void visitEnd() {
super.visitEnd();
LabelFlowAnalyzer.markLabels(this);
final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
methodProbes, ClassProbesAdapter.this);
if (trackFrames) {
final AnalyzerAdapter analyzer = new AnalyzerAdapter(
ClassProbesAdapter.this.name, access, name, desc,
probesAdapter);
probesAdapter.setAnalyzer(analyzer);
this.accept(analyzer);
} else {
this.accept(probesAdapter);
}
}
};
}

 

如何去修改JaCoCo的源碼?繼承原有的ClassInstrumenter和ClassProbesAdapter,修改其中的visitMethod方法,只對變化了方法進行注入:

@Override
public final MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature, final String[] exceptions) {
if (Utils.shoudHackMethod(name,desc,signature,changedMethods,cv.getClassName())) {
...
} else {
return cv.getCv().visitMethod(access, name, desc, signature, exceptions);
}
}

3.4-差異代碼覆蓋率

生成增量代碼的覆蓋率報告和增量注入的原理類似,通過閱讀源碼,分別需要修改Analyzer(只對變化的類做處理):

 

和ReportClassProbesAdapter(只對變化的方法做處理):

4-探針

JaCoCo通過ASM在字節碼中插入Probe指針(探測指針),每個探測指針都是一個BOOL變量(true表示執行、false表示沒有執行),程序運行時通過改變指針的結果來檢測代碼的執行情況(不會改變原代碼的行為)。

1-插入探針的源碼:

boolean[] arrayOfBoolean = $jacocoInit();
arrayOfBoolean[4] = true;

2-插入探針的字節碼指令:

例子1:
aload_2     # 從局部變量2中裝載引用類型值入棧
iconst_4    # 4(int)值入棧
iconst_1     # 1(int)值入棧
bastore     # 將棧頂boolean類型值或byte類型值保存到指定boolean類型數組或byte類型數組的指定項。

 

例子2:

aload_2      # 從局部變量2中裝載引用類型值入棧
bipush   6   #valuebyte值帶符號擴展成int值入棧
iconst_1      #1(int)值入棧
bastore      #將棧頂boolean類型值或byte類型值保存到指定boolean類型數組或byte類型數組的指定項。

 

探測代碼的大小取決於探測陣列變量的位置和探測標識符的值,因為可以使用不同的操作碼。如下表所示,每個探測器的開銷介於4到7個字節的附加字節碼之間:


3-Java字節碼指令大全:

https://www.cnblogs.com/longjee/p/8675771.html

4-關於switch插樁分析

源碼:

public void testSwitch(int i){
      switch(i) {
        case 1:
        System.out.println("1");
        break;
        case 2:
        System.out.println("2");    
        break;
        case 3:
        System.out.println("3"); 
        break;
        case 4:
        System.out.println("4");
        break;
        case 10:
        System.out.println("10");
        break;
        default:
        System.out.println("...");
        break;
        }//switch

    }

 

插樁后的源碼:

public void testSwitch(int arg1) { 
    boolean[] arrayOfBoolean = $jacocoInit(); 
    switch (i)
    {
    case 1:
      System.out.println("1");
      arrayOfBoolean[4] = true; break;
    case 2:
      System.out.println("2");
      arrayOfBoolean[5] = true; break;
    case 3:
      System.out.println("3");
      arrayOfBoolean[6] = true; break;
    case 4:
      System.out.println("4");
      arrayOfBoolean[7] = true; break;
    case 10:
      System.out.println("10");
      arrayOfBoolean[8] = true; break;
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
    default:
      System.out.println("..."); 
      arrayOfBoolean[9] = true;
    }
     arrayOfBoolean[10] = true; 
 } 

我們可以發現,每一處label處都插入了探針,以及最后的return處也插入了一個探針。

源碼-字節碼(未插樁):

public void testSwitch(int);
    Code:
       0: iload_1
       1: tableswitch   { // 1 to 10
                     1: 56
                     2: 67
                     3: 78
                     4: 89
                     5: 111
                     6: 111
                     7: 111
                     8: 111
                     9: 111
                    10: 100
               default: 111
          }
      56: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      59: ldc           #8                  // String 1
      61: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      64: goto          119
      67: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      70: ldc           #10                 // String 2
      72: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      75: goto          119
      78: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      81: ldc           #11                 // String 3
      83: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      86: goto          119
      89: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      92: ldc           #12                 // String 4
      94: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      97: goto          119
     100: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     103: ldc           #13                 // String 10
     105: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     108: goto          119
     111: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     114: ldc           #14                 // String ...
     116: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     119: return

 

源碼-字節碼(插樁后):

public void testSwitch(int);
    Code:
       0: invokestatic  #65                 // Method $jacocoInit:()[Z
       3: astore_2
       4: iload_1
       5: tableswitch   { // 1 to 10
                     1: 60
                     2: 75
                     3: 90
                     4: 106
                     5: 138
                     6: 138
                     7: 138
                     8: 138
                     9: 138
                    10: 122
               default: 138
          }
      60: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      63: ldc           #8                  // String 1
      65: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      //case 1探針
      68: aload_2
      69: iconst_4
      70: iconst_1
      71: bastore
      72: goto          151
      75: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      78: ldc           #10                 // String 2
      80: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        //case 2探針
      83: aload_2
      84: iconst_5
      85: iconst_1
      86: bastore
      87: goto          151
      90: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      93: ldc           #11                 // String 3
      95: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        //case 3 探針
      98: aload_2
      99: bipush        6
     101: iconst_1
     102: bastore
     103: goto          151
     106: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     109: ldc           #12                 // String 4
     111: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       //case 4 探針
     114: aload_2
     115: bipush        7
     117: iconst_1
     118: bastore
     119: goto          151
     122: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     125: ldc           #13                 // String 10
     127: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       //case 10探針
     130: aload_2
     131: bipush        8
     133: iconst_1
     134: bastore
     135: goto          151
     138: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     141: ldc           #14                 // String ...
     143: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       //default 探針
     146: aload_2
     147: bipush        9
     149: iconst_1
     150: bastore
     //return 探針
     151: aload_2
     152: bipush        10
     154: iconst_1
     155: bastore
     156: return

 

4-插樁策略

源碼:

public static void example() {
    a();
    if (cond()) {
        b();
    } else {
        c();
    }
    d();
}

字節碼:

public static example()V
      INVOKESTATIC a()V
      INVOKESTATIC cond()Z
      IFEQ L1
      INVOKESTATIC b()V
      GOTO L2
  L1: INVOKESTATIC c()V
  L2: INVOKESTATIC d()V
      RETURN

這樣我們可以使用ASM框架在字節碼文件中進行插樁操作,具體的是插入探針probe,一般是Boolean數組,下面是原始的控制流圖,以及插樁完成的控制流圖。

Bytecode Control Flow

可以看出,探針的位置位於分支后

由Java字節碼定義的控制流圖有不同的類型,每個類型連接一個源指令和一個目標指令,當然有時候源指令和目標指令並不存在,或者無法被明確(異常)。不同類型的插入策略也是不一樣的。

Type Source Target Remarks
ENTRY - First instruction in method  
SEQUENCE Instruction, except GOTOxRETURNTHROWTABLESWITCH and LOOKUPSWITCH Subsequent instruction  
JUMP GOTOIFxTABLESWITCH or LOOKUPSWITCH instruction Target instruction TABLESWITCH and LOOKUPSWITCH will define multiple edges.
EXHANDLER Any instruction in handler scope Target instruction  
EXIT xRETURN or THROW instruction -  
EXEXIT Any instruction - Unhandled exception.

 

下面是具體的插入探針的策略:

Type Before After Remarks
SEQUENCE
Sequence
Sequence with Probe
如果是簡單序列,則將探針簡單地插入兩個指令之間。
JUMP (unconditional)
Unconditional Jump
Unconditional Jump with Probe
由於在任何情況下都執行無條件跳轉,因此我們也可以在GOTO指令之前插入探針。
JUMP (conditional)
Conditional Jump
Conditional Jump with Probe
向條件跳轉添加探針會比較棘手。我們反轉操作碼的語義,並在條件跳轉之后立即添加探測。
EXIT
Exit
Exit with Probe
正如RETURN和THROW語句的本質一樣,實際上是將方法留在了我們在這些語句之前添加探針的位置。

 

注意到探針是線程安全的,它不會改變操作棧和本地數組。它也不會通過外部的調用而離開函數。先決條件僅僅是探針數組作為一個本地變量被獲取。在每個函數的開始,附加的指令代碼將會插入以獲得相應類的數組對象,避免代碼復制,這個初始化會在靜態私有方法$jacocoinit()中進行。

6-Jacoco與Jenkins

http://shangyehua-jenkins.cd-qa.qima-inc.com/job/Jacoco-Bit-Commerce/

1-安裝Jacoco plugin

 

2-新建任務

當 “新建”一個任務時,在 構建后操作中點擊 “增加構建后操作步驟”下來框中選擇“Record JaCoCo coverag report”

3-查看報告

 

7-關於一個jacoco的專利

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM