Scripting loading tests and extension points

This commit is contained in:
Sergey Rostov
2019-11-06 13:27:02 +03:00
committed by Natalia Selezneva
parent ca71b2fe90
commit 0fb58ed94c
16 changed files with 802 additions and 249 deletions
@@ -14,13 +14,10 @@ import com.intellij.openapi.util.Ref
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.HyperlinkLabel
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ComponentListener
import javax.swing.JPanel
fun VirtualFile.removeScriptDependenciesNotificationPanel(project: Project) {
withSelectedEditor(project) { manager ->
@@ -53,6 +50,20 @@ fun VirtualFile.addScriptDependenciesNotificationPanel(
}
}
@TestOnly
fun VirtualFile.hasSuggestedScriptConfiguration(project: Project): Boolean {
return FileEditorManager.getInstance(project).getSelectedEditor(this)?.notificationPanel != null
}
@TestOnly
fun VirtualFile.applySuggestedScriptConfiguration(project: Project): Boolean {
val notificationPanel = FileEditorManager.getInstance(project).getSelectedEditor(this)?.notificationPanel
?: return false
notificationPanel.onClick.invoke()
return true
}
private fun VirtualFile.withSelectedEditor(project: Project, f: FileEditor.(FileEditorManager) -> Unit) {
ApplicationManager.getApplication().invokeLater {
if (project.isDisposed) return@invokeLater
@@ -67,7 +78,7 @@ private fun VirtualFile.withSelectedEditor(project: Project, f: FileEditor.(File
private var FileEditor.notificationPanel: NewScriptDependenciesNotificationPanel? by UserDataProperty<FileEditor, NewScriptDependenciesNotificationPanel>(Key.create("script.dependencies.panel"))
private class NewScriptDependenciesNotificationPanel(
onClick: () -> Unit,
val onClick: () -> Unit,
val compilationConfigurationResult: ScriptCompilationConfigurationWrapper,
project: Project
) : EditorNotificationPanel() {
@@ -8,6 +8,8 @@ package org.jetbrains.kotlin.idea.core.script.configuration
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
@@ -15,6 +17,7 @@ import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions.LOADER
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationMemoryCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot
@@ -27,6 +30,9 @@ import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigur
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptOutsiderFileConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.utils.BackgroundExecutor
import org.jetbrains.kotlin.idea.core.script.configuration.utils.DefaultBackgroundExecutor
import org.jetbrains.kotlin.idea.core.script.configuration.utils.TestingBackgroundExecutor
import org.jetbrains.kotlin.idea.core.script.configuration.utils.isUnitTestModeWithoutScriptLoadingNotification
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.psi.KtFile
@@ -96,21 +102,23 @@ import kotlin.script.experimental.api.ScriptDiagnostic
*/
internal class DefaultScriptConfigurationManager(project: Project) :
AbstractScriptConfigurationManager(project) {
private val backgroundExecutor = BackgroundExecutor(project, rootsIndexer)
internal val backgroundExecutor: BackgroundExecutor =
if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(rootsIndexer)
else DefaultBackgroundExecutor(project, rootsIndexer)
private val fileAttributeCache = ScriptConfigurationFileAttributeCache(project)
private val loaders: List<ScriptConfigurationLoader> = listOf(
ScriptOutsiderFileConfigurationLoader(project),
fileAttributeCache,
GradleScriptConfigurationLoader(project),
DefaultScriptConfigurationLoader(project)
)
private val loaders: List<ScriptConfigurationLoader>
get() = listOf(
ScriptOutsiderFileConfigurationLoader(project),
ScriptConfigurationFileAttributeCache(project)
) + project[LOADER] + listOf(
DefaultScriptConfigurationLoader(project)
)
private val listeners: List<ScriptChangeListener> = listOf(
GradleScriptListener(),
DefaultScriptChangeListener()
)
private val listeners: List<ScriptChangeListener>
get() = project[DefaultScriptConfigurationManagerExtensions.LISTENER] + listOf(DefaultScriptChangeListener())
private val notifier = ScriptChangesNotifier(project, updater, listeners)
@@ -123,6 +131,9 @@ internal class DefaultScriptConfigurationManager(project: Project) :
}
}
private operator fun <T> Project.get(epName: ExtensionPointName<T>): List<T> =
Extensions.getArea(this).getExtensionPoint(epName).extensionList
/**
* Will be called on [cache] miss to initiate loading of [file]'s script configuration.
*
@@ -248,7 +259,7 @@ internal class DefaultScriptConfigurationManager(project: Project) :
val autoReload = skipNotification
|| oldConfiguration == null
|| KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
|| ApplicationManager.getApplication().isUnitTestMode
|| ApplicationManager.getApplication().isUnitTestModeWithoutScriptLoadingNotification
if (autoReload) {
if (oldConfiguration != null) {
@@ -303,4 +314,15 @@ internal class DefaultScriptConfigurationManager(project: Project) :
}
}
}
}
}
object DefaultScriptConfigurationManagerExtensions {
val LOADER: ExtensionPointName<ScriptConfigurationLoader> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader")
val LISTENER: ExtensionPointName<ScriptChangeListener> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.listener")
}
val ScriptConfigurationManager.testingBackgroundExecutor
get() = (this as DefaultScriptConfigurationManager).backgroundExecutor as TestingBackgroundExecutor
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2019 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.idea.core.script.configuration.loader
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.psi.KtFile
open class FileContentsDependentConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) {
override fun getInputsStamp(virtualFile: VirtualFile, file: KtFile): CachedConfigurationInputs {
return CachedConfigurationInputs.SourceContentsStamp.get(project, virtualFile, file)
}
}
@@ -13,7 +13,7 @@ import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
*
* @see [org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager] for more details.
*/
internal interface ScriptConfigurationLoader {
interface ScriptConfigurationLoader {
fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean = false
/**
@@ -5,236 +5,8 @@
package org.jetbrains.kotlin.idea.core.script.configuration.utils
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Alarm
import com.intellij.util.containers.HashSetQueue
import org.jetbrains.kotlin.idea.core.script.debug
import java.util.*
/**
* Sequentially loads script configuration in background.
* Loading tasks scheduled by calling [ensureScheduled].
*
* Progress indicator will be shown after [PROGRESS_INDICATOR_DELAY] ms or if
* more then [PROGRESS_INDICATOR_MIN_QUEUE] tasks scheduled.
*
* States:
* silentWorker underProgressWorker
* - sleep
* - silent x
* - silent and under progress x x
* - under progress x
*/
internal class BackgroundExecutor(
val project: Project,
val rootsManager: ScriptClassRootsIndexer
) {
companion object {
const val PROGRESS_INDICATOR_DELAY = 1000
const val PROGRESS_INDICATOR_MIN_QUEUE = 3
}
private val work = Any()
private val queue: Queue<LoadTask> = HashSetQueue()
/**
* Let's fix queue size when progress bar displayed.
* Progress for rest items will be counted from zero
*/
private var currentProgressSize: Int = 0
private var currentProgressDone: Int = 0
private var silentWorker: SilentWorker? = null
private var underProgressWorker: UnderProgressWorker? = null
private val longRunningAlarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, project)
private var longRunningAlaramRequested = false
private var inTransaction: Boolean = false
private var currentFile: VirtualFile? = null
class LoadTask(val key: VirtualFile, val actions: () -> Unit) {
override fun equals(other: Any?) =
this === other || (other is LoadTask && key == other.key)
override fun hashCode() = key.hashCode()
}
@Synchronized
fun ensureScheduled(key: VirtualFile, actions: () -> Unit) {
val task = LoadTask(key, actions)
if (queue.add(task)) {
debug(task.key) { "added to update queue" }
// If the queue is longer than PROGRESS_INDICATOR_MIN_QUEUE, show progress and cancel button
if (queue.size > PROGRESS_INDICATOR_MIN_QUEUE) {
requireUnderProgressWorker()
} else {
requireSilentWorker()
if (!longRunningAlaramRequested) {
longRunningAlaramRequested = true
longRunningAlarm.addRequest(
{
longRunningAlaramRequested = false
requireUnderProgressWorker()
},
PROGRESS_INDICATOR_DELAY
)
}
}
}
}
@Synchronized
private fun requireSilentWorker() {
if (silentWorker == null && underProgressWorker == null) {
silentWorker = SilentWorker().also { it.start() }
}
}
@Synchronized
private fun requireUnderProgressWorker() {
if (queue.isEmpty() && silentWorker == null) return
silentWorker?.stopGracefully()
if (underProgressWorker == null) {
underProgressWorker = UnderProgressWorker().also { it.start() }
restartProgressBar()
updateProgress()
}
}
@Synchronized
private fun restartProgressBar() {
currentProgressSize = queue.size
currentProgressDone = 0
}
@Synchronized
fun updateProgress() {
underProgressWorker?.progressIndicator?.let {
it.text2 = currentFile?.path ?: ""
if (queue.size == 0) {
// last file
it.isIndeterminate = true
} else {
it.isIndeterminate = false
if (currentProgressDone > currentProgressSize) {
restartProgressBar()
}
it.fraction = currentProgressDone.toDouble() / currentProgressSize.toDouble()
}
}
}
@Synchronized
private fun ensureInTransaction() {
if (inTransaction) return
inTransaction = true
rootsManager.startTransaction()
}
@Synchronized
private fun endBatch() {
check(inTransaction)
rootsManager.commit()
inTransaction = false
}
private abstract inner class Worker {
private var shouldStop = false
open fun start() {
ensureInTransaction()
}
fun stopGracefully() {
shouldStop = true
}
protected open fun checkCancelled() = false
protected abstract fun close()
protected fun run() {
try {
while (true) {
// prevent parallel work in both silent and under progress
synchronized(work) {
val next = synchronized(this@BackgroundExecutor) {
if (shouldStop) return
if (checkCancelled()) {
queue.clear()
endBatch()
return
} else if (queue.isEmpty()) {
endBatch()
return
}
queue.poll()?.also {
currentFile = it.key
currentProgressDone++
updateProgress()
}
}
next?.actions?.invoke()
synchronized(work) {
currentFile = null
}
}
}
} finally {
close()
}
}
}
private inner class UnderProgressWorker : Worker() {
var progressIndicator: ProgressIndicator? = null
override fun start() {
super.start()
object : Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) {
override fun run(indicator: ProgressIndicator) {
progressIndicator = indicator
updateProgress()
run()
}
}.queue()
}
override fun checkCancelled(): Boolean = progressIndicator?.isCanceled == true
override fun close() {
synchronized(this@BackgroundExecutor) {
underProgressWorker = null
progressIndicator = null
}
}
}
private inner class SilentWorker : Worker() {
override fun start() {
super.start()
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
run()
})
}
override fun close() {
synchronized(this@BackgroundExecutor) {
silentWorker = null
}
}
}
interface BackgroundExecutor {
fun ensureScheduled(key: VirtualFile, actions: () -> Unit)
}
@@ -0,0 +1,241 @@
/*
* Copyright 2010-2019 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.idea.core.script.configuration.utils
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Alarm
import com.intellij.util.containers.HashSetQueue
import org.jetbrains.kotlin.idea.core.script.debug
import java.util.*
/**
* Sequentially loads script configuration in background.
* Loading tasks scheduled by calling [ensureScheduled].
*
* Progress indicator will be shown after [PROGRESS_INDICATOR_DELAY] ms or if
* more then [PROGRESS_INDICATOR_MIN_QUEUE] tasks scheduled.
*
* States:
* silentWorker underProgressWorker
* - sleep
* - silent x
* - silent and under progress x x
* - under progress x
*/
internal class DefaultBackgroundExecutor(
val project: Project,
val rootsManager: ScriptClassRootsIndexer
) : BackgroundExecutor {
companion object {
const val PROGRESS_INDICATOR_DELAY = 1000
const val PROGRESS_INDICATOR_MIN_QUEUE = 3
}
private val work = Any()
private val queue: Queue<LoadTask> = HashSetQueue()
/**
* Let's fix queue size when progress bar displayed.
* Progress for rest items will be counted from zero
*/
private var currentProgressSize: Int = 0
private var currentProgressDone: Int = 0
private var silentWorker: SilentWorker? = null
private var underProgressWorker: UnderProgressWorker? = null
private val longRunningAlarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, project)
private var longRunningAlaramRequested = false
private var inTransaction: Boolean = false
private var currentFile: VirtualFile? = null
class LoadTask(val key: VirtualFile, val actions: () -> Unit) {
override fun equals(other: Any?) =
this === other || (other is LoadTask && key == other.key)
override fun hashCode() = key.hashCode()
}
@Synchronized
override fun ensureScheduled(key: VirtualFile, actions: () -> Unit) {
val task = LoadTask(key, actions)
if (queue.add(task)) {
debug(task.key) { "added to update queue" }
// If the queue is longer than PROGRESS_INDICATOR_MIN_QUEUE, show progress and cancel button
if (queue.size > PROGRESS_INDICATOR_MIN_QUEUE) {
requireUnderProgressWorker()
} else {
requireSilentWorker()
if (!longRunningAlaramRequested) {
longRunningAlaramRequested = true
longRunningAlarm.addRequest(
{
longRunningAlaramRequested = false
requireUnderProgressWorker()
},
PROGRESS_INDICATOR_DELAY
)
}
}
}
}
@Synchronized
private fun requireSilentWorker() {
if (silentWorker == null && underProgressWorker == null) {
silentWorker = SilentWorker().also { it.start() }
}
}
@Synchronized
private fun requireUnderProgressWorker() {
if (queue.isEmpty() && silentWorker == null) return
silentWorker?.stopGracefully()
if (underProgressWorker == null) {
underProgressWorker = UnderProgressWorker().also { it.start() }
restartProgressBar()
updateProgress()
}
}
@Synchronized
private fun restartProgressBar() {
currentProgressSize = queue.size
currentProgressDone = 0
}
@Synchronized
fun updateProgress() {
underProgressWorker?.progressIndicator?.let {
it.text2 = currentFile?.path ?: ""
if (queue.size == 0) {
// last file
it.isIndeterminate = true
} else {
it.isIndeterminate = false
if (currentProgressDone > currentProgressSize) {
restartProgressBar()
}
it.fraction = currentProgressDone.toDouble() / currentProgressSize.toDouble()
}
}
}
@Synchronized
private fun ensureInTransaction() {
if (inTransaction) return
inTransaction = true
rootsManager.startTransaction()
}
@Synchronized
private fun endBatch() {
check(inTransaction)
rootsManager.commit()
inTransaction = false
}
private abstract inner class Worker {
private var shouldStop = false
open fun start() {
ensureInTransaction()
}
fun stopGracefully() {
shouldStop = true
}
protected open fun checkCancelled() = false
protected abstract fun close()
protected fun run() {
try {
while (true) {
// prevent parallel work in both silent and under progress
synchronized(work) {
val next = synchronized(this@DefaultBackgroundExecutor) {
if (shouldStop) return
if (checkCancelled()) {
queue.clear()
endBatch()
return
} else if (queue.isEmpty()) {
endBatch()
return
}
queue.poll()?.also {
currentFile = it.key
currentProgressDone++
updateProgress()
}
}
next?.actions?.invoke()
synchronized(work) {
currentFile = null
}
}
}
} finally {
close()
}
}
}
private inner class UnderProgressWorker : Worker() {
var progressIndicator: ProgressIndicator? = null
override fun start() {
super.start()
object : Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) {
override fun run(indicator: ProgressIndicator) {
progressIndicator = indicator
updateProgress()
run()
}
}.queue()
}
override fun checkCancelled(): Boolean = progressIndicator?.isCanceled == true
override fun close() {
synchronized(this@DefaultBackgroundExecutor) {
underProgressWorker = null
progressIndicator = null
}
}
}
private inner class SilentWorker : Worker() {
override fun start() {
super.start()
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
run()
})
}
override fun close() {
synchronized(this@DefaultBackgroundExecutor) {
silentWorker = null
}
}
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2019 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.idea.core.script.configuration.utils
import com.intellij.openapi.application.impl.LaterInvocator
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.containers.HashSetQueue
class TestingBackgroundExecutor internal constructor(
private val rootsManager: ScriptClassRootsIndexer
) : BackgroundExecutor {
val backgroundQueue = HashSetQueue<BackgroundTask>()
class BackgroundTask(val file: VirtualFile, val actions: () -> Unit) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BackgroundTask
if (file != other.file) return false
return true
}
override fun hashCode(): Int {
return file.hashCode()
}
}
@Synchronized
override fun ensureScheduled(key: VirtualFile, actions: () -> Unit) {
backgroundQueue.add(BackgroundTask(key, actions))
}
fun doAllBackgroundTaskWith(actions: () -> Unit): Boolean {
val copy = backgroundQueue.toList()
backgroundQueue.clear()
actions()
rootsManager.transaction {
copy.forEach {
it.actions()
}
}
LaterInvocator.ensureFlushRequested()
LaterInvocator.dispatchPendingFlushes()
return copy.isNotEmpty()
}
}
@@ -0,0 +1,13 @@
/*
* Copyright 2010-2019 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.idea.core.script.configuration.utils
import com.intellij.openapi.application.Application
var testScriptConfigurationNotification: Boolean = false
val Application.isUnitTestModeWithoutScriptLoadingNotification: Boolean
get() = isUnitTestMode && !testScriptConfigurationNotification
@@ -59,6 +59,14 @@
<extensionPoint qualifiedName="org.jetbrains.kotlin.scriptDiagnosticFixProvider"
interface="org.jetbrains.kotlin.idea.script.ScriptDiagnosticFixProvider"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.loader"
interface="org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader"
area="IDEA_PROJECT"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.listener"
interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener"
area="IDEA_PROJECT"/>
</extensionPoints>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
@@ -67,6 +67,14 @@
<extensionPoint qualifiedName="org.jetbrains.kotlin.scriptDiagnosticFixProvider"
interface="org.jetbrains.kotlin.idea.script.ScriptDiagnosticFixProvider"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.loader"
interface="org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader"
area="IDEA_PROJECT"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.listener"
interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener"
area="IDEA_PROJECT"/>
</extensionPoints>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
@@ -59,6 +59,14 @@
<extensionPoint qualifiedName="org.jetbrains.kotlin.scriptDiagnosticFixProvider"
interface="org.jetbrains.kotlin.idea.script.ScriptDiagnosticFixProvider"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.loader"
interface="org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader"
area="IDEA_PROJECT"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.listener"
interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener"
area="IDEA_PROJECT"/>
</extensionPoints>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
@@ -0,0 +1 @@
initial
@@ -0,0 +1,42 @@
package custom.scriptDefinition
import kotlin.script.dependencies.*
import kotlin.script.experimental.dependencies.*
import kotlin.script.templates.*
import java.io.File
import kotlin.script.experimental.location.*
class TestDependenciesResolver : AsyncDependenciesResolver {
suspend override fun resolveAsync(scriptContents: ScriptContents, environment: Environment): DependenciesResolver.ResolveResult {
val text = scriptContents.text as String
val result = when {
text.startsWith("#BAD:") -> DependenciesResolver.ResolveResult.Failure(
reports = listOf(ScriptReport(text))
)
else -> DependenciesResolver.ResolveResult.Success(
dependencies = ScriptDependencies(
classpath = listOf(environment["template-classes"] as File),
imports = listOf("x_" + text.replace(Regex("#IGNORE_IN_CONFIGURATION"), ""))
),
reports = listOf(ScriptReport(text))
)
}
javaClass.classLoader
.loadClass("org.jetbrains.kotlin.idea.script.ScriptConfigurationLoadingTest")
.methods.single { it.name == "loadingScriptConfigurationCallback" }
.invoke(null)
return result
}
}
@ScriptExpectedLocations([ScriptExpectedLocation.Everywhere])
@ScriptTemplateDefinition(TestDependenciesResolver::class, scriptFilePattern = "script.kts")
class Template : Base()
open class Base {
val i = 3
val str = ""
}
@@ -0,0 +1,145 @@
/*
* Copyright 2010-2019 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.idea.script
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.IdeScriptReportSink
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.applySuggestedScriptConfiguration
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions
import org.jetbrains.kotlin.idea.core.script.configuration.loader.FileContentsDependentConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.testingBackgroundExecutor
import org.jetbrains.kotlin.idea.core.script.configuration.utils.testScriptConfigurationNotification
import org.jetbrains.kotlin.idea.core.script.hasSuggestedScriptConfiguration
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtFile
abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurationTest() {
val ktFile: KtFile get() = myFile as KtFile
val virtualFile get() = myFile.virtualFile
lateinit var scriptConfigurationManager: ScriptConfigurationManager
companion object {
private var occurredLoadings = 0
private var currentLoadingScriptConfigurationCallback: (() -> Unit)? = null
@JvmStatic
@Suppress("unused")
fun loadingScriptConfigurationCallback() {
// this method is called from testData/script/definition/loading/async/template/template.kt
currentLoadingScriptConfigurationCallback?.invoke()
occurredLoadings++
}
}
override fun setUp() {
super.setUp()
testScriptConfigurationNotification = true
addExtensionPointInTest(
DefaultScriptConfigurationManagerExtensions.LOADER,
project,
FileContentsDependentConfigurationLoader(project),
testRootDisposable
)
configureScriptFile("idea/testData/script/definition/loading/async/")
scriptConfigurationManager = ServiceManager.getService(project, ScriptConfigurationManager::class.java)
}
override fun tearDown() {
super.tearDown()
testScriptConfigurationNotification = false
occurredLoadings = 0
currentLoadingScriptConfigurationCallback = null
}
override fun loadScriptConfigurationSynchronously(script: VirtualFile) {
// do nothings
}
protected fun assertAndDoAllBackgroundTasks() {
assertDoAllBackgroundTaskAndDoWhileLoading { }
}
protected fun assertDoAllBackgroundTaskAndDoWhileLoading(actions: () -> Unit) {
assertTrue(doAllBackgroundTasksWith(actions))
}
protected fun doAllBackgroundTasksWith(actions: () -> Unit): Boolean {
return scriptConfigurationManager.testingBackgroundExecutor.doAllBackgroundTaskWith {
currentLoadingScriptConfigurationCallback = {
actions()
currentLoadingScriptConfigurationCallback = null
}
}
}
protected fun assertAppliedConfiguration(contents: String) {
val secondConfiguration = scriptConfigurationManager.getConfiguration(ktFile)!!
assertEquals(
contents,
secondConfiguration.defaultImports.single().let {
check(it.startsWith("x_"))
it.removePrefix("x_")
}
)
}
protected fun makeChanges(contents: String) {
changeContents(contents)
scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(ktFile)
}
protected fun changeContents(contents: String) {
runWriteAction {
val fileDocumentManager = FileDocumentManager.getInstance()
fileDocumentManager.reloadFiles(virtualFile)
val document = fileDocumentManager.getDocument(virtualFile)!!
document.setText(contents)
fileDocumentManager.saveDocument(document)
psiManager.reloadFromDisk(myFile)
myFile = psiManager.findFile(virtualFile)
}
}
protected fun assertReports(expected: String) {
val actual = IdeScriptReportSink.getReports(virtualFile).single().message
assertEquals(expected, actual)
}
protected fun assertSuggestedConfiguration() {
assertTrue(virtualFile.hasSuggestedScriptConfiguration(project))
}
protected fun assertAndApplySuggestedConfiguration() {
assertTrue(virtualFile.applySuggestedScriptConfiguration(project))
}
protected fun assertNoSuggestedConfiguration() {
assertFalse(virtualFile.applySuggestedScriptConfiguration(project))
}
protected fun assertNoLoading() {
assertEquals(0, occurredLoadings)
occurredLoadings = 0
}
protected fun assertSingleLoading() {
assertEquals(1, occurredLoadings)
occurredLoadings = 0
}
protected fun assertAndLoadInitialConfiguration() {
assertNull(scriptConfigurationManager.getConfiguration(ktFile))
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAppliedConfiguration("initial")
}
}
@@ -265,6 +265,10 @@ abstract class AbstractScriptConfigurationTest : KotlinCompletionTestCase() {
if (script == null) error("Test file with script couldn't be found in test project")
configureByExistingFile(script)
loadScriptConfigurationSynchronously(script)
}
protected open fun loadScriptConfigurationSynchronously(script: VirtualFile) {
updateScriptDependenciesSynchronously(myFile, project)
VfsUtil.markDirtyAndRefresh(false, true, true, project.baseDir)
@@ -0,0 +1,205 @@
/*
* Copyright 2010-2019 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.idea.script
class ScriptConfigurationLoadingTest : AbstractScriptConfigurationLoadingTest() {
fun testSimple() {
assertAndLoadInitialConfiguration()
makeChanges("A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("A")
}
fun testConcurrentLoadingWhileInQueue() {
assertAndLoadInitialConfiguration()
makeChanges("A")
makeChanges("B")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("B")
}
fun testConcurrentLoadingWhileInQueueABA() {
assertAndLoadInitialConfiguration()
makeChanges("A")
makeChanges("B")
makeChanges("A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("A")
}
fun testConcurrentLoadingWhileInQueueABA2() {
assertAndLoadInitialConfiguration()
makeChanges("A")
makeChanges("initial")
assertAndDoAllBackgroundTasks()
assertNoLoading()
assertNoSuggestedConfiguration()
}
fun testConcurrentLoadingWhileAnotherLoadInProgress() {
assertAndLoadInitialConfiguration()
makeChanges("A")
assertDoAllBackgroundTaskAndDoWhileLoading {
makeChanges("B")
}
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("B")
}
fun testConcurrentLoadingWhileAnotherLoadInProgressABA() {
assertAndLoadInitialConfiguration()
makeChanges("A")
assertDoAllBackgroundTaskAndDoWhileLoading {
makeChanges("B")
makeChanges("A")
}
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("A")
assertAndDoAllBackgroundTasks()
assertNoLoading()
assertNoSuggestedConfiguration()
assertAppliedConfiguration("A")
}
fun mutedTestConcurrentLoadingWhileAnotherLoadInProgressABA2() { // todo: fix and unmute
assertAndLoadInitialConfiguration()
makeChanges("A")
assertDoAllBackgroundTaskAndDoWhileLoading {
makeChanges("initial")
}
assertSingleLoading()
assertSuggestedConfiguration()
assertAndDoAllBackgroundTasks()
assertNoLoading()
assertNoSuggestedConfiguration()
}
fun testConcurrentLoadingWhileNotApplied() {
assertAndLoadInitialConfiguration()
makeChanges("A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAppliedConfiguration("initial")
// we have loaded and not applied configuration for A
// let's invalidate file again and check that loading will occur
makeChanges("B")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("B")
}
fun testConcurrentLoadingWhileNotAppliedABA() {
assertAndLoadInitialConfiguration()
makeChanges("A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAppliedConfiguration("initial")
// we have loaded and not applied configuration for A
// let's invalidate file and change it back
// and check that loading will NOT occur
makeChanges("B")
makeChanges("A")
assertAndDoAllBackgroundTasks()
assertNoLoading()
assertAppliedConfiguration("initial")
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("A")
}
fun testConcurrentLoadingWhileNotAppliedABA2() {
assertAndLoadInitialConfiguration()
makeChanges("A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertSuggestedConfiguration()
makeChanges("initial")
assertAndDoAllBackgroundTasks()
assertNoLoading()
assertNoSuggestedConfiguration()
}
fun testLoadingForUsagesSearch() {
assertAndLoadInitialConfiguration()
changeContents("A")
assertNoLoading()
assertNoSuggestedConfiguration()
assertFalse(doAllBackgroundTasksWith {})
scriptConfigurationManager.updater.ensureConfigurationUpToDate(listOf(ktFile))
}
fun testReportsOnAutoApply() {
assertAndLoadInitialConfiguration()
assertReports("initial")
}
fun testReportsOnFailedConfiguration() {
assertAndLoadInitialConfiguration()
makeChanges("#BAD:A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertNoSuggestedConfiguration()
assertReports("#BAD:A")
}
fun testReportsOnSameConfiguration() {
assertAndLoadInitialConfiguration()
makeChanges("initial#IGNORE_IN_CONFIGURATION")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertNoSuggestedConfiguration()
assertAppliedConfiguration("initial")
assertReports("initial#IGNORE_IN_CONFIGURATION")
}
fun testReportsAfterApply() {
assertAndLoadInitialConfiguration()
makeChanges("A")
assertAndDoAllBackgroundTasks()
assertSingleLoading()
assertAndApplySuggestedConfiguration()
assertAppliedConfiguration("A")
assertReports("A")
}
}