[Gradle] Introduce 'Future' to represent deferred values/calculations

^KT-34662 Verification Pending
This commit is contained in:
Sebastian Sellmair
2023-03-10 12:24:32 +01:00
committed by Space Team
parent 028780ae97
commit 02120a6822
3 changed files with 149 additions and 2 deletions
@@ -426,7 +426,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
enqueuedActions.getValue(stage).addLast(action)
if (stage == Stage.Configure) {
if (stage == Stage.Configure || isFinished.get()) {
loopIfNecessary()
}
}
@@ -434,7 +434,6 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
override fun launch(block: suspend KotlinPluginLifecycle.() -> Unit) {
val lifecycle = this
check(isStarted.get()) { "Cannot launch when ${KotlinPluginLifecycle::class.simpleName} is not started" }
check(!isFinished.get()) { "Cannot launch when ${KotlinPluginLifecycle::class.simpleName} is already finished" }
val coroutine = block.createCoroutine(this, object : Continuation<Unit> {
override val context: CoroutineContext = EmptyCoroutineContext +
@@ -475,6 +474,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
override val finaliseIn: Stage,
override val property: Property<T>
) : LifecycleAwareProperty<T> {
override suspend fun awaitFinalValue(): T? {
await(finaliseIn)
return property.orNull
@@ -0,0 +1,77 @@
/*
* Copyright 2010-2023 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.
*/
package org.jetbrains.kotlin.gradle.utils
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.completeWith
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.HasProject
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException
import org.jetbrains.kotlin.gradle.plugin.kotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.launch
import org.jetbrains.kotlin.tooling.core.ExtrasLazyProperty
import org.jetbrains.kotlin.tooling.core.HasMutableExtras
import org.jetbrains.kotlin.tooling.core.extrasLazyProperty
/**
* See [KotlinPluginLifecycle]:
* This [Future] represents a value that will be available in some 'future' time.
*
*
* #### Simple use case example: Deferring the value of a property to a given [KotlinPluginLifecycle.Stage]:
*
* ```kotlin
* val myFutureProperty: Future<Int> = project.future {
* await(FinaliseDsl) // <- suspends
* 42
* }
* ```
*
* Futures can also be used to dynamically extend entities implementing [HasMutableExtras] and [HasProject]
* #### Example Usage: Extending KotlinSourceSet with a property that relies on the final refines edges.
*
* ```kotlin
* internal val KotlinSourceSet.dependsOnCommonMain: Future<Boolean> by lazyFuture("dependsOnCommonMain") {
* await(AfterFinaliseRefinesEdges)
* return dependsOn.contains { it.name == "commonMain") }
* }
* ```
*/
internal interface Future<T> {
suspend fun await(): T
fun getOrThrow(): T
}
internal inline fun <Receiver, reified T> lazyFuture(
name: String? = null, noinline block: suspend Receiver.() -> T
): ExtrasLazyProperty<Receiver, Future<T>> where Receiver : HasMutableExtras, Receiver : HasProject {
return extrasLazyProperty<Receiver, Future<T>>(name) {
project.future { block() }
}
}
@OptIn(ExperimentalCoroutinesApi::class)
internal fun <T> Project.future(block: suspend Project.() -> T): Future<T> {
val deferred = CompletableDeferred<T>()
launch {
deferred.completeWith(runCatching { block() })
}
return object : Future<T> {
override suspend fun await(): T {
return deferred.await()
}
override fun getOrThrow(): T {
return if (deferred.isCompleted) deferred.getCompleted() else throw IllegalLifecycleException(
"Future was not completed yet. Stage: '${kotlinPluginLifecycle.stage}'"
)
}
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2010-2023 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.unitTests
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.FinaliseDsl
import org.jetbrains.kotlin.gradle.plugin.await
import org.jetbrains.kotlin.gradle.plugin.currentKotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.startKotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.util.buildProject
import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest
import org.jetbrains.kotlin.gradle.utils.future
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class FutureTest {
private val project = buildProject().also { project ->
project.startKotlinPluginLifecycle()
}
@Test
fun `test - simple deferred future`() = project.runLifecycleAwareTest {
val future = project.future {
await(FinaliseDsl)
42
}
assertFailsWith<IllegalLifecycleException> { future.getOrThrow() }
assertEquals(42, future.await())
assertEquals(42, future.getOrThrow())
assertEquals(FinaliseDsl, currentKotlinPluginLifecycle().stage)
}
@Test
fun `test - future depending on another future`() = project.runLifecycleAwareTest {
val futureA = project.future {
await(FinaliseDsl)
42
}
val futureB = project.future {
futureA.await().toString()
}
assertEquals("42", futureB.await())
assertEquals("42", futureB.getOrThrow())
assertEquals(FinaliseDsl, currentKotlinPluginLifecycle().stage)
}
@Test
fun `test - after lifecycle finished`() {
val project = buildProject()
project.startKotlinPluginLifecycle()
project.evaluate()
val future = project.future {
await(FinaliseDsl)
42
}
assertEquals(42, future.getOrThrow())
}
}