IJPL-158: Deprecate DumbService.setDumbMode and suggest alternative API (runInDumbMode)

New API is currently `@TestOnly`. The plan is that `DumbService.runInDumbMode` will become a recommended production API.
`{run/compute}InDumbModeSynchronously` will remain as `@TestOnly`, and (likely) will be moved to DumbModeTestUtils later.

GitOrigin-RevId: c9f35ed8162b5ea53caa0abb859193d2918376fe
This commit is contained in:
Andrei.Kuznetsov
2023-08-18 18:32:37 +02:00
committed by intellij-monorepo-bot
parent dd39f1be13
commit a0cf94f7e6
4 changed files with 138 additions and 14 deletions

View File

@@ -276,7 +276,7 @@ public class ClsPsiTest extends LightIdeaTestCase {
assertEquals("o", parameters[1].getName());
};
DumbServiceImpl.getInstance(getProject()).runInDumbModeSynchronously(checkNames);
DumbServiceImpl.getInstance(getProject()).runInDumbModeSynchronously(checkNames::run);
checkNames.run();
}

View File

@@ -5,10 +5,7 @@ import com.intellij.icons.AllIcons
import com.intellij.ide.IdeBundle
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.AccessToken
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.application.*
import com.intellij.openapi.application.impl.ApplicationImpl
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
@@ -24,19 +21,23 @@ import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.ModificationTracker
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.wm.WindowManager
import com.intellij.openapi.wm.ex.StatusBarEx
import com.intellij.serviceContainer.NonInjectable
import com.intellij.util.ConcurrencyUtil
import com.intellij.util.SystemProperties
import com.intellij.util.ThrowableRunnable
import com.intellij.util.application
import com.intellij.util.indexing.IndexingBundle
import com.intellij.util.ui.DeprecationStripePanel
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Async
import org.jetbrains.annotations.TestOnly
@@ -173,7 +174,7 @@ open class DumbServiceImpl @NonInjectable @VisibleForTesting constructor(private
}
return myState.value.isDumb
}
@TestOnly set(dumb) {
@TestOnly @Deprecated("Use runInDumbMode instead (or {run/compute}InDumbModeSynchronously)") set(dumb) {
ApplicationManager.getApplication().assertIsDispatchThread()
if (dumb) {
myState.update { it.makeDumb() }
@@ -184,14 +185,70 @@ open class DumbServiceImpl @NonInjectable @VisibleForTesting constructor(private
}
}
/**
* This method starts dumb mode (if not started), then runs the runnable, then ends dumb mode (if no other dumb tasks are running).
*
* This method can be invoked from any thread. It will switch to EDT to start/stop dumb mode. Runnable itself will be invoked from
* method's invocation thread.
*/
@TestOnly
fun runInDumbModeSynchronously(runnable: Runnable) {
isDumb = true
try {
fun runInDumbModeSynchronously(runnable: ThrowableRunnable<in Throwable>) {
computeInDumbModeSynchronously {
runnable.run()
}
}
/**
* This method starts dumb mode (if not started), then runs the computable, then ends dumb mode (if no other dumb tasks are running).
*
* This method can be invoked from any thread. It will switch to EDT to start/stop dumb mode. Runnable itself will be invoked from
* method's invocation thread.
*/
@TestOnly
fun <T> computeInDumbModeSynchronously(computable: ThrowableComputable<T, in Throwable>): T {
application.invokeAndWait {
isDumb = true
}
try {
return computable.compute()
}
finally {
isDumb = false
application.invokeAndWait {
isDumb = false
}
}
}
/**
* This method starts dumb mode (if not started), then runs suspend lambda, then ends dumb mode (if no other dumb tasks are running).
*
* This method can be invoked from any thread. It will switch to EDT to start/stop dumb mode. Runnable itself will be invoked from
* method's invocation thread.
*/
@TestOnly
suspend fun <T> runInDumbMode(block: suspend () -> T): T {
executeImmediatelyOrScheduleOnEDT {
isDumb = true
}
try {
return block()
}
finally {
executeImmediatelyOrScheduleOnEDT {
isDumb = false
}
}
}
private suspend fun executeImmediatelyOrScheduleOnEDT(block: suspend () -> Unit) {
//Dispatchers.EDT, Dispatchers.Main, and even Dispatchers.Main.immediate may never execute if already on EDT. See SwiftAttributeCompletionTest
if (application.isDispatchThread) {
block()
}
else {
withContext(Dispatchers.EDT) {
block()
}
}
}

View File

@@ -29,10 +29,7 @@ import com.intellij.util.indexing.diagnostic.ProjectDumbIndexingHistoryImpl
import com.intellij.util.indexing.diagnostic.ProjectIndexingHistoryImpl
import com.intellij.util.indexing.diagnostic.ScanningType
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import org.junit.*
import org.junit.Assert.*
import org.junit.runner.RunWith
@@ -550,6 +547,13 @@ class DumbServiceImplTest {
assertNull(exception.get())
}
@Test
fun `test startEternalDumbModeTask and endEternalDumbModeTaskAndWaitForSmartMode do not hang when invoked from EDT`() {
runInEdtAndWait {
val dumbTask = DumbModeTestUtils.startEternalDumbModeTask(project)
DumbModeTestUtils.endEternalDumbModeTaskAndWaitForSmartMode(project, dumbTask)
}
}
private fun waitForSmartModeFiveSecondsOrThrow() {
if (!dumbService.waitForSmartMode(5_000)) {

View File

@@ -0,0 +1,63 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.testFramework
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.DumbServiceImpl
import com.intellij.openapi.project.Project
import com.intellij.util.application
import kotlinx.coroutines.*
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.jupiter.api.fail
import kotlin.time.Duration
import kotlin.time.DurationUnit.SECONDS
import kotlin.time.toDuration
object DumbModeTestUtils {
/**
* "Eternal" means that test framework will not terminate the task. Please stop dumb mode in the end of test. Use wisely.
*
* Always invoke [Job.cancel] or [endEternalDumbModeTaskAndWaitForSmartMode] in test's `tearDown` in `finally` block.
*/
@JvmStatic
fun startEternalDumbModeTask(project: Project): Job {
var dumbModeJob: Job? = null
@Suppress("RAW_RUN_BLOCKING")
runBlocking {
val dumbModeStarted = CompletableDeferred<Boolean>()
withTimeout(10.toDuration(SECONDS)) {
dumbModeJob = CoroutineScope(Dispatchers.Main.immediate + Job()).launch {
DumbServiceImpl.getInstance(project).runInDumbMode {
dumbModeStarted.complete(true)
delay(Duration.INFINITE)
}
}
dumbModeStarted.await()
}
}
assertTrue("Dumb mode didn't start", DumbService.isDumb(project))
return dumbModeJob ?: fail("Could not start dumb mode task")
}
@JvmStatic
fun endEternalDumbModeTaskAndWaitForSmartMode(project: Project, job: Job) {
job.cancel()
waitForSmartMode(project)
}
/**
* Waits for smart mode at most 10 seconds and throws AssertionError if smart mode didn't start.
*
* Can be invoked from any thread (even from EDT).
*/
@JvmStatic
fun waitForSmartMode(project: Project) {
if (application.isDispatchThread) {
PlatformTestUtil.waitWithEventsDispatching("Dumb mode didn't finish", { !DumbService.isDumb(project) }, 10)
}
else {
DumbServiceImpl.getInstance(project).waitForSmartMode(10_000)
}
assertFalse("Dumb mode didn't finish", DumbService.isDumb(project))
}
}