[platform] updating ConfigImportHelper and the surroundings (IJPL-198038 preparation)

Pure NIO (most tests are now using the in-memory FS); no more deprecated code; leaner API; typos; formatting.

GitOrigin-RevId: e97f7088100ecf191b653202753b320f9bdab12a
This commit is contained in:
Roman Shevchenko
2025-09-20 10:58:00 +00:00
committed by intellij-monorepo-bot
parent af090daf62
commit 464bc08113
10 changed files with 698 additions and 844 deletions
@@ -1,8 +1,7 @@
// 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.application
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.NioFiles
import com.intellij.util.io.delete
import org.jetbrains.annotations.ApiStatus
@@ -26,7 +25,7 @@ class ConfigBackup(private val configDir: Path) {
val backupPath = getNextBackupPath(configDir)
LOG.info("Move backup from $dirToMove to $backupPath")
FileUtil.copyDir(dirToMove.toFile(), backupPath.toFile())
NioFiles.copyRecursively(dirToMove, backupPath)
NioFiles.deleteRecursively(dirToMove)
}
@@ -36,7 +35,7 @@ class ConfigBackup(private val configDir: Path) {
val oldBackup = backupDir.resolve("1970-01-01-00-00").createDirectory()
for (file in backupDir.listDirectoryEntries()) {
if (!file.isDirectory() || !file.name.looksLikeDate()) {
FileUtil.copyDir(file.toFile(), oldBackup.resolve(file.name).toFile())
NioFiles.copyRecursively(file, oldBackup.resolve(file.name))
NioFiles.deleteRecursively(file)
}
}
File diff suppressed because it is too large Load Diff
@@ -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.application;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
@@ -51,19 +51,6 @@ public interface ConfigImportSettings {
@Nullable Map<PluginId, Set<String>> brokenPluginVersions,
@NotNull List<IdeaPluginDescriptor> pluginsToMigrate,
@NotNull List<IdeaPluginDescriptor> pluginsToDownload
) {
processPluginsToMigrate(newConfigDir, oldConfigDir, pluginsToMigrate, pluginsToDownload);
}
/**
* Override {@link #processPluginsToMigrate(Path, Path, Path, ConfigImportHelper.ConfigImportOptions, Map, List, List)} instead
*/
@ApiStatus.Obsolete
default void processPluginsToMigrate(
@NotNull Path newConfigDir,
@NotNull Path oldConfigDir,
@NotNull List<IdeaPluginDescriptor> pluginsToMigrate,
@NotNull List<IdeaPluginDescriptor> pluginsToDownload
) { }
/**
@@ -1,7 +1,8 @@
// 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.application;
import com.intellij.ide.BootstrapBundle;
import com.intellij.ide.actions.ImportSettingsFilenameFilter;
import com.intellij.openapi.MnemonicHelper;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.fileChooser.impl.FileChooserFactoryImpl;
@@ -10,26 +11,23 @@ import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.local.CoreLocalFileSystem;
import com.intellij.openapi.vfs.local.CoreLocalVirtualFile;
import com.intellij.ui.CollectionComboBoxModel;
import com.intellij.ui.ComponentUtil;
import com.intellij.util.system.OS;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.List;
import java.util.function.Function;
import java.util.zip.ZipFile;
final class ImportOldConfigsPanel extends JDialog {
private JPanel myRootPanel;
@@ -41,12 +39,12 @@ final class ImportOldConfigsPanel extends JDialog {
private ComboBox<Path> myComboBoxOldPaths;
private final List<Path> myGuessedOldConfigDirs;
private final Function<? super Path, ? extends Pair<Path, Path>> myValidator;
private final Function<Path, Pair<Path, Path>> myValidator;
private final String myProductName;
private Path myLastSelection = null;
private Pair<Path, Path> myResult;
ImportOldConfigsPanel(List<Path> guessedOldConfigDirs, Function<? super Path, ? extends Pair<Path, Path>> validator) {
ImportOldConfigsPanel(@NotNull List<Path> guessedOldConfigDirs, @NotNull Function<Path, Pair<Path, Path>> validator) {
super((Dialog)null, true);
ComponentUtil.decorateWindowHeader(rootPane);
@@ -61,7 +59,7 @@ final class ImportOldConfigsPanel extends JDialog {
private void init() {
MnemonicHelper.init(getContentPane());
ButtonGroup group = new ButtonGroup();
var group = new ButtonGroup();
group.add(myRbImportAuto);
group.add(myRbImport);
group.add(myRbDoNotImport);
@@ -73,51 +71,53 @@ final class ImportOldConfigsPanel extends JDialog {
}
else {
myComboBoxOldPaths.setModel(new CollectionComboBoxModel<>(myGuessedOldConfigDirs));
myComboBoxOldPaths.setSelectedItem(myGuessedOldConfigDirs.get(0));
myComboBoxOldPaths.setSelectedItem(myGuessedOldConfigDirs.getFirst());
myRbImportAuto.setSelected(true);
}
for (Enumeration<AbstractButton> e = group.getElements(); e.hasMoreElements(); ) {
for (var e = group.getElements(); e.hasMoreElements(); ) {
e.nextElement().addChangeListener(event -> update());
}
if (SystemInfo.isMac) {
myLastSelection = Paths.get("/Applications");
if (OS.CURRENT == OS.macOS) {
myLastSelection = Path.of("/Applications");
}
else if (SystemInfo.isWindows) {
String programFiles = System.getenv("ProgramFiles");
else if (OS.CURRENT == OS.Windows) {
var programFiles = System.getenv("ProgramFiles");
if (programFiles != null) {
Path candidate = Paths.get(programFiles, "JetBrains");
myLastSelection = Files.isDirectory(candidate) ? candidate : Paths.get(programFiles);
var candidate = Path.of(programFiles, "JetBrains");
myLastSelection = Files.isDirectory(candidate) ? candidate : Path.of(programFiles);
}
}
myPrevInstallation.setTextFieldPreferredWidth(50);
myPrevInstallation.addActionListener(e -> {
var chooserDescriptor = FileChooserDescriptorFactory.createSingleLocalFileDescriptor().withHideIgnored(false);
var chooserDescriptor = FileChooserDescriptorFactory.singleFile().withHideIgnored(false);
ConfigImportHelper.setSettingsFilter(chooserDescriptor);
var fileRef = Ref.<File>create();
var fileRef = Ref.<Path>create();
var chooser = FileChooserFactoryImpl.createNativePathChooserIfEnabled(chooserDescriptor, null, myRootPanel);
if (chooser != null) {
VirtualFile vf = myLastSelection != null ? new CoreLocalVirtualFile(new CoreLocalFileSystem(), myLastSelection) : null;
chooser.choose(vf, files -> fileRef.set(new File(files.get(0).getPresentableUrl())));
var vf = myLastSelection != null ? new CoreLocalVirtualFile(new CoreLocalFileSystem(), myLastSelection) : null;
chooser.choose(vf, files -> fileRef.set(Path.of(files.getFirst().getPresentableUrl())));
}
else {
JFileChooser fc = new JFileChooser(myLastSelection != null ? myLastSelection.getParent().toFile() : null);
fc.setSelectedFile(myLastSelection != null ? myLastSelection.toFile() : null);
@SuppressWarnings("IO_FILE_USAGE") var directory = myLastSelection != null ? myLastSelection.getParent().toFile() : null;
@SuppressWarnings("IO_FILE_USAGE") var selectedFile = myLastSelection != null ? myLastSelection.toFile() : null;
var fc = new JFileChooser();
fc.setCurrentDirectory(directory);
fc.setSelectedFile(selectedFile);
fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
fc.setFileHidingEnabled(SystemInfo.isWindows || SystemInfo.isMac);
fc.setFileHidingEnabled(OS.CURRENT == OS.Windows || OS.CURRENT == OS.macOS);
fc.setFileFilter(new FileNameExtensionFilter(BootstrapBundle.message("import.settings.filter"), "zip", "jar"));
@SuppressWarnings("DuplicatedCode")
int returnVal = fc.showOpenDialog(this);
var returnVal = fc.showOpenDialog(this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
var file = fc.getSelectedFile();
if (file != null) {
fileRef.set(file);
fileRef.set(file.toPath());
myPrevInstallation.setText(file.getAbsolutePath());
}
}
}
if (!fileRef.isNull()) {
myLastSelection = fileRef.get().toPath();
myPrevInstallation.setText(fileRef.get().getAbsolutePath());
myLastSelection = fileRef.get();
myPrevInstallation.setText(fileRef.get().toString());
}
});
@@ -140,29 +140,28 @@ final class ImportOldConfigsPanel extends JDialog {
private void close() {
if (myRbImport.isSelected()) {
String text = myPrevInstallation.getText();
if (StringUtil.isEmptyOrSpaces(text)) {
var text = myPrevInstallation.getText().trim();
if (text.isEmpty()) {
showError(BootstrapBundle.message("import.chooser.error.empty", myProductName));
return;
}
Path selectedDir = Paths.get(FileUtil.toCanonicalPath(text.trim()));
var selectedDir = Path.of(text).toAbsolutePath().normalize();
if (Files.isRegularFile(selectedDir)) {
if (!ConfigImportHelper.isValidSettingsFile(selectedDir.toFile())) {
if (!isValidSettingsFile(selectedDir)) {
showError(BootstrapBundle.message("import.chooser.error.invalid", selectedDir));
return;
}
myResult = new Pair<>(selectedDir, null);
}
else {
if (FileUtil.pathsEqual(selectedDir.toString(), PathManager.getHomePath()) ||
FileUtil.pathsEqual(selectedDir.toString(), PathManager.getConfigPath())) {
if (selectedDir.equals(PathManager.getHomeDir()) || selectedDir.equals(PathManager.getConfigDir())) {
showError(BootstrapBundle.message("import.chooser.error.current", myProductName));
return;
}
Pair<Path, Path> result = myValidator.apply(selectedDir);
var result = myValidator.apply(selectedDir);
if (result == null) {
showError(BootstrapBundle.message("import.chooser.error.unrecognized", selectedDir, myProductName));
return;
@@ -176,10 +175,19 @@ final class ImportOldConfigsPanel extends JDialog {
}
private void showError(@NlsContexts.DialogMessage String message) {
String title = BootstrapBundle.message("import.chooser.error.title");
var title = BootstrapBundle.message("import.chooser.error.title");
JOptionPane.showMessageDialog(this, message, title, JOptionPane.ERROR_MESSAGE);
}
private static boolean isValidSettingsFile(Path file) {
try (@SuppressWarnings("IO_FILE_USAGE") var zip = new ZipFile(file.toFile())) {
return zip.getEntry(ImportSettingsFilenameFilter.SETTINGS_JAR_MARKER) != null;
}
catch (IOException ignored) {
return false;
}
}
@Nullable Pair<Path, Path> getSelectedFile() {
ImportOldConfigsState.Companion.getInstance().saveImportOldConfigType(myRbImportAuto, myRbImport, myRbDoNotImport, myResult != null);
@@ -1,7 +1,6 @@
// Copyright 2000-2023 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.application
import com.intellij.openapi.application.ConfigBackup.Companion.MAX_BACKUPS_NUMBER
import com.intellij.util.io.write
import org.junit.Assert.*
import org.junit.Before
@@ -18,37 +17,33 @@ class ConfigBackupTest : ConfigImportHelperBaseTest() {
private lateinit var configDir: Path
private lateinit var backupDir: Path
@Before
fun setup() {
dirToBackup = createConfigDirToBackup()
configDir = localTempDir.rootPath.resolve(CONFIG_PREFIX)
backupDir = configDir.resolveSibling("$CONFIG_PREFIX-backup")
@Before fun setup() {
dirToBackup = newTempDir("temp-settings").apply { resolve("options/config.xml").write("config data") }
configDir = dirToBackup.resolveSibling(CONFIG_PREFIX)
backupDir = dirToBackup.resolveSibling("${CONFIG_PREFIX}-backup")
}
@Test
fun `next backup path`() {
@Test fun `next backup path`() {
val now = LocalDateTime.now()
val date = getDateFormattedForBackupDir(now)
val dir = memoryFs.fs.getPath("${PathManager.getConfigPath()}-backup").resolve(date)
val dir = memoryFs.fs.getPath("${PathManager.getConfigDir()}-backup").resolve(date)
val path = ConfigBackup.getNextBackupPath(memoryFs.fs.getPath(PathManager.getConfigPath()), now)
val path = ConfigBackup.getNextBackupPath(memoryFs.fs.getPath(PathManager.getConfigDir().toString()), now)
assertEquals("Next backup path is incorrect", dir, path)
}
@Test
fun `make simple backup`() {
@Test fun `make simple backup`() {
moveDirToBackup()
assertTrue("Backup dir doesn't exist", backupDir.exists())
val child = backupDir.getSingleChild()
val backedupDir = child.getSingleChild()
assertEquals("Wrong backed up dir", "options", backedupDir.name)
val backedupFile = backedupDir.getSingleChild()
assertFile(backedupFile, "config.xml", "config data")
val backedUpDir = child.getSingleChild()
assertEquals("Wrong backed up dir", "options", backedUpDir.name)
val backedUpFile = backedUpDir.getSingleChild()
assertFile(backedUpFile, "config.xml", "config data")
}
@Test
fun `migrate previous backup format`() {
@Test fun `migrate previous backup format`() {
val optionsDir = backupDir.resolve("options").createDirectories()
optionsDir.resolve("other.xml").createFile().writeText("old content")
val inspectionsDir = backupDir.resolve("inspections").createDirectories()
@@ -71,16 +66,15 @@ class ConfigBackupTest : ConfigImportHelperBaseTest() {
val migratedOptionsFile = migratedOptions.getSingleChild()
assertFile(migratedOptionsFile, "other.xml", "old content")
val backedupDir = children[1].getSingleChild()
assertEquals("Wrong backed up dir", "options", backedupDir.name)
val backedupFile = backedupDir.getSingleChild()
assertFile(backedupFile, "config.xml", "config data")
val backedUpDir = children[1].getSingleChild()
assertEquals("Wrong backed up dir", "options", backedUpDir.name)
val backedUpFile = backedUpDir.getSingleChild()
assertFile(backedUpFile, "config.xml", "config data")
}
@Test
fun `cleanup backups if there are too many of them`() {
@Test fun `cleanup backups if there are too many of them`() {
val now = LocalDateTime.now()
for (i in 1..MAX_BACKUPS_NUMBER) {
for (i in 1..ConfigBackup.MAX_BACKUPS_NUMBER) {
val date = getDateFormattedForBackupDir(now.minusDays(i.toLong()))
createBackupDirForDate(date)
}
@@ -88,13 +82,12 @@ class ConfigBackupTest : ConfigImportHelperBaseTest() {
moveDirToBackup()
val children = backupDir.listDirectoryEntries().sortedBy { it.name }
assertEquals("Unexpected number of entries inside $backupDir: $children", MAX_BACKUPS_NUMBER, children.size)
val oldestDate = getDateFormattedForBackupDir(now.minusDays(MAX_BACKUPS_NUMBER.toLong()))
assertEquals("Unexpected number of entries inside $backupDir: $children", ConfigBackup.MAX_BACKUPS_NUMBER, children.size)
val oldestDate = getDateFormattedForBackupDir(now.minusDays(ConfigBackup.MAX_BACKUPS_NUMBER.toLong()))
assertFalse("The oldest dir should have been deleted", children.any { it.name == oldestDate })
}
@Test
fun `create backup with index if there is already folder with current date`() {
@Test fun `create backup with index if there is already folder with current date`() {
// during the test this date can become not now, i.e. non-conflicting with the next backup, effectively making the test useless,
// however, it is ok if the test will be useful
val now = LocalDateTime.now()
@@ -111,10 +104,10 @@ class ConfigBackupTest : ConfigImportHelperBaseTest() {
val children = backupDir.listDirectoryEntries().sortedBy { it.name }
assertEquals("Unexpected number of entries inside $backupDir: $children", 4, children.size)
val createdDir = children.find { it.name !in dates }!!
val backedupDir = createdDir.getSingleChild()
assertEquals("Wrong backed up dir", "options", backedupDir.name)
val backedupFile = backedupDir.getSingleChild()
assertFile(backedupFile, "config.xml", "config data")
val backedUpDir = createdDir.getSingleChild()
assertEquals("Wrong backed up dir", "options", backedUpDir.name)
val backedUpFile = backedUpDir.getSingleChild()
assertFile(backedUpFile, "config.xml", "config data")
}
private fun moveDirToBackup() {
@@ -123,12 +116,6 @@ class ConfigBackupTest : ConfigImportHelperBaseTest() {
private fun createBackupDirForDate(date1: String): Path = backupDir.resolve(date1).createDirectories()
private fun createConfigDirToBackup(): Path {
val configDir = localTempDir.newDirectoryPath("temp-settings")
configDir.resolve("options/config.xml").write("config data")
return configDir
}
private fun getDateFormattedForBackupDir(dateTime: LocalDateTime): String =
dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm"))
@@ -1,29 +1,30 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// 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.application
import com.intellij.openapi.application.ConfigImportHelper.ConfigImportOptions.BrokenPluginsFetcher
import com.intellij.openapi.application.ConfigImportHelper.ConfigImportOptions.LastCompatiblePluginUpdatesFetcher
import com.intellij.openapi.application.ConfigImportHelper.findConfigDirectories
import com.intellij.openapi.components.StoragePathMacros
import com.intellij.openapi.util.SystemInfo
import com.intellij.testFramework.fixtures.BareTestFixtureTestCase
import com.intellij.testFramework.rules.InMemoryFsRule
import com.intellij.testFramework.rules.TempDirectory
import com.intellij.util.SystemProperties
import com.intellij.util.system.OS
import org.junit.Rule
import org.junit.rules.ExternalResource
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import java.util.function.Function
abstract class ConfigImportHelperBaseTest : BareTestFixtureTestCase() {
@JvmField @Rule val memoryFs = InMemoryFsRule(SystemInfo.isWindows)
@JvmField @Rule val localTempDir = TempDirectory()
@JvmField @Rule val memoryFs = InMemoryFsRule(windows = OS.CURRENT == OS.Windows)
@JvmField @Rule val configImportMarketplaceStub = ConfigImportMarketplaceStub()
protected fun newTempDir(name: String): Path =
Files.createDirectories(memoryFs.fs.getPath("_temp", name).toAbsolutePath())
protected fun createConfigDir(version: String, modern: Boolean = version >= "2020.1", product: String = "IntelliJIdea", storageTS: Long = 0): Path {
val path = when {
modern -> PathManager.getDefaultConfigPathFor("${product}${version}")
SystemInfo.isMac -> "${SystemProperties.getUserHome()}/Library/Preferences/${product}${version}"
OS.CURRENT == OS.macOS -> "${SystemProperties.getUserHome()}/Library/Preferences/${product}${version}"
else -> "${SystemProperties.getUserHome()}/.${product}${version}/config"
}
val dir = Files.createDirectories(memoryFs.fs.getPath(path).normalize())
@@ -34,19 +35,19 @@ abstract class ConfigImportHelperBaseTest : BareTestFixtureTestCase() {
protected fun writeStorageFile(config: Path, lastModified: Long) {
val file = config.resolve("${PathManager.OPTIONS_DIRECTORY}/${StoragePathMacros.NON_ROAMABLE_FILE}")
Files.createDirectories(file.parent)
Files.write(file, "<application/>".toByteArray())
Files.writeString(file, "<application/>")
Files.setLastModifiedTime(file, FileTime.fromMillis(lastModified))
}
protected fun findConfigDirectories(newConfigPath: Path): List<Path> = ConfigImportHelper.findConfigDirectories(newConfigPath).paths
protected fun findConfigDirectories(newConfigPath: Path): List<Path> = findConfigDirectories(newConfigPath, null, emptyList()).paths
// disables broken plugins fetcher from the Marketplace by default
class ConfigImportMarketplaceStub : ExternalResource() {
override fun before() {
assert(ConfigImportHelper.testBrokenPluginsFetcherStub == null)
ConfigImportHelper.testBrokenPluginsFetcherStub = BrokenPluginsFetcher { null } // force use of brokenPlugins from the distribution
ConfigImportHelper.testBrokenPluginsFetcherStub = Function { null } // using broken plugins from the distribution
assert(ConfigImportHelper.testLastCompatiblePluginUpdatesFetcher == null)
ConfigImportHelper.testLastCompatiblePluginUpdatesFetcher = LastCompatiblePluginUpdatesFetcher { null }
ConfigImportHelper.testLastCompatiblePluginUpdatesFetcher = Function { null }
}
fun unset() {
@@ -12,8 +12,6 @@ import com.intellij.ide.startup.StartupActionScriptManager
import com.intellij.ide.util.PropertiesComponent
import com.intellij.idea.TestFor
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ConfigImportHelper.ConfigImportOptions.BrokenPluginsFetcher
import com.intellij.openapi.application.ConfigImportHelper.ConfigImportOptions.LastCompatiblePluginUpdatesFetcher
import com.intellij.openapi.components.StoragePathMacros
import com.intellij.openapi.components.impl.stores.stateStore
import com.intellij.openapi.diagnostic.logger
@@ -22,7 +20,6 @@ import com.intellij.openapi.observable.util.setSystemProperty
import com.intellij.openapi.observable.util.whenDisposed
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.util.BuildNumber
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.testFramework.plugins.buildMainJar
import com.intellij.platform.testFramework.plugins.buildZip
@@ -34,31 +31,28 @@ import com.intellij.testFramework.replaceService
import com.intellij.util.SystemProperties
import com.intellij.util.io.createDirectories
import com.intellij.util.queryParameters
import com.intellij.util.system.OS
import com.sun.net.httpserver.HttpServer
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Condition
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.IOException
import java.net.InetSocketAddress
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Function
import java.util.function.Predicate
import kotlin.io.path.isDirectory
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.readLines
import kotlin.io.path.writeLines
import kotlin.io.path.*
import kotlin.test.fail
private val LOG = logger<ConfigImportHelperTest>()
class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
val options = ConfigImportHelper.ConfigImportOptions(LOG).apply { isHeadless = true; }
val options = ConfigImportHelper.ConfigImportOptions(LOG).apply { headless = true }
@Test fun `config directory is valid for import`() {
PropertiesComponent.getInstance().setValue("property.ConfigImportHelperTest", true)
@@ -141,7 +135,7 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
}
private fun doKeyMapTest(version: String, isMigrationExpected: Boolean) {
assumeTrue("macOS-only", SystemInfo.isMac)
assumeTrue("macOS-only", OS.CURRENT == OS.macOS)
val oldConfigDir = createConfigDir(version, product = "DataGrip")
val newConfigDir = createConfigDir("2019.2", product = "DataGrip")
@@ -200,69 +194,67 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
}
@Test fun `migrate plugins to empty directory`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
plugin("my-plugin") { dependsIntellijModulesLang() }.buildMainJar(oldPluginsDir.resolve("my-plugin.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(newPluginsDir).isDirectoryContaining { it.fileName.toString() == "my-plugin.jar" }
assertThat(newPluginsDir).isDirectoryContaining { it.name == "my-plugin.jar" }
}
@Test fun `download incompatible plugin`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
plugin("my-plugin") {
dependsIntellijModulesLang()
untilBuild = "193.1"
}.buildMainJar(oldPluginsDir.resolve("my-plugin.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
Registry.get("marketplace.certificate.signature.check").setValue(false, testRootDisposable) // skip verifying plugin certificates
options.compatibleBuildNumber = BuildNumber.fromString("201.1")
options.downloadService = object : MarketplacePluginDownloadService() {
override fun downloadPlugin(pluginUrl: String, indicator: ProgressIndicator?): Path {
val path = localTempDir.newDirectory("pluginTemp").toPath().resolve("my-plugin-new.jar")
val path = newTempDir("pluginTemp").resolve("my-plugin-new.jar")
plugin("my-plugin") { dependsIntellijModulesLang() }.buildMainJar(path)
return path
}
}
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(newPluginsDir).isDirectoryContaining { it.fileName.toString() == "my-plugin-new.jar" }
assertThat(newPluginsDir).isDirectoryContaining { it.name == "my-plugin-new.jar" }
}
@Test fun `keep incompatible plugin if can't download compatible`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
plugin("my-plugin") {
dependsIntellijModulesLang()
untilBuild = "193.1"
}.buildMainJar(oldPluginsDir.resolve("my-plugin.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
options.compatibleBuildNumber = BuildNumber.fromString("201.1")
options.downloadService = object : MarketplacePluginDownloadService() {
override fun downloadPlugin(pluginUrl: String, indicator: ProgressIndicator?) =
throw IOException("404")
override fun downloadPlugin(pluginUrl: String, indicator: ProgressIndicator?) = throw IOException("404")
}
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(newPluginsDir).isDirectoryContaining { it.fileName.toString() == "my-plugin.jar" }
assertThat(newPluginsDir).isDirectoryContaining { it.name == "my-plugin.jar" }
}
@Test fun `skip bundled plugins`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
val oldBundledPluginsDir = localTempDir.newDirectory("oldBundled").toPath()
val oldBundledPluginsDir = newTempDir("oldBundled")
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.1" }.buildMainJar(oldBundledPluginsDir.resolve("my-plugin-bundled.jar"))
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.0" }.buildMainJar(oldPluginsDir.resolve("my-plugin.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
options.bundledPluginPath = oldBundledPluginsDir
@@ -271,22 +263,22 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
}
@Test fun `skip broken plugins`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.0" }.buildMainJar(oldPluginsDir.resolve("my-plugin.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
options.brokenPluginsFetcher = BrokenPluginsFetcher { mapOf(PluginId.getId("my-plugin") to setOf("1.0")) }
ConfigImportHelper.testBrokenPluginsFetcherStub = Function { mapOf(PluginId.getId("my-plugin") to setOf("1.0")) }
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(newPluginsDir).doesNotExist()
}
@Test fun `skip pending upgrades`() {
val oldConfigDir = localTempDir.newDirectory("old/config").toPath()
val oldConfigDir = newTempDir("old/config")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
val oldPluginsTempDir = localTempDir.newDirectory("old/system/plugins").toPath()
val oldPluginsTempDir = newTempDir("old/system/plugins")
val tempPath = oldPluginsTempDir.resolve("my-plugin.jar")
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.1" }.buildMainJar(tempPath)
@@ -296,19 +288,19 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.0" }.buildMainJar(oldPluginsDir.resolve("my-plugin-1.0.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(newPluginsDir)
.isDirectoryContaining { it.fileName.toString() == "my-plugin-1.1.jar" }
.isDirectoryNotContaining { it.fileName.toString() == "my-plugin-1.0.jar" }
.isDirectoryContaining { it.name == "my-plugin-1.1.jar" }
.isDirectoryNotContaining { it.name == "my-plugin-1.0.jar" }
}
@Test fun `do not download updates for plugins with pending updates`() {
val oldConfigDir = localTempDir.newDirectory("old/config").toPath()
val oldConfigDir = newTempDir("old/config")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
val oldPluginsTempDir = localTempDir.newDirectory("old/system/plugins").toPath()
val oldPluginsTempDir = newTempDir("old/system/plugins")
val tempPath = oldPluginsTempDir.resolve("my-plugin.jar")
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.1" }.buildMainJar(tempPath)
@@ -322,7 +314,7 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
untilBuild = "193.1"
}.buildMainJar(oldPluginsDir.resolve("my-plugin-1.0.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
options.compatibleBuildNumber = BuildNumber.fromString("201.1")
@@ -333,14 +325,14 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
}
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(newPluginsDir)
.isDirectoryContaining { it.fileName.toString() == "my-plugin-1.1.jar" }
.isDirectoryNotContaining { it.fileName.toString() == "my-plugin-1.0.jar" }
.isDirectoryContaining { it.name == "my-plugin-1.1.jar" }
.isDirectoryNotContaining { it.name == "my-plugin-1.0.jar" }
}
@Test fun `skip pending upgrades for plugin zips`() {
val oldConfigDir = localTempDir.newDirectory("old/config").toPath()
val oldConfigDir = newTempDir("old/config")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
val oldPluginsTempDir = localTempDir.newDirectory("old/system/plugins").toPath()
val oldPluginsTempDir = newTempDir("old/system/plugins")
val tempPath = oldPluginsTempDir.resolve("my-plugin.zip")
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.1" }.buildZip(tempPath)
@@ -350,21 +342,21 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.0" }.buildMainJar(oldPluginsDir.resolve("my-plugin-1.0.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(newPluginsDir)
.isDirectoryContaining { it.fileName.toString() == "my-plugin" && it.isDirectory() }
.isDirectoryNotContaining { it.fileName.toString() == "my-plugin-1.0.jar" }
.isDirectoryContaining { it.name == "my-plugin" && it.isDirectory() }
.isDirectoryNotContaining { it.name == "my-plugin-1.0.jar" }
}
@Test fun `do not migrate plugins to existing directory`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
val oldPluginZip = Files.createFile(oldPluginsDir.resolve("old-plugin.zip"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = Files.createDirectories(newConfigDir.resolve("plugins"))
val newPluginZip = Files.createFile(newPluginsDir.resolve("new-plugin.zip"))
@@ -376,11 +368,11 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
}
@Test fun `filtering custom VM options`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
@Suppress("SpellCheckingInspection") val outlaws = listOf(
"-XX:MaxJavaStackTraceDepth=-1", "-Xverify:none", "-noverify", "-agentlib:yjpagent=opts", "-agentpath:/path/to/lib-yjpagent.so=opts")
Files.write(oldConfigDir.resolve(VMOptions.getFileName()), outlaws)
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldConfigDir.resolve("plugins"), newConfigDir.resolve("plugins"), options)
@@ -390,7 +382,7 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
@Test fun `de-duplicating custom VM options`() {
val platformOptions = listOf("-Xms128m", "-Xmx750m", "-XX:ReservedCodeCacheSize=512m", "-XX:+UseG1GC")
val userOptions = listOf("-Xms512m", "-Xmx2g", "-XX:ReservedCodeCacheSize=240m", "-XX:+UseZGC")
@Suppress("SpellCheckingInspection") val commonOptions = listOf(
val commonOptions = listOf(
"-XX:SoftRefLRUPolicyMSPerMB=50", "-XX:CICompilerCount=2", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:-OmitStackTraceInFastThrow",
"-ea", "-Dsun.io.useCanonCaches=false", "-Djdk.http.auth.tunneling.disabledSchemes=\"\"", "-Djdk.attach.allowAttachSelf=true",
"-Djdk.module.illegalAccess.silent=true", "-Dkotlinx.coroutines.debug=off")
@@ -418,37 +410,37 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
val cfg191 = createConfigDir("2019.1")
populate(cfg191, null, null, null)
if (!SystemInfo.isMac) {
if (OS.CURRENT != OS.macOS) {
Files.writeString(cfg191.parent.resolve("some_file.txt"), "...")
}
val cfg192 = createConfigDir("2019.2")
populate(cfg192, null, null, null)
val expected192 = when {
SystemInfo.isMac -> listOf(cfg192)
val expected192 = when (OS.CURRENT) {
OS.macOS -> listOf(cfg192)
else -> listOf(cfg192.parent)
}
val cfg193 = createConfigDir("2019.3")
val plugins193 = when {
SystemInfo.isMac -> cfg193.parent.parent.resolve("Application Support").resolve(cfg193.fileName)
val plugins193 = when (OS.CURRENT) {
OS.macOS -> cfg193.parent.parent.resolve("Application Support").resolve(cfg193.fileName)
else -> cfg193.resolve("plugins")
}
val sys193 = when {
SystemInfo.isMac -> cfg193.parent.parent.resolve("Caches").resolve(cfg193.fileName)
val sys193 = when (OS.CURRENT) {
OS.macOS -> cfg193.parent.parent.resolve("Caches").resolve(cfg193.fileName)
else -> cfg193.parent.resolve("system")
}
val logs193 = when {
SystemInfo.isMac -> cfg193.parent.parent.resolve("Logs").resolve(cfg193.fileName)
val logs193 = when (OS.CURRENT) {
OS.macOS -> cfg193.parent.parent.resolve("Logs").resolve(cfg193.fileName)
else -> sys193.resolve("logs")
}
populate(cfg193, plugins193, sys193, logs193)
val expected193 = when {
SystemInfo.isMac -> listOf(cfg193, sys193, plugins193, logs193)
val expected193 = when (OS.CURRENT) {
OS.macOS -> listOf(cfg193, sys193, plugins193, logs193)
else -> listOf(cfg193.parent)
}
val cachesAndLogs193 = when {
SystemInfo.isMac -> listOf(sys193, logs193)
val cachesAndLogs193 = when (OS.CURRENT) {
OS.macOS -> listOf(sys193, logs193)
else -> listOf(sys193)
}
@@ -456,26 +448,26 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
populate(cfg201, null, null, null)
val cfg202 = createConfigDir("2020.2")
val sys202 = cfg202.fileSystem.getPath(PathManager.getDefaultSystemPathFor(cfg202.fileName.toString()))
val sys202 = cfg202.fileSystem.getPath(PathManager.getDefaultSystemPathFor(cfg202.name))
populate(cfg202, null, sys202, null)
val cfg203 = createConfigDir("2020.3")
val sys203 = cfg203.fileSystem.getPath(PathManager.getDefaultSystemPathFor(cfg203.fileName.toString()))
val plugins203 = cfg203.fileSystem.getPath(PathManager.getDefaultPluginPathFor(cfg203.fileName.toString()))
val logs203 = cfg203.fileSystem.getPath(PathManager.getDefaultLogPathFor(cfg203.fileName.toString()))
val sys203 = cfg203.fileSystem.getPath(PathManager.getDefaultSystemPathFor(cfg203.name))
val plugins203 = cfg203.fileSystem.getPath(PathManager.getDefaultPluginPathFor(cfg203.name))
val logs203 = cfg203.fileSystem.getPath(PathManager.getDefaultLogPathFor(cfg203.name))
populate(cfg203, plugins203, sys203, logs203)
val expected203 = when {
SystemInfo.isWindows -> listOf(cfg203, sys203)
SystemInfo.isMac -> listOf(cfg203, sys203, logs203)
val expected203 = when (OS.CURRENT) {
OS.Windows -> listOf(cfg203, sys203)
OS.macOS -> listOf(cfg203, sys203, logs203)
else -> listOf(cfg203, sys203, plugins203)
}
val cachesAndLogs203 = when {
SystemInfo.isMac -> listOf(sys203, logs203)
OS.CURRENT == OS.macOS -> listOf(sys203, logs203)
else -> listOf(sys203)
}
val current = createConfigDir("2021.2")
val result = ConfigImportHelper.findConfigDirectories(current)
val result = ConfigImportHelper.findConfigDirectories(current, null, emptyList())
assertThat(result.paths).containsExactlyInAnyOrder(cfg191, cfg192, cfg193, cfg201, cfg202, cfg203)
val related = result.paths.map { result.findRelatedDirectories(it, false) }
@@ -491,21 +483,21 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
val defaultProjectPath = "${SystemProperties.getUserHome()}/PhpstormProjects"
Files.createDirectories(memoryFs.fs.getPath(defaultProjectPath))
val current = createConfigDir("2021.2", product = "PhpStorm")
val result = ConfigImportHelper.findConfigDirectories(current)
val result = ConfigImportHelper.findConfigDirectories(current, null, emptyList())
assertThat(result.paths).isEmpty()
}
@Test fun `suffix-less directories are excluded`() {
createConfigDir(product = "Rider", version = "", modern = true)
val current = createConfigDir(product = "Rider", version = "2022.1")
val result = ConfigImportHelper.findConfigDirectories(current)
val result = ConfigImportHelper.findConfigDirectories(current, null, emptyList())
assertThat(result.paths).isEmpty()
}
@Test fun `suffix-less directories are excluded case-insensitively`() {
createConfigDir(product = "RIDER", version = "", modern = true)
val current = createConfigDir(product = "Rider", version = "2022.1")
val result = ConfigImportHelper.findConfigDirectories(current)
val result = ConfigImportHelper.findConfigDirectories(current, null, emptyList())
assertThat(result.paths).isEmpty()
}
@@ -513,7 +505,7 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
createConfigDir(product = "RiderFlow", version = "", modern = true)
createConfigDir(product = "RiderRemoteDebugger", version = "", modern = true)
val current = createConfigDir(product = "Rider", version = "2023.2")
val result = ConfigImportHelper.findConfigDirectories(current)
val result = ConfigImportHelper.findConfigDirectories(current, null, emptyList())
assertThat(result.paths).isEmpty()
}
@@ -521,18 +513,17 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
createConfigDir(product = ".clion-vcpkg", version = "", modern = false) // was created at the user dir by older versions
createConfigDir(product = "CLionNova", version = "2023.2", modern = true) // "CLion" + RADLER_SUFFIX = "CLionNova"
val current = createConfigDir(product = "CLion", version = "2023.2")
val result = ConfigImportHelper.findConfigDirectories(current)
val result = ConfigImportHelper.findConfigDirectories(current, null, emptyList())
assertThat(result.paths).isEmpty()
}
@Suppress("SpellCheckingInspection")
@Test fun `merging VM options`() {
val oldConfigDir = createConfigDir(version = "2023.1")
val oldVmOptionsFile = oldConfigDir.resolve(VMOptions.getFileName()).writeLines(listOf("-Xmx4g", "-Dsome.prop=old.val"))
val newConfigDir = createConfigDir(version = "2023.2")
val newVmOptionsFile = newConfigDir.resolve(VMOptions.getFileName()).writeLines(listOf("-Xmx2048m", "-Dsome.prop=new.val"))
options.mergeVmOptions = true
options.isMergeVmOptions = true
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldConfigDir.resolve("plugins"), newConfigDir.resolve("plugins"), options)
assertThat(newVmOptionsFile.readLines()).containsExactly("-Xmx4g", "-Dsome.prop=new.val")
@@ -543,11 +534,10 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
assertThat(newVmOptionsFile.readLines()).containsExactly("-Xmx2048m", "-Dunique.prop=some.val")
}
@Suppress("SpellCheckingInspection")
@TestFor(issues = ["IDEA-341860"])
@Test fun `don't ask for VM options restart, if they are actual`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val newConfigDir = newTempDir("newConfig")
val otherXml = oldConfigDir.resolve(PathManager.OPTIONS_DIRECTORY + '/' + StoragePathMacros.NON_ROAMABLE_FILE)
Files.createDirectories(otherXml.parent)
@@ -573,12 +563,12 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
}
@Test fun `uses broken plugins from marketplace by default`() {
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
plugin("my-plugin") { dependsIntellijModulesLang(); version = "1.0" }.buildMainJar(oldPluginsDir.resolve("my-plugin.jar"))
plugin("my-plugin-2") { dependsIntellijModulesLang(); version = "1.0" }.buildMainJar(oldPluginsDir.resolve("my-plugin-2.jar"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
val brokenPluginsDownloaded = AtomicInteger()
@@ -601,31 +591,27 @@ class ConfigImportHelperTest : ConfigImportHelperBaseTest() {
}, testRootDisposable)
configImportMarketplaceStub.unset() // enable marketplace fetching
val options = ConfigImportHelper.ConfigImportOptions(LOG).apply { isHeadless = true } // reinstantiate
options.compatibleBuildNumber = BuildNumber.fromString("201.1")
val options = ConfigImportHelper.ConfigImportOptions(LOG).apply {
headless = true
compatibleBuildNumber = BuildNumber.fromString("201.1")
}
ConfigImportHelper.doImport(oldConfigDir, newConfigDir, null, oldPluginsDir, newPluginsDir, options)
assertThat(brokenPluginsDownloaded).hasValue(1)
assertThat(newPluginsDir).exists()
.isDirectoryContaining { it.fileName.toString() == "my-plugin-2.jar" }
.isDirectoryNotContaining { it.fileName.toString() == "my-plugin.jar" }
}
}
@RunWith(Parameterized::class)
class ConfigImportHelperPluginUpdateModeTest(val updateIncompatibleOnly: Boolean) : ConfigImportHelperBaseTest() {
companion object {
@JvmStatic
@Parameterized.Parameters(name = "updateIncompatibleOnly={0}")
fun data() = listOf(false, true)
.isDirectoryContaining { it.name == "my-plugin-2.jar" }
.isDirectoryNotContaining { it.name == "my-plugin.jar" }
}
@Test
fun `update plugins mode`() {
@Test fun `update only incompatible plugins`() = updatePlugins(updateIncompatibleOnly = true)
@Test fun `update all plugins`() = updatePlugins(updateIncompatibleOnly = false)
private fun updatePlugins(updateIncompatibleOnly: Boolean) {
// com.intellij.openapi.application.ConfigImportHelper.UPDATE_INCOMPATIBLE_PLUGINS_PROPERTY
setSystemProperty("idea.config.import.update.incompatible.plugins.only", updateIncompatibleOnly.toString(), testRootDisposable)
val oldConfigDir = localTempDir.newDirectory("oldConfig").toPath()
val oldConfigDir = newTempDir("oldConfig")
val oldPluginsDir = Files.createDirectories(oldConfigDir.resolve("plugins"))
fun spec(id: String, version: String) = plugin(id) { dependsIntellijModulesLang(); this@plugin.version = version }
@@ -634,14 +620,14 @@ class ConfigImportHelperPluginUpdateModeTest(val updateIncompatibleOnly: Boolean
spec("migrate", "1.0").buildMainJar(oldPluginsDir.resolve("migrate.jar"))
spec("disabled", "1.0").buildMainJar(oldPluginsDir.resolve("disabled.jar"))
val repoDir = localTempDir.newDirectory("repo").toPath()
val repoDir = newTempDir("repo")
spec("broken", "1.1").buildMainJar(repoDir.resolve("broken.jar"))
spec("update", "1.1").buildMainJar(repoDir.resolve("update.jar"))
spec("disabled", "1.1").buildMainJar(repoDir.resolve("disabled.jar"))
saveDisabledPluginsAndInvalidate(oldConfigDir, listOf("disabled"))
val newConfigDir = localTempDir.newDirectory("newConfig").toPath()
val newConfigDir = newTempDir("newConfig")
val newPluginsDir = newConfigDir.resolve("plugins")
val server = createTestServer(testRootDisposable)
@@ -660,29 +646,34 @@ class ConfigImportHelperPluginUpdateModeTest(val updateIncompatibleOnly: Boolean
handler.sendResponseHeaders(404, -1) // incompatible
return@createContext
}
val content = repoDir.resolve("$id.jar").toFile().readBytes()
handler.responseHeaders.add("Content-Disposition", "attachment; filename=$id.jar")
val content = repoDir.resolve("${id}.jar").readBytes()
handler.responseHeaders.add("Content-Disposition", "attachment; filename=${id}.jar")
handler.sendResponseHeaders(200, content.size.toLong())
handler.responseBody.use {
it.write(content)
}
}
ApplicationManager.getApplication().replaceService(MarketplaceCustomizationService::class.java, object : MarketplaceCustomizationService {
override fun getPluginManagerUrl(): String = server.url
override fun getPluginDownloadUrl(): String = server.url.trimEnd('/') + "/download"
override fun getPluginsListUrl(): String = throw AssertionError("unexpected")
override fun getPluginHomepageUrl(pluginId: PluginId): String = throw AssertionError("unexpected")
}, testRootDisposable)
ApplicationManager.getApplication().replaceService(
MarketplaceCustomizationService::class.java,
object : MarketplaceCustomizationService {
override fun getPluginManagerUrl(): String = server.url
override fun getPluginDownloadUrl(): String = server.url.trimEnd('/') + "/download"
override fun getPluginsListUrl(): String = throw AssertionError("unexpected")
override fun getPluginHomepageUrl(pluginId: PluginId): String = throw AssertionError("unexpected")
},
testRootDisposable
)
configImportMarketplaceStub.unset() // enable marketplace fetching
val options = ConfigImportHelper.ConfigImportOptions(LOG).apply { isHeadless = true } // reinstantiate
options.compatibleBuildNumber = BuildNumber.fromString("201.1")
options.pluginUpdatesFetcher = LastCompatiblePluginUpdatesFetcher {
val options = ConfigImportHelper.ConfigImportOptions(LOG).apply {
headless = true
compatibleBuildNumber = BuildNumber.fromString("201.1")
}
ConfigImportHelper.testLastCompatiblePluginUpdatesFetcher = Function {
buildMap {
for (id in listOf("update", "disabled", "migrate")) {
val pid = PluginId.getId(id)
val node = PluginNode(pid)
node.version = if (id == "migrate") "1.0" else "1.1"
val node = PluginNode(pid).apply { version = if (id == "migrate") "1.0" else "1.1" }
put(pid, node)
}
}
@@ -696,12 +687,12 @@ class ConfigImportHelperPluginUpdateModeTest(val updateIncompatibleOnly: Boolean
assertThat(newPluginsDir.resolve("migrate.jar")).hasSameBinaryContentAs(oldPluginsDir.resolve("migrate.jar"))
assertThat(newPluginsDir.resolve("disabled.jar")).hasSameBinaryContentAs((if (updateIncompatibleOnly) oldPluginsDir else repoDir).resolve("disabled.jar"))
}
}
private fun createTestServer(disposable: Disposable): HttpServer {
val server = HttpServer.create()!!
server.bind(InetSocketAddress(0), 1)
server.start()
disposable.whenDisposed { server.stop(0) }
return server
private fun createTestServer(disposable: Disposable): HttpServer {
val server = HttpServer.create()!!
server.bind(InetSocketAddress(0), 1)
server.start()
disposable.whenDisposed { server.stop(0) }
return server
}
}
@@ -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.ide.startup.importSettings.jb
import com.intellij.ide.AppLifecycleListener
@@ -49,7 +49,7 @@ private class JbAfterRestartSettingsApplier(private val coroutineScope: Coroutin
pluginIds.add(it.trim())
}
}
val importer = JbSettingsImporter(oldConfDir, oldConfDir, null)
val importer = JbSettingsImporter(oldConfDir, oldConfDir)
coroutineScope.launch {
withContext(Dispatchers.EDT) {
importer.importOptionsAfterRestart(options, pluginIds)
@@ -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.ide.startup.importSettings.jb
import com.intellij.configurationStore.getPerOsSettingsStorageFolderName
@@ -208,7 +208,7 @@ class JbImportServiceImpl(private val coroutineScope: CoroutineScope) : JbServic
val modalityState = ModalityState.current()
ImportSettingsEventsCollector.customDirectorySelected()
coroutineScope.async(modalityState.asContextElement()) {
val importer = JbSettingsImporter(folderPath, folderPath, null)
val importer = JbSettingsImporter(folderPath, folderPath)
importer.importRaw()
logger.info("Performing raw import from '$folderPath'")
withContext(Dispatchers.EDT) {
@@ -416,7 +416,7 @@ class JbImportServiceImpl(private val coroutineScope: CoroutineScope) : JbServic
&& unselectedPlugins.isNullOrEmpty()
val importData = TransferSettingsProgress(productInfo)
val importer = JbSettingsImporter(productInfo.configDir, productInfo.pluginDir, null)
val importer = JbSettingsImporter(productInfo.configDir, productInfo.pluginDir)
val progressIndicator = importData.createProgressIndicatorAdapter()
val importLifetime = LifetimeDefinition()
var importStartedDeferred: Deferred<Unit>? = null
@@ -700,4 +700,4 @@ private fun RawProgressReporter.toBridgeIndicator(): ProgressIndicator {
}
}
private val logger = logger<JbImportServiceImpl>()
private val logger = logger<JbImportServiceImpl>()
@@ -1,6 +1,5 @@
// 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.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.ide.startup.importSettings.jb
import com.intellij.configurationStore.*
@@ -44,7 +43,6 @@ import com.intellij.util.application
import com.intellij.util.io.copy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.InputStream
import java.nio.file.FileVisitResult
import java.nio.file.Files
@@ -53,15 +51,13 @@ import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import kotlin.io.path.*
class JbSettingsImporter(private val configDirPath: Path,
private val pluginsPath: Path,
private val prevIdeHome: Path?
) {
private val LOG = logger<JbSettingsImporter>()
class JbSettingsImporter(private val configDirPath: Path, private val pluginsPath: Path) {
private val componentStore = ApplicationManager.getApplication().stateStore as ComponentStoreImpl
private val defaultNewUIValue = true
private val additionalSchemeDirs = mapOf(FileTemplatesScheme.TEMPLATES_DIR to SettingsCategory.CODE)
// will be used as toposort for dependencies
// will be used as topological ordering for dependencies
// TODO: move to the component declaration instead
private val componentNamesDependencies = mapOf(
//IDEA-342818
@@ -105,12 +101,12 @@ class JbSettingsImporter(private val configDirPath: Path,
val parentElement = JDOMUtil.load(projectDefaultXmlPath)
val defaultProjectElement = parentElement.getChild("component")?.getChild("defaultProject") ?: return emptySet()
val retval = mutableSetOf<String>()
val retVal = mutableSetOf<String>()
for (componentElement in defaultProjectElement.getChildren("component")) {
val componentName = componentElement.getAttributeValue("name")
retval.add(componentName)
retVal.add(componentName)
}
return retval
return retVal
}
private fun findComponentsAndFiles(): Pair<Set<String>, Set<String>> {
@@ -217,7 +213,7 @@ class JbSettingsImporter(private val configDirPath: Path,
}
// we use LinkedHashSet, because we need ordering here
val appComponentNames: LinkedHashSet<String> = toposortComponentNames(componentAndFilesMap.keys)
val appComponentNames: LinkedHashSet<String> = topoSortComponentNames(componentAndFilesMap.keys)
withExternalStreamProvider(arrayOf(storageManager, defaultProjectStore.storageManager)) {
progressIndicator.checkCanceled()
@@ -241,18 +237,18 @@ class JbSettingsImporter(private val configDirPath: Path,
return Registry.getInstance().isRestartNeeded
}
// very basic and primitive toposort. Doesn't traverse, doesn't support transitive deps, etc.
private fun toposortComponentNames(components: Collection<String>): LinkedHashSet<String> {
val retval = LinkedHashSet<String>()
// very basic and primitive topological sort. Doesn't traverse, doesn't support transitive deps, etc.
private fun topoSortComponentNames(components: Collection<String>): LinkedHashSet<String> {
val retVal = LinkedHashSet<String>()
for (c in components) {
for (d in componentNamesDependencies[c]?:emptyList()) {
if (!retval.contains(d)){
retval.add(d)
if (!retVal.contains(d)){
retVal.add(d)
}
}
retval.add(c)
retVal.add(c)
}
return retval
return retVal
}
private suspend fun withExternalStreamProvider(storageManagers: Array<StateStorageManager>, action: () -> Unit) {
@@ -306,29 +302,29 @@ class JbSettingsImporter(private val configDirPath: Path,
}
private fun filesFromFolder(dir: Path, prefix: String = dir.name): Collection<String> {
val retval = ArrayList<String>()
val retVal = ArrayList<String>()
for (entry in dir.listDirectoryEntries()) {
if (entry.isRegularFile()) {
if (prefix.isEmpty()) {
retval.add(entry.name)
retVal.add(entry.name)
}
else {
retval.add("$prefix/${entry.name}")
retVal.add("$prefix/${entry.name}")
}
}
else {
val folderFiles = filesFromFolder(entry, "$prefix/${entry.name}")
retval.addAll(folderFiles)
retVal.addAll(folderFiles)
}
}
return retval
return retVal
}
// key: PSC, value - file
private fun filterComponents(allFiles: Set<String>, categories: Set<SettingsCategory>): Map<String, String> {
val componentManager = ApplicationManager.getApplication() as ComponentManagerEx
val retval = hashMapOf<String, String>()
val retVal = hashMapOf<String, String>()
val osFolderName = getPerOsSettingsStorageFolderName()
componentManager.processAllImplementationClasses { aClass, _ ->
val state = getStateOrNull(aClass) ?: return@processAllImplementationClasses
@@ -341,13 +337,13 @@ class JbSettingsImporter(private val configDirPath: Path,
return@processAllImplementationClasses
if (activeStorage.roamingType.isOsSpecific && allFiles.contains("$osFolderName/${activeStorage.value}")) {
retval[state.name] = "$osFolderName/${activeStorage.value}"
retVal[state.name] = "$osFolderName/${activeStorage.value}"
}
else if (allFiles.contains(activeStorage.value)) {
retval[state.name] = activeStorage.value
retVal[state.name] = activeStorage.value
}
}
return retval
return retVal
}
private fun getStateOrNull(aClass: Class<*>): State? {
@@ -362,9 +358,9 @@ class JbSettingsImporter(private val configDirPath: Path,
}
private fun filterSchemes(allFiles: Set<String>, categories: Set<SettingsCategory>): Set<String> {
val retval = hashSetOf<String>()
val retVal = hashSetOf<String>()
val schemeCategories = hashSetOf<String>()
// fileSpec is e.g. keymaps/mykeymap.xml
// fileSpec is e.g. `keymaps/my-keymap.xml`
(SchemeManagerFactory.getInstance() as SchemeManagerFactoryBase).process {
if (categories.contains(it.getSettingsCategory())) {
schemeCategories.add(it.fileSpec)
@@ -381,10 +377,10 @@ class JbSettingsImporter(private val configDirPath: Path,
continue
if (schemeCategories.contains(split[0])) {
retval.add(file)
retVal.add(file)
}
}
return retval
return retVal
}
fun installPlugins(
@@ -456,15 +452,17 @@ class JbSettingsImporter(private val configDirPath: Path,
}
}
private fun configImportOptions(progressIndicator: ProgressIndicator,
pluginIds: Collection<PluginId>): ConfigImportHelper.ConfigImportOptions {
private fun configImportOptions(progressIndicator: ProgressIndicator, pluginIds: Collection<PluginId>): ConfigImportHelper.ConfigImportOptions {
val importOptions = ConfigImportHelper.ConfigImportOptions(LOG)
importOptions.isHeadless = true
importOptions.headless = true
importOptions.headlessProgressIndicator = progressIndicator
importOptions.importSettings = object : ConfigImportSettings {
override fun processPluginsToMigrate(
newConfigDir: Path,
oldConfigDir: Path,
oldPluginsDir: Path,
options: ConfigImportHelper.ConfigImportOptions,
brokenPluginVersions: Map<PluginId?, Set<String?>?>?,
bundledPlugins: MutableList<IdeaPluginDescriptor>, // FIXME wrong arg name
nonBundledPlugins: MutableList<IdeaPluginDescriptor>, // FIXME wrong arg name
) {
@@ -503,43 +501,40 @@ class JbSettingsImporter(private val configDirPath: Path,
internal class ImportStreamProvider(private val configDirPath: Path) : StreamProvider {
override val isExclusive = false
override val saveStorageDataOnReload: Boolean
get() = false
override val saveStorageDataOnReload: Boolean get() = false
override fun isApplicable(fileSpec: String, roamingType: RoamingType): Boolean {
return false
}
override fun isApplicable(fileSpec: String, roamingType: RoamingType): Boolean = false
override fun write(fileSpec: String, content: ByteArray, roamingType: RoamingType) {
}
override fun write(fileSpec: String, content: ByteArray, roamingType: RoamingType) { }
override fun read(fileSpec: String, roamingType: RoamingType, consumer: (InputStream?) -> Unit): Boolean {
if (fileSpec == PROJECT_DEFAULT_FILE_SPEC) {
val path = configDirPath / PathManager.OPTIONS_DIRECTORY / PROJECT_DEFAULT_FILE_NAME
if (!path.isRegularFile())
return false
consumer(FileInputStream(path.toFile()))
val path = configDirPath.resolve(PathManager.OPTIONS_DIRECTORY).resolve(PROJECT_DEFAULT_FILE_NAME)
if (!path.isRegularFile()) return false
consumer(Files.newInputStream(path))
return true
}
(configDirPath / PathManager.OPTIONS_DIRECTORY / fileSpec).let {
(configDirPath.resolve(PathManager.OPTIONS_DIRECTORY).resolve(fileSpec)).let {
if (it.exists()) {
consumer(FileInputStream(it.toFile()))
consumer(Files.newInputStream(it))
return true
}
}
(configDirPath / fileSpec).let {
(configDirPath.resolve(fileSpec)).let {
if (it.exists()) {
consumer(FileInputStream(it.toFile()))
consumer(Files.newInputStream(it))
return true
}
}
return false
}
override fun processChildren(path: String,
roamingType: RoamingType,
filter: (name: String) -> Boolean,
processor: (name: String, input: InputStream, readOnly: Boolean) -> Boolean): Boolean {
override fun processChildren(
path: String,
roamingType: RoamingType,
filter: (name: String) -> Boolean,
processor: (name: String, input: InputStream, readOnly: Boolean) -> Boolean,
): Boolean {
LOG.debug("Process Children $path")
val folder = configDirPath.resolve(path)
if (!folder.exists()) return true
@@ -566,8 +561,5 @@ class JbSettingsImporter(private val configDirPath: Path,
LOG.debug("Deleting $fileSpec")
return false
}
}
}
private val LOG = logger<JbSettingsImporter>()