FIR IDE: introduce memory leak checking in symbols test
This commit is contained in:
@@ -89,10 +89,7 @@ import org.jetbrains.kotlin.idea.folding.AbstractKotlinFoldingTest
|
|||||||
import org.jetbrains.kotlin.idea.frontend.api.fir.AbstractResolveCallTest
|
import org.jetbrains.kotlin.idea.frontend.api.fir.AbstractResolveCallTest
|
||||||
import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.AbstractProjectWideOutOfBlockKotlinModificationTrackerTest
|
import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.AbstractProjectWideOutOfBlockKotlinModificationTrackerTest
|
||||||
import org.jetbrains.kotlin.idea.frontend.api.scopes.AbstractMemberScopeByFqNameTest
|
import org.jetbrains.kotlin.idea.frontend.api.scopes.AbstractMemberScopeByFqNameTest
|
||||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolFromLibraryPointerRestoreTest
|
import org.jetbrains.kotlin.idea.frontend.api.symbols.*
|
||||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolsByFqNameBuildingTest
|
|
||||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolFromSourcePointerRestoreTest
|
|
||||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolsByPsiBuildingTest
|
|
||||||
import org.jetbrains.kotlin.idea.hierarchy.AbstractHierarchyTest
|
import org.jetbrains.kotlin.idea.hierarchy.AbstractHierarchyTest
|
||||||
import org.jetbrains.kotlin.idea.hierarchy.AbstractHierarchyWithLibTest
|
import org.jetbrains.kotlin.idea.hierarchy.AbstractHierarchyWithLibTest
|
||||||
import org.jetbrains.kotlin.idea.highlighter.*
|
import org.jetbrains.kotlin.idea.highlighter.*
|
||||||
@@ -1020,6 +1017,10 @@ fun main(args: Array<String>) {
|
|||||||
testClass<AbstractSymbolFromLibraryPointerRestoreTest> {
|
testClass<AbstractSymbolFromLibraryPointerRestoreTest> {
|
||||||
model("resoreSymbolFromLibrary", extension = "txt")
|
model("resoreSymbolFromLibrary", extension = "txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testClass<AbstractMemoryLeakInSymbolsTest> {
|
||||||
|
model("symbolMemoryLeak")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testGroup("idea/idea-frontend-fir/idea-fir-low-level-api/tests", "idea/testData") {
|
testGroup("idea/idea-frontend-fir/idea-fir-low-level-api/tests", "idea/testData") {
|
||||||
|
|||||||
+7
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.idea.fir.low.level.api.trackers
|
|||||||
import com.intellij.ProjectTopics
|
import com.intellij.ProjectTopics
|
||||||
import com.intellij.lang.ASTNode
|
import com.intellij.lang.ASTNode
|
||||||
import com.intellij.openapi.Disposable
|
import com.intellij.openapi.Disposable
|
||||||
|
import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.module.Module
|
import com.intellij.openapi.module.Module
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.roots.ModuleRootEvent
|
import com.intellij.openapi.roots.ModuleRootEvent
|
||||||
@@ -18,6 +19,7 @@ import com.intellij.pom.event.PomModelEvent
|
|||||||
import com.intellij.pom.event.PomModelListener
|
import com.intellij.pom.event.PomModelListener
|
||||||
import com.intellij.pom.tree.TreeAspect
|
import com.intellij.pom.tree.TreeAspect
|
||||||
import com.intellij.pom.tree.events.TreeChangeEvent
|
import com.intellij.pom.tree.events.TreeChangeEvent
|
||||||
|
import org.jetbrains.annotations.TestOnly
|
||||||
import org.jetbrains.kotlin.idea.KotlinLanguage
|
import org.jetbrains.kotlin.idea.KotlinLanguage
|
||||||
import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.getNonLocalContainingInBodyDeclarationWith
|
import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.getNonLocalContainingInBodyDeclarationWith
|
||||||
import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.FileElementFactory
|
import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.FileElementFactory
|
||||||
@@ -52,6 +54,11 @@ internal class KotlinFirModificationTrackerService(project: Project) : Disposabl
|
|||||||
moduleModificationsState.increaseModificationCountForAllModules()
|
moduleModificationsState.increaseModificationCountForAllModules()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestOnly
|
||||||
|
fun incrementModificationsCount() {
|
||||||
|
increaseModificationCountForAllModules()
|
||||||
|
}
|
||||||
|
|
||||||
private inner class Listener : PomModelListener {
|
private inner class Listener : PomModelListener {
|
||||||
override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean =
|
override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean =
|
||||||
treeAspect == aspect
|
treeAspect == aspect
|
||||||
|
|||||||
+6
@@ -9,6 +9,7 @@ import com.intellij.openapi.components.service
|
|||||||
import com.intellij.openapi.module.Module
|
import com.intellij.openapi.module.Module
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.util.ModificationTracker
|
import com.intellij.openapi.util.ModificationTracker
|
||||||
|
import org.jetbrains.annotations.TestOnly
|
||||||
|
|
||||||
class KotlinFirOutOfBlockModificationTrackerFactory(private val project: Project) {
|
class KotlinFirOutOfBlockModificationTrackerFactory(private val project: Project) {
|
||||||
fun createProjectWideOutOfBlockModificationTracker(): ModificationTracker =
|
fun createProjectWideOutOfBlockModificationTracker(): ModificationTracker =
|
||||||
@@ -16,6 +17,11 @@ class KotlinFirOutOfBlockModificationTrackerFactory(private val project: Project
|
|||||||
|
|
||||||
fun createModuleWithoutDependenciesOutOfBlockModificationTracker(module: Module): ModificationTracker =
|
fun createModuleWithoutDependenciesOutOfBlockModificationTracker(module: Module): ModificationTracker =
|
||||||
KotlinFirOutOfBlockModuleModificationTracker(module)
|
KotlinFirOutOfBlockModuleModificationTracker(module)
|
||||||
|
|
||||||
|
@TestOnly
|
||||||
|
fun incrementModificationsCount() {
|
||||||
|
project.service<KotlinFirModificationTrackerService>().incrementModificationsCount()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Project.createProjectWideOutOfBlockModificationTracker() =
|
fun Project.createProjectWideOutOfBlockModificationTracker() =
|
||||||
|
|||||||
+5
@@ -42,4 +42,9 @@ internal class KtFirAnalysisSessionProvider(project: Project) : KtAnalysisSessio
|
|||||||
analysisSessionByModuleInfoCache.value.getOrPut(firModuleResolveState.moduleInfo) {
|
analysisSessionByModuleInfoCache.value.getOrPut(firModuleResolveState.moduleInfo) {
|
||||||
KtFirAnalysisSession.createAnalysisSessionByResolveState(firModuleResolveState)
|
KtFirAnalysisSession.createAnalysisSessionByResolveState(firModuleResolveState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestOnly
|
||||||
|
fun clearCaches() {
|
||||||
|
analysisSessionByModuleInfoCache.value.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2020 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.frontend.api.fir.utils
|
||||||
|
|
||||||
|
internal class EntityWasGarbageCollectedException(entity: String) : IllegalStateException() {
|
||||||
|
override val message: String = "$entity was garbage collected while KtAnalysisSession session is still valid"
|
||||||
|
}
|
||||||
+6
-6
@@ -21,9 +21,9 @@ internal class FirRefWithValidityCheck<D : FirDeclaration>(fir: D, resolveState:
|
|||||||
inline fun <R> withFir(phase: FirResolvePhase = FirResolvePhase.RAW_FIR, crossinline action: (fir: D) -> R): R {
|
inline fun <R> withFir(phase: FirResolvePhase = FirResolvePhase.RAW_FIR, crossinline action: (fir: D) -> R): R {
|
||||||
token.assertIsValid()
|
token.assertIsValid()
|
||||||
val fir = firWeakRef.get()
|
val fir = firWeakRef.get()
|
||||||
?: error("FirElement was garbage collected while analysis session is still valid")
|
?: throw EntityWasGarbageCollectedException("FirElement")
|
||||||
val resolveState =
|
val resolveState = resolveStateWeakRef.get()
|
||||||
resolveStateWeakRef.get() ?: error("FirModuleResolveState was garbage collected while analysis session is still valid")
|
?: throw EntityWasGarbageCollectedException("FirModuleResolveState")
|
||||||
LowLevelFirApiFacade.resolvedFirToPhase(fir, phase, resolveState)
|
LowLevelFirApiFacade.resolvedFirToPhase(fir, phase, resolveState)
|
||||||
return resolveState.withFirDeclaration(fir) { action(it) }
|
return resolveState.withFirDeclaration(fir) { action(it) }
|
||||||
}
|
}
|
||||||
@@ -31,9 +31,9 @@ internal class FirRefWithValidityCheck<D : FirDeclaration>(fir: D, resolveState:
|
|||||||
inline fun <R> withFirResolvedToBodyResolve(action: (fir: D) -> R): R {
|
inline fun <R> withFirResolvedToBodyResolve(action: (fir: D) -> R): R {
|
||||||
token.assertIsValid()
|
token.assertIsValid()
|
||||||
val fir = firWeakRef.get()
|
val fir = firWeakRef.get()
|
||||||
?: error("FirElement was garbage collected while analysis session is still valid")
|
?: throw EntityWasGarbageCollectedException("FirElement")
|
||||||
val resolveState =
|
val resolveState = resolveStateWeakRef.get()
|
||||||
resolveStateWeakRef.get() ?: error("FirModuleResolveState was garbage collected while analysis session is still valid")
|
?: throw EntityWasGarbageCollectedException("FirModuleResolveState")
|
||||||
LowLevelFirApiFacade.resolvedFirToPhase(fir, FirResolvePhase.BODY_RESOLVE, resolveState)
|
LowLevelFirApiFacade.resolvedFirToPhase(fir, FirResolvePhase.BODY_RESOLVE, resolveState)
|
||||||
return action(resolveState.withFirDeclaration(fir) { it })
|
return action(resolveState.withFirDeclaration(fir) { it })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
fun <R> x(p: R): Int {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Y<T> {
|
||||||
|
fun a() = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var z: Int
|
||||||
|
get = 10
|
||||||
|
set(value) {}
|
||||||
|
|
||||||
|
object Q
|
||||||
|
|
||||||
|
val z: String = ""
|
||||||
|
|
||||||
|
fun yyy() {
|
||||||
|
// val q = 10
|
||||||
|
// fun aaa() {}
|
||||||
|
//
|
||||||
|
// class F {}
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias Str = String
|
||||||
|
|
||||||
|
interface I {
|
||||||
|
fun str(): String
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2020 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.frontend.api.symbols
|
||||||
|
|
||||||
|
import com.intellij.openapi.components.service
|
||||||
|
import com.intellij.openapi.util.io.FileUtil
|
||||||
|
import junit.framework.Assert
|
||||||
|
import org.jetbrains.kotlin.idea.executeOnPooledThreadInReadAction
|
||||||
|
import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.KotlinFirOutOfBlockModificationTrackerFactory
|
||||||
|
import org.jetbrains.kotlin.idea.frontend.api.InvalidWayOfUsingAnalysisSession
|
||||||
|
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSessionProvider
|
||||||
|
import org.jetbrains.kotlin.idea.frontend.api.analyze
|
||||||
|
import org.jetbrains.kotlin.idea.frontend.api.fir.KtFirAnalysisSessionProvider
|
||||||
|
import org.jetbrains.kotlin.idea.frontend.api.fir.symbols.KtFirSymbol
|
||||||
|
import org.jetbrains.kotlin.idea.frontend.api.fir.utils.EntityWasGarbageCollectedException
|
||||||
|
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
|
||||||
|
import org.jetbrains.kotlin.psi.KtDeclaration
|
||||||
|
import org.jetbrains.kotlin.psi.KtFile
|
||||||
|
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class AbstractMemoryLeakInSymbolsTest : KotlinLightCodeInsightFixtureTestCase() {
|
||||||
|
override fun isFirPlugin() = true
|
||||||
|
|
||||||
|
protected fun doTest(path: String) {
|
||||||
|
val testDataFile = File(path)
|
||||||
|
val ktFile = myFixture.configureByText(testDataFile.name, FileUtil.loadFile(testDataFile)) as KtFile
|
||||||
|
val symbols = executeOnPooledThreadInReadAction {
|
||||||
|
analyze(ktFile) {
|
||||||
|
ktFile.collectDescendantsOfType<KtDeclaration>().map { it.getSymbol() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateAllCaches(ktFile)
|
||||||
|
System.gc()
|
||||||
|
|
||||||
|
val leakedSymbols = executeOnPooledThreadInReadAction {
|
||||||
|
symbols.map { it.hasNoFirElementLeak() }.filterIsInstance<LeakCheckResult.Leak>()
|
||||||
|
}
|
||||||
|
if (leakedSymbols.isNotEmpty()) {
|
||||||
|
Assert.fail(
|
||||||
|
"""The following symbols leaked (${leakedSymbols.size}/${symbols.size})
|
||||||
|
${leakedSymbols.joinToString(separator = "\n") { it.symbol }}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(InvalidWayOfUsingAnalysisSession::class)
|
||||||
|
private fun invalidateAllCaches(ktFile: KtFile) {
|
||||||
|
project.service<KotlinFirOutOfBlockModificationTrackerFactory>().incrementModificationsCount()
|
||||||
|
(project.service<KtAnalysisSessionProvider>() as KtFirAnalysisSessionProvider).clearCaches()
|
||||||
|
executeOnPooledThreadInReadAction { analyze(ktFile) {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun KtSymbol.hasNoFirElementLeak(): LeakCheckResult {
|
||||||
|
require(this is KtFirSymbol<*>)
|
||||||
|
return try {
|
||||||
|
firRef.withFir { LeakCheckResult.Leak(this::class.simpleName!!) }
|
||||||
|
} catch (_: EntityWasGarbageCollectedException) {
|
||||||
|
LeakCheckResult.NoLeak
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LeakCheckResult {
|
||||||
|
object NoLeak : LeakCheckResult()
|
||||||
|
data class Leak(val symbol: String) : LeakCheckResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2020 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.frontend.api.symbols;
|
||||||
|
|
||||||
|
import com.intellij.testFramework.TestDataPath;
|
||||||
|
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
|
||||||
|
import org.jetbrains.kotlin.test.KotlinTestUtils;
|
||||||
|
import org.jetbrains.kotlin.test.TestMetadata;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
@TestMetadata("idea/idea-frontend-fir/testData/symbolMemoryLeak")
|
||||||
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
public class MemoryLeakInSymbolsTestGenerated extends AbstractMemoryLeakInSymbolsTest {
|
||||||
|
private void runTest(String testDataFilePath) throws Exception {
|
||||||
|
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testAllFilesPresentInSymbolMemoryLeak() throws Exception {
|
||||||
|
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/idea-frontend-fir/testData/symbolMemoryLeak"), Pattern.compile("^(.+)\\.kt$"), null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("symbols.kt")
|
||||||
|
public void testSymbols() throws Exception {
|
||||||
|
runTest("idea/idea-frontend-fir/testData/symbolMemoryLeak/symbols.kt");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user