天天看點

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

,

sourcefile

method

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,可以是: 

INSTRUCTION

LINE

BRANCH

COMPLEXITY

METHOD

 and 

CLASS

.
INSTRUCTION
value 需要被檢查的counter的值,可以是: 

TOTALCOUNT

MISSEDCOUNT

COVEREDCOUNT

MISSEDRATIO

 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:指令全覆寫,不代表分支全覆寫!

Jacoco詳解
Jacoco詳解

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-插樁方式

Jacoco詳解

上圖包含了幾種不同的收集覆寫率資訊的方法,每個方法的實作都不太一樣,這裡主要關心位元組碼注入這種方式(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大緻流程:

Jacoco詳解

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數組,下面是原始的控制流圖,以及插樁完成的控制流圖。

Jacoco詳解

可以看出,探針的位置位于分支後

由Java位元組碼定義的控制流圖有不同的類型,每個類型連接配接一個源指令和一個目标指令,當然有時候源指令和目标指令并不存在,或者無法被明确(異常)。不同類型的插入政策也是不一樣的。

Type Source Target Remarks
ENTRY - First instruction in method
SEQUENCE Instruction, except 

GOTO

xRETURN

THROW

TABLESWITCH

 and 

LOOKUPSWITCH

Subsequent instruction
JUMP

GOTO

IFx

TABLESWITCH

 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
Jacoco詳解
Jacoco詳解
如果是簡單序列,則将探針簡單地插入兩個指令之間。      
JUMP (unconditional)
Jacoco詳解
Jacoco詳解
由于在任何情況下都執行無條件跳轉,是以我們也可以在GOTO指令之前插入探針。
JUMP (conditional)
Jacoco詳解
Jacoco詳解
向條件跳轉添加探針會比較棘手。我們反轉操作碼的語義,并在條件跳轉之後立即添加探測。
EXIT
Jacoco詳解
Jacoco詳解
正如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的專利

Jacoco詳解