Link Search Menu Expand Document

Kotlin Support

Requires pitest 1.15.0 or above.

Maven Central

Background

Although Kotlin compiles to normal java bytecode, some language features require compiler generated constructs that do not map back to the source code. This results in confusing mutants that are hard to interpret, and junk mutations which cannot be reproduced by mistakes in the source code.

The kotlin plugin filters out these junk mutations and removes confusing noise from mutant descriptions.

Support is currently provided for the following language features

  • Coroutines
  • Destructuring
  • Intrinsics
  • Safe casts
  • Autogenerated accessors
  • Lateinit
  • Unmatched when clauses in enums and sealed classes
  • Var accessors
  • Inlined code
  • Non-null types

The plugin also filters equivalent mutants where pitest has introduced ‘empty’ returns into methods that already return empty collections using Kotlin library methods, and supports ‘empty’ return values for Kotlin specific types including

  • IntRange
  • LongRange
  • CharRange
  • Sequence
  • CoroutineContext

Kotlin Extra

Most filtering of junk mutations is enabled by default. Additional filters with a higher chance of also removing legitimate mutations can be enabled by setting the feature KOTLIN_EXTRA to true.

Inline Functions

Inline functions are a challenge for bytecode based mutation testing. When an inline function is used, its bytecode is copied into the client class. This creates two problems

  • The client class contains instructions that do not map to its source code
  • The inline function itself is never executed (instead copies of it are executed in various locations)

Since release 1.0.0 these problems are addressed by combining mutations made to inlined copies and rewriting them so they appear to occur in the original function definition. Line coverage information is also rewritten, so the original function will appear to have been called if any of its inlined copies are run.

When multiple copies of an inlined mutant exists, the results are combined by choosing the most favourable result. So, if there are three copies of a mutant with statuses of

  • NO_COVERAGE
  • SURVIVED
  • KILLED

It would be rewritten as a single mutant with a status of KILLED.

If the KILLED copy did not exist, the combined mutant would be given a status of SURVIVED.

Inlined mutants are marked in their description with a preceding “(inlined)”.

The Kotlin compiler may not include all instructions in every inlined copy.

For example, for the following code

inline fun callMe(i: Int) {
  if (i > 42) {  
    println("foo") 
  }
}

The compiler might omit the instructions for the if statement if it is able to determine that i will always be greater than 42 for one of the call sites.

This can result in the following unintuitive behaviours

  • Changing numbers of mutants
  • NO_COVERAGE mutants on apparently covered lines
  • Missing mutants compared to equivalent non inline functions

If the simple function shown earlier had no clients, then mutations to the if statement would be shown with a status of NO_COVERAGE.

If a single client were introduced in which the compiler omitted the if statement, these mutations would disappear. They would however reappear again if a second client were introduced which included the bytecode instructions for the if statement.

Although this behaviour is confusing, it is preferable to the alternative. If the mutations to the if statement were left in place, but the compiler was always able to optimise them away within the clients, it would be impossible to kill the mutant even though making the equivalent change to the source code would be detected by the tests.

Similarly, if a client with no test coverage included the if statement, but a second client with full line coverage did not include it, mutants would be produced with a status of ‘NO_COVERAGE’ even though they appear to live on a covered line.

Some instructions such as return statements will never be included in an inline copy, so fewer mutants will be seeded into an inline function compared to if were not inline.

Configuration

The analysis required to support inline function can be expensive. By default, inline methods with more than 500 instructions will not be mutated in order to reduce analysis time. This default can be overridden by supplying the maxMethodSize to the pitest KOTLIN feature string.

e.g.

+KOTLIN(maxMethodSize[100])

Kotlin Mutators

A small number of Kotlin specific mutators are provided. These enable pitest to mutate code constructs that appear similar to Java equivalents, but produce different bytecode.

KOTLIN_REMOVE_DISTINCT

Removes calls to distinct on Kotlin lists

fun foo(i : Int) : Int {
    return listOf(i,1,2,3)
       // .distinct() <- removed
        .size
}

The mutator is disabled by default, but will be enabled automatically if the REMOVE_DISTINCT mutator from the extended mutator set is activated.

KOTLIN_REMOVE_SORTED

Removes calls to sorted on Kotlin lists

fun foo(i : Int) : Int {
    return listOf(i,1,1,1)
       // sorted() <- removed
        .last
}

The mutator is disabled by default, but will be enabled automatically if the REMOVE_SORTED mutator from the extended mutator set is activated.

Installation

Before you can use the integration, you must first acquire a licence.

The licence file must be named arcmutate-licence.txt and placed at the root of the project. Alternatively dynamic licence retrieval can be used.

The plugin must be placed on the classpath of the pitest tool (not on the classpath of the project being mutated).

E.g for maven

<plugin>
  <groupId>org.pitest</groupId>
  <artifactId>pitest-maven</artifactId>
  <version>1.15.8</version>
    <dependencies>
      <dependency>
        <groupId>com.arcmutate</groupId>
        <artifactId>pitest-kotlin-plugin</artifactId>
        <version>1.2.5</version>
      </dependency>
    </dependencies>
</plugin>

Or for gradle

dependencies {
  pitest 'com.arcmutate:pitest-kotlin-plugin:1.2.5'
}

The default version of pitest used by the gradle plugin is often very out of date. A modern version compatible with the Kotlin plugin can be configured with

pitest {
  ...  
  pitestVersion = '1.15.8'
  ...
}

See gradle-pitest-plugin documentation for more details.

If the pitestReportAggregate task has been configured for use in the project, the plugin must also be added as a dependency for pitestReport.

dependency {
  pitestReport 'com.arcmutate:pitest-kotlin-plugin:1.2.5'
}

This is required in order for inline code correction to work when aggregating.

See the pitest kotlin repo for an example of a working project.