Add kotlin-test variant dependency in a lazy way
^KT-41642 In Progress
This commit is contained in:
+36
@@ -250,6 +250,42 @@ class KotlinSpecificDependenciesIT : KGPBaseTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmGradlePluginTests
|
||||
@DisplayName("KT-41642: adding kotlin.test should not resolve dependencies eagerly")
|
||||
@GradleTest
|
||||
fun testKotlinTestEagerDependencies(gradleVersion: GradleVersion) {
|
||||
project("simpleProject", gradleVersion) {
|
||||
|
||||
// Disabling auto-adding kotlin stdlib dependencies
|
||||
gradleProperties.appendText(
|
||||
"""
|
||||
|
||||
kotlin.stdlib.default.dependency=false
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
buildGradle.appendText(
|
||||
//language=Groovy
|
||||
"""
|
||||
|
||||
configurations.each { config ->
|
||||
config.dependencies.addAllLater(
|
||||
project.objects.listProperty(Dependency.class).value(
|
||||
project.provider {
|
||||
throw new Throwable("Dependency resolved in ${'$'}{config.name}!")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
build("help") {
|
||||
assertOutputDoesNotContain("Dependency resolved in")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AndroidGradlePluginTests
|
||||
@DisplayName("Android: Kotlin test single dependency in unit tests")
|
||||
@GradleAndroidTest
|
||||
|
||||
+133
-109
@@ -7,13 +7,14 @@ package org.jetbrains.kotlin.gradle.internal
|
||||
|
||||
import com.android.build.gradle.api.TestVariant
|
||||
import com.android.build.gradle.api.UnitTestVariant
|
||||
import org.gradle.api.InvalidUserDataException
|
||||
import org.gradle.api.NamedDomainObjectSet
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.*
|
||||
import org.gradle.api.artifacts.dsl.DependencyHandler
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.tasks.testing.AbstractTestTask
|
||||
import org.gradle.api.tasks.TaskContainer
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.gradle.api.tasks.testing.Test
|
||||
import org.gradle.api.tasks.testing.junit.JUnitOptions
|
||||
import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions
|
||||
@@ -33,7 +34,6 @@ import org.jetbrains.kotlin.gradle.plugin.sources.android.AndroidVariantType
|
||||
import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull
|
||||
import org.jetbrains.kotlin.gradle.plugin.sources.dependsOnClosure
|
||||
import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope
|
||||
import org.jetbrains.kotlin.gradle.plugin.sources.withDependsOnClosure
|
||||
import org.jetbrains.kotlin.gradle.targets.js.npm.SemVer
|
||||
import org.jetbrains.kotlin.gradle.targets.jvm.JvmCompilationsTestRunSource
|
||||
import org.jetbrains.kotlin.gradle.tasks.locateTask
|
||||
@@ -48,15 +48,19 @@ internal const val PLATFORM_INTEGERS_SUPPORT_LIBRARY = "platform-integers"
|
||||
|
||||
internal fun customizeKotlinDependencies(project: Project) {
|
||||
val topLevelExtension = project.topLevelExtension
|
||||
val propertiesProvider = PropertiesProvider(project)
|
||||
val coreLibrariesVersion = project.objects.providerWithLazyConvention {
|
||||
topLevelExtension.coreLibrariesVersion
|
||||
}
|
||||
|
||||
if (PropertiesProvider(project).stdlibDefaultDependency)
|
||||
if (propertiesProvider.stdlibDefaultDependency)
|
||||
project.configureStdlibDefaultDependency(topLevelExtension, coreLibrariesVersion)
|
||||
|
||||
if (topLevelExtension is KotlinProjectExtension) { // TODO: extend this logic to PM20
|
||||
configureKotlinTestDependency(project)
|
||||
if (propertiesProvider.kotlinTestInferJvmVariant) { // TODO: extend this logic to PM20
|
||||
project.configureKotlinTestDependency(
|
||||
topLevelExtension,
|
||||
coreLibrariesVersion,
|
||||
)
|
||||
}
|
||||
|
||||
project.configurations.configureDefaultVersionsResolutionStrategy(
|
||||
@@ -129,6 +133,7 @@ private fun Project.configureStdlibDefaultDependency(
|
||||
topLevelExtension is KotlinSingleTargetExtension<*> -> topLevelExtension
|
||||
.target
|
||||
.addStdlibDependency(configurations, dependencies, coreLibrariesVersion)
|
||||
|
||||
topLevelExtension is KotlinMultiplatformExtension -> topLevelExtension
|
||||
.targets
|
||||
.configureEach { target ->
|
||||
@@ -278,107 +283,144 @@ private val stdlibModules = setOf("kotlin-stdlib-common", "kotlin-stdlib", "kotl
|
||||
//endregion
|
||||
|
||||
//region kotlin-test
|
||||
internal fun configureKotlinTestDependency(project: Project) {
|
||||
if (!PropertiesProvider(project).kotlinTestInferJvmVariant)
|
||||
return
|
||||
private val Dependency.isKotlinTestRootDependency: Boolean
|
||||
get() = group == KOTLIN_MODULE_GROUP && name == KOTLIN_TEST_ROOT_MODULE_NAME
|
||||
|
||||
fun isKotlinTestRootDependency(dependency: Dependency) =
|
||||
dependency.group == KOTLIN_MODULE_GROUP && dependency.name == KOTLIN_TEST_ROOT_MODULE_NAME
|
||||
private val kotlin150Version = SemVer(1.toBigInteger(), 5.toBigInteger(), 0.toBigInteger())
|
||||
|
||||
val versionPrefixRegex = Regex("""^(\d+)\.(\d+)""")
|
||||
fun isAtLeast1_5(version: String) = versionPrefixRegex.find(version)?.let {
|
||||
val c1 = it.groupValues[1].toInt()
|
||||
val c2 = it.groupValues[2].toInt()
|
||||
c1 > 1 || c1 == 1 && c2 >= 5
|
||||
} ?: false
|
||||
private fun isAtLeast1_5(version: String) = SemVer.from(version) >= kotlin150Version
|
||||
|
||||
KotlinDependencyScope.values().forEach { scope ->
|
||||
val versionOrNullBySourceSet = mutableMapOf<KotlinSourceSet, String?>()
|
||||
private val jvmPlatforms = setOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm)
|
||||
|
||||
project.kotlinExtension.sourceSets.all { kotlinSourceSet ->
|
||||
val configuration = project.configurations.sourceSetDependencyConfigurationByScope(kotlinSourceSet, scope)
|
||||
var finalizingDependencies = false
|
||||
private fun Project.configureKotlinTestDependency(
|
||||
topLevelExtension: KotlinTopLevelExtension,
|
||||
coreLibrariesVersion: Provider<String>,
|
||||
) {
|
||||
when (topLevelExtension) {
|
||||
is KotlinSingleTargetExtension<*> -> topLevelExtension.target.configureKotlinTestDependency(
|
||||
configurations,
|
||||
coreLibrariesVersion,
|
||||
dependencies,
|
||||
tasks
|
||||
)
|
||||
|
||||
configuration.nonProjectDependencies().matching(::isKotlinTestRootDependency).apply {
|
||||
firstOrNull()?.let { versionOrNullBySourceSet[kotlinSourceSet] = it.version }
|
||||
whenObjectRemoved {
|
||||
if (!finalizingDependencies && !any())
|
||||
versionOrNullBySourceSet.remove(kotlinSourceSet)
|
||||
is KotlinMultiplatformExtension -> topLevelExtension.targets.configureEach { target ->
|
||||
target.configureKotlinTestDependency(
|
||||
configurations,
|
||||
coreLibrariesVersion,
|
||||
dependencies,
|
||||
tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun KotlinTarget.configureKotlinTestDependency(
|
||||
configurations: ConfigurationContainer,
|
||||
coreLibrariesVersion: Provider<String>,
|
||||
dependencyHandler: DependencyHandler,
|
||||
tasks: TaskContainer
|
||||
) {
|
||||
compilations.configureEach { compilation ->
|
||||
val platformType = compilation.platformType
|
||||
if (platformType in jvmPlatforms) {
|
||||
// Checking dependencies which were added via dependsOn(KotlinSourceSet) call
|
||||
// Compilation has own configurations for these that are different from KotlinSourceSet configurations
|
||||
KotlinDependencyScope.values()
|
||||
.map { configurations.sourceSetDependencyConfigurationByScope(compilation, it) }
|
||||
.forEach {
|
||||
it.maybeAddTestDependencyCapability(
|
||||
compilation,
|
||||
coreLibrariesVersion,
|
||||
dependencyHandler,
|
||||
tasks
|
||||
)
|
||||
}
|
||||
whenObjectAdded { item ->
|
||||
versionOrNullBySourceSet[kotlinSourceSet] = item.version
|
||||
}
|
||||
}
|
||||
|
||||
project.tryWithDependenciesIfUnresolved(configuration) { dependencies ->
|
||||
val parentOrOwnVersions: List<String?> =
|
||||
kotlinSourceSet.withDependsOnClosure.filter(versionOrNullBySourceSet::contains)
|
||||
.map(versionOrNullBySourceSet::get)
|
||||
|
||||
finalizingDependencies = true
|
||||
|
||||
for (version in parentOrOwnVersions.distinct()) { // add dependencies with each version and let Gradle disambiguate them
|
||||
val effectiveVersion = version ?: project.kotlinExtension.coreLibrariesVersion
|
||||
if (!isAtLeast1_5(effectiveVersion)) continue
|
||||
val clarifyCapability = kotlinTestCapabilityForJvmSourceSet(project, kotlinSourceSet) ?: continue
|
||||
val clarifiedDependency =
|
||||
(project.dependencies.kotlinDependency(KOTLIN_TEST_ROOT_MODULE_NAME, version) as ExternalDependency).apply {
|
||||
if (version == null) {
|
||||
version { constraint -> constraint.require(project.kotlinExtension.coreLibrariesVersion) }
|
||||
}
|
||||
capabilities {
|
||||
it.requireCapability(clarifyCapability)
|
||||
}
|
||||
}
|
||||
dependencies.add(clarifiedDependency)
|
||||
}
|
||||
compilation.kotlinSourceSets.forEach { sourceSet ->
|
||||
KotlinDependencyScope.values()
|
||||
.map { configurations.sourceSetDependencyConfigurationByScope(sourceSet, it) }
|
||||
.forEach {
|
||||
it.maybeAddTestDependencyCapability(
|
||||
compilation,
|
||||
coreLibrariesVersion,
|
||||
dependencyHandler,
|
||||
tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun kotlinTestCapabilityForJvmSourceSet(project: Project, kotlinSourceSet: KotlinSourceSet): String? {
|
||||
val compilations = CompilationSourceSetUtil.compilationsBySourceSets(project).getValue(kotlinSourceSet)
|
||||
.filter { it.target !is KotlinMetadataTarget }
|
||||
.ifEmpty { return null }
|
||||
private fun Configuration.maybeAddTestDependencyCapability(
|
||||
compilation: KotlinCompilation<*>,
|
||||
coreLibrariesVersion: Provider<String>,
|
||||
dependencyHandler: DependencyHandler,
|
||||
tasks: TaskContainer
|
||||
) {
|
||||
withDependencies { dependencies ->
|
||||
val testRootDependency = allNonProjectDependencies()
|
||||
.singleOrNull { it.isKotlinTestRootDependency }
|
||||
|
||||
val platformTypes = compilations.map { it.platformType }
|
||||
// TODO: Extract jvmPlatformTypes to public constant?
|
||||
val jvmPlatforms = setOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm)
|
||||
if (!jvmPlatforms.containsAll(platformTypes)) return null
|
||||
if (testRootDependency != null) {
|
||||
val depVersion = testRootDependency.version ?: coreLibrariesVersion.get()
|
||||
if (!isAtLeast1_5(depVersion)) return@withDependencies
|
||||
|
||||
val testTaskLists: List<List<Task>?> = compilations.map { compilation ->
|
||||
val target = compilation.target
|
||||
when {
|
||||
target is KotlinTargetWithTests<*, *> ->
|
||||
target.findTestRunsByCompilation(compilation)?.filterIsInstance<KotlinTaskTestRun<*, *>>()?.map { it.executionTask.get() }
|
||||
target is KotlinWithJavaTarget<*> ->
|
||||
if (compilation.name == KotlinCompilation.TEST_COMPILATION_NAME)
|
||||
project.locateTask<AbstractTestTask>(target.testTaskName)?.get()?.let(::listOf)
|
||||
else null
|
||||
compilation is KotlinJvmAndroidCompilation -> when (compilation.androidVariant) {
|
||||
is UnitTestVariant ->
|
||||
project.locateTask<AbstractTestTask>(lowerCamelCaseName("test", compilation.androidVariant.name))?.get()?.let(::listOf)
|
||||
is TestVariant -> (compilation.androidVariant as TestVariant).connectedInstrumentTestProvider.orNull?.let(::listOf)
|
||||
else -> null
|
||||
val testCapability = compilation.kotlinTestCapabilityForJvmSourceSet(tasks)
|
||||
if (testCapability != null) {
|
||||
dependencies.addLater(
|
||||
testCapability.map { capability ->
|
||||
dependencyHandler
|
||||
.kotlinDependency(KOTLIN_TEST_ROOT_MODULE_NAME, depVersion)
|
||||
.apply {
|
||||
(this as ExternalDependency).capabilities {
|
||||
it.requireCapability(capability)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (null in testTaskLists) {
|
||||
return null
|
||||
}
|
||||
val testTasks = testTaskLists.flatMap { checkNotNull(it) }
|
||||
val frameworks = testTasks.mapTo(mutableSetOf()) { testTask ->
|
||||
when (testTask) {
|
||||
is Test -> testFrameworkOf(testTask)
|
||||
else -> // Android connected test tasks don't inherit from Test, but we use JUnit for them
|
||||
KotlinTestJvmFramework.junit
|
||||
}
|
||||
|
||||
private fun KotlinCompilation<*>.kotlinTestCapabilityForJvmSourceSet(
|
||||
tasks: TaskContainer
|
||||
): Provider<String>? {
|
||||
val compilationTarget = target
|
||||
val testTaskList: List<TaskProvider<out Task>> = when {
|
||||
compilationTarget is KotlinTargetWithTests<*, *> -> compilationTarget
|
||||
.findTestRunsByCompilation(this)
|
||||
.matching { it is KotlinTaskTestRun<*, *> }
|
||||
.mapNotNull { (it as KotlinTaskTestRun<*, *>).executionTask }
|
||||
|
||||
compilationTarget is KotlinWithJavaTarget<*> &&
|
||||
name == KotlinCompilation.TEST_COMPILATION_NAME ->
|
||||
listOfNotNull(tasks.locateTask(compilationTarget.testTaskName))
|
||||
|
||||
this is KotlinJvmAndroidCompilation -> when (androidVariant) {
|
||||
is UnitTestVariant -> listOfNotNull(tasks.locateTask(lowerCamelCaseName("test", androidVariant.name)))
|
||||
is TestVariant -> listOfNotNull((androidVariant as TestVariant).connectedInstrumentTestProvider)
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
// TODO: Review Sergey Igushkin: https://youtrack.jetbrains.com/issue/KT-48885
|
||||
return "$KOTLIN_MODULE_GROUP:$KOTLIN_TEST_ROOT_MODULE_NAME-framework-${frameworks.singleOrNull() ?: return null}"
|
||||
if (testTaskList.isEmpty()) return null
|
||||
|
||||
return testTaskList
|
||||
.singleOrNull()
|
||||
?.map { task ->
|
||||
val framework = when (task) {
|
||||
is Test -> testFrameworkOf(task)
|
||||
else -> // Android connected test tasks don't inherit from Test, but we use JUnit for them
|
||||
KotlinTestJvmFramework.junit
|
||||
}
|
||||
|
||||
"$KOTLIN_MODULE_GROUP:$KOTLIN_TEST_ROOT_MODULE_NAME-framework-$framework"
|
||||
}
|
||||
}
|
||||
|
||||
internal const val KOTLIN_TEST_ROOT_MODULE_NAME = "kotlin-test"
|
||||
@@ -395,38 +437,20 @@ private fun testFrameworkOf(testTask: Test): KotlinTestJvmFramework = when (test
|
||||
KotlinTestJvmFramework.junit
|
||||
}
|
||||
|
||||
private fun KotlinTargetWithTests<*, *>.findTestRunsByCompilation(byCompilation: KotlinCompilation<*>): List<KotlinTargetTestRun<*>>? {
|
||||
private fun KotlinTargetWithTests<*, *>.findTestRunsByCompilation(
|
||||
byCompilation: KotlinCompilation<*>
|
||||
): NamedDomainObjectSet<out KotlinTargetTestRun<*>> {
|
||||
fun KotlinExecution.ExecutionSource.isProducedFromTheCompilation(): Boolean = when (this) {
|
||||
is CompilationExecutionSource<*> -> compilation == byCompilation
|
||||
is JvmCompilationsTestRunSource -> byCompilation in testCompilations
|
||||
is KotlinAggregateExecutionSource<*> -> this.executionSources.any { it.isProducedFromTheCompilation() }
|
||||
else -> false
|
||||
}
|
||||
return testRuns.filter { it.executionSource.isProducedFromTheCompilation() }.takeIf { it.isNotEmpty() }
|
||||
return testRuns.matching { it.executionSource.isProducedFromTheCompilation() }
|
||||
}
|
||||
//endregion
|
||||
|
||||
internal fun DependencyHandler.kotlinDependency(moduleName: String, versionOrNull: String?) =
|
||||
create("$KOTLIN_MODULE_GROUP:$moduleName${versionOrNull?.prependIndent(":").orEmpty()}")
|
||||
|
||||
private fun Project.tryWithDependenciesIfUnresolved(configuration: Configuration, action: (DependencySet) -> Unit) {
|
||||
fun reportAlreadyResolved() {
|
||||
logger.info("Could not setup Kotlin-specific dependencies for $configuration as it is already resolved")
|
||||
}
|
||||
|
||||
if (configuration.state != Configuration.State.UNRESOLVED)
|
||||
return reportAlreadyResolved()
|
||||
|
||||
// Gradle doesn't provide any public API to check if it's safe to modify a configuration's dependencies.
|
||||
// The state of the configuration may still be UNRESOLVED but it might have 'participated' in dependency resolution and its dependencies
|
||||
// may now be frozen.
|
||||
try {
|
||||
configuration.withDependencies(action)
|
||||
} catch (e: InvalidUserDataException) {
|
||||
reportAlreadyResolved()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Configuration.allNonProjectDependencies() = allDependencies.matching { it !is ProjectDependency }
|
||||
|
||||
private fun Configuration.nonProjectDependencies() = dependencies.matching { it !is ProjectDependency }
|
||||
|
||||
+7
@@ -20,6 +20,7 @@ import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.UnknownTaskException
|
||||
import org.gradle.api.tasks.TaskCollection
|
||||
import org.gradle.api.tasks.TaskContainer
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
|
||||
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrLink
|
||||
@@ -69,6 +70,12 @@ internal inline fun <reified T : Task> Project.locateTask(name: String): TaskPro
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates a task by [name] and [type], without triggering its creation or configuration.
|
||||
*/
|
||||
internal inline fun <reified T : Task> TaskContainer.locateTask(name: String): TaskProvider<T>? =
|
||||
if (names.contains(name)) named(name, T::class.java) else null
|
||||
|
||||
/**
|
||||
* Locates a task by [name] and [type], without triggering its creation or configuration or registers new task
|
||||
* with [name], type [T] and initialization script [body]
|
||||
|
||||
Reference in New Issue
Block a user