From a15ecb51c59d1e545c39e2efdbcbc6812f9511f6 Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Tue, 25 Feb 2025 20:34:57 +0100 Subject: [PATCH] IJPL-339 refactor multiverseProjectFixture GitOrigin-RevId: 1e419998ac8352b209b10e88c73a2cf742acf67b --- .idea/modules.xml | 1 + java/java-tests/intellij.java.tests.iml | 1 + .../JavaMultiverseCodeVisionProviderTest.kt | 16 +- .../intellij.platform.lang.tests.iml | 1 + .../module/impl/scopes/ModuleScopeTest.kt | 4 +- .../psi/impl/file/impl/FileContextTest.kt | 11 +- ....testFramework.junit5.projectStructure.iml | 17 + .../fixture/ContentRootBuilder.kt | 12 + .../fixture/DirectoryBuilder.kt | 8 + .../projectStructure/fixture/ModuleBuilder.kt | 11 + .../fixture/ProjectBuilder.kt | 6 + .../fixture/ProjectStructureFixtures.kt | 33 ++ .../fixture/impl/ContentRootBuilderImpl.kt | 39 ++ .../fixture/impl/DirectoryBuilderBase.kt | 41 ++ .../fixture/impl/DirectoryBuilderImpl.kt | 8 + .../fixture/impl/DirectoryContainer.kt | 12 + .../fixture/impl/FileBuilderImpl.kt | 7 + .../fixture/impl/ModuleBuilderImpl.kt | 40 ++ .../impl/MultiverseFixtureInitializer.kt | 104 ++++ .../fixture/impl/ProjectStructure.kt | 16 + .../fixture/impl/SourceRootBuilderImpl.kt | 9 + .../projectStructure/fixture/impl/fixtures.kt | 109 ++++ .../fixture}/JUnit5MultiverseFixtureTest.kt | 71 +-- .../fixture/MultiverseFixtureTest.kt | 44 ++ .../fixture/SimpleProjectTest.kt | 40 ++ .../src/fixture/MultiverseProjectBuilder.kt | 508 ------------------ 26 files changed, 600 insertions(+), 569 deletions(-) create mode 100644 platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ContentRootBuilder.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/DirectoryBuilder.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ModuleBuilder.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectBuilder.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectStructureFixtures.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ContentRootBuilderImpl.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderBase.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderImpl.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryContainer.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/FileBuilderImpl.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ModuleBuilderImpl.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/MultiverseFixtureInitializer.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ProjectStructure.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/SourceRootBuilderImpl.kt create mode 100644 platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/fixtures.kt rename platform/testFramework/junit5/{test/showcase => projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture}/JUnit5MultiverseFixtureTest.kt (81%) create mode 100644 platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/MultiverseFixtureTest.kt create mode 100644 platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/SimpleProjectTest.kt delete mode 100644 platform/testFramework/junit5/src/fixture/MultiverseProjectBuilder.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 4ffd9a37be72..f33e918c2c11 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -882,6 +882,7 @@ + diff --git a/java/java-tests/intellij.java.tests.iml b/java/java-tests/intellij.java.tests.iml index 8835634c8806..c97d477c1baa 100644 --- a/java/java-tests/intellij.java.tests.iml +++ b/java/java-tests/intellij.java.tests.iml @@ -101,5 +101,6 @@ + \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt index 02c49bbb5163..e2bbdb91070b 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt @@ -1,24 +1,26 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.java.codeInsight.codeVision import com.intellij.codeInsight.daemon.impl.JavaInheritorsCodeVisionProvider import com.intellij.codeInsight.daemon.impl.JavaReferencesCodeVisionProvider -import com.intellij.codeInsight.multiverse.* -import com.intellij.openapi.application.* +import com.intellij.codeInsight.multiverse.CodeInsightContext +import com.intellij.codeInsight.multiverse.EditorContextManager +import com.intellij.codeInsight.multiverse.ProjectModelContextBridge +import com.intellij.codeInsight.multiverse.SingleEditorContext +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.writeAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.module.Module +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.withSharedSourceEnabled import com.intellij.psi.PsiDirectory +import com.intellij.psi.impl.file.impl.sharedSourceRootFixture import com.intellij.testFramework.junit5.TestApplication import com.intellij.testFramework.junit5.fixture.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import com.intellij.psi.impl.file.impl.withSharedSourceEnabled -import com.intellij.psi.impl.file.impl.sharedSourceRootFixture import org.junit.jupiter.api.Test - - @TestApplication internal class JavaMultiverseCodeVisionProviderTest { diff --git a/platform/lang-impl/intellij.platform.lang.tests.iml b/platform/lang-impl/intellij.platform.lang.tests.iml index fb87db6a6bd9..267006599adb 100644 --- a/platform/lang-impl/intellij.platform.lang.tests.iml +++ b/platform/lang-impl/intellij.platform.lang.tests.iml @@ -59,5 +59,6 @@ + \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/openapi/module/impl/scopes/ModuleScopeTest.kt b/platform/lang-impl/testSources/com/intellij/openapi/module/impl/scopes/ModuleScopeTest.kt index e4f1daeaf76a..24239ff38fe9 100644 --- a/platform/lang-impl/testSources/com/intellij/openapi/module/impl/scopes/ModuleScopeTest.kt +++ b/platform/lang-impl/testSources/com/intellij/openapi/module/impl/scopes/ModuleScopeTest.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.module.impl.scopes import com.intellij.codeInsight.multiverse.CodeInsightContext @@ -6,9 +6,9 @@ import com.intellij.codeInsight.multiverse.anyContext import com.intellij.openapi.application.readAction import com.intellij.openapi.module.Module import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.withSharedSourceEnabled import com.intellij.psi.impl.file.impl.moduleContext import com.intellij.psi.impl.file.impl.sharedSourceRootFixture -import com.intellij.psi.impl.file.impl.withSharedSourceEnabled import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.SearchScope import com.intellij.psi.search.contains diff --git a/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/FileContextTest.kt b/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/FileContextTest.kt index 055d464ce709..0160bb5d8639 100644 --- a/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/FileContextTest.kt +++ b/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/FileContextTest.kt @@ -1,12 +1,12 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.psi.impl.file.impl import com.intellij.codeInsight.multiverse.* -import com.intellij.openapi.application.readAction import com.intellij.openapi.application.edtWriteAction +import com.intellij.openapi.application.readAction import com.intellij.openapi.module.Module -import com.intellij.openapi.project.Project import com.intellij.openapi.project.rootManager +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.withSharedSourceEnabled import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager @@ -126,11 +126,6 @@ private fun CodeInsightContext.asText() = when (this) { else -> this.toString() } -fun TestFixture.withSharedSourceEnabled(): TestFixture = testFixture("shared-source-enabling-fixture") { - MultiverseTestEnabler.enableSharedSourcesForTheNextProject() - initialized(this@withSharedSourceEnabled.init()) {} -} - fun sharedSourceRootFixture(vararg moduleFixtures: TestFixture): TestFixture = testFixture("shared-source-root-fixture") { require(moduleFixtures.isNotEmpty()) diff --git a/platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml b/platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml new file mode 100644 index 000000000000..04c61d91b5d6 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ContentRootBuilder.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ContentRootBuilder.kt new file mode 100644 index 000000000000..2204c7e567cb --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ContentRootBuilder.kt @@ -0,0 +1,12 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture + +interface ContentRootBuilder : ProjectBuilder { + fun sourceRoot( + name: String, + sourceRootId: String? = null, + init: DirectoryBuilder.() -> Unit, + ) + + fun sharedSourceRoot(sourceRootId: String) +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/DirectoryBuilder.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/DirectoryBuilder.kt new file mode 100644 index 000000000000..9ff8f58b9e59 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/DirectoryBuilder.kt @@ -0,0 +1,8 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture + +interface DirectoryBuilder { + fun dir(name: String, init: DirectoryBuilder.() -> Unit) + + fun file(fileName: String, content: String) +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ModuleBuilder.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ModuleBuilder.kt new file mode 100644 index 000000000000..16d072c6d6a5 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ModuleBuilder.kt @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture + +interface ModuleBuilder : ProjectBuilder { + fun contentRoot( + name: String, + init: ContentRootBuilder.() -> Unit, + ) + + fun sharedSourceRoot(sourceRootId: String) +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectBuilder.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectBuilder.kt new file mode 100644 index 000000000000..07f6c1123fd6 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectBuilder.kt @@ -0,0 +1,6 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture + +interface ProjectBuilder : DirectoryBuilder { + fun module(moduleName: String, init: ModuleBuilder.() -> Unit) +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectStructureFixtures.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectStructureFixtures.kt new file mode 100644 index 000000000000..45dbd328c125 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/ProjectStructureFixtures.kt @@ -0,0 +1,33 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture + +import com.intellij.codeInsight.multiverse.MultiverseTestEnabler +import com.intellij.openapi.project.Project +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl.MultiverseFixtureInitializer +import com.intellij.testFramework.junit5.fixture.TestFixture +import com.intellij.testFramework.junit5.fixture.testFixture +import org.jetbrains.annotations.TestOnly + +/** + * This fixture allows setting up the project structure via a simple DSL + */ +@TestOnly +fun multiverseProjectFixture(withSharedSourceEnabled: Boolean = true, init: ProjectBuilder.() -> Unit): TestFixture { + val fixture = testFixture("multiverse-project-fixture") { + val project = with(MultiverseFixtureInitializer(init)) { + initializeProjectModel() + } + + initialized(project) { /* Nothing to dispose of */ } + } + return if (withSharedSourceEnabled) fixture.withSharedSourceEnabled() else fixture +} + +/** + * Enables Shared Source support in the given project. + */ +@TestOnly +fun TestFixture.withSharedSourceEnabled(): TestFixture = testFixture("shared-source-enabling-fixture") { + MultiverseTestEnabler.enableSharedSourcesForTheNextProject() + initialized(this@withSharedSourceEnabled.init()) {} +} diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ContentRootBuilderImpl.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ContentRootBuilderImpl.kt new file mode 100644 index 000000000000..1bc6b963f6a7 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ContentRootBuilderImpl.kt @@ -0,0 +1,39 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.ContentRootBuilder +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.DirectoryBuilder + +internal class ContentRootBuilderImpl( + path: String, + projectStructure: ProjectStructure, +) : DirectoryBuilderBase(path, projectStructure), ContentRootBuilder { + private val _sourceRoots: MutableList = mutableListOf() + + val sourceRoots: List get() = _sourceRoots + + fun addSourceRoot(sourceRoot: SourceRootBuilderImpl) { + _sourceRoots.add(sourceRoot) + } + + override fun sourceRoot( + name: String, + sourceRootId: String?, + init: DirectoryBuilder.() -> Unit, + ) { + val sourceRootPath = "$path/$name" + val sourceRootBuilder = SourceRootBuilderImpl(name, sourceRootPath, projectStructure).apply(init) + _sourceRoots.add(sourceRootBuilder) + + if (sourceRootId != null) { + projectStructure.addSourceRoot(sourceRootId, sourceRootBuilder) + } + } + + override fun sharedSourceRoot(sourceRootId: String) { + val sharedSourceRootBuilder = projectStructure.findSourceRoot(sourceRootId) + ?: throw IllegalArgumentException("Source root ID '$sourceRootId' does not exist in the registry!") + + _sourceRoots.add(sharedSourceRootBuilder) + } +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderBase.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderBase.kt new file mode 100644 index 000000000000..eb7cb22829bb --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderBase.kt @@ -0,0 +1,41 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.DirectoryBuilder +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.ModuleBuilder +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.ProjectBuilder + +internal open class DirectoryBuilderBase( + override val path: String, + protected val projectStructure: ProjectStructure, +) : DirectoryContainer, ProjectBuilder { + private val _directories: MutableList = mutableListOf() + private val _nestedModules: MutableList = mutableListOf() + private val _files: MutableList = mutableListOf() + + override val directories: List get() = _directories + override val modules: List get() = _nestedModules + override val files: List get() = _files + + override fun file(fileName: String, content: String) { + _files.add(FileBuilderImpl(fileName, content)) + } + + override fun dir(name: String, init: DirectoryBuilder.() -> Unit) { + val directoryPath = subPath(name) + val directory = DirectoryBuilderImpl(name, directoryPath, projectStructure) + directory.init() + _directories.add(directory) + } + + override fun module(moduleName: String, init: ModuleBuilder.() -> Unit) { + val nestedModulePath = subPath(moduleName) + val nestedModule = ModuleBuilderImpl(moduleName, nestedModulePath, projectStructure).apply(init) + _nestedModules.add(nestedModule) + } + + private fun subPath(name: String): String { + if (path.isEmpty()) return name + return "$path/$name" + } +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderImpl.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderImpl.kt new file mode 100644 index 000000000000..3c361b759e2d --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryBuilderImpl.kt @@ -0,0 +1,8 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +internal class DirectoryBuilderImpl( + val name: String, + path: String, + projectStructure: ProjectStructure, +) : DirectoryBuilderBase(path, projectStructure) \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryContainer.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryContainer.kt new file mode 100644 index 000000000000..6c217172532a --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/DirectoryContainer.kt @@ -0,0 +1,12 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +internal interface DirectoryContainer { + val path: String + + val directories: List + + val files: List + + val modules: List +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/FileBuilderImpl.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/FileBuilderImpl.kt new file mode 100644 index 000000000000..5e8f292b8b49 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/FileBuilderImpl.kt @@ -0,0 +1,7 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +internal class FileBuilderImpl( + val name: String, + val content: String, +) \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ModuleBuilderImpl.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ModuleBuilderImpl.kt new file mode 100644 index 000000000000..98c14cc29aeb --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ModuleBuilderImpl.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.ContentRootBuilder +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.ModuleBuilder + +internal class ModuleBuilderImpl( + val moduleName: String, + path: String, + projectStructure: ProjectStructure, +) : DirectoryBuilderBase(path, projectStructure), ModuleBuilder { + + private val _contentRoots: MutableList = mutableListOf() + + val contentRoots: List get() = _contentRoots + + override fun contentRoot(name: String, init: ContentRootBuilder.() -> Unit) { + val contentRootPath = "$path/$name" + val contentRootBuilder = ContentRootBuilderImpl(contentRootPath, projectStructure) + _contentRoots.add(contentRootBuilder) + contentRootBuilder.init() + } + + override fun sharedSourceRoot(sourceRootId: String) { + val originalSourceRootBuilder = projectStructure.findSourceRoot(sourceRootId) + ?: throw IllegalArgumentException("Source root ID '$sourceRootId' does not exist in the registry!") + + val contentRoot = ContentRootBuilderImpl( + path = originalSourceRootBuilder.path, + projectStructure = projectStructure, + ) + _contentRoots.add(contentRoot) + + val newSourceRoot = SourceRootBuilderImpl(name = originalSourceRootBuilder.name, + path = originalSourceRootBuilder.path, + projectStructure = projectStructure, + isExisting = true) + contentRoot.addSourceRoot(newSourceRoot) + } +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/MultiverseFixtureInitializer.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/MultiverseFixtureInitializer.kt new file mode 100644 index 000000000000..3c8e9eb3a309 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/MultiverseFixtureInitializer.kt @@ -0,0 +1,104 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.platform.testFramework.junit5.projectStructure.fixture.ProjectBuilder +import com.intellij.testFramework.junit5.fixture.TestFixture +import com.intellij.testFramework.junit5.fixture.TestFixtureInitializer +import com.intellij.testFramework.junit5.fixture.moduleFixture +import com.intellij.testFramework.junit5.fixture.projectFixture +import org.jetbrains.annotations.TestOnly +import java.nio.file.Path +import kotlin.io.path.Path + +@TestOnly +internal class MultiverseFixtureInitializer( + private val init: ProjectBuilder.() -> Unit, +) { + private lateinit var projectFixture: TestFixture + private lateinit var projectRootPath: Path + private val structure = ProjectStructure() + + suspend fun TestFixtureInitializer.R.initializeProjectModel(): Project { + projectFixture = projectFixture() + val project = projectFixture.init() + + projectRootPath = project.basePath?.let { Path(it) } ?: error("Project base path is not available") + + val projectRootAsFixture = dirFixture(projectRootPath) + projectRootAsFixture.init() + + val builder = DirectoryBuilderBase("", structure) + builder.init() + + initializeChildren(builder, projectRootAsFixture) + + return project + } + + private suspend fun TestFixtureInitializer.R.initializeModule( + module: ModuleBuilderImpl + ) { + val modulePath = module.path.resolvePath() + val modulePathFixture = dirFixture(modulePath) + val moduleFixture = projectFixture.moduleFixture(modulePathFixture) + moduleFixture.init() + + module.contentRoots.forEach { contentRoot -> + initializeContentRoot(contentRoot, moduleFixture) + } + + initializeChildren(module, modulePathFixture) + } + + private suspend fun TestFixtureInitializer.R.initializeContentRoot( + contentRoot: ContentRootBuilderImpl, + moduleFixture: TestFixture, + ) { + val contentRootPath = contentRoot.path.resolvePath() + val contentRootFixture = moduleFixture.customContentRootFixture(dirFixture(contentRootPath)) + contentRootFixture.init() + + contentRoot.sourceRoots.forEach { sourceRoot -> + initializeSourceRoot(moduleFixture, contentRootFixture, sourceRoot) + } + + initializeChildren(contentRoot, contentRootFixture) + } + + private suspend fun TestFixtureInitializer.R.initializeSourceRoot( + moduleFixture: TestFixture, + contentRootFixture: TestFixture, + sourceRoot: SourceRootBuilderImpl, + ) { + val pathFixture = dirFixture(sourceRoot.path.resolvePath()) + val sourceRootFixture = moduleFixture.customSourceRootFixture(pathFixture, contentRootFixture) + sourceRootFixture.init() + + if (!sourceRoot.isExisting) { + initializeChildren(sourceRoot, sourceRootFixture) + } + } + + private suspend fun TestFixtureInitializer.R.initializeChildren( + container: DirectoryContainer, + containerFixture: TestFixture, + ) { + container.modules.forEach { nestedModule -> + initializeModule(nestedModule) + } + + container.files.forEach { file -> + containerFixture.fileFixture(file.name, file.content).init() + } + + container.directories.forEach { directory -> + val directoryFixture = containerFixture.subDirFixture(directory.name) + directoryFixture.init() + initializeChildren(directory, directoryFixture) + } + } + + private fun String.resolvePath(): Path = projectRootPath.resolve(this) +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ProjectStructure.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ProjectStructure.kt new file mode 100644 index 000000000000..5f3303cc88cc --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/ProjectStructure.kt @@ -0,0 +1,16 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +internal class ProjectStructure { + private val sourceRoots: MutableMap = mutableMapOf() + + fun findSourceRoot(sourceRootId: String): SourceRootBuilderImpl? = + sourceRoots[sourceRootId] + + fun addSourceRoot(sourceRootId: String, sourceRootBuilder: SourceRootBuilderImpl) { + if (sourceRoots.containsKey(sourceRootId)) { + throw IllegalArgumentException("Source root ID '$sourceRootId' already exists in the registry!") + } + sourceRoots[sourceRootId] = sourceRootBuilder + } +} \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/SourceRootBuilderImpl.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/SourceRootBuilderImpl.kt new file mode 100644 index 000000000000..d9dc0a3b22ae --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/SourceRootBuilderImpl.kt @@ -0,0 +1,9 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +internal class SourceRootBuilderImpl( + val name: String, + path: String, + projectStructure: ProjectStructure, + val isExisting: Boolean = false, +) : DirectoryBuilderBase(path, projectStructure) \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/fixtures.kt b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/fixtures.kt new file mode 100644 index 000000000000..21354ccae08d --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/src/com/intellij/platform/testFramework/junit5/projectStructure/fixture/impl/fixtures.kt @@ -0,0 +1,109 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture.impl + +import com.intellij.openapi.application.edtWriteAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.testFramework.junit5.fixture.TestFixture +import com.intellij.testFramework.junit5.fixture.testFixture +import com.intellij.util.io.createDirectories +import com.intellij.util.io.delete +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.TestOnly +import org.jetbrains.jps.model.java.JavaResourceRootType +import org.jetbrains.jps.model.java.JavaSourceRootType +import org.jetbrains.jps.model.module.JpsModuleSourceRootType +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.writeText + +@TestOnly +internal fun TestFixture.customContentRootFixture( + pathFixture: TestFixture, +): TestFixture = testFixture("customContentRootFixture") { + val module = this@customContentRootFixture.init() + val path = pathFixture.init() + val dir = VfsUtil.findFile(path, true) ?: error("Failed to find VFS file for path: $path") + edtWriteAction { + ModuleRootModificationUtil.updateModel(module) { model -> + model.addContentEntry(dir) + } + } + initialized(path) {} +} + +@TestOnly +internal fun TestFixture.customSourceRootFixture( + pathFixture: TestFixture, + contentRootFixture: TestFixture, + isTestSource: Boolean = false, + isResource: Boolean = false, +): TestFixture = testFixture("customSourceRootFixture") { + val path = pathFixture.init() + val module = this@customSourceRootFixture.init() + val contentRootPath = contentRootFixture.init() + edtWriteAction { + val dir = VfsUtil.findFile(path, true) ?: error("Failed to find VFS file for path: $path") + ModuleRootModificationUtil.updateModel(module) { model -> + val contentEntry = model.contentEntries + .find { it.file?.toNioPath() == contentRootPath } + ?: error("Content entry with path '$path' was not found in module ${module.name}.") + val type = getSourceRootType(isTestSource, isResource) + contentEntry.addSourceFolder(dir, type) + } + } + initialized(path) {} +} + +@TestOnly +internal fun dirFixture(dir: Path): TestFixture = testFixture("dirFixture") { + val tempDir = withContext(Dispatchers.IO) { + if (!dir.exists()) { + dir.createDirectories() + } + dir + } + initialized(tempDir) { + withContext(Dispatchers.IO) { + if (tempDir.exists()) { + tempDir.delete(recursively = true) + } + } + } +} + +@TestOnly +internal fun TestFixture.subDirFixture(name: String): TestFixture = testFixture("subDirFixture") { + val path = this@subDirFixture.init().resolve(name) + val result = dirFixture(path).init() + initialized(result) {} +} + +@TestOnly +internal fun TestFixture.fileFixture(fileName: String, content: CharSequence): TestFixture = testFixture("fileFixture") { + val file = withContext(Dispatchers.IO) { + val dir = this@fileFixture.init() + if (!dir.exists()) { + dir.createDirectories() + } + val file = dir.resolve(fileName) + file.writeText(content) + file + } + initialized(file) { + withContext(Dispatchers.IO) { + if (file.exists()) { + file.delete() + } + } + } +} + +private fun getSourceRootType(isTestSource: Boolean, isResource: Boolean): JpsModuleSourceRootType<*> = when { + isTestSource && isResource -> JavaResourceRootType.TEST_RESOURCE + !isTestSource && isResource -> JavaResourceRootType.RESOURCE + isTestSource && !isResource -> JavaSourceRootType.TEST_SOURCE + else -> JavaSourceRootType.SOURCE +} diff --git a/platform/testFramework/junit5/test/showcase/JUnit5MultiverseFixtureTest.kt b/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/JUnit5MultiverseFixtureTest.kt similarity index 81% rename from platform/testFramework/junit5/test/showcase/JUnit5MultiverseFixtureTest.kt rename to platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/JUnit5MultiverseFixtureTest.kt index fa794d3b29df..af3a422c9c1d 100644 --- a/platform/testFramework/junit5/test/showcase/JUnit5MultiverseFixtureTest.kt +++ b/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/JUnit5MultiverseFixtureTest.kt @@ -1,5 +1,5 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.testFramework.junit5.showcase +package com.intellij.platform.testFramework.junit5.projectStructure.fixture import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager @@ -7,15 +7,16 @@ import com.intellij.openapi.project.rootManager import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.junit5.TestApplication -import com.intellij.testFramework.junit5.fixture.multiverseProjectFixture -import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import kotlin.io.path.Path +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue @TestApplication class JUnit5MultiverseFixtureTest { companion object { - val multiverseFixture = multiverseProjectFixture("test-multiverse-project") { + val multiverseFixture = multiverseProjectFixture { module("module1") { contentRoot("module1PrimaryContent") { sourceRoot("source1", sourceRootId = "source1ID") { @@ -112,12 +113,6 @@ class JUnit5MultiverseFixtureTest { } module("nestedModule3") { - contentRoot("nestedModule3ContentRoot") { - sharedSourceRoot("source1ID") - } - } - - sharedContentRoot("module1PrimaryContent", "module1") { sharedSourceRoot("source1ID") } @@ -181,9 +176,6 @@ class JUnit5MultiverseFixtureTest { val module2 = moduleManager.modules.find { it.name == "module2" } ?: error("Module2 not found") val sourceRoots = module2.rootManager.sourceRoots - val sharedSourceRoot = sourceRoots.find { it.path.contains("source1") } - assertNotNull(sharedSourceRoot) { "Shared Source Root 'source1' should exist in module2" } - val uniqueSourceRoot = sourceRoots.find { it.path.contains("source2") } assertNotNull(uniqueSourceRoot) { "Unique Source Root 'source2' should exist in module2" } } @@ -207,15 +199,15 @@ class JUnit5MultiverseFixtureTest { val module1 = moduleManager.modules.find { it.name == "module1" } assertNotNull(module1, "Module1 should exist") - val module1SourceRoots = module1!!.rootManager.sourceRoots + val module1SourceRoots = module1.rootManager.sourceRoots val module1Dir1 = module1SourceRoots.flatMap { it.children.toList() } .find { it.name == "dir1" } assertNotNull(module1Dir1, "Directory dir1 should exist in module1") - val module1File1 = module1Dir1!!.findChild("file1.java") + val module1File1 = module1Dir1.findChild("file1.java") assertNotNull(module1File1, "File file1.java should exist in dir1 of module1") - val file1Content = module1File1!!.contentsToByteArray().decodeToString() + val file1Content = module1File1.contentsToByteArray().decodeToString() assert(file1Content.contains("public class A1")) { "File file1.java content does not match expected content" } @@ -228,7 +220,7 @@ class JUnit5MultiverseFixtureTest { val module1 = moduleManager.modules.find { it.name == "module1" } assertNotNull(module1, "Module1 should exist") - val module1ContentRoots = module1!!.rootManager.contentRoots + val module1ContentRoots = module1.rootManager.contentRoots assert(module1ContentRoots.size == 1) { "Expected only 1 content root for module1, but found: ${module1ContentRoots.size}" } @@ -241,23 +233,14 @@ class JUnit5MultiverseFixtureTest { val module2 = moduleManager.modules.find { it.name == "module2" } assertNotNull(module2, "Module2 should exist") - val module2SourceRoots = module2!!.rootManager.sourceRoots.map { it.toNioPath().toFile() } - - val sharedSource1 = module2SourceRoots.find { it.name == "source1" } - assertNotNull(sharedSource1, "Shared Source Root 'source1' (source1ID) should exist in module2") + val module2SourceRoots = module2.rootManager.sourceRoots.map { it.toNioPath().toFile() } val sharedSource2 = module2SourceRoots.find { it.name == "source2" } assertNotNull(sharedSource2, "Shared Source Root 'source2' (source2ID) should exist in module2") - val file1InSource1 = sharedSource1!!.walkTopDown().find { it.name == "file1.java" } - assertNotNull(file1InSource1, "File 'file1.java' should exist in shared source1 (source1ID)") - assert(file1InSource1!!.readText().contains("public class A1")) { - "File 'file1.java' content does not match expected content in shared source1" - } - - val file3InSource2 = sharedSource2!!.walkTopDown().find { it.name == "file3.txt" } + val file3InSource2 = sharedSource2.walkTopDown().find { it.name == "file3.txt" } assertNotNull(file3InSource2, "File 'file3.txt' should exist in shared source2 (source2ID)") - assert(file3InSource2!!.readText().contains("Key=Value")) { + assert(file3InSource2.readText().contains("Key=Value")) { "File 'file3.txt' content does not match expected content in shared source2" } } @@ -287,19 +270,19 @@ class JUnit5MultiverseFixtureTest { val nestedModule1 = moduleManager.modules.find { it.name == "nestedModule1" } assertNotNull(nestedModule1, "'nestedModule1' should exist") - val nestedContentRoot1 = nestedModule1!!.rootManager.contentRoots.find { it.name == "nestedContentRoot1" } + val nestedContentRoot1 = nestedModule1.rootManager.contentRoots.find { it.name == "nestedContentRoot1" } assertNotNull(nestedContentRoot1, "'nestedContentRoot1' should be the content root for nestedModule1") val nestedSourceRoot1 = nestedModule1.rootManager.sourceRoots.find { it.name == "nestedSourceRoot1" } assertNotNull(nestedSourceRoot1, "'nestedSourceRoot1' should exist in nestedModule1") - val nestedDir = nestedSourceRoot1!!.children.find { it.name == "nestedDir" } + val nestedDir = nestedSourceRoot1.children.find { it.name == "nestedDir" } assertNotNull(nestedDir, "'nestedDir' should exist in nestedModule1's source root") - val nestedFile = nestedDir!!.findChild("nestedFile.java") + val nestedFile = nestedDir.findChild("nestedFile.java") assertNotNull(nestedFile, "'nestedFile.java' should exist in 'nestedDir'") - val nestedFileContent = nestedFile!!.contentsToByteArray().decodeToString() + val nestedFileContent = nestedFile.contentsToByteArray().decodeToString() assert(nestedFileContent.contains("public class NestedClass")) { "File 'nestedFile.java' content does not match expected content in nestedModule1" } @@ -307,19 +290,19 @@ class JUnit5MultiverseFixtureTest { val nestedModule2 = moduleManager.modules.find { it.name == "nestedModule2" } assertNotNull(nestedModule2, "'nestedModule2' should exist") - val nestedContentRoot2 = nestedModule2!!.rootManager.contentRoots.find { it.name == "nestedModuleContent" } + val nestedContentRoot2 = nestedModule2.rootManager.contentRoots.find { it.name == "nestedModuleContent" } assertNotNull(nestedContentRoot2, "'nestedModuleContent' should be the content root for nestedModule2") val nestedSourceRoot2 = nestedModule2.rootManager.sourceRoots.find { it.name == "nestedSourceInContent" } assertNotNull(nestedSourceRoot2, "'nestedSourceInContent' should exist in nestedModule2") - val docsDir = nestedSourceRoot2!!.children.find { it.name == "docs" } + val docsDir = nestedSourceRoot2.children.find { it.name == "docs" } assertNotNull(docsDir, "'docs' directory should exist in nestedModule2's nested source root") - val readmeFile = docsDir!!.findChild("README.md") + val readmeFile = docsDir.findChild("README.md") assertNotNull(readmeFile, "'README.md' should exist in 'docs'") - val readmeContent = readmeFile!!.contentsToByteArray().decodeToString() + val readmeContent = readmeFile.contentsToByteArray().decodeToString() assert(readmeContent.contains("# Documentation")) { "File 'README.md' content does not match expected content in nestedModule2" } @@ -333,17 +316,17 @@ class JUnit5MultiverseFixtureTest { val nestedModule3 = moduleManager.modules.find { it.name == "nestedModule3" } assertNotNull(nestedModule3, "'nestedModule3' should exist in module2") - val nestedModule3SourceRoots = nestedModule3!!.rootManager.sourceRoots + val nestedModule3SourceRoots = nestedModule3.rootManager.sourceRoots val sharedSource1InNestedModule3 = nestedModule3SourceRoots.find { it.name == "source1" } assertNotNull(sharedSource1InNestedModule3, "'source1' should be accessible in nestedModule3 from module1") - val source1Dir = sharedSource1InNestedModule3!!.children.toList() + val source1Dir = sharedSource1InNestedModule3.children val sharedDir1 = source1Dir.find { it.name == "dir1" } assertNotNull(sharedDir1, "'dir1' should exist in the shared source1 of nestedModule3") - val sharedFile1 = sharedDir1!!.findChild("file1.java") + val sharedFile1 = sharedDir1.findChild("file1.java") assertNotNull(sharedFile1, "'file1.java' should exist in the shared source1 in nestedModule3") - val file1Content = sharedFile1!!.contentsToByteArray().decodeToString() + val file1Content = sharedFile1.contentsToByteArray().decodeToString() assert(file1Content.contains("public class A1")) { "File 'file1.java' content in shared source1 does not match expected content" } @@ -351,9 +334,9 @@ class JUnit5MultiverseFixtureTest { val sharedDir2 = source1Dir.find { it.name == "dir2" } assertNotNull(sharedDir2, "'dir2' should exist in the shared source1 of nestedModule3") - val sharedFile2 = sharedDir2!!.findChild("file2.java") + val sharedFile2 = sharedDir2.findChild("file2.java") assertNotNull(sharedFile2, "'file2.java' should exist in the shared source1 in nestedModule3") - val file2Content = sharedFile2!!.contentsToByteArray().decodeToString() + val file2Content = sharedFile2.contentsToByteArray().decodeToString() assert(file2Content.contains("public class A2")) { "File 'file2.java' content in shared source1 does not match expected content" } @@ -369,7 +352,7 @@ class JUnit5MultiverseFixtureTest { private fun assertFileExist(path: String) { val project = multiverseFixture.get() val basePath = project.basePath!! - val file = VfsUtil.findFile(Path("$basePath/$path"), false) + val file = VfsUtil.findFile(Path("$basePath/$path"), true) assertNotNull(file, path) } } \ No newline at end of file diff --git a/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/MultiverseFixtureTest.kt b/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/MultiverseFixtureTest.kt new file mode 100644 index 000000000000..876a1e761cd9 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/MultiverseFixtureTest.kt @@ -0,0 +1,44 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture + +import com.intellij.openapi.application.readAction +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.rootManager +import com.intellij.testFramework.common.timeoutRunBlocking +import com.intellij.testFramework.junit5.TestApplication +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@TestApplication +internal class MultiverseFixtureTest { + companion object { + val projectFixture = multiverseProjectFixture { + module("a") { + contentRoot("root") { + sourceRoot("src", "shared") { + file("A.java", "class A {}") + } + } + } + module("b") { + sharedSourceRoot("shared") + } + } + } + + @Test + fun `assert project structure`() = timeoutRunBlocking { + readAction { + val project = projectFixture.get() + val moduleManager = ModuleManager.getInstance(project) + val modules = moduleManager.modules + val sourceRoots = modules.map { module -> + module.rootManager.sourceRoots.single() + }.distinct() + + assertEquals(1, sourceRoots.size) { sourceRoots.toString() } + } + } +} + + diff --git a/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/SimpleProjectTest.kt b/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/SimpleProjectTest.kt new file mode 100644 index 000000000000..21428a471204 --- /dev/null +++ b/platform/testFramework/junit5/projectStructure/test/com/intellij/platform/testFramework/junit5/projectStructure/fixture/SimpleProjectTest.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.testFramework.junit5.projectStructure.fixture + +import com.intellij.openapi.application.readAction +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.rootManager +import com.intellij.testFramework.common.timeoutRunBlocking +import com.intellij.testFramework.junit5.TestApplication +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +@TestApplication +internal class SimpleProjectTest { + val projectFixture = multiverseProjectFixture { + module("a") { + contentRoot("root") { + sourceRoot("src", "shared") { + file("A.java", "class A {}") + } + } + } + } + + @Test + fun `assert project structure`() = timeoutRunBlocking { + readAction { + val project = projectFixture.get() + val moduleManager = ModuleManager.getInstance(project) + val modules = moduleManager.modules + assertEquals(1, modules.size, modules.toString()) + + val sourceRoots = modules.single().rootManager.sourceRoots + assertEquals(1, sourceRoots.size, sourceRoots.toString()) + + val sourceRoot = sourceRoots.single() + val path = sourceRoot.path + assert(path.endsWith("a/root/src")) { path } + } + } +} \ No newline at end of file diff --git a/platform/testFramework/junit5/src/fixture/MultiverseProjectBuilder.kt b/platform/testFramework/junit5/src/fixture/MultiverseProjectBuilder.kt deleted file mode 100644 index ffaffe04aac9..000000000000 --- a/platform/testFramework/junit5/src/fixture/MultiverseProjectBuilder.kt +++ /dev/null @@ -1,508 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.testFramework.junit5.fixture - -import com.intellij.openapi.application.readAction -import com.intellij.openapi.application.edtWriteAction -import com.intellij.openapi.module.Module -import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ModuleRootModificationUtil -import com.intellij.openapi.util.io.toCanonicalPath -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.openapi.vfs.VfsUtil -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiDirectory -import com.intellij.psi.PsiManager -import com.intellij.util.io.createDirectories -import com.intellij.util.io.delete -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.jetbrains.annotations.TestOnly -import java.nio.file.Files -import java.nio.file.Path -import kotlin.io.path.exists - -@TestOnly -fun multiverseProjectFixture(name: String, init: MultiverseProjectBuilder.() -> Unit): TestFixture { - val builder = MultiverseProjectBuilder(name) - builder.init() - return buildMultiverseFixture(builder) -} - -class MultiverseProjectBuilder(val name: String) { - private val directories: MutableList = mutableListOf() - private val modules: MutableList = mutableListOf() - private val sourceRootRegistry: MutableMap = mutableMapOf() - private val contentRoots: MutableMap> = mutableMapOf() - - fun getModules(): List = modules - fun getContentRoot(name: String): Map? = contentRoots[name] - - fun dir(name: String, init: DirectoryBuilder.() -> Unit) { - val directory = DirectoryBuilder(name, path = name) - directory.init() - directories.add(directory) - } - - fun module(moduleName: String, init: ModuleBuilder.() -> Unit) { - val moduleBuilder = ModuleBuilder(moduleName, modulePath = moduleName) - moduleBuilder.init() - modules.add(moduleBuilder) - } - - inner class ModuleBuilder(val moduleName: String, private val modulePath: String) : DirectoryContainer, ModuleContainer { - private val directories: MutableList = mutableListOf() - private val files: MutableList = mutableListOf() - private val nestedModules: MutableList = mutableListOf() - - override fun getDirectories(): List = directories - override fun getFiles(): List = files - - override fun getNestedModules(): List = nestedModules - fun getPath(): String = modulePath - - fun contentRoot(name: String, init: ContentRootBuilder.() -> Unit) { - val contentRootPath = "$modulePath/$name" - val moduleMap = contentRoots.getOrPut(moduleName) { mutableMapOf() } - val contentRootBuilder = moduleMap[name] - ?: ContentRootBuilder(name, contentRootPath).also { moduleMap[name] = it } - contentRootBuilder.init() - } - - fun sharedContentRoot(contentRootName: String, sourceModuleName: String, sharedSourceRootInit: ContentRootBuilder.() -> Unit = {}) { - val sourceContentMap = contentRoots[sourceModuleName] - ?: throw IllegalArgumentException("Module '$sourceModuleName' has no content roots registered!") - - val sourceContentRoot = sourceContentMap[contentRootName] - ?: throw IllegalArgumentException("Content root '$contentRootName' does not exist in module '$sourceModuleName'.") - - val moduleMap = contentRoots.getOrPut(moduleName) { mutableMapOf() } - moduleMap[contentRootName] = ContentRootBuilder( - name = sourceContentRoot.name, - path = sourceContentRoot.getContentRootPath(), - isExisting = true - ).apply(sharedSourceRootInit) - } - - fun sharedSourceRoot(sourceRootId: String) { - val sharedSourceRootBuilder = sourceRootRegistry[sourceRootId] - ?: throw IllegalArgumentException("Source root ID '$sourceRootId' does not exist in the registry!") - - val contentRootName = sharedSourceRootBuilder.name - val moduleMap = contentRoots.getOrPut(moduleName) { mutableMapOf() } - val existingContentRoot = moduleMap[contentRootName] - - val contentRoot = existingContentRoot ?: ContentRootBuilder( - name = contentRootName, - path = sharedSourceRootBuilder.getSourceRootPath() ?: "", - isExisting = true - ).also { moduleMap[contentRootName] = it } - - contentRoot.addSourceRoot(sharedSourceRootBuilder) - } - - override fun dir(name: String, init: DirectoryBuilder.() -> Unit) { - val directoryPath = "$modulePath/$name" - val directory = DirectoryBuilder(name, directoryPath) - directory.init() - directories.add(directory) - } - - override fun file(fileName: String, content: String) { - files.add(FileBuilder(fileName, content, "$modulePath/$fileName")) - } - - override fun module(moduleName: String, init: ModuleBuilder.() -> Unit) { - val nestedModulePath = "$modulePath/$moduleName" - val nestedModule = ModuleBuilder(moduleName, nestedModulePath).apply(init) - nestedModules.add(nestedModule) - } - } - - inner class ContentRootBuilder( - val name: String, - private val path: String, - val isExisting: Boolean = false, - ) : DirectoryContainer, ModuleContainer { - private val directories: MutableList = mutableListOf() - private val files: MutableList = mutableListOf() - private val nestedModules: MutableList = mutableListOf() - private val sourceRoots: MutableList = mutableListOf() - - override fun getDirectories(): List = directories - override fun getFiles(): List = files - override fun getNestedModules(): List = nestedModules - fun getSourceRoots(): List = sourceRoots - fun addSourceRoot(sourceRoot: SourceRootBuilder) { - if (sourceRoots.none { it.name == sourceRoot.name }) { - sourceRoots.add(sourceRoot) - } - } - - fun sourceRoot(name: String, sourceRootId: String? = null, init: SourceRootBuilder.() -> Unit) { - val sourceRootPath = "$path/$name" - val sourceRootBuilder = SourceRootBuilder(name, sourceRootPath).apply(init) - sourceRoots.add(sourceRootBuilder) - - sourceRootId?.let { id -> - if (!sourceRootRegistry.containsKey(id)) { - sourceRootRegistry[id] = sourceRootBuilder - } - } - } - - fun sharedSourceRoot(sourceRootId: String) { - val sharedSourceRootBuilder = sourceRootRegistry[sourceRootId] - ?: throw IllegalArgumentException("Source root ID '$sourceRootId' does not exist in the registry!") - - if (sourceRoots.none { it.name == sharedSourceRootBuilder.name }) { - sourceRoots.add(sharedSourceRootBuilder) - } - } - - override fun dir(name: String, init: DirectoryBuilder.() -> Unit) { - val directoryPath = "$path/$name" - val directory = DirectoryBuilder(name, directoryPath) - directory.init() - directories.add(directory) - } - - override fun file(fileName: String, content: String) { - files.add(FileBuilder(fileName, content, "$path/$fileName")) - } - - override fun module(moduleName: String, init: ModuleBuilder.() -> Unit) { - val nestedModulePath = "$path/$moduleName" - val nestedModule = ModuleBuilder(moduleName, nestedModulePath).apply(init) - nestedModules.add(nestedModule) - } - - fun getContentRootPath(): String = path - } - - inner class SourceRootBuilder( - val name: String, - private val path: String? = null, - val isExisting: Boolean = false, - ) : DirectoryContainer, ModuleContainer { - private val directories: MutableList = mutableListOf() - private val nestedModules: MutableList = mutableListOf() - private val files: MutableList = mutableListOf() - - override fun getDirectories(): List = directories - override fun getNestedModules(): List = nestedModules - override fun getFiles(): List = files - - override fun file(fileName: String, content: String) { - files.add(FileBuilder(fileName, content, "$path/$fileName")) - } - - override fun dir(name: String, init: DirectoryBuilder.() -> Unit) { - val directoryPath = "$path/$name" - val directory = DirectoryBuilder(name, directoryPath) - directory.init() - directories.add(directory) - } - - override fun module(moduleName: String, init: ModuleBuilder.() -> Unit) { - val nestedModulePath = "$path/$moduleName" - val nestedModule = ModuleBuilder(moduleName, nestedModulePath).apply(init) - nestedModules.add(nestedModule) - } - - fun getSourceRootPath(): String? = path - } - - inner class DirectoryBuilder(val name: String, private val path: String) : DirectoryContainer, ModuleContainer { - private val directories: MutableList = mutableListOf() - private val nestedModules: MutableList = mutableListOf() - private val files: MutableList = mutableListOf() - - override fun getDirectories(): List = directories - override fun getNestedModules(): List = nestedModules - override fun getFiles(): List = files - - override fun file(fileName: String, content: String) { - files.add(FileBuilder(fileName, content, "$path/$fileName")) - } - - override fun dir(name: String, init: DirectoryBuilder.() -> Unit) { - val directoryPath = "$path/$name" - val directory = DirectoryBuilder(name, directoryPath) - directory.init() - directories.add(directory) - } - - override fun module(moduleName: String, init: ModuleBuilder.() -> Unit) { - val nestedModulePath = "$path/$moduleName" - val nestedModule = ModuleBuilder(moduleName, nestedModulePath).apply(init) - nestedModules.add(nestedModule) - } - } - - inner class FileBuilder(private val name: String, private val content: String, private val path: String) { - fun getFileName(): String = name - fun getFilePath(): String = path - fun getFileContent(): String = content - } - - interface DirectoryContainer { - fun getDirectories(): List - fun getFiles(): List - fun dir(name: String, init: DirectoryBuilder.() -> Unit) - fun file(fileName: String, content: String) - } - - interface ModuleContainer { - fun getNestedModules(): List - fun module(moduleName: String, init: ModuleBuilder.() -> Unit) - } -} - -private suspend fun getPsiDirectoryFromVfs(psiManager: PsiManager, directoryVfs: VirtualFile): PsiDirectory { - return readAction { - psiManager.findDirectory(directoryVfs) - ?: error("Failed to find PsiDirectory for VirtualFile: $directoryVfs") - } -} - -private suspend fun createDirectory(targetPath: Path): VirtualFile { - return edtWriteAction { - VfsUtil.createDirectories(targetPath.toCanonicalPath()) - ?: error("Failed to create directory at path: $targetPath") - } -} - -private suspend fun deleteDirectorySafely(psiDirectory: PsiDirectory, targetPath: Path, project: Project) { - edtWriteAction { - if (!psiDirectory.isValid) { - val resolvedVfsDirectory = LocalFileSystem.getInstance().findFileByPath(targetPath.toString()) - resolvedVfsDirectory?.delete(project) - } - else { - psiDirectory.delete() - } - } -} - -@TestOnly -private fun TestFixture.customDirectoryFixture( - root: Path, - dirName: String, - getProject: (T) -> Project, - onDirectoryCreated: (VirtualFile, T) -> Unit, -): TestFixture = testFixture { - val targetPath = root.resolve(dirName).toAbsolutePath() - val parent = this@customDirectoryFixture.init() - - val parentPath = targetPath.parent - if (parentPath != null && !Files.exists(parentPath)) { - createDirectory(parentPath) - } - - val directoryVfs = createDirectory(targetPath) - - onDirectoryCreated(directoryVfs, parent) - - val psiDirectory = getPsiDirectoryFromVfs(PsiManager.getInstance(getProject(parent)), directoryVfs) - - initialized(psiDirectory) { - deleteDirectorySafely(psiDirectory, targetPath, getProject(parent)) - } -} - -private val getProjectFromModule: (Module) -> Project = { it.project } -private val getProjectFromPsiDirectory: (PsiDirectory) -> Project = { it.project } - -@TestOnly -private fun TestFixture.customPsiDirectoryFixture( - root: Path, - dirName: String, -): TestFixture = customDirectoryFixture(root, dirName, getProjectFromPsiDirectory) { _, _ -> - // No additional actions are needed for PsiDirectory fixture. -} - -@TestOnly -private fun TestFixture.customContentRootFixture( - root: Path, - dirName: String, -): TestFixture = customDirectoryFixture(root, dirName, getProjectFromModule) { directoryVfs, module -> - ModuleRootModificationUtil.updateModel(module) { model -> - model.addContentEntry(directoryVfs) - } -} - -@TestOnly -private fun TestFixture.customSourceRootFixture( - isTestSource: Boolean = false, - contentEntry: Path, - dirName: String, -): TestFixture = customDirectoryFixture(contentEntry, dirName, getProjectFromModule) { directoryVfs, module -> - ModuleRootModificationUtil.updateModel(module) { model -> - val entry = model.contentEntries.find { it.file?.path == contentEntry.toAbsolutePath().toString() } - if (entry == null) { - println("Warning: Content entry with path '${contentEntry.toAbsolutePath()}' was not found in module ${module.name}.") - return@updateModel - } - entry.addSourceFolder(directoryVfs, isTestSource) - } -} - -/** - * Creates path fixture without generating a random name for directory inside. - */ -@TestOnly -private fun customPathFixture(root: Path): TestFixture = testFixture { - val tempDir = withContext(Dispatchers.IO) { - if (!root.exists()) { - root.createDirectories() - } - root - } - initialized(tempDir) { - withContext(Dispatchers.IO) { - if (tempDir.exists()) { - tempDir.delete(recursively = true) - } - } - } -} - -@TestOnly -private fun buildMultiverseFixture(builder: MultiverseProjectBuilder): TestFixture = testFixture(builder.name) { - val initializer = MultiverseFixtureInitializer(builder) - val project = with(initializer) { - initializeProjectModel() - } - - initialized(project) { - /* - Nothing to dispose - */ - } -} - -@TestOnly -private class MultiverseFixtureInitializer(private val builder: MultiverseProjectBuilder) { - private lateinit var projectFixture: TestFixture - - suspend fun TestFixtureInitializer.R.initializeProjectModel(): Project { - this@MultiverseFixtureInitializer.projectFixture = projectFixture() - val project = projectFixture.init() - - val basePath = project.basePath?.let { Path.of(it) } - ?: throw IllegalStateException("Project base path is not available") - - builder.getModules().forEach { module -> - initializeModule(basePath, module) - } - - return project - } - - private suspend fun TestFixtureInitializer.R.initializeModule( - parentPath: Path, - module: MultiverseProjectBuilder.ModuleBuilder, - ) { - val modulePath = parentPath.resolve(module.moduleName) - val modulePathFixture = customPathFixture(modulePath) - val moduleFixture = projectFixture.moduleFixture(modulePathFixture) - moduleFixture.init() - - val contentRoots = builder.getContentRoot(module.moduleName) - ?: throw IllegalStateException("Content roots entity for module '${module.moduleName}' is null") - - contentRoots.values.forEach { contentRoot -> - initializeContentRoot(modulePath, contentRoot, moduleFixture) - } - - module.getNestedModules().forEach { nestedModule -> - initializeModule(modulePath, nestedModule) - } - - initializeDirectoriesAndFiles(modulePath, modulePathFixture.asPsiDirectoryFixture(), module) - } - - private suspend fun TestFixtureInitializer.R.initializeContentRoot( - modulePath: Path, - contentRoot: MultiverseProjectBuilder.ContentRootBuilder, - moduleFixture: TestFixture, - ) { - val contentRootPath = modulePath.resolve(contentRoot.name) - val contentRootFixture = if (contentRoot.isExisting) { - moduleFixture.customContentRootFixture(contentRootPath.parent, contentRoot.name) - } - else { - moduleFixture.customContentRootFixture(modulePath, contentRoot.name) - } - contentRootFixture.init() - - initializeDirectoriesAndFiles( - parentPath = contentRootPath, - psiFixture = contentRootFixture, - parent = contentRoot - ) - - contentRoot.getSourceRoots().forEach { sourceRoot -> - val sourceRootPath = contentRootPath.resolve(sourceRoot.name) - val sourceRootFixture = moduleFixture.customSourceRootFixture(contentEntry = contentRootPath, dirName = sourceRoot.name) - sourceRootFixture.init() - - if (!sourceRoot.isExisting) { - initializeDirectoriesAndFiles( - parentPath = sourceRootPath, - psiFixture = sourceRootFixture, - parent = sourceRoot - ) - - sourceRoot.getNestedModules().forEach { nestedModule -> - initializeModule(sourceRootPath, nestedModule) - } - } - } - - contentRoot.getNestedModules().forEach { nestedModule -> - initializeModule(contentRootPath, nestedModule) - } - } - - private suspend fun TestFixtureInitializer.R.initializeDirectoriesAndFiles( - parentPath: Path, - psiFixture: TestFixture, - parent: MultiverseProjectBuilder.DirectoryContainer, - ) { - val directories = parent.getDirectories() - directories.forEach { directory -> - val directoryFixture = psiFixture.customPsiDirectoryFixture( - root = parentPath, - dirName = directory.name - ) - - directoryFixture.init() - - val directoryPath = parentPath.resolve(directory.name) - - initializeDirectoriesAndFiles( - parentPath = directoryPath, - psiFixture = directoryFixture, - parent = directory - ) - - directory.getNestedModules().forEach { nestedModule -> - initializeModule(directoryPath, nestedModule) - } - } - - val files = parent.getFiles() - files.forEach { file -> - psiFixture.psiFileFixture(file.getFileName(), file.getFileContent()).init() - } - } - - private fun TestFixture.asPsiDirectoryFixture(): TestFixture = testFixture { - val dirPath = this@asPsiDirectoryFixture.init() - val project = projectFixture.init() - val vDir = VfsUtil.findFile(dirPath, true) ?: throw IllegalStateException("Virtual file for path '$dirPath' was not found") - val dir = readAction { PsiManager.getInstance(project).findDirectory(vDir) } ?: throw IllegalStateException("PsiDirectory for path '$dirPath' was not found") - initialized(dir) {/*nothing to clean up*/ } - } -}