From eaf61770150ddb798083f46cc598ea07f9c1e831 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Mon, 11 Jul 2022 16:01:57 +0200 Subject: [PATCH] Add kotlin-test variant dependency in a lazy way ^KT-41642 In Progress --- .../gradle/KotlinSpecificDependenciesIT.kt | 36 +++ .../internal/KotlinDependenciesManagement.kt | 242 ++++++++++-------- .../kotlin/gradle/tasks/TasksProvider.kt | 7 + 3 files changed, 176 insertions(+), 109 deletions(-) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt index 3421e246486..058c9dbfbaf 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt @@ -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 diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt index 5f188723923..db0a95052d0 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt @@ -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() +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, +) { + 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, + 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 = - 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, + 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?> = compilations.map { compilation -> - val target = compilation.target - when { - target is KotlinTargetWithTests<*, *> -> - target.findTestRunsByCompilation(compilation)?.filterIsInstance>()?.map { it.executionTask.get() } - target is KotlinWithJavaTarget<*> -> - if (compilation.name == KotlinCompilation.TEST_COMPILATION_NAME) - project.locateTask(target.testTaskName)?.get()?.let(::listOf) - else null - compilation is KotlinJvmAndroidCompilation -> when (compilation.androidVariant) { - is UnitTestVariant -> - project.locateTask(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? { + val compilationTarget = target + val testTaskList: List> = 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>? { +private fun KotlinTargetWithTests<*, *>.findTestRunsByCompilation( + byCompilation: KotlinCompilation<*> +): NamedDomainObjectSet> { 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 } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt index 11df9600c01..7f707d394b7 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt @@ -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 Project.locateTask(name: String): TaskPro null } +/** + * Locates a task by [name] and [type], without triggering its creation or configuration. + */ +internal inline fun TaskContainer.locateTask(name: String): TaskProvider? = + 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]