Kotlin Support
Requires pitest 1.16.0 or above.
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
Null Filtering
By default, the plugin removes mutations a subset of mutations to null handling code produced by the compiler. Some teams find these defaults fit well with their approach to development, but not everyone finds the remaining mutants useful.
Much more aggressive null filtering can be enabled with the feature +KOTLIN_NO_NULLS
. This will remove all mutations to null check instructions, including hand rolled null checking code.
Kotlin Extra
Most filtering of junk mutations is enabled by default. Additional filters with a higher chance of also removing legitimate mutations can be enable with the feature +KOTLIN_EXTRA
.
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.16.1</version>
<dependencies>
<dependency>
<groupId>com.arcmutate</groupId>
<artifactId>pitest-kotlin-plugin</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
</plugin>
Or for gradle
dependencies {
pitest 'com.arcmutate:pitest-kotlin-plugin:1.3.0'
}
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.16.1'
...
}
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.3.0'
}
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.