Elvis operations do not work in tests using UnconfinedTestDispatcher when using kotlin 2.0.20. It works with kotlin 1.9.25. Has something changed in kotlin 2 or is this a bug?
It works if I move the contents of the collect block in a separate function.
Below is a simplified example of ?: run not being called.
fun fail() {
runTest {
var result: String = "fail"
val job = this.launch(UnconfinedTestDispatcher(testScheduler)) {
println("do some processing with the data")
"test".let {
null
} ?: run { // this is not called if it is the last thing in the collect block
result = "success"
println("This is not called when using kotlin 2.0.20!")
}
}
dataFlow.emit("one")
job.join()
println("Result is $result")
}
}
I tested this with kotlin playground:
Edited: Removed the flow collecting as it seemst that it does not need to be in a flow collect.
It seems that it doesn’t even work with normal runBlocking
runBlocking {
var result: String = "fail"
val job = this.launch {
println("Collect a flow and do some processing with the data")
"test".let {
null
} ?: run { // this is not called if it is the last thing in the collect block
result = "success"
println("This is not called when using kotlin 2.0.20!")
}
}
job.join()
println("Result is $result")
}
To add to the mystery, specifying types explicitly works for some reason
import kotlinx.coroutines.*
fun main() {
runBlocking {
var result: String = "fail"
val job = this.launch {
println("Collect a flow and do some processing with the data")
"test".let<String, Nothing?> {
null
} ?: run {
result = "success"
println("This is not called when using kotlin 2.0.20!")
}
}
job.join()
println("Result is $result")
}
}
Ohhhh, I think I figured it out! It has to do with implicitly returning Unit out of a lambda.
Consider this code:
import kotlinx.coroutines.*
fun main() {
runBlocking {
var result: String = "fail"
val job = this.launch {
println("Collect a flow and do some processing with the data")
"test".let<_, Unit> {
null
} ?: run {
result = "success"
println("This is not called when using kotlin 2.0.20!")
}
}
job.join()
println("Result is $result")
}
}
Obviously, it’s expected here that “Result is fail” would get printed, because Unit is not null. This seems like a type inference edge case where, because launch expects a Unit to be returned, let’s block is also expected to return Unit, which is fine because that implicit return exists. That’s why it only occurs for the last expression in a launch block.
Small reproducer:
fun main() = run<Unit> {
run { null } ?: println("OK!")
}
Hitting “Generate JS” on the Kotlin Playground seems to confirm this hypothesis because the main function contains the following:
function main() {
// Inline function 'kotlin.run' call
// Inline function 'kotlin.contracts.contract' call
// Inline function 'kotlin.run' call
// Inline function 'kotlin.contracts.contract' call
if (Unit_instance == null) {
// Inline function 'kotlin.run' call
// Inline function 'kotlin.contracts.contract' call
println('OK!');
}
return Unit_instance;
}
Edit: I found another reproducer that makes somewhat less sense type-inference wise:
fun main() = run<Unit> {
run { null } ?: "OK!".also(::println)
}
The RHS of the elvis is clearly not Unit, so it’s strange that the LHS is inferred to be Unit