https://kotlinlang.org logo
Title
c

chrmelchior

05/25/2023, 1:57 PM
I’m trying to add support for the new Android source set layout introduced in 1.8.0 and is running into a few problems running the code both as Unit and Instrumentation test. The problem I have is that my Android variant of the library contain code that only works on a device, not on JVM tests. I do have a JVM variant of the library that should work, but I cannot figure out how to include it just for the AndroidUnitTest source set. It looks something like this:
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
            	// This will pull in library-base-android for both 
                // Android Unit Test and Instrumented Tests
                implementation("io.realm.kotlin:library-base:1.10.0-SNAPSHOT")

            }
        }

        val androidUnitTest by getting {
        	dependencies {
				// Here I need to override the library-base-android variant  
                // with the library-base-jvm dependency.        	
        	}
        }

        val androidInstrumentedTest by getting {
        	dependencies {
				// This should continue to use the library-base-android variant.
        	}
        }

    }	
}
Anyone have any idea on how to achieve this? 🤔 Can I make a release of the library with some metadata for the source set resolution to “just work” or is there some way to manipulate the Gradle depedency inside the the two different test closures?
j

Jeff Lockhart

05/25/2023, 5:10 PM
There's a new API coming in Kotlin 1.9 that allows you to select which source set tree the
androidInstrumentedTest
and
androidUnitTest
source sets should be added to: https://kotlinlang.slack.com/archives/C3PQML5NU/p1682443478051419?thread_ts=1675016886.869689&cid=C3PQML5NU Note the API has been updated slightly since this post. You now call it directly from the
androidTarget()
declaration (which replaces the previous
android()
target declaration):
androidTarget {
    publishLibraryVariants("release")
    instrumentedTestVariant.sourceSetTree.set(SourceSetTree.test)
    unitTestVariant.sourceSetTree.set(SourceSetTree.unitTest)
}
You can check it out in the latest 1.9.0-Beta release: https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/
As for wanting to use the JVM variant in
androidUnitTest
, if you're supporting the JVM target as well, I'd just use the
jvmTest
source set to run those tests and omit the
androidUnitTest
by assigning it to the
unitTest
source set tree as my code above does. This is what I'm doing in my library.
c

chrmelchior

05/25/2023, 5:45 PM
I’m not 100% sure that will solve my use case? 🤔 The thing is that we want to allow users of our library to run tests on either JVM or Android as they see fit, and we also support pure android projects, not just multiplatform projects. As I understand the new API it “just” allows decoupling of unit tests from instrumentation tests? In our case the problem is that the Android target internally does some things based on
android.os.Build
(which is not available in the unit test variant), but our users shouldn’t care about that. We also load some native code, which behaves very different on Android devices vs. JVM. All of those things are kinda hidden for people right now because they just interact with the Multiplatform API, and it “just works” because the variant metadata allows Gradle to select the proper dependency based on the projet type. But the androidUnitTest is some sort of hybrid between Jvm and Android which makes it hard for libraries to handle (as far as I can tell). Not sure if that made sense?
j

Jeff Lockhart

05/25/2023, 5:58 PM
Do you have tests specific to the
androidUnitTest
source set? Or just
commonTest
?
You aren't publishing your test source sets, right? So users of your library should be consuming the
android
or
jvm
variant, depending on their use case, right?
As I understand the new API it “just” allows decoupling of unit tests from instrumentation tests?
I don't know that I'd call it decoupling these test source sets. They have always been their own separate source sets, but a sort of anomaly as they don't fit with the regular KMP source set layout (main + test). This API now gives control over which source set tree each Android test source set inherits from. In Kotlin <=1.7, both unit and instrumented tests inherited from the
commonTest
source set tree. In 1.8, the
androidInstrumentedTest
source set was decoupled from this tree by default (you can recouple it with a
dependsOn
call), but there was no way to remove the
dependsOn
relationship for the
androidUnitTest
source set. So this API provides the ability to configure both
androidInsturmentedTest
and
androidUnitTest
to belong to whichever source set tree makes sense for your code.
I guess I'm trying to understand, what is your use case for the
androidUnitTest
source set? And how do your library users depend on your tests?
we want to allow users of our library to run tests on either JVM or Android as they see fit
Do you mean consumers of your library being able to run their own tests as either JVM or Android instrumented or unit tests? Because they should be able to do this depending on the
jvm
or
android
publications of your library, right?
Or is your entire question posed from the perspective of your library users' app tests? Maybe I'm just misreading it from the perspective of your library tests. 😅
c

chrmelchior

05/25/2023, 6:31 PM
Well, right now I’m doing it for our own library tests, but ideally users of our library can use a similar approach in their code. But yes, right now people use the
android
or
jvm
publications we publish. The multiplatform plugin selects the right one automatically and pure android projects use the
-android
artifact directly. I suspect we need to do some gradle dependency substitution for pure android projects no matter what, but for multiplatform projects it would be nice if we could either release some artifact that
androidUniTest
would automatically pick or modify the gradle metadata some the
jvm
publication was chosen by
androidUnitTest
rather than the
android
publication.
j

Jeff Lockhart

05/25/2023, 6:36 PM
Ok, I think I understand more now. So do you have tests specifically in
androidUnitTest
or are you just running
commonTest
tests?
I've been playing around with Gradle Module Metadata lately as well. I've not thought about it from the perspective of
androidUnitTest
, what platform Gradle would select on in this case. KMP publishes attributes on the platform variants. But will gradle choose
android
or
standard-jvm
for the
org.gradle.jvm.environment
attribute? There's also the
org.jetbrains.kotlin.platform.type
attribute with
jvm
and
androidJvm
.
c

chrmelchior

05/25/2023, 6:41 PM
Right now most of the tests are “pure” common tests with some tests for android, jvm, ios and macos respectively. So far that split has been done by us symlinking a “shared” package between the variants and then having platform tests outside that package. That worked fine
But yes, I have also been looking into the attribute data, but it is a pretty black-box to me 🙈
But ideally we want to move to a world where all the common tests are actually in “commonTest” and then each platform has its own. So there wouldn’t be
androidUnitTests
that only was available on Android. In that case they would go to the
androidInstrumentedTest
folder
j

Jeff Lockhart

05/25/2023, 6:47 PM
If Android unit tests do select the
jvm
attribute variant, I'd expect you should be able to declare the base artifact:
dependencies {
    implementation("lib")
}
And it would choose the
-android
variant for the main build and instrumented tests automatically, and then choose the
-jvm
variant for unit tests, if it does work this way. You could manually declare the variant for the unit tests:
dependencies {
    implementation("lib-android")
    testImplementation("lib-jvm")
}
In this case, it's forcing both onto the classpath and it should exclude the variant that doesn't match the platform attribute. If that's
jvm
for Android unit tests, then it should work as expected, just not sure if that's the case or not. (You should be able to do this similarly with multiplatform dependency declaration.)
c

chrmelchior

05/25/2023, 6:49 PM
Interesting, I haven’t tried that yet. Definitely worth a shot
But yeah, not sure if it is documented anywhere how Gradle does dependency resolution in this particular case since
androidUnitTest
is a mix of both Android and JVM
j

Jeff Lockhart

05/25/2023, 6:51 PM
I'm really curious what the outcome is, which platform gradle uses in this case. I want to test it now.
But ideally we want to move to a world where all the common tests are actually in “commonTest” and then each platform has its own.
So there wouldn’t be
androidUnitTests
that only was available on Android. In that case they would go to the
androidInstrumentedTest
folder
This sounds like you'd then be able to do what I described initially, removing
androidUnitTest
from and adding
androidInstrumentedTest
to
SourceSetTree.test
.
Ok, so I tested this and it looks like Gradle does select the Android platform for Android unit tests, the same as Android instrumented tests and the main compilation. There might be a way to override this behavior or trick it by forcing the
jvm
variant's platform attribute to be
android
and the
android
variant's attribute
jvm
, specifically for the
androidUniTest
source set. But that would be pretty hacky.
From what I can tell, the Gradle API would let you override the metadata rules for a specific module, but not for a specific source set.
c

chrmelchior

05/25/2023, 7:53 PM
Thanks for looking into this. Looks like I have some more hacking to so tomorrow to see if there is a way around this.
j

Jeff Lockhart

05/25/2023, 7:56 PM
You might ask in #gradle if there's a way to override the
org.gradle.jvm.environment
and
org.jetbrains.kotlin.platform.type
attributes that a source set selects variants on.
My guess is ultimately this is controlled by the Android gradle plugin.
c

chrmelchior

05/26/2023, 8:08 AM
I managed to get this to work using dependency substitution:
configurations.all {
    // Ensure that androidUnitTest uses the Realm JVM variant rather than Android.
    if (name == "debugUnitTestRuntimeClasspath") {
        resolutionStrategy.dependencySubstitution {
            substitute(module("io.realm.kotlin:library-base:${Realm.version}")).using(
                module("io.realm.kotlin:library-base-jvm:${Realm.version}")
            )
            substitute(module("io.realm.kotlin:cinterop:${Realm.version}")).using(
                module("io.realm.kotlin:cinterop-jvm:${Realm.version}")
            )
        }
    }
}
And that will work fine for our internal tests, but is going to be pretty annoying for our external users. I’ll try to ask in #gradle and see if they have a better idea. Thanks for the hints 🙏
j

Jeff Lockhart

05/26/2023, 3:21 PM
Awesome, good find. I suppose you could provide the code snippet via a gradle plugin, where it could be applied more easily to a project. Are the APIs 100% equal between the Android and JVM variants?
c

chrmelchior

05/26/2023, 3:36 PM
Yes, it is just the internals that are problematic