jqwik-team / jqwik

Property-Based Testing on the JUnit Platform
http://jqwik.net
Eclipse Public License 2.0
576 stars 64 forks source link

More Kotlin Support #250

Open jlink opened 3 years ago

jlink commented 3 years ago

Testing Problem

With version 1.6.0 jqwik offers an optional kotlin module. Not everything from the initial issue https://github.com/jlink/jqwik/issues/151 has been implemented yet.

Suggested Solution

Add the following Kotlin-related features:

vlsi commented 2 years ago

One more item: declaration-site variance.

Currently, Kotlin treats Java's Arbitrary<T> as invariant. In other words, if I declare interface ArbitraryAction<T> : Arbitrary<MyActionType<T>> (e.g. to reduce nesting everywhere), then Kotlin does not allow returning sub-types without casts.


The idea of Kotlin generics is that you can specify "usage type" once at type parameter declaration, and then it would be reused in all the usages.

In other words, in Kotlin, the following would mean "the interface only produces objects of T, and it never consumes them":

interface Arbitrary<out T> {
...

Sample:

interface RegularArbitrary<T>
interface OutArbitrary<out T>

class Test {
    lateinit var regularString: RegularArbitrary<String>
    lateinit var regularAny: RegularArbitrary<Any>

    lateinit var outString: OutArbitrary<String>
    lateinit var outAny: OutArbitrary<Any>

    fun withRegularArbitrary(regularAny: RegularArbitrary<Any>) {
    }
    fun withOutArbitrary(regularAny: OutArbitrary<Any>) {
    }

    fun test() {
        // Type mismatch. Required: RegularArbitrary<Any> Found: RegularArbitrary<String>
        withRegularArbitrary(regularString)
        // Compiles ok
        withOutArbitrary(outString)
    }
}

In the ideal world, there should be an annotation (or something else) that could clarify Kotlin compiler that given interface declaration Arbitrary<T> means Arbitrary<out T> (here's the issue for Kotlin compiler: https://youtrack.jetbrains.com/issue/KT-41062 )

However, as a middle ground, it might be great if Arbitrary interface could be converted to Kotlin so generic variance could be declared explicitly. It would enable clients to have just Arbitrary<String> in declarations and Kotlin compiler would be able to pass Arbitrary<String> into methods that receive Arbitrary<Any>.

jlink commented 2 years ago

However, as a middle ground, it might be great if Arbitrary interface could be converted to Kotlin so generic variance could be declared explicitly. It would enable clients to have just Arbitrary<String> in declarations and Kotlin compiler would be able to pass Arbitrary<String> into methods that receive Arbitrary<Any>.

Moving Arbitrary to the Kotlin side would make all jqwik code dependent on Kotlin, right?

vlsi commented 2 years ago

Moving Arbitrary to the Kotlin side would make all jqwik code dependent on Kotlin, right?

That depends. There's an option to skip dependency on kotlin-stdlib, so it would look pretty much the same for existing Java clients. In other words, kotlin-stdlib is optional, and it is needed only if the code uses kotlin stdlib in the implementation.

You could add something like the following to gradle.properties, and the compilation would fail if you accidentally use something from stdlib:

# See https://kotlinlang.org/docs/gradle.html#dependency-on-the-standard-library
kotlin.stdlib.default.dependency=false

However, if you convert the interface to Kotlin, it would indeed require Kotlin compiler for compiling the interface. I think it should not be a problem since you already have jqwik-kotlin module which requires Kotlin compiler.

As a side-effect, Kotlin compiler could produce nullability annotations based on Kotlin nullability types, so the generated bytecode is better even when using from Java.

vlsi commented 2 years ago

@jlink , there's an update: https://youtrack.jetbrains.com/issue/KT-41062#focus=Comments-27-5752006.0-0

They suggest two options: a) Migrate code to Kotlin b) Write metadata explicitly. For instance, copy-paste @Metadata annotation from a Kotlin-generated bytecode, or generate the metadata via kotlinx.metadata

jlink commented 2 years ago

b) sounds much more realistic to me than a) At least short- or mid-term.

vlsi commented 2 years ago

What's wrong with converting a single (or several) interface to Kotlin? I guess it would be easier than hand-crafting the metadata and praying for it to work.

jlink commented 2 years ago

Cross Java/Kotlin build is slow IME. And will probably lead to unknown complications.

vlsi commented 2 years ago

Just in case, :api compilation takes ~1-2 seconds in total.

jlink commented 2 years ago

@vlsi Moving core stuff over to Kotlin is an option for Version 2.0. There are a couple of features that could benefit from it. But I'd rather do it right than do it fast in this case.

I assume that as Arbitrary<Any> is a workaround, right?

vlsi commented 2 years ago

No, I do not do as Arbitrary<Any>.

Sometimes specifying variance via Arbitrary<out ...> helps. Sometimes typealias Arbitrary<T> = net.jqwik.api.Arbitrary<out T> is good enough.

I ran into variance issues when I tried to introduce an interface for deduplicating signatures: interface ArbitraryAction<T> : Arbitrary<MyActionType<T>> For now, I just ignore the idea and write Arbitrary<out MyActionType<...>> everywhere.

jlink commented 2 years ago

For now, I just ignore the idea and write Arbitrary<out MyActionType<...>> everywhere.

That could be a type alias then.

vlsi commented 2 years ago

Typealias does not work when more nesting is needed. For instance, an interface can't extend typealias