FIR IDE: add tests for checking module invalidation

This commit is contained in:
Ilya Kirillov
2020-11-27 20:25:16 +01:00
parent 2fb4a917f6
commit d5979ffded
13 changed files with 286 additions and 0 deletions
@@ -85,6 +85,7 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.AbstractFirLazyDeclarationRes
import org.jetbrains.kotlin.idea.fir.low.level.api.AbstractFirMultiModuleLazyResolveTest
import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.AbstractFileStructureTest
import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.AbstractFileStructureAndOutOfBlockModificationTrackerConsistencyTest
import org.jetbrains.kotlin.idea.fir.low.level.api.sessions.AbstractSessionsInvalidationTest
import org.jetbrains.kotlin.idea.folding.AbstractKotlinFoldingTest
import org.jetbrains.kotlin.idea.frontend.api.fir.AbstractResolveCallTest
import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.AbstractProjectWideOutOfBlockKotlinModificationTrackerTest
@@ -1049,6 +1050,9 @@ fun main(args: Array<String>) {
testClass<AbstractFileStructureTest> {
model("fileStructure")
}
testClass<AbstractSessionsInvalidationTest> {
model("sessionInvalidation", recursive = false, extension = null)
}
}
testGroup("idea/idea-fir/tests", "idea") {
@@ -18,6 +18,7 @@ import com.intellij.pom.event.PomModelEvent
import com.intellij.pom.event.PomModelListener
import com.intellij.pom.tree.TreeAspect
import com.intellij.pom.tree.events.TreeChangeEvent
import org.jetbrains.annotations.TestOnly
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.file.structure.FileElementFactory
@@ -45,6 +46,12 @@ internal class KotlinFirModificationTrackerService(project: Project) : Disposabl
moduleModificationsState.increaseModificationCountForAllModules()
}
@TestOnly
fun increaseModificationCountForModule(module: Module) {
moduleModificationsState.increaseModificationCountForModule(module)
}
private fun subscribeForRootChanges(project: Project) {
project.messageBus.connect(this).subscribe(
ProjectTopics.PROJECT_ROOTS,
@@ -0,0 +1,14 @@
{
"modules" : [
{ "name": "A", "dependsOn": ["B", "C"] },
{ "name": "B", "dependsOn": ["D", "E"] },
{ "name": "C", "dependsOn": ["F", "G"] },
{ "name": "D", "dependsOn": [] },
{ "name": "E", "dependsOn": [] },
{ "name": "F", "dependsOn": [] },
{ "name": "G", "dependsOn": [] }
],
"rootModule": "A",
"modulesToMakeOOBM": ["F"],
"expectedInvalidatedModules": ["A", "C", "F"]
}
@@ -0,0 +1,14 @@
{
"modules" : [
{ "name": "A", "dependsOn": ["B", "C"] },
{ "name": "B", "dependsOn": ["D", "E"] },
{ "name": "C", "dependsOn": ["F", "G"] },
{ "name": "D", "dependsOn": [] },
{ "name": "E", "dependsOn": [] },
{ "name": "F", "dependsOn": [] },
{ "name": "G", "dependsOn": [] }
],
"rootModule": "A",
"modulesToMakeOOBM": [],
"expectedInvalidatedModules": []
}
@@ -0,0 +1,14 @@
{
"modules" : [
{ "name": "A", "dependsOn": ["B", "C"] },
{ "name": "B", "dependsOn": ["D", "E"] },
{ "name": "C", "dependsOn": ["F", "G"] },
{ "name": "D", "dependsOn": [] },
{ "name": "E", "dependsOn": [] },
{ "name": "F", "dependsOn": ["B"] },
{ "name": "G", "dependsOn": [] }
],
"rootModule": "A",
"modulesToMakeOOBM": ["E"],
"expectedInvalidatedModules": ["A", "B", "C", "E", "F"]
}
@@ -0,0 +1,14 @@
{
"modules" : [
{ "name": "A", "dependsOn": ["B", "C"] },
{ "name": "B", "dependsOn": ["D", "E"] },
{ "name": "C", "dependsOn": ["F", "G"] },
{ "name": "D", "dependsOn": [] },
{ "name": "E", "dependsOn": [] },
{ "name": "F", "dependsOn": [] },
{ "name": "G", "dependsOn": [] }
],
"rootModule": "A",
"modulesToMakeOOBM": ["A"],
"expectedInvalidatedModules": ["A"]
}
@@ -0,0 +1,12 @@
{
"modules" : [
{ "name": "A", "dependsOn": ["B"] },
{ "name": "B", "dependsOn": ["C"] },
{ "name": "C", "dependsOn": ["D"] },
{ "name": "D", "dependsOn": ["E"] },
{ "name": "E", "dependsOn": [] }
],
"rootModule": "A",
"modulesToMakeOOBM": ["C"],
"expectedInvalidatedModules": ["A", "B", "C"]
}
@@ -0,0 +1,11 @@
{
"modules" : [
{ "name": "A", "dependsOn": ["B", "C"] },
{ "name": "B", "dependsOn": ["D"] },
{ "name": "C", "dependsOn": ["D"] },
{ "name": "D", "dependsOn": [] }
],
"rootModule": "A",
"modulesToMakeOOBM": ["D"],
"expectedInvalidatedModules": ["A", "B", "C", "D"]
}
@@ -0,0 +1,11 @@
{
"modules" : [
{ "name": "A", "dependsOn": ["B", "C"] },
{ "name": "B", "dependsOn": ["D"] },
{ "name": "C", "dependsOn": ["D"] },
{ "name": "D", "dependsOn": [] }
],
"rootModule": "A",
"modulesToMakeOOBM": ["C", "D"],
"expectedInvalidatedModules": ["A", "B", "C", "D"]
}
@@ -55,4 +55,10 @@ internal object TestProjectStructureReader {
val json = JsonParser().parse(FileUtil.loadFile(jsonFile.toFile(), /*convertLineSeparators=*/true))
return TestProjectStructure.parse(json)
}
fun <T> readToTestStructure(
testDirectory: Path,
jsonFileName: String = "structure.json",
toTestStructure: (TestProjectStructure) -> T,
): T = read(testDirectory, jsonFileName).let(toTestStructure)
}
@@ -5,10 +5,13 @@
package org.jetbrains.kotlin.idea.fir.low.level.api
import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.caches.project.getModuleInfo
import org.jetbrains.kotlin.idea.fir.low.level.api.api.FirModuleResolveState
import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.KotlinFirModificationTrackerService
import org.jetbrains.kotlin.psi.KtElement
internal fun Project.allModules() = ModuleManager.getInstance(this).modules.toList()
@@ -16,4 +19,8 @@ internal fun Project.allModules() = ModuleManager.getInstance(this).modules.toLi
inline fun resolveWithClearCaches(context: KtElement, action: (FirModuleResolveState) -> Unit) {
val resolveState = createResolveStateForNoCaching(context.getModuleInfo())
action(resolveState)
}
internal fun Module.incModificationTracker() {
project.service<KotlinFirModificationTrackerService>().increaseModificationCountForModule(this)
}
@@ -0,0 +1,107 @@
/*
* 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.fir.low.level.api.sessions
import com.google.common.collect.Sets
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.testFramework.PsiTestUtil
import junit.framework.Assert
import org.jetbrains.kotlin.idea.caches.project.ModuleSourceInfo
import org.jetbrains.kotlin.idea.caches.project.productionSourceInfo
import org.jetbrains.kotlin.idea.fir.low.level.api.TestProjectModule
import org.jetbrains.kotlin.idea.fir.low.level.api.TestProjectStructure
import org.jetbrains.kotlin.idea.fir.low.level.api.TestProjectStructureReader
import org.jetbrains.kotlin.idea.fir.low.level.api.incModificationTracker
import org.jetbrains.kotlin.idea.jsonUtils.getString
import org.jetbrains.kotlin.idea.stubs.AbstractMultiModuleTest
import org.jetbrains.kotlin.test.KotlinTestUtils
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.io.path.writeText
abstract class AbstractSessionsInvalidationTest : AbstractMultiModuleTest() {
override fun getTestDataPath(): String =
"${KotlinTestUtils.getHomeDirectory()}/idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/"
protected fun doTest(path: String) {
val testStructure = TestProjectStructureReader.readToTestStructure(
Paths.get(path),
toTestStructure = MultiModuleTestProjectStructure.Companion::fromTestProjectStructure
)
val modulesByNames = testStructure.modules.associate { moduleData ->
moduleData.name to createEmptyModule(moduleData.name)
}
testStructure.modules.forEach { moduleData ->
val module = modulesByNames.getValue(moduleData.name)
moduleData.dependsOnModules.forEach { dependencyName ->
module.addDependency(modulesByNames.getValue(dependencyName))
}
}
val rootModule = modulesByNames[testStructure.rootModule]
?: error("${testStructure.rootModule} is not present in the list of modules")
val modulesToMakeOOBM = testStructure.modulesToMakeOOBM.map {
modulesByNames[it]
?: error("$it is not present in the list of modules")
}
val rootModuleSourceInfo = rootModule.productionSourceInfo()!!
val storage = FirIdeSessionProviderStorage(project)
val initialSessions = storage.getFirSessions(rootModuleSourceInfo)
modulesToMakeOOBM.forEach { it.incModificationTracker() }
val sessionsAfterOOBM = storage.getFirSessions(rootModuleSourceInfo)
val changedSessions = Sets.symmetricDifference(initialSessions, sessionsAfterOOBM)
val changedSessionsModulesNamesSorted = changedSessions.map { (it.moduleInfo as ModuleSourceInfo).module.name }.distinct().sorted()
Assert.assertEquals(testStructure.expectedInvalidatedModules, changedSessionsModulesNamesSorted)
}
private fun FirIdeSessionProviderStorage.getFirSessions(rootModuleInfo: ModuleSourceInfo): Set<FirIdeSession> {
val sessionProvider = getSessionProvider(rootModuleInfo)
return sessionProvider.sessions.values.toSet()
}
private fun createEmptyModule(name: String): Module {
val tmpDir = createTempDirectory().toPath()
val module: Module = createModule("$tmpDir/$name", moduleType)
val root = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tmpDir.toFile())!!
WriteCommandAction.writeCommandAction(module.project).run<RuntimeException> {
root.refresh(false, true)
}
PsiTestUtil.addSourceContentToRoots(module, root)
return module
}
}
private data class MultiModuleTestProjectStructure(
val modules: List<TestProjectModule>,
val rootModule: String,
val modulesToMakeOOBM: List<String>,
val expectedInvalidatedModules: List<String>,
) {
companion object {
fun fromTestProjectStructure(testProjectStructure: TestProjectStructure): MultiModuleTestProjectStructure {
val json = testProjectStructure.json
return MultiModuleTestProjectStructure(
testProjectStructure.modules,
json.getString(ROOT_MODULE_FIELD),
json.getAsJsonArray(MODULES_TO_MAKE_OOBM_IN_FIELD).map { it.asString }.sorted(),
json.getAsJsonArray(EXPECTED_INVALIDATED_MODULES_FIELD).map { it.asString }.sorted(),
)
}
private const val ROOT_MODULE_FIELD = "rootModule"
private const val MODULES_TO_MAKE_OOBM_IN_FIELD = "modulesToMakeOOBM"
private const val EXPECTED_INVALIDATED_MODULES_FIELD = "expectedInvalidatedModules"
}
}
@@ -0,0 +1,65 @@
/*
* 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.fir.low.level.api.sessions;
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/idea-fir-low-level-api/testdata/sessionInvalidation")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class SessionsInvalidationTestGenerated extends AbstractSessionsInvalidationTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInSessionInvalidation() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation"), Pattern.compile("^([^\\.]+)$"), null, false);
}
@TestMetadata("binaryTree")
public void testBinaryTree() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/binaryTree/");
}
@TestMetadata("binaryTreeNoInvalidated")
public void testBinaryTreeNoInvalidated() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/binaryTreeNoInvalidated/");
}
@TestMetadata("binaryTreeWithAdditionalEdge")
public void testBinaryTreeWithAdditionalEdge() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/binaryTreeWithAdditionalEdge/");
}
@TestMetadata("binaryTreeWithInvalidInRoot")
public void testBinaryTreeWithInvalidInRoot() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/binaryTreeWithInvalidInRoot/");
}
@TestMetadata("linear")
public void testLinear() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/linear/");
}
@TestMetadata("rhombus")
public void testRhombus() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/rhombus/");
}
@TestMetadata("rhombusWithTwoInvalid")
public void testRhombusWithTwoInvalid() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/sessionInvalidation/rhombusWithTwoInvalid/");
}
}