[AA] Improve modification event test directives

- Rewrite `TestModule.publishModificationEventByDirective`: we can get
  the `KtModule` to publish an event for via the test module. The
  directive can now be made optional, which is necessary when we want to
  specify it in potentially multiple modules.
- Introduce the `WILDCARD_MODIFICATION_EVENT` directive, which allows
  specifying THAT some modification event should be raised for a module,
  but not WHICH one. This allows generating multiple tests which raise
  different modification events over the same test data.
- Add various convenience functions for publishing wildcard modification
  events. `publishWildcardModificationEventsByDirective` for the test
  module structure is quite opinionated, but takes a lot of work from
  test implementations.
- Because `ModificationEventDirectives` are part of the analysis test
  framework, we register them as a general Analysis API test directive.

^KT-56288
This commit is contained in:
Marco Pennekamp
2024-01-24 22:44:31 +01:00
committed by Space Team
parent 3044941201
commit 56fd4b562b
3 changed files with 97 additions and 25 deletions
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.analysis.test.framework.base
import com.intellij.openapi.Disposable
import org.jetbrains.kotlin.analysis.test.framework.AnalysisApiTestDirectives
import org.jetbrains.kotlin.analysis.test.framework.directives.ModificationEventDirectives
import org.jetbrains.kotlin.analysis.test.framework.project.structure.AnalysisApiKtModuleProvider
import org.jetbrains.kotlin.analysis.test.framework.project.structure.AnalysisApiKtModuleProviderImpl
import org.jetbrains.kotlin.analysis.test.framework.project.structure.AnalysisApiTestCodeFragmentDirectives
@@ -33,6 +34,5 @@ fun TestConfigurationBuilder.registerAnalysisApiBaseTestServices(
useCustomCompilerConfigurationProvider(::AnalysisApiTestCompilerConfiguratorProvider)
usePreAnalysisHandlers(::ProjectStructureInitialisationPreAnalysisHandler.bind(configurator))
useDirectives(AnalysisApiTestDirectives)
useDirectives(AnalysisApiTestCodeFragmentDirectives)
useDirectives(AnalysisApiTestDirectives, AnalysisApiTestCodeFragmentDirectives, ModificationEventDirectives)
}
@@ -10,14 +10,31 @@ import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.providers.analysisMessageBus
import org.jetbrains.kotlin.analysis.providers.topics.KotlinModuleStateModificationKind
import org.jetbrains.kotlin.analysis.providers.topics.KotlinTopics
import org.jetbrains.kotlin.analysis.test.framework.project.structure.getKtModule
import org.jetbrains.kotlin.analysis.test.framework.services.environmentManager
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.directives.model.singleValue
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestModuleStructure
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.testFramework.runWriteAction
/**
* Modification event directives allow publishing modification events in a specific module or globally.
*
* These directives are available by default in Analysis API tests and do not need to be registered separately. They are not applied
* automatically, however, and tests interested in publishing modification events must call one of the following publishing functions for
* the appropriate [TestModule]: [publishModificationEventByDirective] or [publishWildcardModificationEventByDirectiveIfPresent].
*/
object ModificationEventDirectives : SimpleDirectivesContainer() {
val MODIFICATION_EVENT by enumDirective<ModificationEventKind>(
description = "The modification event to be raised by the test.",
description = "The modification event to be raised for the module or globally.",
)
val WILDCARD_MODIFICATION_EVENT by directive(
description = """
Specifies that a modification event should be raised for the module or globally, but the test has to specify the modification
event kind itself. This allows generating multiple tests which raise different modification events over the same test data.
""".trimIndent()
)
}
@@ -29,29 +46,93 @@ enum class ModificationEventKind {
GLOBAL_SOURCE_OUT_OF_BLOCK_MODIFICATION,
}
val ModificationEventKind.isModuleLevel: Boolean
get() = this == ModificationEventKind.MODULE_STATE_MODIFICATION || this == ModificationEventKind.MODULE_OUT_OF_BLOCK_MODIFICATION
val ModificationEventKind.isGlobalLevel: Boolean
get() = !isModuleLevel
/**
* Publishes a modification event as defined in [KotlinTopics][org.jetbrains.kotlin.analysis.providers.topics.KotlinTopics] based on the
* [ModificationEventDirectives.MODIFICATION_EVENT] directive in a write action.
* [ModificationEventDirectives.MODIFICATION_EVENT] directive present in the test module in a write action.
*
* The function expects exactly one `MODIFICATION_EVENT` directive to be present in the registered directives.
* Module-level modification events will be published for the [TestModule]'s [KtModule].
*
* @param module The module for which module-level modification events should be published, or `null` if only global modification events
* should be published. (Specifying a module-level modification event kind then raises an error.)
* The function expects exactly one `MODIFICATION_EVENT` directive to be present, unless [isOptional] is `true`.
*/
fun TestModule.publishModificationEventByDirective(project: Project, module: KtModule?) {
val modificationEventKind = directives.singleValue(ModificationEventDirectives.MODIFICATION_EVENT)
publishModificationEventByKind(modificationEventKind, project, module)
fun TestModule.publishModificationEventByDirective(testServices: TestServices, isOptional: Boolean = false) {
val modificationEventKinds = directives[ModificationEventDirectives.MODIFICATION_EVENT]
val modificationEventKind = when (modificationEventKinds.size) {
0 -> {
if (isOptional) return
error("Expected a `${ModificationEventDirectives.MODIFICATION_EVENT.name}` to be present in the test module `$this`.")
}
1 -> modificationEventKinds.single()
else -> error("The test module `$this` must not specify multiple modification events.")
}
publishModificationEvent(modificationEventKind, getKtModule(testServices))
}
private fun publishModificationEventByKind(modificationEventKind: ModificationEventKind, project: Project, module: KtModule?) {
/**
* If the given test module contains a [ModificationEventDirectives.WILDCARD_MODIFICATION_EVENT] directive, publishes a modification event
* as defined in [KotlinTopics][org.jetbrains.kotlin.analysis.providers.topics.KotlinTopics] based on the given [modificationEventKind] in
* a write action.
*
* Module-level modification events will be published for the [TestModule]'s [KtModule].
*/
fun TestModule.publishWildcardModificationEventByDirectiveIfPresent(
modificationEventKind: ModificationEventKind,
testServices: TestServices,
) {
if (ModificationEventDirectives.WILDCARD_MODIFICATION_EVENT !in directives) {
return
}
publishModificationEvent(modificationEventKind, getKtModule(testServices))
}
/**
* For each test module that contains a [ModificationEventDirectives.WILDCARD_MODIFICATION_EVENT] directive, publishes a modification event
* as defined in [KotlinTopics][org.jetbrains.kotlin.analysis.providers.topics.KotlinTopics] based on the given [modificationEventKind] in
* a write action.
*
* Global-level modification events will only be published *once*, regardless of how many `WILDCARD_MODIFICATION_EVENT` directives the test
* modules contain, as long as at least one test module contains it (to support test cases which don't want to publish any modification
* events).
*/
fun TestModuleStructure.publishWildcardModificationEventsByDirective(
modificationEventKind: ModificationEventKind,
testServices: TestServices,
) {
if (modificationEventKind.isModuleLevel) {
modules.forEach { testModule ->
testModule.publishWildcardModificationEventByDirectiveIfPresent(modificationEventKind, testServices)
}
} else {
if (!allDirectives.contains(ModificationEventDirectives.WILDCARD_MODIFICATION_EVENT)) {
return
}
publishGlobalModificationEvent(modificationEventKind, testServices.environmentManager.getProject())
}
}
fun publishModificationEvent(modificationEventKind: ModificationEventKind, ktModule: KtModule) {
publishModificationEventByKind(modificationEventKind, ktModule.project, ktModule)
}
fun publishGlobalModificationEvent(modificationEventKind: ModificationEventKind, project: Project) {
require(modificationEventKind.isGlobalLevel)
publishModificationEventByKind(modificationEventKind, project, ktModule = null)
}
private fun publishModificationEventByKind(modificationEventKind: ModificationEventKind, project: Project, ktModule: KtModule?) {
runWriteAction {
when (modificationEventKind) {
ModificationEventKind.MODULE_STATE_MODIFICATION -> {
project.analysisMessageBus
.syncPublisher(KotlinTopics.MODULE_STATE_MODIFICATION)
.onModification(
module ?: errorModuleRequired(),
ktModule ?: errorModuleRequired(),
KotlinModuleStateModificationKind.UPDATE,
)
}
@@ -59,7 +140,7 @@ private fun publishModificationEventByKind(modificationEventKind: ModificationEv
ModificationEventKind.MODULE_OUT_OF_BLOCK_MODIFICATION -> {
project.analysisMessageBus
.syncPublisher(KotlinTopics.MODULE_OUT_OF_BLOCK_MODIFICATION)
.onModification(module ?: errorModuleRequired())
.onModification(ktModule ?: errorModuleRequired())
}
ModificationEventKind.GLOBAL_MODULE_STATE_MODIFICATION -> {