浅析jacoco Off-line instrumentation

18914 ワード

JAcocoはコードオーバーライドテストのオープンソースツール(java)であり、多くの統合方法があり、統合後、それらのコードが実行され、実行されていないことがわかります.これがコードオーバーライドテストです.独自の書き込みユニットテストを開発したり、手動でポイントを削除したりすることができます.実行するだけで、記録があり、コードがテストに上書きされていても.
引用文:
この2,3日コードオーバーライドテストを書いたもので、プロジェクトでjacocoを使った同僚がいました.注釈&&ドキュメントは何もありませんが、私は少しバック調をして、資料を見れば見るほど霧の中にいます.夜はjacocoの公式ドキュメントから入手し、少し分析します.一つ一つ包んだフレームワークを捨てて、jacocoはいったい何をしたのだろうか.
まず簡単なjavaを書きましょう~
public final class Test{
        public String me = "Yeshen";

        public static void main(String[] args){
                Test t = new Test();
                System.out.println(t.me);
                if(args != null && args.length > 0){
                        System.out.println(args[0]);
                }else{
                        t.hi();
                }
        }

        public void hi(){
                System.out.println("hi");
        }

        public void nonono(){
                System.out.println("nonono");
        }
}
javac Test.java
java Test longlongArgs

OK、これはオリジナルのjavaと実行結果です.jacocoで処理したら?
jacocoの取得
wget http://search.maven.org/remotecontent?filepath=org/jacoco/jacoco/0.8.1/jacoco-0.8.1.zip
7z x remotecontent?filepath=org/jacoco/jacoco/0.8.1/jacoco-0.8.1.zip
# jacococli.jar lib/jacocoagent.jar is what we need

JAcocoはclassファイルを処理し、cliを参照
# jacoco offline   
java -jar jacococli.jar instrument Test.class --dest out
# cp jacococli.jar out && cp lib/jacocoagent.jar out && cd out
# exec Test
java -cp .:jacocoagent.jar Test anotherArgs
# check the code coverage
java -jar jacococli.jar execinfo jacoco.exec

jacocoが見えます.execはコードのオーバーライド実行レポートです.JAcocoはすでにその機能を完成しました.
次に、バイトコードの部分がどれだけ修正されたかを見てみましょう.
javap -c -v Test.class
cd out && javap -c -v Test.class

定数プールにはjacocoagentに依存するこれらのコードが組み込まれていることがわかる.jar.(だから修正したら、このjarパッケージを手動で参照します.)
#42 = Utf8               $jacocoInit
#43 = Utf8               ()[Z
#44 = NameAndType        #42:#43        // $jacocoInit:()[Z
#45 = Methodref          #21.#44        // Test.$jacocoInit:()[Z
#46 = Utf8               [Z
#47 = Class              #46            // "[Z"
#48 = Utf8               $jacocoData
#49 = NameAndType        #48:#46        // $jacocoData:[Z
#50 = Fieldref           #4.#49         // Test.$jacocoData:[Z
#51 = Long               4767435040597437437l
#53 = String             #29            // Test
#54 = Utf8               org/jacoco/agent/rt/internal_c13123e/Offline
#55 = Class              #54            // org/jacoco/agent/rt/internal_c13123e/Offline
#56 = Utf8               getProbes
#57 = Utf8               (JLjava/lang/String;I)[Z
#58 = NameAndType        #56:#57        // getProbes:(JLjava/lang/String;I)[Z
#59 = Methodref          #55.#58        // org/jacoco/agent/rt/internal_c13123e/Offline.getProbes:(JLjava/lang/String;I)[Z

各関数セグメントには、呼び出しのバイトコードがいくつか追加されています.例を挙げます.
public void hi();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=2, locals=1, args_size=1
     0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #9                  // String hi
     5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return
  LineNumberTable:
    line 15: 0
    line 16: 8
public void hi();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=5, locals=2, args_size=1
     0: invokestatic  #45                 // Method $jacocoInit:()[Z
     3: astore_1
     4: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #9                  // String hi
     9: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: aload_1
    13: bipush        8
    15: iconst_1
    16: bastore
    17: return
  LineNumberTable:
    line 15: 4
    line 16: 12

この2つはdiffを取って、jacocoが何をしたかを見ることができて、主にいくつかのバイトコードを追加しました
#      
invokestatic
astore_1   # JVM                 returnAddress   ,          1        ,        returnAddress         1

#      
aload_1   #             1            
bipush    #     
iconst_1  #      1   (         )
bastore   #              

表象から推測すると,各関数には呼び出し回数があり,呼び出す前にこの数を出し,呼び出し後に1を加えて戻す.しかしclassから修正後のバイトコードの保存は見られず,これらの呼び出し回数のデータをjacoco.execに戻すグローバルな方法があると推測される.
ドキュメントを振り返ると、ASMが使われていて、よく知っている操作コードがいくつか見えます.少し理解を間違えました.boolean[]arrayを使用しています.
質問に戻ったので、何をしましたか?はい、上記のバイトコードを変更しました.
コードを結合して見ると
git clone https://github.com/jacoco/jacoco jacoco/org.jacoco.core/src/org/jacoco/core/instr/Instrumenter.java
主に修正されたコードは
private byte[] instrument(final byte[] source) {
    final long classId = CRC64.classId(source);
    final int originalVersion = BytecodeVersion.get(source);
    final byte[] b = BytecodeVersion.downgradeIfNeeded(originalVersion,
            source);
    final ClassReader reader = new ClassReader(b);
    final ClassWriter writer = new ClassWriter(reader, 0) {
        @Override
        protected String getCommonSuperClass(final String type1,
                final String type2) {
            throw new IllegalStateException();
        }
    };
    final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
            .createFor(classId, reader, accessorGenerator);
    final ClassVisitor visitor = new ClassProbesAdapter(
            new ClassInstrumenter(strategy, writer),
            InstrSupport.needsFrames(originalVersion));
    reader.accept(visitor, ClassReader.EXPAND_FRAMES);
    final byte[] instrumented = writer.toByteArray();
    BytecodeVersion.set(instrumented, originalVersion);
    return instrumented;
}

classの修正はこれらのクラスで行われていることがわかります
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

中に入るとよく知っている定数プールが見え、よく知っているバイトコードの修正が見えます.この部分がofflineの修正です.では、どのように統計しますか.
定数プールにはorg/jacoco/agent/rt/internal_c13123e/Offlineの呼び出しが表示されます
コードはjacoco/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/Offline.java
public final class Offline {

    private static final RuntimeData DATA;
    private static final String CONFIG_RESOURCE = "/jacoco-agent.properties";

    static {
        final Properties config = ConfigLoader.load(CONFIG_RESOURCE,
                System.getProperties());
        DATA = Agent.getInstance(new AgentOptions(config)).getData();
    }

    private Offline() {
        // no instances
    }

    /**
     * API for offline instrumented classes.
     * 
     * @param classid
     *            class identifier
     * @param classname
     *            VM class name
     * @param probecount
     *            probe count for this class
     * @return probe array instance for this class
     */
    public static boolean[] getProbes(final long classid,
            final String classname, final int probecount) {
        return DATA.getExecutionData(Long.valueOf(classid), classname,
                probecount).getProbes();
    }

}

getProbesがコードアクセスの統計データをここに格納していることがわかります.ここでclassのバイトコード情報は終わり、次はofflineという方法を呼び出して統計を取ります.
ExecutionDataはデータベースに似ていて、これらのデータが保存されていて、メモリに存在します.
private final long id;
private final String name;
private final boolean[] probes;

保存が必要な場合は、ExecutionDataWriterでディスクにファイルを書き込みます.これはオプションの構成で、jacoco/org.jacoco.core/src/org/jacoco/core/runtime/AgentOptions.java
/**
 * Sets whether coverage data should be dumped on exit.
 * 
 * @param dumpOnExit
 * true if coverage data should be written on VM
 * exit
 */
public void setDumpOnExit(final boolean dumpOnExit) {
    setOption(DUMPONEXIT, dumpOnExit);
}
public static synchronized Agent getInstance(final AgentOptions options) {
    if (singleton == null) {
        final Agent agent = new Agent(options, IExceptionLogger.SYSTEM_ERR);
        agent.startup();
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                agent.shutdown();
            }
        });
        singleton = agent;
    }
    return singleton;
}
/**
 * Shutdown the agent again.
 */
public void shutdown() {
    try {
        if (options.getDumpOnExit()) {
            output.writeExecutionData(false);
        }
        output.shutdown();
        if (jmxRegistration != null) {
            jmxRegistration.call();
        }
    } catch (final Exception e) {
        logger.logExeption(e);
    }
}

簡単に言えばVMが終了したときにHookが追加され、保存された属性が設定されている場合は、終了した時点でメモリの統計をファイルにシーケンス化し、もちろんこのdumpの方法も手動で呼び出すことができます.
PS:nannyでこの文章を見たら、それは私が送ったのです:)