IJPL-185294 [refactoring] Move loadExtensionWithText to intellij.platform.testFramework

GitOrigin-RevId: fd41e2f70858313b1b56a7aae6e2e4fed995c9f7
This commit is contained in:
Ilia Malakhov
2025-05-06 14:38:37 +02:00
committed by intellij-monorepo-bot
parent 48f8d29dd0
commit 32a5548e23
25 changed files with 383 additions and 345 deletions

View File

@@ -6,10 +6,10 @@ import com.intellij.facet.FacetManager;
import com.intellij.facet.mock.*;
import com.intellij.framework.detection.impl.FacetBasedDetectedFrameworkDescription;
import com.intellij.framework.detection.impl.FrameworkDetectionManager;
import com.intellij.ide.plugins.DynamicPluginsTestUtil;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.testFramework.DynamicPluginTestUtilsKt;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.testFramework.VfsTestUtil;
@@ -52,7 +52,7 @@ public class FrameworkDetectionTest extends FrameworkDetectionTestCase {
VirtualFile file = createFrameworkConfig("my-config.xml");
assertNoFrameworksDetected();
Disposer.register(getTestRootDisposable(), DynamicPluginsTestUtil.loadExtensionWithText(
Disposer.register(getTestRootDisposable(), DynamicPluginTestUtilsKt.loadExtensionWithText(
"<framework.detector implementation=\"" + MockFacetDetector.class.getName() + "\"/>",
"com.intellij"));

View File

@@ -6,10 +6,10 @@ import com.intellij.facet.mock.MockFacetDetector;
import com.intellij.facet.mock.MockSubFacetDetector;
import com.intellij.framework.detection.impl.FrameworkDetectionManager;
import com.intellij.framework.detection.impl.FrameworkDetectionUtil;
import com.intellij.ide.plugins.DynamicPluginsTestUtil;
import com.intellij.openapi.roots.PlatformModifiableModelsProvider;
import com.intellij.openapi.roots.ui.configuration.DefaultModulesProvider;
import com.intellij.openapi.util.Disposer;
import com.intellij.platform.testFramework.DynamicPluginTestUtilsKt;
import java.util.List;
@@ -19,11 +19,11 @@ public abstract class FrameworkDetectionTestCase extends FacetTestCase {
super.setUp();
if (!"dynamicDetector".equals(getTestName(true))) {
//todo we can get rid of this ugly check by converting facet tests to JUnit4 and using test rules to enable facet detection
Disposer.register(getTestRootDisposable(), DynamicPluginsTestUtil.loadExtensionWithText(
Disposer.register(getTestRootDisposable(), DynamicPluginTestUtilsKt.loadExtensionWithText(
"<framework.detector implementation=\"" + MockFacetDetector.class.getName() + "\"/>",
"com.intellij"));
Disposer.register(getTestRootDisposable(), DynamicPluginsTestUtil.loadExtensionWithText(
Disposer.register(getTestRootDisposable(), DynamicPluginTestUtilsKt.loadExtensionWithText(
"<framework.detector implementation=\"" + MockSubFacetDetector.class.getName() + "\"/>",
"com.intellij"));
}

View File

@@ -1,7 +1,7 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.index.propertyBased
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.openapi.extensions.InternalIgnoreDependencyViolation
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypeManager

View File

@@ -1,7 +1,7 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.lang
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.fileTypes.PlainTextLanguage
import com.intellij.openapi.project.Project

View File

@@ -5,13 +5,13 @@ import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.daemon.LineMarkerInfo;
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl;
import com.intellij.codeInsight.daemon.impl.MarkerType;
import com.intellij.ide.plugins.DynamicPluginsTestUtil;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.extensions.InternalIgnoreDependencyViolation;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.impl.LibraryScopeCache;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.platform.testFramework.DynamicPluginTestUtilsKt;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
@@ -43,7 +43,7 @@ public class LibraryUseSearchUsingScopeEnlargerTest extends JavaCodeInsightFixtu
@Override
protected void setUp() throws Exception {
super.setUp();
Disposer.register(getTestRootDisposable(), DynamicPluginsTestUtil.loadExtensionWithText(
Disposer.register(getTestRootDisposable(), DynamicPluginTestUtilsKt.loadExtensionWithText(
"<useScopeEnlarger implementation=\"com.intellij.scopes.LibraryUseSearchUsingScopeEnlargerTest$LibraryUseScopeEnlarger\"/>",
"com.intellij"));
//bug? Test seems to use stale data.

View File

@@ -1,7 +1,7 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.indexing
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.openapi.extensions.InternalIgnoreDependencyViolation
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypeRegistry

View File

@@ -1,7 +1,7 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.indexing
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.openapi.Disposable
import com.intellij.openapi.extensions.InternalIgnoreDependencyViolation
import com.intellij.openapi.util.Disposer

View File

@@ -1,7 +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.util.indexing
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.extensions.InternalIgnoreDependencyViolation
import com.intellij.openapi.project.Project

View File

@@ -2,7 +2,7 @@
package com.intellij.util.indexing
import com.intellij.find.ngrams.TrigramIndex
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.openapi.extensions.InternalIgnoreDependencyViolation
import com.intellij.openapi.module.JavaModuleType
import com.intellij.openapi.project.DumbService

View File

@@ -1,10 +1,10 @@
// 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.util.indexing
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.openapi.Disposable
import com.intellij.openapi.extensions.InternalIgnoreDependencyViolation
import com.intellij.openapi.util.Disposer
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor
import com.intellij.util.io.VoidDataExternalizer

View File

@@ -62,6 +62,5 @@
<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" />
<orderEntry type="module" module-name="intellij.platform.tests" scope="TEST" />
</component>
</module>

View File

@@ -4,7 +4,7 @@ package com.intellij.util.indexing
import com.intellij.find.TextSearchService
import com.intellij.ide.impl.ProjectUtil
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.plugins.loadExtensionWithText
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.openapi.application.*
import com.intellij.openapi.components.service
import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher

View File

@@ -7,6 +7,7 @@ import com.intellij.ide.plugins.cl.PluginClassLoader
import com.intellij.openapi.util.BuildNumber
import com.intellij.platform.ide.bootstrap.ZipFilePoolImpl
import com.intellij.platform.plugins.parser.impl.PluginDescriptorBuilder
import com.intellij.platform.testFramework.PluginBuilder
import com.intellij.testFramework.assertions.Assertions.assertThat
import com.intellij.testFramework.rules.InMemoryFsExtension
import com.intellij.util.io.directoryStreamIfExists

View File

@@ -40,6 +40,11 @@ import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.use
import com.intellij.platform.testFramework.PluginBuilder
import com.intellij.platform.testFramework.loadAndInitDescriptorInTest
import com.intellij.platform.testFramework.loadExtensionWithText
import com.intellij.platform.testFramework.setPluginClassLoaderForMainAndSubPlugins
import com.intellij.platform.testFramework.unloadAndUninstallPlugin
import com.intellij.psi.PsiFile
import com.intellij.testFramework.EdtRule
import com.intellij.testFramework.ProjectRule
@@ -86,7 +91,7 @@ class DynamicPluginsTest {
pluginBuilder: PluginBuilder,
disabledPlugins: Set<String> = emptySet(),
): Disposable {
return loadPluginWithText(
return com.intellij.platform.testFramework.loadPluginWithText(
pluginBuilder = pluginBuilder,
path = rootPath.resolve(Ksuid.generate()),
disabledPlugins = disabledPlugins,
@@ -811,7 +816,8 @@ class DynamicPluginsTest {
private fun loadPluginWithOptionalDependency(pluginDescriptor: PluginBuilder,
optionalDependencyDescriptor: PluginBuilder,
dependsOn: PluginBuilder): Disposable {
dependsOn: PluginBuilder
): Disposable {
pluginDescriptor.depends(dependsOn.id, optionalDependencyDescriptor)
return loadPluginWithText(pluginDescriptor)
}

View File

@@ -1,27 +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.ide.plugins
import com.intellij.openapi.extensions.PluginId
import com.intellij.platform.plugins.parser.impl.PluginDescriptorBuilder
import com.intellij.platform.plugins.parser.impl.PluginDescriptorFromXmlStreamConsumer
import com.intellij.platform.plugins.parser.impl.PluginXmlConst
import com.intellij.platform.plugins.parser.impl.ReadModuleContext
import com.intellij.platform.plugins.parser.impl.consume
import com.intellij.platform.plugins.parser.impl.elements.OS
import com.intellij.util.io.Compressor
import com.intellij.util.io.createParentDirectories
import com.intellij.util.io.write
import com.intellij.util.xml.dom.NoOpXmlInterner
import org.intellij.lang.annotations.Language
import org.jetbrains.annotations.TestOnly
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.createDirectories
private val pluginIdCounter = AtomicInteger()
fun plugin(outDir: Path, @Language("XML") descriptor: String) {
val rawDescriptor = try {
@@ -53,306 +42,6 @@ fun module(outDir: Path, ownerId: String, moduleId: String, @Language("XML") des
outDir.resolve("$ownerId/$moduleId.xml").write(descriptor.trimIndent())
}
class PluginBuilder private constructor() {
private data class ExtensionBlock(val ns: String, val text: String)
private data class DependsTag(val pluginId: String, val configFile: String?)
// counter is used to reduce plugin id length
var id: String = "p_${pluginIdCounter.incrementAndGet()}"
private set
private var implementationDetail = false
private var separateJar = false
private var name: String? = null
private var description: String? = null
private var packagePrefix: String? = null
private val dependsTags = mutableListOf<DependsTag>()
private var applicationListeners: String? = null
private var resourceBundleBaseName: String? = null
private var actions: String? = null
private val extensions = mutableListOf<ExtensionBlock>()
private var extensionPoints: String? = null
private var untilBuild: String? = null
private var sinceBuild: String? = null
private var version: String? = null
private val pluginAliases = mutableListOf<String>()
private val content = mutableListOf<PluginContentDescriptor.ModuleItem>()
private val dependencies = mutableListOf<ModuleDependencies.ModuleReference>()
private val pluginDependencies = mutableListOf<ModuleDependencies.PluginReference>()
private val incompatibleWith = mutableListOf<ModuleDependencies.PluginReference>()
private data class SubDescriptor(val filename: String, val builder: PluginBuilder)
private val subDescriptors = ArrayList<SubDescriptor>()
fun dependsIntellijModulesLang(): PluginBuilder {
depends("com.intellij.modules.lang")
return this
}
fun id(id: String): PluginBuilder {
this.id = id
return this
}
fun randomId(idPrefix: String): PluginBuilder {
this.id = "${idPrefix}_${pluginIdCounter.incrementAndGet()}"
return this
}
fun name(name: String): PluginBuilder {
this.name = name
return this
}
fun description(description: String): PluginBuilder {
this.description = description
return this
}
fun packagePrefix(value: String?): PluginBuilder {
packagePrefix = value
return this
}
fun separateJar(value: Boolean): PluginBuilder {
separateJar = value
return this
}
fun depends(pluginId: String, configFile: String? = null): PluginBuilder {
dependsTags.add(DependsTag(pluginId, configFile))
return this
}
fun depends(pluginId: String, subDescriptor: PluginBuilder, filename: String? = null): PluginBuilder {
val fileName = filename ?: "dep_${pluginIdCounter.incrementAndGet()}.xml"
subDescriptors.add(SubDescriptor(PluginManagerCore.META_INF + fileName, subDescriptor))
depends(pluginId, fileName)
return this
}
fun module(moduleName: String, moduleDescriptor: PluginBuilder, loadingRule: ModuleLoadingRule = ModuleLoadingRule.OPTIONAL,
moduleFile: String = "$moduleName.xml"): PluginBuilder {
subDescriptors.add(SubDescriptor(moduleFile, moduleDescriptor))
content.add(PluginContentDescriptor.ModuleItem(name = moduleName, configFile = null, descriptorContent = null, loadingRule = loadingRule))
return this
}
fun pluginAlias(alias: String): PluginBuilder {
pluginAliases.add(alias)
return this
}
fun dependency(moduleName: String): PluginBuilder {
dependencies.add(ModuleDependencies.ModuleReference(moduleName))
return this
}
fun pluginDependency(pluginId: String): PluginBuilder {
pluginDependencies.add(ModuleDependencies.PluginReference(PluginId.getId(pluginId)))
return this
}
fun incompatibleWith(pluginId: String): PluginBuilder {
incompatibleWith.add(ModuleDependencies.PluginReference(PluginId.getId(pluginId)))
return this
}
fun resourceBundle(resourceBundle: String?): PluginBuilder {
resourceBundleBaseName = resourceBundle
return this
}
fun untilBuild(buildNumber: String): PluginBuilder {
untilBuild = buildNumber
return this
}
fun sinceBuild(buildNumber: String): PluginBuilder {
sinceBuild = buildNumber
return this
}
fun version(version: String): PluginBuilder {
this.version = version
return this
}
fun applicationListeners(text: String): PluginBuilder {
applicationListeners = text
return this
}
fun actions(text: String): PluginBuilder {
actions = text
return this
}
fun extensions(text: String, ns: String = "com.intellij"): PluginBuilder {
extensions.add(ExtensionBlock(ns, text))
return this
}
fun extensionPoints(@Language("XML") text: String): PluginBuilder {
extensionPoints = text
return this
}
fun implementationDetail(): PluginBuilder {
implementationDetail = true
return this
}
fun text(requireId: Boolean = true): String {
return buildString {
append("<idea-plugin")
if (implementationDetail) {
append(""" ${PluginXmlConst.PLUGIN_IMPLEMENTATION_DETAIL_ATTR}="true"""")
}
packagePrefix?.let {
append(""" ${PluginXmlConst.PLUGIN_PACKAGE_ATTR}="$it"""")
}
if (separateJar) {
append(""" separate-jar="true"""") // todo change to const from xml reader
}
append(">")
if (requireId) {
append("<id>$id</id>")
}
name?.let { append("<name>$it</name>") }
description?.let { append("<description>$it</description>") }
for (dependsTag in dependsTags) {
val configFile = dependsTag.configFile
if (configFile != null) {
append("""<depends optional="true" config-file="$configFile">${dependsTag.pluginId}</depends>""")
}
else {
append("<depends>${dependsTag.pluginId}</depends>")
}
}
version?.let { append("<version>$it</version>") }
if (sinceBuild != null && untilBuild != null) {
append("""<idea-version since-build="${sinceBuild}" until-build="${untilBuild}"/>""")
}
else if (sinceBuild != null) {
append("""<idea-version since-build="${sinceBuild}"/>""")
}
else if (untilBuild != null) {
append("""<idea-version until-build="${untilBuild}"/>""")
}
for (extensionBlock in extensions) {
append("""<extensions defaultExtensionNs="${extensionBlock.ns}">${extensionBlock.text}</extensions>""")
}
extensionPoints?.let { append("<extensionPoints>$it</extensionPoints>") }
applicationListeners?.let { append("<applicationListeners>$it</applicationListeners>") }
resourceBundleBaseName?.let { append("""<resource-bundle>$it</resource-bundle>""") }
actions?.let { append("<actions>$it</actions>") }
if (content.isNotEmpty()) {
append("\n<content>\n ")
content.joinTo(this, separator = "\n ") { moduleItem ->
val loadingAttribute = when (moduleItem.loadingRule) {
ModuleLoadingRule.OPTIONAL -> ""
ModuleLoadingRule.REQUIRED -> "loading=\"required\" "
ModuleLoadingRule.EMBEDDED -> "loading=\"embedded\" "
ModuleLoadingRule.ON_DEMAND -> "loading=\"on-demand\" "
}
"""<module name="${moduleItem.name}" $loadingAttribute/>"""
}
append("\n</content>")
}
if (incompatibleWith.isNotEmpty()) {
incompatibleWith.joinTo(this, separator = "\n ") {
"""<incompatible-with>${it.id}</incompatible-with>"""
}
}
if (dependencies.isNotEmpty() || pluginDependencies.isNotEmpty()) {
append("\n<dependencies>\n ")
if (dependencies.isNotEmpty()) {
dependencies.joinTo(this, separator = "\n ") { """<module name="${it.name}" />""" }
}
if (pluginDependencies.isNotEmpty()) {
pluginDependencies.joinTo(this, separator = "\n ") { """<plugin id="${it.id}" />""" }
}
append("\n</dependencies>")
}
for (alias in pluginAliases) {
append("\n")
append("""<module value="$alias"/>""")
}
append("</idea-plugin>")
}
}
fun build(path: Path): PluginBuilder {
val allDescriptors = collectAllSubDescriptors(subDescriptors).toList()
if (allDescriptors.any { it.builder.separateJar }) {
val modulesDir = path.resolve("lib/modules")
modulesDir.createDirectories()
buildJar(path.resolve("lib/$id.jar"))
for ((fileName, subDescriptor) in allDescriptors) {
if (subDescriptor.separateJar) {
val jarPath = modulesDir.resolve("${fileName.removeSuffix(".xml")}.jar")
subDescriptor.buildJarToStream(Files.newOutputStream(jarPath), mainDescriptorRelativePath = fileName)
}
}
}
else {
path.resolve(PluginManagerCore.PLUGIN_XML_PATH).write(text())
for (subDescriptor in allDescriptors) {
path.resolve(subDescriptor.filename).createParentDirectories().write(subDescriptor.builder.text(requireId = false))
}
}
return this
}
private fun collectAllSubDescriptors(descriptors: List<SubDescriptor>): Sequence<SubDescriptor> {
return descriptors.asSequence().flatMap { sequenceOf(it) + collectAllSubDescriptors(it.builder.subDescriptors) }
}
fun buildJar(path: Path): PluginBuilder {
buildJarToStream(Files.newOutputStream(path), PluginManagerCore.PLUGIN_XML_PATH)
return this
}
private fun buildJarToStream(outputStream: OutputStream, mainDescriptorRelativePath: String) {
Compressor.Zip(outputStream).use {
it.addFile(mainDescriptorRelativePath, text(requireId = mainDescriptorRelativePath == PluginManagerCore.PLUGIN_XML_PATH).toByteArray())
for ((fileName, subDescriptor) in subDescriptors) {
if (!subDescriptor.separateJar) {
it.addFile(fileName, subDescriptor.text(requireId = false).toByteArray())
}
}
}
}
fun buildZip(path: Path): PluginBuilder {
val jarStream = ByteArrayOutputStream()
buildJarToStream(jarStream, PluginManagerCore.PLUGIN_XML_PATH)
val pluginName = name ?: id
Compressor.Zip(Files.newOutputStream(path)).use {
it.addDirectory(pluginName)
it.addDirectory("$pluginName/lib")
it.addFile("$pluginName/lib/$pluginName.jar", jarStream.toByteArray())
}
return this
}
companion object {
fun withModulesLang(): PluginBuilder = PluginBuilder().dependsIntellijModulesLang()
fun empty(): PluginBuilder = PluginBuilder()
}
}
@TestOnly
fun readModuleDescriptorForTest(input: ByteArray): PluginDescriptorBuilder {
return PluginDescriptorFromXmlStreamConsumer(readContext = object : ReadModuleContext {

View File

@@ -2,6 +2,7 @@
package com.intellij.ide.plugins
import com.intellij.ide.plugins.cl.PluginClassLoader
import com.intellij.platform.testFramework.PluginBuilder
import com.intellij.platform.plugins.testFramework.PluginSetTestBuilder
import com.intellij.testFramework.LoggedErrorProcessor
import com.intellij.testFramework.assertions.Assertions.assertThat

View File

@@ -3,6 +3,8 @@
package com.intellij.ide.plugins
import com.intellij.openapi.diagnostic.logger
import com.intellij.platform.testFramework.PluginBuilder
import com.intellij.platform.testFramework.loadAndInitDescriptorInTest
import com.intellij.openapi.util.BuildNumber
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.TestDataPath

View File

@@ -13,6 +13,7 @@ import com.intellij.platform.plugins.parser.impl.PluginDescriptorFromXmlStreamCo
import com.intellij.platform.plugins.parser.impl.ReadModuleContext
import com.intellij.platform.plugins.parser.impl.XIncludeLoader.LoadedXIncludeReference
import com.intellij.platform.plugins.parser.impl.consume
import com.intellij.platform.testFramework.loadAndInitDescriptorInTest
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.TestDataPath
import com.intellij.testFramework.UsefulTestCase

View File

@@ -3,6 +3,7 @@ package com.intellij.ide.plugins
import com.intellij.openapi.util.BuildNumber
import com.intellij.platform.plugins.testFramework.PluginSetTestBuilder
import com.intellij.platform.testFramework.PluginBuilder
import com.intellij.testFramework.rules.InMemoryFsRule
import com.intellij.util.io.write
import org.assertj.core.api.Assertions.assertThat

View File

@@ -9,6 +9,8 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.registry.RegistryKeyBean
import com.intellij.openapi.util.registry.RegistryKeyDescriptor
import com.intellij.platform.testFramework.PluginBuilder
import com.intellij.platform.testFramework.loadPluginWithText
import com.intellij.testFramework.*
import com.intellij.testFramework.rules.InMemoryFsRule
import com.intellij.util.io.Ksuid

View File

@@ -5,7 +5,7 @@ import com.intellij.configurationStore.getPerOsSettingsStorageFolderName
import com.intellij.diagnostic.VMOptions
import com.intellij.ide.SpecialConfigFiles
import com.intellij.ide.plugins.DisabledPluginsState.Companion.saveDisabledPluginsAndInvalidate
import com.intellij.ide.plugins.PluginBuilder
import com.intellij.platform.testFramework.PluginBuilder
import com.intellij.ide.plugins.PluginNode
import com.intellij.ide.plugins.marketplace.MarketplacePluginDownloadService
import com.intellij.ide.plugins.marketplace.utils.MarketplaceCustomizationService

View File

@@ -1,7 +1,6 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.vfs.local;
import com.intellij.ide.plugins.DynamicPluginsTestUtil;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ApplicationManager;
@@ -22,6 +21,7 @@ import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.VfsImplUtil;
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.platform.testFramework.DynamicPluginTestUtilsKt;
import com.intellij.testFramework.EdtTestUtil;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.TestActionEvent;
@@ -370,7 +370,7 @@ public class JarFileSystemTest extends BareTestFixtureTestCase {
EdtTestUtil.runInEdtAndWait(() -> {
assertNotNull(LocalFileSystem.getInstance().refreshAndFindFileByNioFile(copiedJar));
var fsRegistration = DynamicPluginsTestUtil.loadExtensionWithText(fsExtText, "com.intellij");
var fsRegistration = DynamicPluginTestUtilsKt.loadExtensionWithText(fsExtText, "com.intellij");
try {
Disposer.register(fsRegistration, JarFileSystemImpl::cleanupForNextTest);

View File

@@ -2,7 +2,6 @@
package com.intellij.openapi.vfs.newvfs.persistent;
import com.intellij.CacheSwitcher;
import com.intellij.ide.plugins.DynamicPluginsTestUtil;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
@@ -31,6 +30,7 @@ import com.intellij.openapi.vfs.newvfs.*;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
import com.intellij.platform.testFramework.DynamicPluginTestUtilsKt;
import com.intellij.testFramework.*;
import com.intellij.testFramework.fixtures.BareTestFixtureTestCase;
import com.intellij.testFramework.rules.TempDirectory;
@@ -748,7 +748,7 @@ public class PersistentFsTest extends BareTestFixtureTestCase {
String text = "<virtualFileSystem implementationClass=\"" +
TracingJarFileSystemTestWrapper.class.getName() +
"\" key=\"jar-wrapper\" physical=\"true\"/>";
Disposable disposable = runInEdtAndGet(() -> DynamicPluginsTestUtil.loadExtensionWithText(text, "com.intellij"));
Disposable disposable = runInEdtAndGet(() -> DynamicPluginTestUtilsKt.loadExtensionWithText(text, "com.intellij"));
try {
File generationDir = tempDirectory.newDirectory("gen");
@@ -795,7 +795,7 @@ public class PersistentFsTest extends BareTestFixtureTestCase {
String text = "<virtualFileSystem implementationClass=\"" +
TracingJarFileSystemTestWrapper.class.getName() +
"\" key=\"jar-wrapper\" physical=\"true\"/>";
Disposable disposable = runInEdtAndGet(() -> DynamicPluginsTestUtil.loadExtensionWithText(text, "com.intellij"));
Disposable disposable = runInEdtAndGet(() -> DynamicPluginTestUtilsKt.loadExtensionWithText(text, "com.intellij"));
try {
File generationDir = tempDirectory.newDirectory("gen");

View File

@@ -1,19 +1,23 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("DynamicPluginsTestUtil")
@file:Suppress("UsePropertyAccessSyntax")
package com.intellij.ide.plugins
// 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
import com.intellij.ide.plugins.DynamicPlugins
import com.intellij.ide.plugins.IdeaPluginDescriptorImpl
import com.intellij.ide.plugins.PluginDescriptorLoadingContext
import com.intellij.ide.plugins.PluginInitializationContext
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.plugins.loadDescriptorFromFileOrDirInTests
import com.intellij.openapi.Disposable
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.BuildNumber
import com.intellij.openapi.util.io.FileUtil
import com.intellij.testFramework.IndexingTestUtil
import com.intellij.testFramework.assertions.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThat
import java.nio.file.Files
import java.nio.file.Path
@JvmOverloads
internal fun loadAndInitDescriptorInTest(
fun loadAndInitDescriptorInTest(
dir: Path,
isBundled: Boolean = false,
disabledPlugins: Set<String> = emptySet(),
@@ -59,7 +63,7 @@ fun loadExtensionWithText(extensionTag: String, ns: String = "com.intellij"): Di
}
}
internal fun loadPluginWithText(
fun loadPluginWithText(
pluginBuilder: PluginBuilder,
path: Path,
disabledPlugins: Set<String> = emptySet(),
@@ -86,7 +90,7 @@ internal fun loadPluginWithText(
}
}
internal fun loadAndInitDescriptorInTest(
fun loadAndInitDescriptorInTest(
pluginBuilder: PluginBuilder,
rootPath: Path,
disabledPlugins: Set<String> = emptySet(),
@@ -106,7 +110,7 @@ internal fun loadAndInitDescriptorInTest(
)
}
internal fun setPluginClassLoaderForMainAndSubPlugins(rootDescriptor: IdeaPluginDescriptorImpl, classLoader: ClassLoader?) {
fun setPluginClassLoaderForMainAndSubPlugins(rootDescriptor: IdeaPluginDescriptorImpl, classLoader: ClassLoader?) {
rootDescriptor.pluginClassLoader = classLoader
for (dependency in rootDescriptor.dependencies) {
dependency.subDescriptor?.let {
@@ -115,7 +119,7 @@ internal fun setPluginClassLoaderForMainAndSubPlugins(rootDescriptor: IdeaPlugin
}
}
internal fun unloadAndUninstallPlugin(descriptor: IdeaPluginDescriptorImpl): Boolean {
fun unloadAndUninstallPlugin(descriptor: IdeaPluginDescriptorImpl): Boolean {
return DynamicPlugins.unloadPlugin(
descriptor,
DynamicPlugins.UnloadPluginOptions(disable = false),

View File

@@ -0,0 +1,332 @@
// 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
import com.intellij.ide.plugins.ModuleDependencies
import com.intellij.ide.plugins.ModuleLoadingRule
import com.intellij.ide.plugins.PluginContentDescriptor
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.extensions.PluginId
import com.intellij.util.io.Compressor
import com.intellij.util.io.createParentDirectories
import com.intellij.util.io.write
import org.intellij.lang.annotations.Language
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.createDirectories
/** Constants from [com.intellij.platform.plugins.parser.impl.PluginXmlConst]
* We can't access PluginXmlConst directly because it's in an implementation module */
private object PluginBuilderConsts {
const val PLUGIN_IMPLEMENTATION_DETAIL_ATTR: String = "implementation-detail"
const val PLUGIN_PACKAGE_ATTR: String = "package"
}
private val pluginIdCounter = AtomicInteger()
class PluginBuilder private constructor() {
private data class ExtensionBlock(val ns: String, val text: String)
private data class DependsTag(val pluginId: String, val configFile: String?)
// counter is used to reduce plugin id length
var id: String = "p_${pluginIdCounter.incrementAndGet()}"
private set
private var implementationDetail = false
private var separateJar = false
private var name: String? = null
private var description: String? = null
private var packagePrefix: String? = null
private val dependsTags = mutableListOf<DependsTag>()
private var applicationListeners: String? = null
private var resourceBundleBaseName: String? = null
private var actions: String? = null
private val extensions = mutableListOf<ExtensionBlock>()
private var extensionPoints: String? = null
private var untilBuild: String? = null
private var sinceBuild: String? = null
private var version: String? = null
private val pluginAliases = mutableListOf<String>()
private val content = mutableListOf<PluginContentDescriptor.ModuleItem>()
private val dependencies = mutableListOf<ModuleDependencies.ModuleReference>()
private val pluginDependencies = mutableListOf<ModuleDependencies.PluginReference>()
private val incompatibleWith = mutableListOf<ModuleDependencies.PluginReference>()
private data class SubDescriptor(val filename: String, val builder: PluginBuilder)
private val subDescriptors = ArrayList<SubDescriptor>()
fun dependsIntellijModulesLang(): PluginBuilder {
depends("com.intellij.modules.lang")
return this
}
fun id(id: String): PluginBuilder {
this.id = id
return this
}
fun randomId(idPrefix: String): PluginBuilder {
this.id = "${idPrefix}_${pluginIdCounter.incrementAndGet()}"
return this
}
fun name(name: String): PluginBuilder {
this.name = name
return this
}
fun description(description: String): PluginBuilder {
this.description = description
return this
}
fun packagePrefix(value: String?): PluginBuilder {
packagePrefix = value
return this
}
fun separateJar(value: Boolean): PluginBuilder {
separateJar = value
return this
}
fun depends(pluginId: String, configFile: String? = null): PluginBuilder {
dependsTags.add(DependsTag(pluginId, configFile))
return this
}
fun depends(pluginId: String, subDescriptor: PluginBuilder, filename: String? = null): PluginBuilder {
val fileName = filename ?: "dep_${pluginIdCounter.incrementAndGet()}.xml"
subDescriptors.add(SubDescriptor(PluginManagerCore.META_INF + fileName, subDescriptor))
depends(pluginId, fileName)
return this
}
fun module(
moduleName: String, moduleDescriptor: PluginBuilder, loadingRule: ModuleLoadingRule = ModuleLoadingRule.OPTIONAL,
moduleFile: String = "$moduleName.xml",
): PluginBuilder {
subDescriptors.add(SubDescriptor(moduleFile, moduleDescriptor))
content.add(PluginContentDescriptor.ModuleItem(name = moduleName, configFile = null, descriptorContent = null, loadingRule = loadingRule))
return this
}
fun pluginAlias(alias: String): PluginBuilder {
pluginAliases.add(alias)
return this
}
fun dependency(moduleName: String): PluginBuilder {
dependencies.add(ModuleDependencies.ModuleReference(moduleName))
return this
}
fun pluginDependency(pluginId: String): PluginBuilder {
pluginDependencies.add(ModuleDependencies.PluginReference(PluginId.getId(pluginId)))
return this
}
fun incompatibleWith(pluginId: String): PluginBuilder {
incompatibleWith.add(ModuleDependencies.PluginReference(PluginId.getId(pluginId)))
return this
}
fun resourceBundle(resourceBundle: String?): PluginBuilder {
resourceBundleBaseName = resourceBundle
return this
}
fun untilBuild(buildNumber: String): PluginBuilder {
untilBuild = buildNumber
return this
}
fun sinceBuild(buildNumber: String): PluginBuilder {
sinceBuild = buildNumber
return this
}
fun version(version: String): PluginBuilder {
this.version = version
return this
}
fun applicationListeners(text: String): PluginBuilder {
applicationListeners = text
return this
}
fun actions(text: String): PluginBuilder {
actions = text
return this
}
fun extensions(text: String, ns: String = "com.intellij"): PluginBuilder {
extensions.add(ExtensionBlock(ns, text))
return this
}
fun extensionPoints(@Language("XML") text: String): PluginBuilder {
extensionPoints = text
return this
}
fun implementationDetail(): PluginBuilder {
implementationDetail = true
return this
}
fun text(requireId: Boolean = true): String {
return buildString {
append("<idea-plugin")
if (implementationDetail) {
append(""" ${PluginBuilderConsts.PLUGIN_IMPLEMENTATION_DETAIL_ATTR}="true"""")
}
packagePrefix?.let {
append(""" ${PluginBuilderConsts.PLUGIN_PACKAGE_ATTR}="$it"""")
}
if (separateJar) {
append(""" separate-jar="true"""") // todo change to const from xml reader
}
append(">")
if (requireId) {
append("<id>$id</id>")
}
name?.let { append("<name>$it</name>") }
description?.let { append("<description>$it</description>") }
for (dependsTag in dependsTags) {
val configFile = dependsTag.configFile
if (configFile != null) {
append("""<depends optional="true" config-file="$configFile">${dependsTag.pluginId}</depends>""")
}
else {
append("<depends>${dependsTag.pluginId}</depends>")
}
}
version?.let { append("<version>$it</version>") }
if (sinceBuild != null && untilBuild != null) {
append("""<idea-version since-build="${sinceBuild}" until-build="${untilBuild}"/>""")
}
else if (sinceBuild != null) {
append("""<idea-version since-build="${sinceBuild}"/>""")
}
else if (untilBuild != null) {
append("""<idea-version until-build="${untilBuild}"/>""")
}
for (extensionBlock in extensions) {
append("""<extensions defaultExtensionNs="${extensionBlock.ns}">${extensionBlock.text}</extensions>""")
}
extensionPoints?.let { append("<extensionPoints>$it</extensionPoints>") }
applicationListeners?.let { append("<applicationListeners>$it</applicationListeners>") }
resourceBundleBaseName?.let { append("""<resource-bundle>$it</resource-bundle>""") }
actions?.let { append("<actions>$it</actions>") }
if (content.isNotEmpty()) {
append("\n<content>\n ")
content.joinTo(this, separator = "\n ") { moduleItem ->
val loadingAttribute = when (moduleItem.loadingRule) {
ModuleLoadingRule.OPTIONAL -> ""
ModuleLoadingRule.REQUIRED -> "loading=\"required\" "
ModuleLoadingRule.EMBEDDED -> "loading=\"embedded\" "
ModuleLoadingRule.ON_DEMAND -> "loading=\"on-demand\" "
}
"""<module name="${moduleItem.name}" $loadingAttribute/>"""
}
append("\n</content>")
}
if (incompatibleWith.isNotEmpty()) {
incompatibleWith.joinTo(this, separator = "\n ") {
"""<incompatible-with>${it.id}</incompatible-with>"""
}
}
if (dependencies.isNotEmpty() || pluginDependencies.isNotEmpty()) {
append("\n<dependencies>\n ")
if (dependencies.isNotEmpty()) {
dependencies.joinTo(this, separator = "\n ") { """<module name="${it.name}" />""" }
}
if (pluginDependencies.isNotEmpty()) {
pluginDependencies.joinTo(this, separator = "\n ") { """<plugin id="${it.id}" />""" }
}
append("\n</dependencies>")
}
for (alias in pluginAliases) {
append("\n")
append("""<module value="$alias"/>""")
}
append("</idea-plugin>")
}
}
fun build(path: Path): PluginBuilder {
val allDescriptors = collectAllSubDescriptors(subDescriptors).toList()
if (allDescriptors.any { it.builder.separateJar }) {
val modulesDir = path.resolve("lib/modules")
modulesDir.createDirectories()
buildJar(path.resolve("lib/$id.jar"))
for ((fileName, subDescriptor) in allDescriptors) {
if (subDescriptor.separateJar) {
val jarPath = modulesDir.resolve("${fileName.removeSuffix(".xml")}.jar")
subDescriptor.buildJarToStream(Files.newOutputStream(jarPath), mainDescriptorRelativePath = fileName)
}
}
}
else {
path.resolve(PluginManagerCore.PLUGIN_XML_PATH).write(text())
for (subDescriptor in allDescriptors) {
path.resolve(subDescriptor.filename).createParentDirectories().write(subDescriptor.builder.text(requireId = false))
}
}
return this
}
private fun collectAllSubDescriptors(descriptors: List<SubDescriptor>): Sequence<SubDescriptor> {
return descriptors.asSequence().flatMap { sequenceOf(it) + collectAllSubDescriptors(it.builder.subDescriptors) }
}
fun buildJar(path: Path): PluginBuilder {
buildJarToStream(Files.newOutputStream(path), PluginManagerCore.PLUGIN_XML_PATH)
return this
}
private fun buildJarToStream(outputStream: OutputStream, mainDescriptorRelativePath: String) {
Compressor.Zip(outputStream).use {
it.addFile(mainDescriptorRelativePath, text(requireId = mainDescriptorRelativePath == PluginManagerCore.PLUGIN_XML_PATH).toByteArray())
for ((fileName, subDescriptor) in subDescriptors) {
if (!subDescriptor.separateJar) {
it.addFile(fileName, subDescriptor.text(requireId = false).toByteArray())
}
}
}
}
fun buildZip(path: Path): PluginBuilder {
val jarStream = ByteArrayOutputStream()
buildJarToStream(jarStream, PluginManagerCore.PLUGIN_XML_PATH)
val pluginName = name ?: id
Compressor.Zip(Files.newOutputStream(path)).use {
it.addDirectory(pluginName)
it.addDirectory("$pluginName/lib")
it.addFile("$pluginName/lib/$pluginName.jar", jarStream.toByteArray())
}
return this
}
companion object {
fun withModulesLang(): PluginBuilder = PluginBuilder().dependsIntellijModulesLang()
fun empty(): PluginBuilder = PluginBuilder()
}
}