Incorrect Jacoco code coverage for Kotlin coroutine
Asked Answered
A

2

14

I am using Jacoco for unit test code coverage. Jacoco's generated report shows that few branches are missed in my Kotlin code. I noticed that the coroutine code and the code after it, is not properly covered according to Jacoco. I am not sure if it is because of coroutine or something else. While running my unit test with the IntelliJ Code Coverage my Kotlin class shows 100% coverage.

I don't know why Jacoco is showing lesser coverage. I have written my Unit Tests using Spock (Groovy).

Please refer the below images:

Missed Branches: enter image description here

enter image description here

Original Code: enter image description here

Arrowy answered 26/11, 2018 at 16:31 Comment(0)
C
15

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)

intellij

And here is corresponding screenshots of JaCoCo report for execution of the same source file

jacoco source level

Going up to the package level you can see 100% line coverage, but 50% branch coverage

jacoco package level

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

jacoco class level


Update (29/01/2019)

JaCoCo version 0.8.3 has filter for branches added by the Kotlin compiler for suspending lambdas and functions:

before

after

Cuba answered 1/12, 2018 at 5:41 Comment(4)
Thanks for the detailed explanation. Although, I figured this out already and thus bypassed coverage for these kotlin compiler generated code with Function2 keyword. I hope that was the right approach as I should not be bothered for the code coverage of the compiler generated code. What’s your opinion?Arrowy
@SahilChhabra I doubt that this is correct, because as far as I can see - "body" of runBlocking { /* body, e.g. println("hello") */ } is inside of method invokeSuspend together with additional branches.Cuba
I got your point. That seems correct. But then how should I cover those extra lines or branches of invokeSuspend.Arrowy
@SahilChhabra for the time being you need to put up with this limitation. You can focus on line coverage - quoting eclemma.org/jacoco/trunk/doc/counters.html "A source line is considered executed when at least one instruction that is assigned to this line has been executed." As was already shown above metric "line coverage" is identical to metric shown by IntelliJ IDEA and 100% for this metric is perfectly reachable.Cuba
B
1

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

Bessbessarabia answered 25/1, 2019 at 21:10 Comment(3)
Is there something you have to do? I still have this issue with version 0.8.8.202204050719Helenehelenka
Me to facing the same issueIntercalation
Yeah not fixed. runBlocking reports as yellow (partially covered)Palmary

© 2022 - 2024 — McMap. All rights reserved.