diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt index c40a68537d6..3f08ce9d061 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt @@ -115,12 +115,26 @@ abstract class KotlinTopLevelExtension(internal val project: Project) : KotlinTo explicitApi = ExplicitApiMode.Warning } + /** + * Can be used to configure objects that are not yet created, or will be created in + * 'afterEvaluate' (e.g. typically Android source sets containing flavors and buildTypes) + * + * Will fail project evaluation if the domain object is not created before 'afterEvaluate' listeners in the buildscript. + * + * @param configure: Called inline, if the value is already present. Called once the domain object is created. + */ @ExperimentalKotlinGradlePluginApi fun NamedDomainObjectContainer.invokeWhenCreated(name: String, configure: T.() -> Unit) { val invoked = AtomicBoolean(false) matching { it.name == name }.all { value -> if (!invoked.getAndSet(true)) value.configure() } project.whenEvaluated { - if (!invoked.getAndSet(true)) named(name).configure(configure) + if (!invoked.getAndSet(true)) { + /* + Expected to fail, since the listener was not called. + Letting Gradle fail here will result in a more natural error message + */ + named(name).configure(configure) + } } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/InvokeWhenCreatedTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/InvokeWhenCreatedTest.kt new file mode 100644 index 00000000000..0e8162d1f47 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/InvokeWhenCreatedTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("FunctionName") + +package org.jetbrains.kotlin.gradle + +import org.gradle.api.Named +import org.gradle.api.UnknownDomainObjectException +import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.tooling.core.withLinearClosure +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.fail + +class InvokeWhenCreatedTest { + + data class Value(private val name: String) : Named { + override fun getName(): String = name + override fun toString(): String = name + } + + private val project = buildProjectWithJvm() + private val container = project.objects.domainObjectContainer(Value::class.java) { name -> Value(name) } + + @Test + fun `test - values is already present - is called inline`() { + var invocations = 0 + + container.create("x") + project.kotlinExtension.apply { + container.invokeWhenCreated("x") { invocations++; assertEquals("x", name) } + assertEquals(1, invocations, "Expected 'invokeWhenCreated' to be called inline, once") + } + + project.evaluate() + assertEquals(1, invocations, "Expected 'invokeWhenCreated' to be called only once") + } + + @Test + fun `test - value will be created later`() { + var invocations = 0 + + project.kotlinExtension.apply { + container.invokeWhenCreated("x") { invocations++; assertEquals("x", name) } + assertEquals(0, invocations, "Expected 'invokeWhenCreated' to be not called") + } + + container.create("y") + assertEquals(0, invocations, "Expected 'invokeWhenCreated' to be not called") + + container.create("x") + assertEquals(1, invocations, "Expected 'invokeWhenCreated' to be called once") + } + + @Test + fun `test - value is missing - evaluate will fail`() { + project.kotlinExtension.apply { + container.invokeWhenCreated("x") { + fail("Called 'invokeWhenCreated' unexpectedly") + } + } + + val causes = assertFails { project.evaluate() }.withLinearClosure { it.cause } + assertEquals>(UnknownDomainObjectException::class.java, causes.last().javaClass) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt index b889b631c1c..e13fff5229c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt @@ -11,6 +11,7 @@ import org.gradle.api.internal.project.ProjectInternal import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformJvmPlugin import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20ProjectExtension fun buildProject( @@ -35,6 +36,11 @@ fun buildProjectWithKPM(code: Project.() -> Unit= {}) = buildProject { code() } +fun buildProjectWithJvm(code: Project.() -> Unit = {}) = buildProject { + project.plugins.apply(KotlinPlatformJvmPlugin::class.java) + code() +} + fun Project.kotlin(code: KotlinMultiplatformExtension.() -> Unit) { val kotlin = project.kotlinExtension as KotlinMultiplatformExtension kotlin.code()