[Gradle] KotlinMultiplatformPluginLifecycle: Support suspend and await
KT-34662
This commit is contained in:
committed by
Space Team
parent
cc9bab4105
commit
96edfe9048
+44
-7
@@ -10,6 +10,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Ill
|
|||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage
|
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage
|
||||||
import org.jetbrains.kotlin.gradle.utils.getOrPut
|
import org.jetbrains.kotlin.gradle.utils.getOrPut
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.coroutines.*
|
||||||
|
|
||||||
internal val Project.kotlinMultiplatformPluginLifecycle: KotlinMultiplatformPluginLifecycle
|
internal val Project.kotlinMultiplatformPluginLifecycle: KotlinMultiplatformPluginLifecycle
|
||||||
get() = extraProperties.getOrPut(KotlinMultiplatformPluginLifecycle::class.java.name) { KotlinMultiplatformPluginLifecycleImpl() }
|
get() = extraProperties.getOrPut(KotlinMultiplatformPluginLifecycle::class.java.name) { KotlinMultiplatformPluginLifecycleImpl() }
|
||||||
@@ -26,8 +27,15 @@ internal interface KotlinMultiplatformPluginLifecycle {
|
|||||||
enum class Stage {
|
enum class Stage {
|
||||||
Configure,
|
Configure,
|
||||||
AfterEvaluate,
|
AfterEvaluate,
|
||||||
|
|
||||||
|
BeforeFinaliseRefinesEdges,
|
||||||
FinaliseRefinesEdges,
|
FinaliseRefinesEdges,
|
||||||
|
AfterFinaliseRefinesEdges,
|
||||||
|
|
||||||
|
BeforeFinaliseCompilations,
|
||||||
FinaliseCompilations,
|
FinaliseCompilations,
|
||||||
|
AfterFinaliseCompilations,
|
||||||
|
|
||||||
Finalised
|
Finalised
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,14 +43,19 @@ internal interface KotlinMultiplatformPluginLifecycle {
|
|||||||
|
|
||||||
fun enqueue(stage: Stage, action: () -> Unit)
|
fun enqueue(stage: Stage, action: () -> Unit)
|
||||||
|
|
||||||
|
fun launch(block: suspend KotlinMultiplatformPluginLifecycle.() -> Unit)
|
||||||
|
|
||||||
|
suspend fun await(stage: Stage)
|
||||||
|
|
||||||
class IllegalLifecycleException(message: String) : IllegalStateException(message)
|
class IllegalLifecycleException(message: String) : IllegalStateException(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPluginLifecycle {
|
private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPluginLifecycle {
|
||||||
private val enqueuedStages = ArrayDeque(Stage.values().toList())
|
private val enqueuedStages: ArrayDeque<Stage> = ArrayDeque(Stage.values().toList())
|
||||||
private val enqueuedActions = Stage.values().associateWith { ArrayDeque<() -> Unit>() }
|
private val enqueuedActions: Map<Stage, ArrayDeque<() -> Unit>> = Stage.values().associateWith { ArrayDeque() }
|
||||||
private var configureLoopRunning = false
|
private var configureLoopRunning = AtomicBoolean(false)
|
||||||
private var isStarted = AtomicBoolean(false)
|
private var isStarted = AtomicBoolean(false)
|
||||||
|
private var isFinished = AtomicBoolean(false)
|
||||||
|
|
||||||
fun start(project: Project) {
|
fun start(project: Project) {
|
||||||
check(!isStarted.getAndSet(true)) {
|
check(!isStarted.getAndSet(true)) {
|
||||||
@@ -65,7 +78,10 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
|||||||
val action = queue.removeFirstOrNull()
|
val action = queue.removeFirstOrNull()
|
||||||
action?.invoke()
|
action?.invoke()
|
||||||
} while (action != null)
|
} while (action != null)
|
||||||
val nextStage = enqueuedStages.removeFirstOrNull() ?: return
|
val nextStage = enqueuedStages.removeFirstOrNull() ?: run {
|
||||||
|
isFinished.set(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project.afterEvaluate {
|
project.afterEvaluate {
|
||||||
executeStage(project, nextStage)
|
executeStage(project, nextStage)
|
||||||
@@ -74,8 +90,7 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
|||||||
|
|
||||||
private fun startConfigureLoopIfNecessary() {
|
private fun startConfigureLoopIfNecessary() {
|
||||||
check(stage == Stage.Configure) { "Cannot start 'configure loop' on stage '$stage'" }
|
check(stage == Stage.Configure) { "Cannot start 'configure loop' on stage '$stage'" }
|
||||||
if (configureLoopRunning) return
|
if (configureLoopRunning.getAndSet(true)) return
|
||||||
configureLoopRunning = true
|
|
||||||
try {
|
try {
|
||||||
val queue = enqueuedActions.getValue(Stage.Configure)
|
val queue = enqueuedActions.getValue(Stage.Configure)
|
||||||
do {
|
do {
|
||||||
@@ -83,7 +98,7 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
|||||||
action?.invoke()
|
action?.invoke()
|
||||||
} while (action != null)
|
} while (action != null)
|
||||||
} finally {
|
} finally {
|
||||||
configureLoopRunning = false
|
configureLoopRunning.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,4 +115,26 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
|||||||
startConfigureLoopIfNecessary()
|
startConfigureLoopIfNecessary()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(block: suspend KotlinMultiplatformPluginLifecycle.() -> Unit) {
|
||||||
|
check(isStarted.get()) { "Cannot launch when ${KotlinMultiplatformPluginLifecycle::class.simpleName} is not started" }
|
||||||
|
check(!isFinished.get()) { "Cannot launch when ${KotlinMultiplatformPluginLifecycle::class.simpleName} is already finished" }
|
||||||
|
val coroutine = block.createCoroutine(this, object : Continuation<Unit> {
|
||||||
|
override val context: CoroutineContext = EmptyCoroutineContext
|
||||||
|
override fun resumeWith(result: Result<Unit>) = result.getOrThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
enqueue(stage) {
|
||||||
|
coroutine.resume(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun await(stage: Stage) {
|
||||||
|
if (this.stage >= stage) return
|
||||||
|
suspendCoroutine<Unit> { continuation ->
|
||||||
|
enqueue(stage) {
|
||||||
|
continuation.resume(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
@@ -7,10 +7,13 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.gradle.unitTests
|
package org.jetbrains.kotlin.gradle.unitTests
|
||||||
|
|
||||||
|
import org.gradle.api.ProjectConfigurationException
|
||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle
|
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle
|
||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage.*
|
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage.*
|
||||||
import org.jetbrains.kotlin.gradle.plugin.kotlinMultiplatformPluginLifecycle
|
import org.jetbrains.kotlin.gradle.plugin.kotlinMultiplatformPluginLifecycle
|
||||||
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
|
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
|
||||||
|
import org.jetbrains.kotlin.tooling.core.linearClosure
|
||||||
|
import org.jetbrains.kotlin.tooling.core.withLinearClosure
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
@@ -210,4 +213,95 @@ class KotlinMultiplatformPluginLifecycleTest {
|
|||||||
assertEquals(1, action2Invocations.get())
|
assertEquals(1, action2Invocations.get())
|
||||||
assertEquals(1, action3Invocations.get())
|
assertEquals(1, action3Invocations.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test - launch in configure`() {
|
||||||
|
val action1Invocations = AtomicInteger(0)
|
||||||
|
val action2Invocations = AtomicInteger(0)
|
||||||
|
val action3Invocations = AtomicInteger(0)
|
||||||
|
|
||||||
|
lifecycle.launch action1@{
|
||||||
|
lifecycle.launch action2@{
|
||||||
|
assertEquals(1, action1Invocations.get())
|
||||||
|
assertEquals(0, action3Invocations.get())
|
||||||
|
assertEquals(1, action2Invocations.incrementAndGet())
|
||||||
|
}
|
||||||
|
assertEquals(0, action2Invocations.get())
|
||||||
|
assertEquals(0, action3Invocations.get())
|
||||||
|
assertEquals(1, action1Invocations.incrementAndGet())
|
||||||
|
|
||||||
|
lifecycle.launch action3@{
|
||||||
|
assertEquals(1, action1Invocations.get())
|
||||||
|
assertEquals(1, action2Invocations.get())
|
||||||
|
assertEquals(1, action3Invocations.incrementAndGet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, action1Invocations.get())
|
||||||
|
assertEquals(1, action2Invocations.get())
|
||||||
|
assertEquals(1, action3Invocations.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test - launch in configure - await Stage`() {
|
||||||
|
val executionPointA = AtomicBoolean(false)
|
||||||
|
val executionPointB = AtomicBoolean(false)
|
||||||
|
lifecycle.launch action1@{
|
||||||
|
assertFalse(executionPointA.getAndSet(true))
|
||||||
|
assertEquals(Configure, stage)
|
||||||
|
await(AfterEvaluate)
|
||||||
|
assertEquals(AfterEvaluate, stage)
|
||||||
|
assertFalse(executionPointB.getAndSet(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(executionPointA.get())
|
||||||
|
assertFalse(executionPointB.get())
|
||||||
|
project.evaluate()
|
||||||
|
assertTrue(executionPointA.get())
|
||||||
|
assertTrue(executionPointB.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test - launch - await - launch`() {
|
||||||
|
val executedInnerAction = AtomicBoolean(false)
|
||||||
|
lifecycle.launch {
|
||||||
|
await(AfterEvaluate)
|
||||||
|
launch {
|
||||||
|
assertEquals(AfterEvaluate, stage)
|
||||||
|
await(FinaliseRefinesEdges)
|
||||||
|
assertEquals(FinaliseRefinesEdges, stage)
|
||||||
|
assertFalse(executedInnerAction.getAndSet(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(executedInnerAction.get())
|
||||||
|
project.evaluate()
|
||||||
|
assertTrue(executedInnerAction.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test - launch - exception`() {
|
||||||
|
assertFailsWith<IllegalStateException> {
|
||||||
|
lifecycle.launch {
|
||||||
|
throw IllegalStateException("42")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test - launch - await - exception`() {
|
||||||
|
val testException = object : Throwable() {}
|
||||||
|
lifecycle.launch {
|
||||||
|
await(AfterEvaluate)
|
||||||
|
launch {
|
||||||
|
throw testException
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val causes = assertFailsWith<ProjectConfigurationException> {
|
||||||
|
project.evaluate()
|
||||||
|
}.withLinearClosure<Throwable> { it.cause }
|
||||||
|
|
||||||
|
assertTrue(testException in causes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user