From 3f12e6c5783b7383a07c2de1b95bb1a8df179de0 Mon Sep 17 00:00:00 2001 From: Sergei Vorobyov Date: Thu, 2 Jul 2026 19:19:21 +0200 Subject: [PATCH] IDEA-390110 [external-system] new: add API for partial resolve With resilient sync a project resolve could produce partial project data but finish with the failure status. As a result every status-driven handler (auto-reload, build output, sync status, etc.) treated a sync that actually failed as a successful one. (cherry picked from commit 095c8651c6fbf5b8e160e9a81fa177f69e30fe03) IJ-CR-214557 GitOrigin-RevId: c316a7a7928adf77f1938cc5bf5280e1b3141e08 --- .../testFramework/ExternalSystemTestUtil.kt | 23 +- ...ernalSystemPartialResolutionException.java | 27 ++ .../ExternalSystemResolveProjectTask.java | 68 ++-- .../ExternalSystemNotificationManager.java | 7 +- .../util/ExternalSystemUtil.java | 45 +- .../ExternalSystemPartialResolveTest.kt | 383 ++++++++++++++++++ .../testFramework/fixtures/fixtures.kt | 14 + 7 files changed, 522 insertions(+), 45 deletions(-) create mode 100644 platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolutionException.java create mode 100644 platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolveTest.kt diff --git a/platform/external-system-api/testFramework/src/com/intellij/platform/externalSystem/testFramework/ExternalSystemTestUtil.kt b/platform/external-system-api/testFramework/src/com/intellij/platform/externalSystem/testFramework/ExternalSystemTestUtil.kt index 5f9f6975e6a4..9dabf3c29435 100644 --- a/platform/external-system-api/testFramework/src/com/intellij/platform/externalSystem/testFramework/ExternalSystemTestUtil.kt +++ b/platform/external-system-api/testFramework/src/com/intellij/platform/externalSystem/testFramework/ExternalSystemTestUtil.kt @@ -1,10 +1,13 @@ // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.externalSystem.testFramework +import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder import com.intellij.openapi.externalSystem.model.DataNode import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.model.project.ProjectData import com.intellij.openapi.externalSystem.service.project.ProjectDataManager +import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings +import com.intellij.openapi.externalSystem.util.ExternalSystemUtil import com.intellij.openapi.project.Project import com.intellij.testFramework.common.DEFAULT_TEST_TIMEOUT import com.intellij.testFramework.common.timeoutRunBlocking @@ -12,11 +15,12 @@ import com.intellij.util.messages.Topic import kotlin.time.Duration object ExternalSystemTestUtil { + @JvmField val TEST_EXTERNAL_SYSTEM_ID: ProjectSystemId = ProjectSystemId("TEST_EXTERNAL_SYSTEM_ID") @JvmField - val SETTINGS_TOPIC: Topic = Topic.create( + val SETTINGS_TOPIC: Topic = Topic.create( "TEST_EXTERNAL_SYSTEM_SETTINGS", TestExternalSystemSettingsListener::class.java ) } @@ -24,6 +28,21 @@ object ExternalSystemTestUtil { @JvmOverloads fun importData(dataNode: DataNode, project: Project, timeout: Duration = DEFAULT_TEST_TIMEOUT) { timeoutRunBlocking(timeout) { - ProjectDataManager.getInstance().importData(dataNode, project) + ProjectDataManager.getInstance().importData(dataNode, project) + } +} + +suspend fun linkProject( + project: Project, + systemId: ProjectSystemId, + projectSettings: ExternalProjectSettings, + configure: ImportSpecBuilder.() -> Unit = {}, +) { + ExternalSystemTestObservation.awaitProjectActivity(project) { + ExternalSystemUtil.linkExternalProject( + projectSettings, + ImportSpecBuilder(project, systemId) + .apply(configure) + ) } } \ No newline at end of file diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolutionException.java b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolutionException.java new file mode 100644 index 000000000000..105f5a6ea9e4 --- /dev/null +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolutionException.java @@ -0,0 +1,27 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.externalSystem.service.internal; + +import com.intellij.openapi.externalSystem.model.ExternalSystemException; +import com.intellij.openapi.externalSystem.model.DataNode; +import com.intellij.openapi.externalSystem.model.project.ProjectData; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; + +/** + * Marker exception to signal that the resolve failed but still produced result. + * + * @see ExternalSystemResolveProjectTask#doExecute + */ +@Internal +public class ExternalSystemPartialResolutionException extends ExternalSystemException { + + private final @NotNull DataNode myPartialProjectData; + + public ExternalSystemPartialResolutionException(@NotNull DataNode partialProjectData) { + myPartialProjectData = partialProjectData; + } + + public @NotNull DataNode getPartialProjectData() { + return myPartialProjectData; + } +} diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemResolveProjectTask.java b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemResolveProjectTask.java index a8418fc3520b..414894ea2a1d 100644 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemResolveProjectTask.java +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/internal/ExternalSystemResolveProjectTask.java @@ -124,24 +124,11 @@ public class ExternalSystemResolveProjectTask extends AbstractExternalSystemTask } }); - if (projectNode != null) { - myExternalProject.set(projectNode); - - var externalSystemManager = ExternalSystemApiUtil.getManager(projectSystemId); - assert externalSystemManager != null; - - var externalModulePaths = new HashSet(); - var moduleNodes = ExternalSystemApiUtil.findAll(projectNode, ProjectKeys.MODULE); - for (DataNode node : moduleNodes) { - externalModulePaths.add(node.getData().getLinkedExternalProjectPath()); - } - var ExternalProjectPath = projectNode.getData().getLinkedExternalProjectPath(); - var externalSystemSettings = externalSystemManager.getSettingsProvider().fun(project); - var externalProjectSettings = externalSystemSettings.getLinkedProjectSettings(ExternalProjectPath); - if (externalProjectSettings != null && !externalModulePaths.isEmpty()) { - externalProjectSettings.setModules(externalModulePaths); - } - } + myExternalProject.set(projectNode); + } + catch (ExternalSystemPartialResolutionException e) { + myExternalProject.set(e.getPartialProjectData()); + throw e; } finally { activity.finished(); @@ -201,15 +188,42 @@ public class ExternalSystemResolveProjectTask extends AbstractExternalSystemTask @Override protected void setState(@NotNull ExternalSystemTaskState state) { super.setState(state); - if (state.isStopped() && - // merging existing cache data with the new partial data is not supported yet - !(myResolverPolicy != null && myResolverPolicy.isPartialDataResolveAllowed())) { - InternalExternalProjectInfo projectInfo = - new InternalExternalProjectInfo(getExternalSystemId(), getExternalProjectPath(), myExternalProject.getAndSet(null)); - final long currentTimeMillis = System.currentTimeMillis(); - projectInfo.setLastImportTimestamp(currentTimeMillis); - projectInfo.setLastSuccessfulImportTimestamp(state == ExternalSystemTaskState.FAILED ? -1 : currentTimeMillis); - ProjectDataManagerImpl.getInstance().updateExternalProjectData(getIdeProject(), projectInfo); + if (!state.isStopped()) return; + // merging existing cache data with the new partial data is not supported yet + if (myResolverPolicy != null && myResolverPolicy.isPartialDataResolveAllowed()) return; + + var projectNode = myExternalProject.getAndSet(null); + + setExternalProjectInfo(state, projectNode); + if (projectNode != null) { + setExternalModulePaths(projectNode); + } + } + + private void setExternalProjectInfo(@NotNull ExternalSystemTaskState state, @Nullable DataNode projectNode) { + var projectInfo = new InternalExternalProjectInfo(getExternalSystemId(), getExternalProjectPath(), projectNode); + var currentTimeMillis = System.currentTimeMillis(); + projectInfo.setLastImportTimestamp(currentTimeMillis); + projectInfo.setLastSuccessfulImportTimestamp(state == ExternalSystemTaskState.FAILED ? -1 : currentTimeMillis); + ProjectDataManagerImpl.getInstance().updateExternalProjectData(getIdeProject(), projectInfo); + } + + private void setExternalModulePaths(@NotNull DataNode projectNode) { + var projectSystemId = getExternalSystemId(); + var project = getIdeProject(); + var externalSystemManager = ExternalSystemApiUtil.getManager(projectSystemId); + assert externalSystemManager != null; + + var externalModulePaths = new HashSet(); + var moduleNodes = ExternalSystemApiUtil.findAll(projectNode, ProjectKeys.MODULE); + for (DataNode node : moduleNodes) { + externalModulePaths.add(node.getData().getLinkedExternalProjectPath()); + } + var externalProjectPath = projectNode.getData().getLinkedExternalProjectPath(); + var externalSystemSettings = externalSystemManager.getSettingsProvider().fun(project); + var externalProjectSettings = externalSystemSettings.getLinkedProjectSettings(externalProjectPath); + if (externalProjectSettings != null && !externalModulePaths.isEmpty()) { + externalProjectSettings.setModules(externalModulePaths); } } } diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/notification/ExternalSystemNotificationManager.java b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/notification/ExternalSystemNotificationManager.java index 66143706c7e8..d589696fcbb4 100644 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/notification/ExternalSystemNotificationManager.java +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/notification/ExternalSystemNotificationManager.java @@ -21,6 +21,7 @@ import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.components.Service; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.externalSystem.issue.BuildIssueException; +import com.intellij.openapi.externalSystem.service.internal.ExternalSystemPartialResolutionException; import com.intellij.openapi.externalSystem.model.LocationAwareExternalSystemException; import com.intellij.openapi.externalSystem.model.ProjectSystemId; import com.intellij.openapi.externalSystem.service.project.manage.ExternalProjectsManagerImpl; @@ -48,6 +49,7 @@ import com.intellij.ui.content.MessageView; import com.intellij.util.ObjectUtils; import com.intellij.util.SmartList; import com.intellij.util.concurrency.ThreadingAssertions; +import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.accessibility.ScreenReader; import com.intellij.util.ui.update.DebouncedUpdates; @@ -189,8 +191,9 @@ public final class ExternalSystemNotificationManager implements Disposable { private static boolean isInternalError(@NotNull Throwable error, @NotNull ProjectSystemId externalSystemId) { if (RemoteUtil.unwrap(error) instanceof BuildIssueException) return false; - return ExternalSystemNotificationExtension.EP_NAME.getExtensionList().stream() - .anyMatch(extension -> externalSystemId.equals(extension.getTargetExternalSystemId()) && extension.isInternalError(error)); + if (RemoteUtil.unwrap(error) instanceof ExternalSystemPartialResolutionException) return true; + return ContainerUtil.exists(ExternalSystemNotificationExtension.EP_NAME.getExtensionList(), it -> + externalSystemId.equals(it.getTargetExternalSystemId()) && it.isInternalError(error)); } public boolean isNotificationActive(@NotNull Key notificationKey) { diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/util/ExternalSystemUtil.java b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/util/ExternalSystemUtil.java index 72a49ce3ee1d..cfa10f023ef4 100644 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/util/ExternalSystemUtil.java +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/util/ExternalSystemUtil.java @@ -50,11 +50,14 @@ import com.intellij.openapi.externalSystem.execution.ExternalSystemExecutionCons import com.intellij.openapi.externalSystem.importing.ImportSpec; import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder; import com.intellij.openapi.externalSystem.issue.BuildIssueException; +import com.intellij.openapi.externalSystem.model.DataNode; import com.intellij.openapi.externalSystem.model.ExternalProjectInfo; import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys; import com.intellij.openapi.externalSystem.model.ExternalSystemException; +import com.intellij.openapi.externalSystem.service.internal.ExternalSystemPartialResolutionException; import com.intellij.openapi.externalSystem.model.ProjectSystemId; import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings; +import com.intellij.openapi.externalSystem.model.project.ProjectData; import com.intellij.openapi.externalSystem.model.task.ExternalSystemTask; import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId; import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationEvent; @@ -603,20 +606,7 @@ public final class ExternalSystemUtil { try { var error = resolveProjectTask.getError(); if (error == null) { - var projectDataManager = ProjectDataManager.getInstance(); - var externalProjectData = projectDataManager.getExternalProjectData(project, externalSystemId, externalProjectPath); - var externalProject = ObjectUtils.doIfNotNull(externalProjectData, it -> it.getExternalProjectStructure()); - if (externalProject != null) { - if (importSpec.shouldCreateDirectoriesForEmptyContentRoots()) { - externalProject.putUserData(ContentRootDataService.CREATE_EMPTY_DIRECTORIES, Boolean.TRUE); - } - if (importSpec.shouldImportProjectData()) { - if (importSpec.shouldSelectProjectDataToImport()) { - selectProjectDataToImport(project, externalProjectData); - } - projectDataManager.importData(externalProject, project); - } - } + var externalProject = importProjectData(project, importSpec, externalSystemId, externalProjectPath); if (callback != null) { callback.onSuccess(taskId, externalProject); } @@ -626,6 +616,10 @@ public final class ExternalSystemUtil { } return; } + if (error instanceof ExternalSystemPartialResolutionException) { + // The resolve failed but produced partial project data. + importProjectData(project, importSpec, externalSystemId, externalProjectPath); + } if (error instanceof ImportCanceledException) { // stop refresh task return; @@ -667,6 +661,29 @@ public final class ExternalSystemUtil { } } + private static @Nullable DataNode importProjectData( + @NotNull Project project, + @NotNull ImportSpec importSpec, + @NotNull ProjectSystemId externalSystemId, + @NotNull String externalProjectPath + ) { + var projectDataManager = ProjectDataManager.getInstance(); + var externalProjectData = projectDataManager.getExternalProjectData(project, externalSystemId, externalProjectPath); + if (externalProjectData == null) return null; + var externalProject = externalProjectData.getExternalProjectStructure(); + if (externalProject == null) return null; + if (importSpec.shouldCreateDirectoriesForEmptyContentRoots()) { + externalProject.putUserData(ContentRootDataService.CREATE_EMPTY_DIRECTORIES, Boolean.TRUE); + } + if (importSpec.shouldImportProjectData()) { + if (importSpec.shouldSelectProjectDataToImport()) { + selectProjectDataToImport(project, externalProjectData); + } + projectDataManager.importData(externalProject, project); + } + return externalProject; + } + private static void selectProjectDataToImport( @NotNull Project project, @NotNull ExternalProjectInfo projectInfo diff --git a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolveTest.kt b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolveTest.kt new file mode 100644 index 000000000000..0e7566095297 --- /dev/null +++ b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/service/internal/ExternalSystemPartialResolveTest.kt @@ -0,0 +1,383 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.externalSystem.service.internal + +import com.intellij.execution.configurations.ConfigurationType +import com.intellij.openapi.Disposable +import com.intellij.openapi.externalSystem.ExternalSystemManager +import com.intellij.openapi.externalSystem.importing.ProjectResolverPolicy +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.ExternalSystemException +import com.intellij.openapi.externalSystem.model.ProjectKeys +import com.intellij.openapi.externalSystem.model.project.ProjectData +import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId +import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener +import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType +import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType.EXECUTE_TASK +import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType.RESOLVE_PROJECT +import com.intellij.openapi.externalSystem.service.execution.AbstractExternalSystemTaskConfigurationType +import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager +import com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallback +import com.intellij.openapi.externalSystem.service.project.ExternalSystemProjectResolver +import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider +import com.intellij.openapi.externalSystem.service.project.manage.ExternalProjectsManagerImpl +import com.intellij.openapi.externalSystem.service.project.manage.ExternalSystemTaskActivator +import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataService +import com.intellij.openapi.externalSystem.task.ExternalSystemTaskManager +import com.intellij.openapi.externalSystem.testFramework.fixtures.multiProjectFixture +import com.intellij.openapi.externalSystem.testFramework.fixtures.projectFixture +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil +import com.intellij.openapi.externalSystem.util.ExternalSystemConstants +import com.intellij.openapi.observable.util.whenDisposed +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Computable +import com.intellij.openapi.util.io.toCanonicalPath +import com.intellij.openapi.util.registry.Registry +import com.intellij.platform.externalSystem.testFramework.ExternalSystemTestUtil.TEST_EXTERNAL_SYSTEM_ID +import com.intellij.platform.externalSystem.testFramework.TestExternalProjectSettings +import com.intellij.platform.externalSystem.testFramework.TestExternalSystemExecutionSettings +import com.intellij.platform.externalSystem.testFramework.TestExternalSystemManager +import com.intellij.platform.externalSystem.testFramework.linkProject +import com.intellij.platform.externalSystem.testFramework.project +import com.intellij.platform.externalSystem.testFramework.toDataNode +import com.intellij.platform.testFramework.assertion.collectionAssertion.CollectionAssertions.assertEmpty +import com.intellij.platform.testFramework.assertion.collectionAssertion.CollectionAssertions.assertSingle +import com.intellij.testFramework.assertions.Assertions.assertThat +import com.intellij.testFramework.common.mock.requireImplemented +import com.intellij.testFramework.junit5.SystemProperty +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.TestDisposable +import com.intellij.testFramework.junit5.fixture.tempPathFixture +import com.intellij.testFramework.junit5.fixture.testFixture +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import org.junit.jupiter.api.assertNull +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +@TestApplication +@SystemProperty("force.execute.activated.tasks", true.toString()) +internal class ExternalSystemPartialResolveTest { + + private val projectRootFixture = tempPathFixture() + private val projectRoot by projectRootFixture + + private val projectsFixture = multiProjectFixture() + + private val projectFixture = projectsFixture.projectFixture(projectRootFixture) + private val project by projectFixture + + private val recordingManager by testFixture { + val project = projectFixture.init() + val manager = RecordingExternalSystemManager(project).apply { + addSyncTask(projectRoot, BEFORE_SYNC_TASK, ExternalSystemTaskActivator.Phase.BEFORE_SYNC) + addSyncTask(projectRoot, AFTER_SYNC_TASK, ExternalSystemTaskActivator.Phase.AFTER_SYNC) + } + initialized(manager) {} + } + + private val recordingDataService by testFixture { + initialized(RecordingProjectDataService()) {} + } + + private val recordingCallback by testFixture { + initialized(RecordingRefreshCallback()) {} + } + + private val recordingListener by testFixture { + val listener = RecordingTaskNotificationListener() + val manager = ExternalSystemProgressNotificationManager.getInstance() + manager.addNotificationListener(listener) + initialized(listener) { + manager.removeNotificationListener(listener) + } + } + + private val projectSettings by testFixture { + val projectRoot = projectRootFixture.init() + val projectSettings = TestExternalProjectSettings().apply { + externalProjectPath = projectRoot.toCanonicalPath() + } + initialized(projectSettings) {} + } + + private val projectData by testFixture { + val projectRoot = projectRootFixture.init() + val projectData = project(PROJECT_NAME, projectRoot.toCanonicalPath()) { + module(MODULE_NAME, projectRoot.resolve(MODULE_NAME).toCanonicalPath()) + } + initialized(projectData.toDataNode()) {} + } + + private val projectPath get() = projectRoot.toCanonicalPath() + private val modulePath get() = projectRoot.resolve(MODULE_NAME).toCanonicalPath() + + @BeforeEach + fun setUp(@TestDisposable disposable: Disposable) { + setRegistry(TEST_EXTERNAL_SYSTEM_ID.id + ExternalSystemConstants.USE_IN_PROCESS_COMMUNICATION_REGISTRY_KEY_SUFFIX, true, disposable) + ExternalSystemManager.EP_NAME.point.registerExtension(recordingManager, disposable) + ProjectDataService.EP_NAME.point.registerExtension(recordingDataService, disposable) + ConfigurationType.CONFIGURATION_TYPE_EP.point.registerExtension(TestExternalSystemConfigurationType(), disposable) + } + + @Test + fun `test resolve without issues`(): Unit = runBlocking { + + recordingManager.setResolveProjectInfo { projectData } + + linkProject(project, TEST_EXTERNAL_SYSTEM_ID, projectSettings) { + withCallback(recordingCallback) + } + + val actualProjectData = ExternalSystemApiUtil.findProjectInfo(project, TEST_EXTERNAL_SYSTEM_ID, projectPath) + assertNotNull(actualProjectData) + assertEquals(projectData, actualProjectData.externalProjectStructure) + assertNotEquals(NO_TIMESTAMP, actualProjectData.lastImportTimestamp) + assertNotEquals(NO_TIMESTAMP, actualProjectData.lastSuccessfulImportTimestamp) + assertSingle(modulePath, projectSettings.modules) + recordingDataService.assertCounters(numImports = 1) + recordingManager.assertCounters(numBeforeSync = 1, numAfterSync = 1) + recordingCallback.assertCounters(numSuccesses = 1, numFailures = 0) + recordingListener.assertCounters(RESOLVE_PROJECT, numSuccesses = 1, numFailures = 0) + recordingListener.assertCounters(EXECUTE_TASK, numSuccesses = 2, numFailures = 0) + } + + @Test + fun `test resolve with exception`(): Unit = runBlocking { + + recordingManager.setResolveProjectInfo { throw ExternalSystemException() } + + linkProject(project, TEST_EXTERNAL_SYSTEM_ID, projectSettings) { + withCallback(recordingCallback) + } + + val actualProjectData = ExternalSystemApiUtil.findProjectInfo(project, TEST_EXTERNAL_SYSTEM_ID, projectPath) + assertNotNull(actualProjectData) + assertNull(actualProjectData.externalProjectStructure) + assertNotEquals(NO_TIMESTAMP, actualProjectData.lastImportTimestamp) + assertEquals(NO_TIMESTAMP, actualProjectData.lastSuccessfulImportTimestamp) + assertEmpty(projectSettings.modules) + recordingDataService.assertCounters(numImports = 0) + recordingManager.assertCounters(numBeforeSync = 1, numAfterSync = 0) + recordingCallback.assertCounters(numSuccesses = 0, numFailures = 1) + recordingListener.assertCounters(RESOLVE_PROJECT, numSuccesses = 0, numFailures = 1) + recordingListener.assertCounters(EXECUTE_TASK, numSuccesses = 1, numFailures = 0) + } + + @Test + fun `test resolve with partial data policy`(): Unit = runBlocking { + + recordingManager.setResolveProjectInfo { projectData } + + linkProject(project, TEST_EXTERNAL_SYSTEM_ID, projectSettings) { + withCallback(recordingCallback) + projectResolverPolicy(ProjectResolverPolicy { true }) + } + + val actualProjectData = ExternalSystemApiUtil.findProjectInfo(project, TEST_EXTERNAL_SYSTEM_ID, projectPath) + assertNull(actualProjectData) + assertEmpty(projectSettings.modules) + recordingDataService.assertCounters(numImports = 0) + recordingManager.assertCounters(numBeforeSync = 1, numAfterSync = 1) + recordingCallback.assertCounters(numSuccesses = 1, numFailures = 0) + recordingListener.assertCounters(RESOLVE_PROJECT, numSuccesses = 1, numFailures = 0) + recordingListener.assertCounters(EXECUTE_TASK, numSuccesses = 2, numFailures = 0) + } + + @Test + fun `test resolve with partial resolve exception`(): Unit = runBlocking { + + recordingManager.setResolveProjectInfo { throw ExternalSystemPartialResolutionException(projectData) } + + linkProject(project, TEST_EXTERNAL_SYSTEM_ID, projectSettings) { + withCallback(recordingCallback) + } + + val actualProjectData = ExternalSystemApiUtil.findProjectInfo(project, TEST_EXTERNAL_SYSTEM_ID, projectPath) + assertNotNull(actualProjectData) + assertEquals(projectData, actualProjectData.externalProjectStructure) + assertNotEquals(NO_TIMESTAMP, actualProjectData.lastImportTimestamp) + assertEquals(NO_TIMESTAMP, actualProjectData.lastSuccessfulImportTimestamp) + assertSingle(modulePath, projectSettings.modules) + recordingDataService.assertCounters(numImports = 1) + recordingManager.assertCounters(numBeforeSync = 1, numAfterSync = 0) + recordingCallback.assertCounters(numSuccesses = 0, numFailures = 1) + recordingListener.assertCounters(RESOLVE_PROJECT, numSuccesses = 0, numFailures = 1) + recordingListener.assertCounters(EXECUTE_TASK, numSuccesses = 1, numFailures = 0) + + } + + class RecordingExternalSystemManager(private val project: Project) : TestExternalSystemManager(project) { + + private var resolveProjectInfo: (() -> DataNode)? = null + + private val beforeSyncTasks = AtomicInteger(0) + private val afterSyncTasks = AtomicInteger(0) + + fun setResolveProjectInfo(task: () -> DataNode) { + resolveProjectInfo = task + } + + fun assertCounters(numBeforeSync: Int, numAfterSync: Int) { + assertEquals(numBeforeSync, beforeSyncTasks.get()) + assertEquals(numAfterSync, afterSyncTasks.get()) + } + + fun addSyncTask(projectRoot: Path, taskName: String, phase: ExternalSystemTaskActivator.Phase) { + ExternalProjectsManagerImpl.getInstance(project).taskActivator.addTask( + ExternalSystemTaskActivator.TaskActivationEntry(TEST_EXTERNAL_SYSTEM_ID, phase, projectRoot.toCanonicalPath(), taskName) + ) + } + + override fun getProjectResolverClass(): Class> = + RecordingExternalSystemProjectResolver::class.java + + override fun getTaskManagerClass(): Class> = + RecordingExternalSystemTaskManager::class.java + + class RecordingExternalSystemProjectResolver : ExternalSystemProjectResolver { + + // @formatter:off + override fun resolveProjectInfo(id: ExternalSystemTaskId, projectPath: String, isPreviewMode: Boolean, settings: TestExternalSystemExecutionSettings?, resolverPolicy: ProjectResolverPolicy?, listener: ExternalSystemTaskNotificationListener): DataNode { + // @formatter:on + val manager = ExternalSystemApiUtil.getManager(id.projectSystemId) as RecordingExternalSystemManager + return manager.requireImplemented(RecordingExternalSystemManager::resolveProjectInfo)() + } + + override fun cancelTask(taskId: ExternalSystemTaskId, listener: ExternalSystemTaskNotificationListener): Boolean { + return false + } + } + + class RecordingExternalSystemTaskManager : ExternalSystemTaskManager { + + // @formatter:off + override fun executeTasks(projectPath: String, id: ExternalSystemTaskId, settings: TestExternalSystemExecutionSettings, listener: ExternalSystemTaskNotificationListener) { + // @formatter:on + val manager = ExternalSystemApiUtil.getManager(id.projectSystemId) as RecordingExternalSystemManager + if (BEFORE_SYNC_TASK in settings.tasks) { + manager.beforeSyncTasks.incrementAndGet() + } + if (AFTER_SYNC_TASK in settings.tasks) { + manager.afterSyncTasks.incrementAndGet() + } + } + + override fun cancelTask(id: ExternalSystemTaskId, listener: ExternalSystemTaskNotificationListener): Boolean { + return false + } + } + } + + private class RecordingProjectDataService : ProjectDataService { + + private val importCounter = AtomicInteger(0) + + fun assertCounters(numImports: Int) { + assertEquals(numImports, importCounter.get()) + } + + override fun getTargetDataKey() = ProjectKeys.PROJECT + + // @formatter:off + override fun importData(toImport: Collection>, projectData: ProjectData?, project: Project, modelsProvider: IdeModifiableModelsProvider) { + // @formatter:on + importCounter.addAndGet(toImport.size) + } + + // @formatter:off + override fun computeOrphanData(toImport: Collection>, projectData: ProjectData, project: Project, modelsProvider: IdeModifiableModelsProvider): Computable> { + // @formatter:on + return Computable { emptyList() } + } + + // @formatter:off + override fun removeData(toRemove: Computable>, toIgnore: Collection>, projectData: ProjectData, project: Project, modelsProvider: IdeModifiableModelsProvider) { + // @formatter:on + } + } + + private class RecordingRefreshCallback : ExternalProjectRefreshCallback { + + private val successCounter = AtomicInteger(0) + private val failureCounter = AtomicInteger(0) + + fun assertCounters(numSuccesses: Int, numFailures: Int) { + assertEquals(numSuccesses, successCounter.get()) + assertEquals(numFailures, failureCounter.get()) + } + + override fun onSuccess(externalProject: DataNode?) { + successCounter.incrementAndGet() + } + + override fun onFailure(errorMessage: String, errorDetails: String?) { + failureCounter.incrementAndGet() + } + } + + private class RecordingTaskNotificationListener : ExternalSystemTaskNotificationListener { + + private val counters = ConcurrentHashMap() + + private fun counter(type: ExternalSystemTaskType): TaskNotificationCounter { + return counters.computeIfAbsent(type) { TaskNotificationCounter() } + } + + fun assertCounters(type: ExternalSystemTaskType, numSuccesses: Int, numFailures: Int) { + counter(type).assertCounters(numSuccesses, numFailures) + } + + override fun onSuccess(projectPath: String, id: ExternalSystemTaskId) { + counter(id.type).onSuccess() + } + + override fun onFailure(projectPath: String, id: ExternalSystemTaskId, exception: Exception) { + counter(id.type).onFailure() + } + + private class TaskNotificationCounter { + + private val successCounter = AtomicInteger(0) + private val failureCounter = AtomicInteger(0) + + fun assertCounters(numSuccesses: Int, numFailures: Int) { + assertEquals(numSuccesses, successCounter.get()) + assertEquals(numFailures, failureCounter.get()) + } + + fun onSuccess() { + successCounter.incrementAndGet() + } + + fun onFailure() { + failureCounter.incrementAndGet() + } + } + } + + private class TestExternalSystemConfigurationType : AbstractExternalSystemTaskConfigurationType(TEST_EXTERNAL_SYSTEM_ID) { + override fun getConfigurationFactoryId(): String = "Test_external_system_id" + } + + companion object { + private const val PROJECT_NAME = "project" + private const val MODULE_NAME = "module" + private const val BEFORE_SYNC_TASK = "beforeSyncTask" + private const val AFTER_SYNC_TASK = "afterSyncTask" + private const val NO_TIMESTAMP = -1L + + private fun setRegistry(name: String, value: Boolean, disposable: Disposable) { + val registryValue = Registry.get(name) + registryValue.setValue(value) + disposable.whenDisposed { registryValue.resetToDefault() } + } + + private fun assertNotEquals(unexpected: Any?, actual: Any?) { + assertThat(actual).isNotEqualTo(unexpected) + } + } +} diff --git a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/testFramework/fixtures/fixtures.kt b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/testFramework/fixtures/fixtures.kt index 06db1e904625..68f338d012fb 100644 --- a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/testFramework/fixtures/fixtures.kt +++ b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/testFramework/fixtures/fixtures.kt @@ -2,9 +2,23 @@ package com.intellij.openapi.externalSystem.testFramework.fixtures import com.intellij.openapi.externalSystem.testFramework.fixtures.impl.MultiProjectTestFixtureImpl +import com.intellij.openapi.project.Project +import com.intellij.testFramework.closeProjectAsync import com.intellij.testFramework.junit5.fixture.TestFixture import com.intellij.testFramework.junit5.fixture.testFixture +import java.nio.file.Path fun multiProjectFixture(): TestFixture = testFixture { initialized(MultiProjectTestFixtureImpl()) {} +} + +fun TestFixture.projectFixture( + projectRootFixture: TestFixture, +): TestFixture = testFixture { + val gradle = this@projectFixture.init() + val projectRoot = projectRootFixture.init() + val project = gradle.openProject(projectRoot) + initialized(project) { + project.closeProjectAsync() + } } \ No newline at end of file