Incorrect Jacoco code coverage for Kotlin coroutine
Similarly to "Why is JaCoCo not covering my String switch statements?" :
JaCoCo performs analysis of bytecode, not source code. Compilation of Example.kt
with kotlinc 1.3.10
package example
fun main(args: Array<String>) {
kotlinx.coroutines.runBlocking { // line 4
}
}
results in two files ExampleKt.class
and ExampleKt$main$1.class
, bytecode of last one (javap -v -p ExampleKt$main$1.class
) contains method invokeSuspend(Object)
public final java.lang.Object invokeSuspend(java.lang.Object);
descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_FINAL
Code:
stack=3, locals=4, args_size=2
0: invokestatic #29 // Method kotlin/coroutines/intrinsics/IntrinsicsKt.getCOROUTINE_SUSPENDED:()Ljava/lang/Object;
3: astore_3
4: aload_0
5: getfield #33 // Field label:I
8: tableswitch { // 0 to 0
0: 28
default: 53
}
28: aload_1
29: dup
30: instanceof #35 // class kotlin/Result$Failure
33: ifeq 43
36: checkcast #35 // class kotlin/Result$Failure
39: getfield #39 // Field kotlin/Result$Failure.exception:Ljava/lang/Throwable;
42: athrow
43: pop
44: aload_0
45: getfield #41 // Field p$:Lkotlinx/coroutines/CoroutineScope;
48: astore_2
49: getstatic #47 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
52: areturn
53: new #49 // class java/lang/IllegalStateException
56: dup
57: ldc #51 // String call to 'resume' before 'invoke' with coroutine
59: invokespecial #55 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
62: athrow
LineNumberTable:
line 4: 3
line 5: 49
which is associated with line 4 of source file and contains branches (ifeq
, tableswitch
).
While latest as of today JaCoCo version (0.8.2) has filters for various compiler-generated artifacts such as String
in switch
statement, bytecode that Kotlin compiler generates for coroutines is not filtered. Changelog can be seen at https://www.jacoco.org/jacoco/trunk/doc/changes.html And among others at https://www.jacoco.org/research/index.html there is also presentation about bytecode pattern matching that shows/explains many compiler-generated artifacts.
What you see in IntelliJ IDEA as 100% - is only line coverage, so you are trying to compare two completely different things. As a proof - here is screenshot of IntelliJ IDEA which shows 100% line coverage, but only one branch of if
was executed (where args.size >= 0
evaluates to true
)
And here is corresponding screenshots of JaCoCo report for execution of the same source file
Going up to the package level you can see 100% line coverage, but 50% branch coverage
And then going down to the class level via the first link ExampleKt.main.new Function2() {...}
you can again see that method invokeSuspend(Object)
contributes missed branches
Update (29/01/2019)
JaCoCo version 0.8.3 has filter for branches added by the Kotlin compiler for suspending lambdas and functions:
Jacoco version 0.8.3 fixes it, it has been released yesterday January 24th.
Full change-log can be found here: https://github.com/jacoco/jacoco/releases