[Gradle] Introduce Project.configured: Future<ProjectConfigurationResult> API
KT-58275
This commit is contained in:
committed by
Space Team
parent
f616fffeb0
commit
d170897dbf
+77
-7
@@ -8,6 +8,8 @@ package org.jetbrains.kotlin.gradle.plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.provider.Property
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.*
|
||||
import org.jetbrains.kotlin.gradle.utils.CompletableFuture
|
||||
import org.jetbrains.kotlin.gradle.utils.Future
|
||||
import org.jetbrains.kotlin.gradle.utils.failures
|
||||
import org.jetbrains.kotlin.gradle.utils.getOrPut
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -114,6 +116,56 @@ internal val Project.kotlinPluginLifecycle: KotlinPluginLifecycle
|
||||
KotlinPluginLifecycleImpl(project)
|
||||
}
|
||||
|
||||
/**
|
||||
* Future that will be completed once the project is considered 'Configured'
|
||||
* ### Happy Path
|
||||
* If the project configuration is successful (no exceptions thrown), then this Future will complete
|
||||
* **after** [KotlinPluginLifecycle.Stage.ReadyForExecution] was fully executed. All coroutines within the regular lifecycle .
|
||||
* In this case the value of this future will be [ProjectConfigurationResult.Success]
|
||||
*
|
||||
* ### Unhappy Path (Project configuration failed via exception)
|
||||
* If the project configuration is unsuccessful (exception thrown) then this future will complete with
|
||||
* [ProjectConfigurationResult.Failure], carrying the thrown exceptions.
|
||||
*
|
||||
* E.g. the following code:
|
||||
* ```kotlin
|
||||
* project.launchInStage(Stage.FinaliseCompilations) {
|
||||
* throw Exception("My Error")
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* will lead to:
|
||||
* ```kotlin
|
||||
* project.launch {
|
||||
* val result = project.configured.await()
|
||||
* val result as Failure
|
||||
* val exception = result.failures.first()
|
||||
* println(exception.message) // 'My Error'
|
||||
* println(stage) // 'Stage.FinaliseCompilations'
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* #### Failure case | Launching coroutines | Future.getOrThrow
|
||||
* Even in case of failure it is still okay to further launch a new coroutine
|
||||
* ```kotlin
|
||||
* project.launch {
|
||||
* val result = project.configured.await() as Failure
|
||||
* val anotherJob = project.launch { ... } // <- executed right away
|
||||
* val someFutureEvaluation = project.someFuture.getOrThrow() // <- will return value if all 'requirements' have been met.
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Note: [Future.getOrThrow] will throw if e.g. the lifecycle fails in a very early stage, but the Future requires
|
||||
* some later data to be available. In this case, the Future still will only return 'sane' data.
|
||||
*/
|
||||
internal val Project.configured: Future<ProjectConfigurationResult>
|
||||
get() = configuredImpl
|
||||
|
||||
|
||||
private val Project.configuredImpl: CompletableFuture<ProjectConfigurationResult>
|
||||
get() = extraProperties.getOrPut("org.jetbrains.kotlin.gradle.plugin.configured") { CompletableFuture() }
|
||||
|
||||
|
||||
/**
|
||||
* Will start the lifecycle, this shall be called before the [kotlinPluginLifecycle] is effectively used
|
||||
*/
|
||||
@@ -299,11 +351,16 @@ internal interface KotlinPluginLifecycle {
|
||||
ReadyForExecution;
|
||||
|
||||
val previousOrFirst: Stage get() = previousOrNull ?: values.first()
|
||||
|
||||
val previousOrNull: Stage? get() = values.getOrNull(ordinal - 1)
|
||||
|
||||
val previousOrThrow: Stage
|
||||
get() = previousOrNull ?: throw IllegalArgumentException("'$this' does not have a next ${Stage::class.simpleName}")
|
||||
|
||||
val nextOrNull: Stage? get() = values.getOrNull(ordinal + 1)
|
||||
|
||||
val nextOrLast: Stage get() = nextOrNull ?: values.last()
|
||||
|
||||
val nextOrThrow: Stage
|
||||
get() = nextOrNull ?: throw IllegalArgumentException("'$this' does not have a next ${Stage::class.simpleName}")
|
||||
|
||||
@@ -323,6 +380,11 @@ internal interface KotlinPluginLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ProjectConfigurationResult {
|
||||
object Success : ProjectConfigurationResult()
|
||||
data class Failure(val failures: List<Throwable>) : ProjectConfigurationResult()
|
||||
}
|
||||
|
||||
val project: Project
|
||||
|
||||
val stage: Stage
|
||||
@@ -372,7 +434,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
|
||||
private val loopRunning = AtomicBoolean(false)
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
private val isFinishedSuccessfully = AtomicBoolean(false)
|
||||
private val isFinishedWithExceptions = AtomicBoolean(false)
|
||||
private val isFinishedWithFailures = AtomicBoolean(false)
|
||||
|
||||
override var stage: Stage = Stage.values.first()
|
||||
private val properties = WeakHashMap<Property<*>, WeakReference<LifecycleAwareProperty<*>>>()
|
||||
@@ -430,6 +492,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
|
||||
try {
|
||||
val queue = enqueuedActions.getValue(stage)
|
||||
do {
|
||||
project.state.rethrowFailure()
|
||||
val action = queue.removeFirstOrNull()
|
||||
action?.invoke(this)
|
||||
} while (action != null)
|
||||
@@ -441,17 +504,19 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
|
||||
private fun finishWithFailures(failures: List<Throwable>) {
|
||||
assert(failures.isNotEmpty())
|
||||
assert(isStarted.get())
|
||||
assert(!isFinishedWithExceptions.getAndSet(true))
|
||||
assert(!isFinishedWithFailures.getAndSet(true))
|
||||
project.configuredImpl.complete(ProjectConfigurationResult.Failure(failures))
|
||||
}
|
||||
|
||||
private fun finishSuccessfully() {
|
||||
assert(isStarted.get())
|
||||
assert(!isFinishedSuccessfully.getAndSet(true))
|
||||
project.configuredImpl.complete(ProjectConfigurationResult.Success)
|
||||
}
|
||||
|
||||
override fun enqueue(stage: Stage, action: KotlinPluginLifecycle.() -> Unit) {
|
||||
if (stage < this.stage) {
|
||||
throw IllegalLifecycleException("Cannot enqueue Action for stage '${stage}' in current stage '${this.stage}'")
|
||||
throw IllegalLifecycleException("Cannot enqueue Action for stage '$stage' in current stage '${this.stage}'")
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -460,12 +525,17 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
|
||||
will be executed right away (no suspend necessary or wanted)
|
||||
*/
|
||||
if (isFinishedSuccessfully.get()) {
|
||||
action()
|
||||
return
|
||||
return action()
|
||||
}
|
||||
|
||||
if (isFinishedWithExceptions.get()) {
|
||||
throw IllegalLifecycleException("Cannot enqueue Action: Lifecycle already finished with Exceptions")
|
||||
/*
|
||||
Lifecycle finished, but some exceptions have been thrown.
|
||||
In this case, an enqueue for future Stages is not allowed, since those will not be executed anymore.
|
||||
Any enqueue in the current stage will be executed right away (no suspend necessary or wanted).
|
||||
*/
|
||||
if (isFinishedWithFailures.get()) {
|
||||
return if (stage == this.stage) action()
|
||||
else Unit
|
||||
}
|
||||
|
||||
enqueuedActions.getValue(stage).addLast(action)
|
||||
|
||||
+2
@@ -256,6 +256,8 @@ abstract class KotlinBasePluginWrapper : DefaultKotlinBasePlugin() {
|
||||
): Plugin<Project>
|
||||
|
||||
private fun Project.scheduleDiagnosticChecksAndReporting() {
|
||||
|
||||
|
||||
launchInStage(KotlinPluginLifecycle.Stage.ReadyForExecution) {
|
||||
// Do not run checkers on projects which configuration finished with failure,
|
||||
// as the internal state can not be trusted at this point (e.g. not entire of the
|
||||
|
||||
+119
-1
@@ -3,21 +3,26 @@
|
||||
* 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")
|
||||
@file:Suppress("FunctionName", "ThrowableNotThrown")
|
||||
|
||||
package org.jetbrains.kotlin.gradle.unitTests
|
||||
|
||||
import org.gradle.api.ProjectConfigurationException
|
||||
import org.jetbrains.kotlin.gradle.plugin.*
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.ProjectConfigurationResult.Failure
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.*
|
||||
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
|
||||
import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest
|
||||
import org.jetbrains.kotlin.gradle.utils.future
|
||||
import org.jetbrains.kotlin.tooling.core.withClosure
|
||||
import org.jetbrains.kotlin.tooling.core.withLinearClosure
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.cast
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.test.*
|
||||
|
||||
class KotlinPluginLifecycleTest {
|
||||
@@ -186,6 +191,119 @@ class KotlinPluginLifecycleTest {
|
||||
assertTrue(executed.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - throwing an exception during buildscript evaluation - shall not execute afterEvaluate based stages`() {
|
||||
val executed = AtomicReference<Throwable>()
|
||||
lifecycle.enqueue(AfterEvaluateBuildscript) {
|
||||
assertNull(executed.getAndSet(Throwable()))
|
||||
}
|
||||
|
||||
val thrownException = Exception()
|
||||
|
||||
/* Provoking an exception during project.evaluate() */
|
||||
val exceptionWasProvoked = AtomicBoolean()
|
||||
project.tasks.whenObjectAdded {
|
||||
assertFalse(exceptionWasProvoked.getAndSet(true))
|
||||
throw thrownException
|
||||
}
|
||||
runCatching { project.evaluate() }
|
||||
assertTrue(exceptionWasProvoked.get(), "Exception during '.evaluate()' was not provoked")
|
||||
|
||||
/* Assert: The 'AfterEvaluate' based stage should not have been executed */
|
||||
executed.get()?.let { throwable ->
|
||||
fail("AfterEvaluate based stage is not expected to be launched because of exception during .evaluate()", throwable)
|
||||
}
|
||||
|
||||
val exceptions = project.configured.getOrThrow().cast<Failure>().failures.withClosure<Throwable> { listOfNotNull(it.cause) }
|
||||
if (thrownException !in exceptions) fail("Expected 'thrownException' in lifecycle.finished")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - throwing an exception during buildscript evaluation - will execute coroutine waiting for finished`() {
|
||||
val thrownException = Exception()
|
||||
|
||||
val executedAfterLifecycleFinished = AtomicBoolean(false)
|
||||
project.launch {
|
||||
val exceptions = project.configured.await().cast<Failure>().failures.withClosure<Throwable> { listOfNotNull(it.cause) }
|
||||
assertFalse(executedAfterLifecycleFinished.getAndSet(true))
|
||||
if (thrownException !in exceptions) fail("Expected 'thrownException' in lifecycle.finished")
|
||||
}
|
||||
|
||||
|
||||
/* Provoking an exception during project.evaluate() */
|
||||
val exceptionWasProvoked = AtomicBoolean()
|
||||
project.tasks.whenObjectAdded {
|
||||
assertFalse(exceptionWasProvoked.getAndSet(true))
|
||||
throw thrownException
|
||||
}
|
||||
runCatching { project.evaluate() }
|
||||
assertTrue(exceptionWasProvoked.get(), "Exception during '.evaluate()' was not provoked")
|
||||
assertTrue(executedAfterLifecycleFinished.get(), "Expected coroutine waiting for '.finished' to be executed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - throwing an exception in afterEvaluate based stage - allows getOrThrow on future`() {
|
||||
val exception = IllegalStateException()
|
||||
val future = project.future { AfterEvaluateBuildscript.await(); 42 }
|
||||
val secondActionExecuted = AtomicBoolean(false)
|
||||
|
||||
project.launchInStage(AfterEvaluateBuildscript) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
project.launch secondAction@{
|
||||
project.configured.await()
|
||||
assertEquals(AfterEvaluateBuildscript, stage)
|
||||
assertEquals(42, future.getOrThrow())
|
||||
assertEquals(420, project.future { AfterEvaluateBuildscript.await(); 420 }.getOrThrow())
|
||||
assertFalse(secondActionExecuted.getAndSet(true))
|
||||
}
|
||||
|
||||
assertTrue(exception in assertFails { project.evaluate() }.withLinearClosure { it.cause })
|
||||
assertTrue(secondActionExecuted.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - throwing an exception in afterEvaluate based stage - will not execute coroutines in later stages`() {
|
||||
val exception = IllegalStateException()
|
||||
|
||||
val secondActionExecuted = AtomicBoolean(false)
|
||||
val thirdActionExecuted = AtomicBoolean(false)
|
||||
val fourthActionExecuted = AtomicBoolean(false)
|
||||
|
||||
project.launchInStage(AfterEvaluateBuildscript) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
project.launchInStage(AfterEvaluateBuildscript.nextOrThrow) secondAction@{
|
||||
assertFalse(secondActionExecuted.getAndSet(true))
|
||||
}
|
||||
|
||||
project.launch {
|
||||
project.configured.await()
|
||||
project.launchInStage(AfterEvaluateBuildscript.nextOrThrow) thirdAction@{
|
||||
assertFalse(thirdActionExecuted.getAndSet(true))
|
||||
}
|
||||
}
|
||||
|
||||
project.launch fourthAction@{
|
||||
project.configured.await()
|
||||
AfterEvaluateBuildscript.nextOrThrow.await()
|
||||
assertFalse(fourthActionExecuted.getAndSet(true))
|
||||
}
|
||||
|
||||
val failure = assertFails { project.evaluate() }
|
||||
|
||||
assertTrue(
|
||||
exception in failure.withLinearClosure { it.cause },
|
||||
"Could not find 'exception' in failure cause\n${failure.stackTraceToString()}",
|
||||
)
|
||||
|
||||
assertFalse(secondActionExecuted.get())
|
||||
assertFalse(thirdActionExecuted.get())
|
||||
assertFalse(fourthActionExecuted.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - stage property is correct`() {
|
||||
Stage.values().forEach { stage ->
|
||||
|
||||
Reference in New Issue
Block a user