kotlincode-coveragespockkotlin-coroutinesjacoco

Incorrect Jacoco code coverage for Kotlin coroutine


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


Solution

  • 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