Code coverage and Kotlin


#1

Hi all,

I’ve just started a new project in Kotlin and I’m trying to integrate Cobertura with it. However, I’ve been running into some issues that seem to be caused by the code the Kotlin compiler is generating. Here is a simple method which exhibits the behavior:

fun trimIt(s: String): String {
    return s.trim()
}

Compiling this function results in the following bytecode:

public static final java.lang.String trimIt(java.lang.String);
  descriptor: (Ljava/lang/String;)Ljava/lang/String;
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  Code:
    stack=4, locals=2, args_size=1
       0: aload_0
       1: astore_1
       2: nop
       3: aload_1
       4: dup
       5: ifnonnull     18
       8: new           #9                  // class kotlin/TypeCastException
      11: dup
      12: ldc           #11                 // String null cannot be cast to non-null type kotlin.CharSequence
      14: invokespecial #15                 // Method kotlin/TypeCastException."<init>":(Ljava/lang/String;)V
      17: athrow
      18: checkcast     #17                 // class java/lang/CharSequence
      21: invokestatic  #23                 // Method kotlin/text/StringsKt.trim:(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
      24: invokevirtual #27                 // Method java/lang/Object.toString:()Ljava/lang/String;
      27: areturn

Between 4 and 17 is a generated branch that performs a null check (you can see the code which generates it here). This is what the bytecode looks like after Cobertura has instrumented it:

public static final java.lang.String trimIt(java.lang.String);
  descriptor: (Ljava/lang/String;)Ljava/lang/String;
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  Code:
    stack=5, locals=3, args_size=1
       0: invokestatic  #27                 // Method __cobertura_init:()V
       3: ldc           #12                 // int 0
       5: istore_1
       6: aload_0
       7: astore_2
       8: nop
       9: aload_2
      10: dup
      11: ldc           #14                 // int 2
      13: istore_1
      14: ifnonnull     41
      17: getstatic     #29                 // Field __cobertura_counters:[I
      20: ldc           #10                 // int 1
      22: dup2
      23: iaload
      24: ldc           #10                 // int 1
      26: iadd
      27: iastore
      28: ldc           #12                 // int 0
      30: istore_1
      31: new           #31                 // class kotlin/TypeCastException
      34: dup
      35: ldc           #33                 // String null cannot be cast to non-null type kotlin.CharSequence
      37: invokespecial #37                 // Method kotlin/TypeCastException."<init>":(Ljava/lang/String;)V
      40: athrow
      41: getstatic     #29                 // Field __cobertura_counters:[I
      44: iload_1
      45: dup2
      46: iaload
      47: ldc           #10                 // int 1
      49: iadd
      50: iastore
      51: ldc           #12                 // int 0
      53: istore_1
      54: checkcast     #39                 // class java/lang/CharSequence
      57: invokestatic  #45                 // Method kotlin/text/StringsKt.trim:(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
      60: invokevirtual #49                 // Method java/lang/Object.toString:()Ljava/lang/String;
      63: areturn

The generated null check is also being instrumented. I can’t easily check for this branch because it is a type error in Kotlin to pass in null to a function which expects a value of non-nullable type. I tried writing a test that calls trimIt(null!!), but this still doesn’t seem to work.

I also found this StackOverflow post which suggests passing in -Xno-param-assertions and -Xno-call-assertions, but turning these on seemed to have no effect on this particular piece of code.

The Run ... with Coverage option in IntelliJ does report that all branches are covered, but I can’t find a way to execute this from the command line / Maven. Since I need to run this during CI, it would be nice if this was possible.

Does anyone know either A) how to fix the issue so that Cobertura stops reporting false negatives, or B) how to call the IntelliJ coverage analysis from the command line, assuming it produces an easily-readable report?


#2

At bytecode level it would be very hard for a coverage analysis tool to distinguish between the synthetic null check and a handwritten one. Suppressing the generation of the check may be the “best” solution for coverage checking. Otherwise you can still call the code with a null value from Java. You can even do it “cleverly”:

NullHelper.java:

class NullHelper {
   public static <T> T nullVar() { return (T) null }
}

You can use this in place of any null (because the actual type of null is that it is a subtype of any other type).