IJPL-339 refactor multiverseProjectFixture

GitOrigin-RevId: 1e419998ac8352b209b10e88c73a2cf742acf67b
This commit is contained in:
Max Medvedev
2025-02-27 01:39:03 +00:00
committed by intellij-monorepo-bot
parent bf6b033b3a
commit a15ecb51c5
26 changed files with 600 additions and 569 deletions
+1
View File
@@ -882,6 +882,7 @@
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/junit5/eel/intellij.platform.testFramework.junit5.eel.iml" filepath="$PROJECT_DIR$/platform/testFramework/junit5/eel/intellij.platform.testFramework.junit5.eel.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/junit5/highlighting/intellij.platform.testFramework.junit5.highlighting.iml" filepath="$PROJECT_DIR$/platform/testFramework/junit5/highlighting/intellij.platform.testFramework.junit5.highlighting.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/junit5.jimfs/intellij.platform.testFramework.junit5.jimfs.iml" filepath="$PROJECT_DIR$/platform/testFramework/junit5.jimfs/intellij.platform.testFramework.junit5.jimfs.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml" filepath="$PROJECT_DIR$/platform/testFramework/junit5/projectStructure/intellij.platform.testFramework.junit5.projectStructure.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/teamCity/intellij.platform.testFramework.teamCity.iml" filepath="$PROJECT_DIR$/platform/testFramework/teamCity/intellij.platform.testFramework.teamCity.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/ui/intellij.platform.testFramework.ui.iml" filepath="$PROJECT_DIR$/platform/testFramework/ui/intellij.platform.testFramework.ui.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/testRunner/intellij.platform.testRunner.iml" filepath="$PROJECT_DIR$/platform/testRunner/intellij.platform.testRunner.iml" />
+1
View File
@@ -101,5 +101,6 @@
<orderEntry type="module" module-name="intellij.platform.searchEverywhere.testFramework" />
<orderEntry type="module" module-name="intellij.java.codeserver.highlighting" scope="TEST" />
<orderEntry type="module" module-name="intellij.java.codeserver.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5.projectStructure" scope="TEST" />
</component>
</module>
@@ -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 {
@@ -59,5 +59,6 @@
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5.eel" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.statistics.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5.projectStructure" scope="TEST" />
</component>
</module>
@@ -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
@@ -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<Project>.withSharedSourceEnabled(): TestFixture<Project> = testFixture("shared-source-enabling-fixture") {
MultiverseTestEnabler.enableSharedSourcesForTheNextProject()
initialized(this@withSharedSourceEnabled.init()) {}
}
fun sharedSourceRootFixture(vararg moduleFixtures: TestFixture<Module>): TestFixture<PsiDirectory> = testFixture("shared-source-root-fixture") {
require(moduleFixtures.isNotEmpty())
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" name="kotlin-test" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.core.impl" />
</component>
</module>
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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<Project> {
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<Project>.withSharedSourceEnabled(): TestFixture<Project> = testFixture("shared-source-enabling-fixture") {
MultiverseTestEnabler.enableSharedSourcesForTheNextProject()
initialized(this@withSharedSourceEnabled.init()) {}
}
@@ -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<SourceRootBuilderImpl> = mutableListOf()
val sourceRoots: List<SourceRootBuilderImpl> 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)
}
}
@@ -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<DirectoryBuilderImpl> = mutableListOf()
private val _nestedModules: MutableList<ModuleBuilderImpl> = mutableListOf()
private val _files: MutableList<FileBuilderImpl> = mutableListOf()
override val directories: List<DirectoryBuilderImpl> get() = _directories
override val modules: List<ModuleBuilderImpl> get() = _nestedModules
override val files: List<FileBuilderImpl> 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"
}
}
@@ -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)
@@ -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<DirectoryBuilderImpl>
val files: List<FileBuilderImpl>
val modules: List<ModuleBuilderImpl>
}
@@ -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,
)
@@ -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<ContentRootBuilderImpl> = mutableListOf()
val contentRoots: List<ContentRootBuilderImpl> 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)
}
}
@@ -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<Project>
private lateinit var projectRootPath: Path
private val structure = ProjectStructure()
suspend fun TestFixtureInitializer.R<Project>.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<Project>.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<Project>.initializeContentRoot(
contentRoot: ContentRootBuilderImpl,
moduleFixture: TestFixture<Module>,
) {
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<Project>.initializeSourceRoot(
moduleFixture: TestFixture<Module>,
contentRootFixture: TestFixture<Path>,
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<Project>.initializeChildren(
container: DirectoryContainer,
containerFixture: TestFixture<Path>,
) {
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)
}
@@ -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<String, SourceRootBuilderImpl> = 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
}
}
@@ -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)
@@ -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<Module>.customContentRootFixture(
pathFixture: TestFixture<Path>,
): TestFixture<Path> = 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<Module>.customSourceRootFixture(
pathFixture: TestFixture<Path>,
contentRootFixture: TestFixture<Path>,
isTestSource: Boolean = false,
isResource: Boolean = false,
): TestFixture<Path> = 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<Path> = 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<Path>.subDirFixture(name: String): TestFixture<Path> = testFixture("subDirFixture") {
val path = this@subDirFixture.init().resolve(name)
val result = dirFixture(path).init()
initialized(result) {}
}
@TestOnly
internal fun TestFixture<Path>.fileFixture(fileName: String, content: CharSequence): TestFixture<Path> = 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
}
@@ -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)
}
}
@@ -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() }
}
}
}
@@ -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 }
}
}
}
@@ -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<Project> {
val builder = MultiverseProjectBuilder(name)
builder.init()
return buildMultiverseFixture(builder)
}
class MultiverseProjectBuilder(val name: String) {
private val directories: MutableList<DirectoryBuilder> = mutableListOf()
private val modules: MutableList<ModuleBuilder> = mutableListOf()
private val sourceRootRegistry: MutableMap<String, SourceRootBuilder> = mutableMapOf()
private val contentRoots: MutableMap<String, MutableMap<String, ContentRootBuilder>> = mutableMapOf()
fun getModules(): List<ModuleBuilder> = modules
fun getContentRoot(name: String): Map<String, ContentRootBuilder>? = 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<DirectoryBuilder> = mutableListOf()
private val files: MutableList<FileBuilder> = mutableListOf()
private val nestedModules: MutableList<ModuleBuilder> = mutableListOf()
override fun getDirectories(): List<DirectoryBuilder> = directories
override fun getFiles(): List<FileBuilder> = files
override fun getNestedModules(): List<ModuleBuilder> = 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<DirectoryBuilder> = mutableListOf()
private val files: MutableList<FileBuilder> = mutableListOf()
private val nestedModules: MutableList<ModuleBuilder> = mutableListOf()
private val sourceRoots: MutableList<SourceRootBuilder> = mutableListOf()
override fun getDirectories(): List<DirectoryBuilder> = directories
override fun getFiles(): List<FileBuilder> = files
override fun getNestedModules(): List<ModuleBuilder> = nestedModules
fun getSourceRoots(): List<SourceRootBuilder> = 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<DirectoryBuilder> = mutableListOf()
private val nestedModules: MutableList<ModuleBuilder> = mutableListOf()
private val files: MutableList<FileBuilder> = mutableListOf()
override fun getDirectories(): List<DirectoryBuilder> = directories
override fun getNestedModules(): List<ModuleBuilder> = nestedModules
override fun getFiles(): List<FileBuilder> = 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<DirectoryBuilder> = mutableListOf()
private val nestedModules: MutableList<ModuleBuilder> = mutableListOf()
private val files: MutableList<FileBuilder> = mutableListOf()
override fun getDirectories(): List<DirectoryBuilder> = directories
override fun getNestedModules(): List<ModuleBuilder> = nestedModules
override fun getFiles(): List<FileBuilder> = 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<DirectoryBuilder>
fun getFiles(): List<FileBuilder>
fun dir(name: String, init: DirectoryBuilder.() -> Unit)
fun file(fileName: String, content: String)
}
interface ModuleContainer {
fun getNestedModules(): List<ModuleBuilder>
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 <T> TestFixture<T>.customDirectoryFixture(
root: Path,
dirName: String,
getProject: (T) -> Project,
onDirectoryCreated: (VirtualFile, T) -> Unit,
): TestFixture<PsiDirectory> = 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<PsiDirectory>.customPsiDirectoryFixture(
root: Path,
dirName: String,
): TestFixture<PsiDirectory> = customDirectoryFixture(root, dirName, getProjectFromPsiDirectory) { _, _ ->
// No additional actions are needed for PsiDirectory fixture.
}
@TestOnly
private fun TestFixture<Module>.customContentRootFixture(
root: Path,
dirName: String,
): TestFixture<PsiDirectory> = customDirectoryFixture(root, dirName, getProjectFromModule) { directoryVfs, module ->
ModuleRootModificationUtil.updateModel(module) { model ->
model.addContentEntry(directoryVfs)
}
}
@TestOnly
private fun TestFixture<Module>.customSourceRootFixture(
isTestSource: Boolean = false,
contentEntry: Path,
dirName: String,
): TestFixture<PsiDirectory> = 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<Path> = 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<Project> = 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<Project>
suspend fun TestFixtureInitializer.R<Project>.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<Project>.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<Project>.initializeContentRoot(
modulePath: Path,
contentRoot: MultiverseProjectBuilder.ContentRootBuilder,
moduleFixture: TestFixture<Module>,
) {
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<Project>.initializeDirectoriesAndFiles(
parentPath: Path,
psiFixture: TestFixture<PsiDirectory>,
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<Path>.asPsiDirectoryFixture(): TestFixture<PsiDirectory> = 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*/ }
}
}