[LL FIR] Test resolve extension disposal after modification events

- These tests simply ensure that a resolve extension is disposed after
  any kind of modification event is raised. It's not meant to test
  complex scenarios, but rather to detect when resolve extension
  disposal isn't invoked *at all*.
- I tried implementing a similar test for resolve extension disposal
  after soft reference garbage collection, but the only way to force the
  GC to collect soft references is to allocate memory until an
  out-of-memory error occurs. That is bad for the test infrastructure,
  because it might allocate A LOT of memory (depending on the max heap),
  which is problematic for running tests locally. Also, our Kotlin tests
  stop on an OOM error altogether (so additional configuration would be
  required) and the heap is dumped into a 20GB file (on my machine),
  which is again problematic for local test runs.

^KT-61222
This commit is contained in:
Marco Pennekamp
2023-08-21 17:45:47 +02:00
committed by Space Team
parent 040b7472a9
commit 5819f4eaa2
8 changed files with 168 additions and 0 deletions
@@ -0,0 +1 @@
// MODIFICATION_EVENT: GLOBAL_MODULE_STATE_MODIFICATION
@@ -0,0 +1 @@
// MODIFICATION_EVENT: GLOBAL_SOURCE_MODULE_STATE_MODIFICATION
@@ -0,0 +1 @@
// MODIFICATION_EVENT: GLOBAL_SOURCE_OUT_OF_BLOCK_MODIFICATION
@@ -0,0 +1 @@
// MODIFICATION_EVENT: MODULE_OUT_OF_BLOCK_MODIFICATION
@@ -0,0 +1 @@
// MODIFICATION_EVENT: MODULE_STATE_MODIFICATION
@@ -0,0 +1,102 @@
/*
* Copyright 2010-2023 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.analysis.low.level.api.fir.resolve.extensions
import com.intellij.mock.MockApplication
import com.intellij.mock.MockProject
import com.intellij.openapi.Disposable
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtension
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionFile
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider
import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.LLFirSessionCache
import org.jetbrains.kotlin.analysis.low.level.api.fir.test.configurators.AnalysisApiFirSourceTestConfigurator
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.ProjectStructureProvider
import org.jetbrains.kotlin.analysis.test.framework.base.AbstractAnalysisApiBasedTest
import org.jetbrains.kotlin.analysis.test.framework.directives.ModificationEventDirectives
import org.jetbrains.kotlin.analysis.test.framework.directives.publishModificationEventByDirective
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestServiceRegistrar
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.assertions
/**
* A simple test which detects when resolve extension disposal after modification events/session invalidation doesn't work *at all*.
*/
abstract class AbstractResolveExtensionDisposalAfterModificationEventTest : AbstractAnalysisApiBasedTest() {
override fun doTestByMainFile(
mainFile: KtFile,
mainModule: TestModule,
testServices: TestServices,
) {
val project = mainFile.project
val module = ProjectStructureProvider.getModule(project, mainFile, contextualModule = null)
val session = LLFirSessionCache.getInstance(project).getSession(module)
val resolveExtension = session.llResolveExtensionTool!!.extensions.single() as KtResolveExtensionWithDisposalTracker
testServices.assertions.assertFalse(resolveExtension.isDisposed) {
"The resolve extension should not be disposed before the modification event is published."
}
mainModule.publishModificationEventByDirective(project, module)
testServices.assertions.assertTrue(resolveExtension.isDisposed) {
"The resolve extension should be disposed after the modification event has been published."
}
}
override val configurator: AnalysisApiTestConfigurator
get() = ResolveExtensionDisposalTestConfigurator
}
class KtResolveExtensionWithDisposalTracker() : KtResolveExtension() {
override fun getKtFiles(): List<KtResolveExtensionFile> = emptyList()
override fun getContainedPackages(): Set<FqName> = emptySet()
override fun getShadowedScope(): GlobalSearchScope = GlobalSearchScope.EMPTY_SCOPE
var isDisposed: Boolean = false
override fun dispose() {
isDisposed = true
}
}
class KtResolveExtensionWithDisposalTrackerProvider() : KtResolveExtensionProvider() {
override fun provideExtensionsFor(module: KtModule): List<KtResolveExtension> = listOf(KtResolveExtensionWithDisposalTracker())
}
object ResolveExtensionDisposalTestConfigurator : AnalysisApiFirSourceTestConfigurator(analyseInDependentSession = false) {
override fun configureTest(builder: TestConfigurationBuilder, disposable: Disposable) {
super.configureTest(builder, disposable)
builder.useDirectives(ModificationEventDirectives)
}
override val serviceRegistrars: List<AnalysisApiTestServiceRegistrar>
get() = buildList {
addAll(super.serviceRegistrars)
add(ResolveExtensionDisposalTestServiceRegistrar)
}
}
object ResolveExtensionDisposalTestServiceRegistrar : AnalysisApiTestServiceRegistrar() {
override fun registerApplicationServices(application: MockApplication, testServices: TestServices) {}
override fun registerProjectExtensionPoints(project: MockProject, testServices: TestServices) {}
override fun registerProjectModelServices(project: MockProject, testServices: TestServices) {}
override fun registerProjectServices(
project: MockProject,
testServices: TestServices,
) {
val extensionPoint = project.extensionArea.getExtensionPoint(KtResolveExtensionProvider.EP_NAME)
extensionPoint.registerExtension(KtResolveExtensionWithDisposalTrackerProvider(), project)
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2024 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.analysis.low.level.api.fir.resolve.extensions;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("analysis/low-level-api-fir/testData/resolveExtensionDisposal")
@TestDataPath("$PROJECT_ROOT")
public class ResolveExtensionDisposalAfterModificationEventTestGenerated extends AbstractResolveExtensionDisposalAfterModificationEventTest {
@Test
@TestMetadata("afterGlobalModuleStateModification.kt")
public void testAfterGlobalModuleStateModification() {
runTest("analysis/low-level-api-fir/testData/resolveExtensionDisposal/afterGlobalModuleStateModification.kt");
}
@Test
@TestMetadata("afterGlobalSourceModuleStateModification.kt")
public void testAfterGlobalSourceModuleStateModification() {
runTest("analysis/low-level-api-fir/testData/resolveExtensionDisposal/afterGlobalSourceModuleStateModification.kt");
}
@Test
@TestMetadata("afterGlobalSourceOutOfBlockModification.kt")
public void testAfterGlobalSourceOutOfBlockModification() {
runTest("analysis/low-level-api-fir/testData/resolveExtensionDisposal/afterGlobalSourceOutOfBlockModification.kt");
}
@Test
@TestMetadata("afterModuleOutOfBlockModification.kt")
public void testAfterModuleOutOfBlockModification() {
runTest("analysis/low-level-api-fir/testData/resolveExtensionDisposal/afterModuleOutOfBlockModification.kt");
}
@Test
@TestMetadata("afterModuleStateModification.kt")
public void testAfterModuleStateModification() {
runTest("analysis/low-level-api-fir/testData/resolveExtensionDisposal/afterModuleStateModification.kt");
}
@Test
public void testAllFilesPresentInResolveExtensionDisposal() {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/low-level-api-fir/testData/resolveExtensionDisposal"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
}
@@ -25,6 +25,7 @@ import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.AbstractScriptLaz
import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.AbstractScriptWholeFileResolvePhaseTest
import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.AbstractSourceLazyDeclarationResolveScopeBasedTest
import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.AbstractSourceWholeFileResolvePhaseTest
import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.extensions.AbstractResolveExtensionDisposalAfterModificationEventTest
import org.jetbrains.kotlin.generators.TestGroup
import org.jetbrains.kotlin.generators.TestGroupSuite
import org.jetbrains.kotlin.generators.util.TestGeneratorUtil
@@ -281,6 +282,10 @@ internal fun TestGroupSuite.generateFirLowLevelApiTests() {
testClass<AbstractContextCollectorScriptTest> {
model("contextCollector", pattern = TestGeneratorUtil.KTS)
}
testClass<AbstractResolveExtensionDisposalAfterModificationEventTest> {
model("resolveExtensionDisposal")
}
}
testGroup("analysis/low-level-api-fir/tests", "analysis/analysis-api/testData") {