天天看點

代碼覆寫工具Jacoco使用示例及源碼分析Content調用的開源架構測試源碼對比兩種插樁模式關于switch插樁分析Report生成插樁政策參考

Jacoco項目首頁:http://www.eclemma.org/jacoco/

本文位址:JacocoAnalyse

Content

  • 調用的開源架構
    • Ant
    • ASM
  • 測試源碼對比
    • 插入前源碼
    • 插入後源碼
    • 插入前位元組碼
    • 插入後位元組碼
  • 兩種插樁模式
    • 插樁方式
    • Offline
    • On-the-fly
    • 比較
  • 關于switch插樁分析
    • TableSwitch
    • Lookupswitch
  • Report生成
    • Task report
    • Element executiondata
    • Element structure
    • Element xml
    • Element check
  • 插樁政策
  • 源碼分析
    • 插樁
    • 生成報告
  • 參考

調用的開源架構

Ant

開發文檔:http://www.jacoco.org/jacoco/trunk/doc/ant.html

ASM

項目首頁:http://asm.ow2.org/

ASM分析:ASM Analyse

測試源碼對比

插入前源碼

public class Hello
{
  public Hello()
  {
    int rand = (int)(Math.random() * D);
    if (rand %  == )
      System.out.println("Hi,0");
    else {
      System.out.println("Hi,1");
    }
    System.out.println("End");
  }
}
           
public class HelloTest
{
  public static void main(String[] args)
  {
    Hello h = new Hello();
  }
}
           

插入後源碼

public class Hello
{
  public Hello()
  {
    arrayOfBoolean[] = true;
    int rand = (int)(Math.random() * D);
    if (rand %  == ) { arrayOfBoolean[] = true;
      System.out.println("Hi,0"); arrayOfBoolean[] = true;
    } else {
      System.out.println("Hi,1"); arrayOfBoolean[] = true;
    }
    System.out.println("End");
    arrayOfBoolean[] = true;
  }
}
           
public class HelloTest
{
  public HelloTest()
  {
    arrayOfBoolean[] = true;
  }
  public static void main(String[] arg0) {
    boolean[] arrayOfBoolean = $jacocoInit(); Hello h = new Hello();
    arrayOfBoolean[] = true;
  }
}
           

插入前位元組碼

F:\Jacoco\target\classes>javap -c Hello
Compiled from "Hello.java"
public class Hello {
  public Hello();
    Code:
       : aload_0
       : invokespecial #1                  // Method java/lang/Object."<init>":()V
       : invokestatic  #2                  // Method java/lang/Math.random:()D
       : ldc2_w        #3                  // double 100.0d
      : dmul
      : d2i
      : istore_1
      : iload_1
      : iconst_2
      : irem
      : ifne          
      : getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #6                  // String Hi,0
      : invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : goto          
      : getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #8                  // String Hi,1
      : invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #9                  // String End
      : invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : return
}
           

插入後位元組碼

F:\Jacoco\target\classes-instr>javap -c Hello
Compiled from "Hello.java"
public class Hello {
  public Hello();
    Code:
       : invokestatic  #49                 // Method $jacocoInit:()[Z
       : astore_1
       : aload_0
       : invokespecial #1                  // Method java/lang/Object."<init>":()V
       : aload_1
       : iconst_0
      : iconst_1
      : bastore
      : invokestatic  #2                  // Method java/lang/Math.random:()D
      : ldc2_w        #3                  // double 100.0d
      : dmul
      : d2i
      : istore_2
      : iload_2
      : iconst_2
      : irem
      : ifne          
      : aload_1
      : iconst_1
      : iconst_1
      : bastore
      : getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #6                  // String Hi,0
      : invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : aload_1
      : iconst_2
      : iconst_1
      : bastore
      : goto          
      : getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #8                  // String Hi,1
      : invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : aload_1
      : iconst_3
      : iconst_1
      : bastore
      : getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #9                  // String End
      : invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : aload_1
      : iconst_4
      : iconst_1
      : bastore
      : return
}
           

兩種插樁模式

插樁方式

代碼覆寫工具Jacoco使用示例及源碼分析Content調用的開源架構測試源碼對比兩種插樁模式關于switch插樁分析Report生成插樁政策參考

Offline

先對位元組碼檔案進行插樁,然後執行插樁後的位元組碼檔案,生成覆寫資訊并導出報告。

On-the-fly

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

比較

  • On-the-fly更加友善擷取代碼覆寫率
    • 無需提前進行位元組碼插樁
    • 無需停機(Offline需要停機),可以實時擷取覆寫率
  • Offline無需額外開啟代理
  • Offline使用場景(From Jacoco Documentation)
    • 運作環境不支援Java Agent
    • 部署環境不允許設定JVM參數
    • 位元組碼需要被轉換成其他虛拟機位元組碼,如Android Dalvik Vm
    • 動态修改位元組碼檔案和其他Agent沖突
    • 無法自定義使用者加載類

關于switch插樁分析

JVM分析:JVM Analyse

TableSwitch

源碼:

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

    }
           

插入後反編譯Java源碼:

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

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

源碼位元組碼檔案:

public void testSwitch(int);
    Code:
       : iload_1
       : tableswitch   { // 1 to 10
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                    : 
               default: 
          }
      : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #                  // String 1
      : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : goto          
      : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #                 // String 2
      : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : goto          
      : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #                 // String 3
      : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : goto          
      : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #                 // String 4
      : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      : goto          
     : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
     : ldc           #                 // String 10
     : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     : goto          
     : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
     : ldc           #                 // String ...
     : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     : return
           

插入後class檔案位元組碼:

public void testSwitch(int);
    Code:
       : invokestatic  #                 // Method $jacocoInit:()[Z
       : astore_2
       : iload_1
       : tableswitch   { // 1 to 10
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                     : 
                    : 
               default: 
          }
      : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #                  // String 1
      : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      //case 1探針
      : aload_2
      : iconst_4
      : iconst_1
      : bastore
      : goto          
      : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #                 // String 2
      : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        //case 2探針
      : aload_2
      : iconst_5
      : iconst_1
      : bastore
      : goto          
      : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
      : ldc           #                 // String 3
      : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        //case 3 探針
      : aload_2
      : bipush        
     : iconst_1
     : bastore
     : goto          
     : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
     : ldc           #                 // String 4
     : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       //case 4 探針
     : aload_2
     : bipush        
     : iconst_1
     : bastore
     : goto          
     : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
     : ldc           #                 // String 10
     : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       //case 10探針
     : aload_2
     : bipush        
     : iconst_1
     : bastore
     : goto          
     : getstatic     #                  // Field java/lang/System.out:Ljava/io/PrintStream;
     : ldc           #                 // String ...
     : invokevirtual #                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       //default 探針
     : aload_2
     : bipush        
     : iconst_1
     : bastore
     //return 探針
     : aload_2
     : bipush        
     : iconst_1
     : bastore
     : return
           

可以發現,在每一個label處都執行了探針插入(具體在goto指令前),return前也插入了一個探針。

LookupSwitch

lookupswitch與tableswitch類似,依舊是每一個label和return處插入探針。

其插樁後的class檔案反編譯:

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

Report生成

在結束插樁後,再次運作class檔案,會産生一個Jacoco.exec的檔案,裡面存放了探針的執行資訊,顯示如下:

代碼覆寫工具Jacoco使用示例及源碼分析Content調用的開源架構測試源碼對比兩種插樁模式關于switch插樁分析Report生成插樁政策參考
代碼覆寫工具Jacoco使用示例及源碼分析Content調用的開源架構測試源碼對比兩種插樁模式關于switch插樁分析Report生成插樁政策參考

下面會用這個資訊來生成代碼覆寫率。

Task report

Ant Task Report

我們使用report task來生成不同格式的報告。report task聲明包含以下幾個部分,和指定輸入的資料;指定輸出的格式。

<!-- Step 4: Create coverage report -->
        <jacoco:report>

            <!-- This task needs the collected execution data and ... -->
            <executiondata>
              <!-- Jacoco.exec-->
                <file file="${result.exec.file}" />
            </executiondata>

            <!-- the class files and optional source files ... -->
            <structure name="JaCoCo Ant Example">
                <classfiles>
                    <!-- ./target/classes:未插樁前的class檔案位元組碼-->
                    <fileset dir="${result.classes.dir}" />
                </classfiles>
                <sourcefiles encoding="UTF-8">
                    <!-- /src:Java源碼處-->
                    <fileset dir="${src.dir}" />
                </sourcefiles>
            </structure>

            <!-- to produce reports in different formats. -->
            <html destdir="${result.report.dir}" />
        </jacoco:report>
           

如上所看到的的,report task是基于幾個嵌套的元素的。

Element executiondata

這個元素指定的Ant resource和resource collections,它們包含了Jacoco 的execution data files(如上圖所示)。如果指定了超過了一種execution data,那麼execution data将被合并。在輸入檔案的任何地方,代碼塊被标記成這樣,那麼就被認為是執行了。

Element structure

這個元素定義了報告結構,它包含以下嵌套元素。

  • classfiles:容器元素,它指定了Java class files,archive (檔案檔案)files(jar,war,ear etc,或者Pack200)或者包含class檔案的檔案夾。在檔案或者檔案夾中的class檔案被遞歸查詢。
  • sourcefiles:可選的容器元素,指定了相關的源檔案。如果源代碼被指定,報告将會包含高亮代碼。源檔案可以被指定為獨立檔案或者檔案目錄。

Sourcefiles元素可以有以下可選的屬性。

屬性 描述 預設
encoding 源碼的字元編碼 平台預設編碼
tabwidth 一個tab字元占的空白字元的數量 4字元

structure可以被精煉為group子元素,這種方式的覆寫報告可以反映項目的不同的子產品。對每一個group元素相關的class檔案和源代碼可以被分别指定。例如:

<structure name="Example Project">
    <group name="Server">
        <classfiles>
            <fileset dir="${workspace.dir}/org.jacoco.example.server/classes"/>
        </classfiles>
        <sourcefiles>
            <fileset dir="${workspace.dir}/org.jacoco.example.server/src"/>
        </sourcefiles>
    </group>
    <group name="Client">
        <classfiles>
            <fileset dir="${workspace.dir}/org.jacoco.example.client/classes"/>
        </classfiles>
        <sourcefiles>
            <fileset dir="${workspace.dir}/org.jacoco.example.client/src"/>
        </sourcefiles>
    </group>    
    ...
</structure>
           

Element xml

使用xml格式建立一個單獨的報告。

屬性 描述 預設
destfile 報告的放置位置
encoding 報告的編碼 UTF-8

Element check

這個元素并沒有在

build.xml

中出現,但是在

ReportTask.java

中有一個

CheckFormatterElement

,它并沒有建立獨立的報告(像html,xml和cvs格式的報告),該元素根據配置的rule檢查coverage counters以及報告的不合法處。每一個rule應用于給定的類型(class,package,bundle,etc)的元素,用于檢查每一個元素的rule有一系列的限制。下面的例子用于檢查每一個包中行覆寫率最少在80%并且沒有class被遺漏。

<check>
    <rule element="PACKAGE">
        <limit counter="LINE" value="COVEREDRATIO" minimum="80%"/>
        <limit counter="CLASS" value="MISSEDCOUNT" maximum="0"/>
    </rule>
</check>
           

check元素有以下的屬性

屬性 描述 預設
rules 用于檢查的

rules

集合
failonviolation 判定 rule violations情況下建立是否失敗 true
violationsproperty 存在violation messages的ant property元素的名稱

我們可以發現,在元素中,有元素,元素中有,事實上,和元素都可以被嵌套。

  • 在check元素中,任何數量的rule元素可以被嵌套
屬性 描述 預設
element rule應用的element,可以取值:

bundle

,

package

,

class

,

sourcefile

method

bundle
includes 應當被檢查的元素集合名 *
excludes 不需要被檢查的元素 empty
limits 用于檢查的

limits

none

- 在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

插樁政策

對于Java源碼:

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使用示例及源碼分析Content調用的開源架構測試源碼對比兩種插樁模式關于switch插樁分析Report生成插樁政策參考

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

代碼覆寫工具Jacoco使用示例及源碼分析Content調用的開源架構測試源碼對比兩種插樁模式關于switch插樁分析Report生成插樁政策參考

下面說明如何在不同的邊緣去情況下具體的插入探針。

代碼覆寫工具Jacoco使用示例及源碼分析Content調用的開源架構測試源碼對比兩種插樁模式關于switch插樁分析Report生成插樁政策參考

Probe探針是通過以下四個指令來設定的。

ALOAD    probearray
xPUSH    probeid
ICONST_1
BASTORE
           

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

$jacocoinit()

中進行。

具體詳見:Control Flow Analysis for Java Methods

# 源碼解析

整個工具主要分為兩個部分,對編譯好的位元組碼檔案插樁以及根據探針的執行情況生成報告。在

build.xml

中,二者的代碼分别是。

插樁:

<target name="instrument" depends="compile">
        <!-- Step 2: Instrument class files -->
        <!-- jacoco:instrument見antlib.xml定義-->
        <jacoco:instrument destdir="${result.classes.instr.dir}">
            <fileset dir="${result.classes.dir}" />
        </jacoco:instrument>
    </target>
           

生成報告:

<target name="report" depends="test">
        <!-- Step 4: Create coverage report -->
        <jacoco:report>

            <!-- This task needs the collected execution data and ... -->
            <executiondata>
                <file file="${result.exec.file}" />
            </executiondata>

            <!-- the class files and optional source files ... -->
            <structure name="JaCoCo Ant Example">
                <classfiles>
                    <!-- ./target/classes:未插樁前的class檔案位元組碼-->
                    <fileset dir="${result.classes.dir}" />
                </classfiles>
                <sourcefiles encoding="UTF-8">
                    <!-- /src:Java源碼處-->
                    <fileset dir="${src.dir}" />
                </sourcefiles>
            </structure>

            <!-- to produce reports in different formats. -->
            <html destdir="${result.report.dir}" />
            <csv destfile="${result.report.dir}/report.csv" />
            <xml destfile="${result.report.dir}/report.xml" />
        </jacoco:report>
    </target>
           

插樁

在插樁中,程式入口是

org.jacoco.ant.InstrumentTask

,向其傳入了兩個參數

destdir

fileset

,分别是存放插入後的位元組碼檔案位置以及位元組碼檔案。

InstrumentTask

類中,由于是自定義Ant Task,是以執行函數是

excute()

,在

instrument()

函數中調用

Instrumenter

類,在

instrument(final ClassReader reader)

函數中,有以下代碼:

final ClassWriter writer = new ClassWriter(reader, );

     final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
                .createFor(reader, accessorGenerator);

     final ClassVisitor visitor = new ClassProbesAdapter(
                new ClassInstrumenter(strategy, writer), true);

        reader.accept(visitor, ClassReader.EXPAND_FRAMES);
           

可以看出來,

ClassProbesAdapter

應該是ASM架構中的擴充卡(即繼承自ClassVisitor,自定義對位元組碼檔案過濾的類),同時在

ClassInstrumenter

中,發現其

visitMethod()

函數傳回了

MethodInstrumenter

對象,在該類中,找到了具體的插樁方法。

首先在

MethodProbesAdapter

中,定義了插樁政策。示例:

public void visitInsn(final int opcode) {
        switch (opcode) {
        case Opcodes.IRETURN:
        case Opcodes.LRETURN:
        case Opcodes.FRETURN:
        case Opcodes.DRETURN:
        case Opcodes.ARETURN:
        case Opcodes.RETURN:
        case Opcodes.ATHROW:
            probesVisitor.visitInsnWithProbe(opcode, idGenerator.nextId());
            break;
        default:
            probesVisitor.visitInsn(opcode);
            break;
        }
    }
           

然後在

MethodInstrumenter

中具體實作了各個政策。示例:

public void visitJumpInsnWithProbe(final int opcode, final Label label,
            final int probeId, final IFrame frame) {
        if (opcode == Opcodes.GOTO) {
            probeInserter.insertProbe(probeId);
            mv.visitJumpInsn(Opcodes.GOTO, label);
        } else {
            final Label intermediate = new Label();
            mv.visitJumpInsn(getInverted(opcode), intermediate);
            probeInserter.insertProbe(probeId);
            mv.visitJumpInsn(Opcodes.GOTO, label);
            mv.visitLabel(intermediate);
            frame.accept(mv);
        }
    }
           

具體插入是

probeInserter.insertProbe(probeId);

,它在

ProbeInster

中被實作:

public void insertProbe(final int id) {

        // For a probe we set the corresponding position in the boolean[] array
        // to true.

        mv.visitVarInsn(Opcodes.ALOAD, variable);

        // Stack[0]: [Z

        InstrSupport.push(mv, id);

        // Stack[1]: I
        // Stack[0]: [Z

        mv.visitInsn(Opcodes.ICONST_1);

        // Stack[2]: I
        // Stack[1]: I
        // Stack[0]: [Z

        mv.visitInsn(Opcodes.BASTORE);
    }
           

到這裡插樁實作。

生成報告

生成報告在程式入口在

ReportTask

中,傳入了executionData,sourcefiles和classfiles。其中executionData是再次運作被插樁的位元組碼問價獲得的探針執行情況,在

report生成

一節有介紹。

參考

Java位元組碼操縱架構ASM小試

使用 ASM 實作 Java 語言的“多重繼承” IBM

從Java代碼到位元組碼ImportNew翻譯

簡書ASM建立函數示例

伯樂線上,簡述ASM各種文檔

官方文檔的中文簡單版

中文簡單版2

ASM-Guide

AOP 的利器:ASM 3.0 介紹 IBM

ASM系列1-5 有廣告那個

ASM系列

擁有構造函數以及成員函數兩種示例

添加時間,輸出那個

美團部落格,關于Jacoco很深講的