diff --git a/plugins/gradle/BUILD.bazel b/plugins/gradle/BUILD.bazel index 2613ed72bef9..da24419ce9d5 100644 --- a/plugins/gradle/BUILD.bazel +++ b/plugins/gradle/BUILD.bazel @@ -146,6 +146,7 @@ jvm_library( "//platform/external-system-impl:externalSystem-tests_test_lib", "//platform/external-system-api/testFramework", "//java/testFramework", + "//plugins/gradle/tooling-extension-impl:toolingExtension-impl", "//plugins/gradle/tooling-extension-impl:toolingExtension-tests_test_lib", "//java/execution/impl", "@lib//:assert_j", diff --git a/plugins/gradle/intellij.gradle.tests.iml b/plugins/gradle/intellij.gradle.tests.iml index d94a47ad7107..a6fc1d30d885 100644 --- a/plugins/gradle/intellij.gradle.tests.iml +++ b/plugins/gradle/intellij.gradle.tests.iml @@ -38,6 +38,7 @@ + diff --git a/plugins/gradle/src/org/jetbrains/plugins/gradle/service/project/GradleProjectResolver.java b/plugins/gradle/src/org/jetbrains/plugins/gradle/service/project/GradleProjectResolver.java index c1a6bd13c336..04e4fa4e027d 100644 --- a/plugins/gradle/src/org/jetbrains/plugins/gradle/service/project/GradleProjectResolver.java +++ b/plugins/gradle/src/org/jetbrains/plugins/gradle/service/project/GradleProjectResolver.java @@ -620,8 +620,8 @@ public final class GradleProjectResolver implements ExternalSystemProjectResolve String rootProjectName = build.getName(); BuildParticipant buildParticipant = new BuildParticipant(); String projectPath = toCanonicalPath(build.getBuildIdentifier().getRootDir().getPath()); - String parentPath = build.getParentBuildIdentifier() != null ? - toCanonicalPath(build.getParentBuildIdentifier().getRootDir().getPath()) : null; + String parentPath = build.getParentBuild() != null ? + toCanonicalPath(build.getParentBuild().getBuildIdentifier().getRootDir().getPath()) : null; buildParticipant.setRootProjectName(rootProjectName); buildParticipant.setRootPath(projectPath); buildParticipant.setParentRootPath(parentPath); diff --git a/plugins/gradle/testSources/org/jetbrains/plugins/gradle/model/DefaultGradleLightBuildTest.kt b/plugins/gradle/testSources/org/jetbrains/plugins/gradle/model/DefaultGradleLightBuildTest.kt new file mode 100644 index 000000000000..21e470d0b796 --- /dev/null +++ b/plugins/gradle/testSources/org/jetbrains/plugins/gradle/model/DefaultGradleLightBuildTest.kt @@ -0,0 +1,145 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.gradle.model + +import org.gradle.tooling.internal.gradle.DefaultBuildIdentifier +import org.gradle.tooling.model.gradle.BasicGradleProject +import org.gradle.tooling.model.gradle.GradleBuild +import org.gradle.tooling.model.internal.ImmutableDomainObjectSet +import org.jetbrains.plugins.gradle.tooling.serialization.internal.adapter.InternalBuildIdentifier +import org.jetbrains.plugins.gradle.tooling.serialization.internal.adapter.InternalProjectIdentifier +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.io.File + +class DefaultGradleLightBuildTest { + + @Test + fun `test DefaultGradleLightBuild#convertGradleBuilds`() { + // GIVEN + val rootBuildPath = "rootBuild" + val includedBuildPath = "rootBuild/includedBuild" + val subIncludedBuildPath = "rootBuild/includedBuild/subIncludedBuild" + + val gradleRootBuild = mockBuildWithProjectAndSubproject(rootBuildPath) + val gradleIncludedBuild = mockBuildWithProjectAndSubproject(includedBuildPath, parent = gradleRootBuild) + val gradleSubIncludedBuild = mockBuildWithProjectAndSubproject(subIncludedBuildPath, parent = gradleIncludedBuild) + val gradleBuilds = listOf(gradleRootBuild, gradleIncludedBuild, gradleSubIncludedBuild) + // WHEN + val convertedBuilds = DefaultGradleLightBuild.convertGradleBuilds(gradleBuilds) + // THEN + Assertions.assertIterableEquals(gradleBuilds.map { it.buildIdentifier.rootDir.name }, + convertedBuilds.map { it.buildIdentifier.rootDir.name }, + "The order of builds in `DefaultGradleLightBuild.convertGradleBuilds` should be the same as in the input") + val rootBuild = convertedBuilds[0] + verifyBuildRootProjectAndSubproject(rootBuild, rootBuildPath) + + val includedBuild = convertedBuilds[1] + verifyBuildRootProjectAndSubproject(includedBuild, includedBuildPath, parentBuild = rootBuild) + + val subIncluded = convertedBuilds[2] + verifyBuildRootProjectAndSubproject(subIncluded, subIncludedBuildPath, parentBuild = includedBuild) + } + + private fun mockBuildWithProjectAndSubproject(path: String, parent: GradleBuild? = null): GradleBuild { + val buildDir = File(path) + val rootProject = mockGradleProject(name = buildDir.name, + path = ":", + buildDir = buildDir, + projectDir = buildDir) + val subproject = mockGradleProject(name = "subproject", + path = ":subproject", + buildDir = buildDir, + projectDir = File("$path/subproject"), + parent = rootProject) + val gradleBuild = mockGradleBuild(buildDir, listOf(rootProject, subproject)) + if (parent != null) { + whenever(parent.includedBuilds).thenReturn(ImmutableDomainObjectSet(listOf(gradleBuild))) + } + return gradleBuild + } + + private fun mockGradleProject( + name: String, + path: String, + buildDir: File, + projectDir: File? = null, + parent: BasicGradleProject? = null, + ): BasicGradleProject = + mock().also { + whenever(it.projectIdentifier).thenReturn( + InternalProjectIdentifier(InternalBuildIdentifier(buildDir), path)) + whenever(it.name).thenReturn(name) + whenever(it.path).thenReturn(path) + whenever(it.projectDirectory).thenReturn(projectDir ?: buildDir) + whenever(it.parent).thenReturn(parent) + whenever(it.children).thenReturn(ImmutableDomainObjectSet(emptyList())) + whenever(it.buildTreePath).thenThrow(NotMockedMemberError::class.java) + + if (parent != null) { + whenever(parent.children).thenReturn(ImmutableDomainObjectSet(listOf(it))) + } + } + + private fun mockGradleBuild(buildDir: File, projects: List): GradleBuild = + mock().also { + whenever(it.buildIdentifier).thenReturn(DefaultBuildIdentifier(buildDir)) + whenever(it.rootProject).thenReturn(projects.first()) + whenever(it.projects).thenReturn(ImmutableDomainObjectSet(projects)) + whenever(it.includedBuilds).thenReturn(ImmutableDomainObjectSet(emptyList())) + whenever(it.editableBuilds).thenThrow(NotMockedMemberError::class.java) + } + + private fun verifyBuildRootProjectAndSubproject( + build: DefaultGradleLightBuild, + buildPath: String, + parentBuild: DefaultGradleLightBuild? = null, + ) { + val buildDir = File(buildPath) + val buildName = buildDir.name + + Assertions.assertEquals(buildName, build.name) + Assertions.assertEquals(buildDir.absolutePath, build.buildIdentifier.rootDir.absolutePath) + Assertions.assertEquals(parentBuild, build.parentBuild) + + Assertions.assertEquals(2, build.projects.count()) + // The order of elements in `DefaultGradleLightBuild.getProjects` is random + val rootProject = build.projects.find { it.name == buildName } + .also { Assertions.assertEquals(build.rootProject, it) }!! + val subproject = build.projects.find { it.name == "subproject" } + .also { assertNotNull(it) }!! + + verifyProject(project = rootProject, + name = buildName, + path = ":", + build = build, + projectDir = buildDir, + subprojects = listOf(subproject)) + verifyProject(project = subproject, + name = "subproject", + path = ":subproject", + build = build, + projectDir = File("$buildPath/subproject")) + } + + private fun verifyProject( + project: DefaultGradleLightProject, + name: String, + path: String, + build: DefaultGradleLightBuild, + projectDir: File, + subprojects: List? = emptyList(), + ) { + Assertions.assertEquals(build, project.build) + Assertions.assertEquals(name, project.name) + Assertions.assertEquals(path, project.path) + Assertions.assertEquals(projectDir.absolutePath, project.projectDirectory.absolutePath) + Assertions.assertEquals(build.buildIdentifier.rootDir.absolutePath, project.projectIdentifier.buildIdentifier.rootDir.absolutePath) + Assertions.assertEquals(path, project.projectIdentifier.projectPath) + Assertions.assertIterableEquals(subprojects, project.childProjects) + } + + private class NotMockedMemberError : IllegalStateException("The accessed member is not mocked. Please mock it in the test.") +} \ No newline at end of file diff --git a/plugins/gradle/tooling-extension-api/src/org/jetbrains/plugins/gradle/model/GradleLightBuild.java b/plugins/gradle/tooling-extension-api/src/org/jetbrains/plugins/gradle/model/GradleLightBuild.java index d1daf2bd5c0c..eda49318cf4a 100644 --- a/plugins/gradle/tooling-extension-api/src/org/jetbrains/plugins/gradle/model/GradleLightBuild.java +++ b/plugins/gradle/tooling-extension-api/src/org/jetbrains/plugins/gradle/model/GradleLightBuild.java @@ -27,7 +27,7 @@ public interface GradleLightBuild extends BuildModel { BuildIdentifier getBuildIdentifier(); @Nullable - BuildIdentifier getParentBuildIdentifier(); + GradleLightBuild getParentBuild(); @NotNull GradleLightProject getRootProject(); diff --git a/plugins/gradle/tooling-extension-impl/src/com/intellij/gradle/toolingExtension/impl/modelAction/GradleDaemonModelHolder.java b/plugins/gradle/tooling-extension-impl/src/com/intellij/gradle/toolingExtension/impl/modelAction/GradleDaemonModelHolder.java index 21130d24ff39..7d3cd5e44f26 100644 --- a/plugins/gradle/tooling-extension-impl/src/com/intellij/gradle/toolingExtension/impl/modelAction/GradleDaemonModelHolder.java +++ b/plugins/gradle/tooling-extension-impl/src/com/intellij/gradle/toolingExtension/impl/modelAction/GradleDaemonModelHolder.java @@ -4,23 +4,21 @@ package com.intellij.gradle.toolingExtension.impl.modelAction; import com.intellij.gradle.toolingExtension.impl.modelSerialization.ToolingSerializerConverter; import com.intellij.gradle.toolingExtension.impl.util.GradleExecutorServiceUtil; import org.gradle.tooling.BuildController; -import org.gradle.tooling.internal.gradle.DefaultBuildIdentifier; -import org.gradle.tooling.model.BuildIdentifier; import org.gradle.tooling.model.BuildModel; import org.gradle.tooling.model.gradle.BasicGradleProject; import org.gradle.tooling.model.gradle.GradleBuild; import org.gradle.util.GradleVersion; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.gradle.model.DefaultGradleLightBuild; import org.jetbrains.plugins.gradle.model.DefaultBuildController; import org.jetbrains.plugins.gradle.model.ProjectImportModelProvider.GradleModelConsumer; -import java.io.File; import java.util.*; import java.util.concurrent.*; +import static org.jetbrains.plugins.gradle.model.DefaultGradleLightBuild.convertGradleBuilds; + /** * Holder for the Gradle models that consumes and holds data on the Gradle Daemon side. *

@@ -53,8 +51,7 @@ public class GradleDaemonModelHolder { private final @NotNull GradleVersion myGradleVersion; - private final @NotNull BlockingQueue> myConvertedRootBuild = new LinkedBlockingQueue<>(); - private final @NotNull BlockingQueue>> myConvertedNestedBuilds = new LinkedBlockingQueue<>(); + private final @NotNull BlockingQueue>> myConvertedBuilds = new LinkedBlockingQueue<>(); private final @NotNull BlockingQueue> myConvertedModelQueue = new LinkedBlockingQueue<>(); public GradleDaemonModelHolder( @@ -68,11 +65,12 @@ public class GradleDaemonModelHolder { myRootGradleBuild = rootGradleBuild; myNestedGradleBuilds = nestedGradleBuilds; myGradleVersion = gradleVersion; - GradleExecutorServiceUtil.submitTask(converterExecutor, myConvertedRootBuild, () -> { - return DefaultGradleLightBuild.convertGradleBuild(rootGradleBuild); - }); - GradleExecutorServiceUtil.submitTask(converterExecutor, myConvertedNestedBuilds, () -> { - return convertNestedGradleBuilds(nestedGradleBuilds); + + GradleExecutorServiceUtil.submitTask(converterExecutor, myConvertedBuilds, () -> { + List gradleBuilds = new ArrayList<>(); + gradleBuilds.add(myRootGradleBuild); + gradleBuilds.addAll(myNestedGradleBuilds); + return convertGradleBuilds(gradleBuilds); }); } @@ -109,18 +107,16 @@ public class GradleDaemonModelHolder { } public @NotNull GradleModelHolderState pollPendingState() { - DefaultGradleLightBuild rootBuild = pollPendingConvertedRootBuild(); - Collection nestedBuilds = pollPendingConvertedNestedBuilds(); + List builds = pollPendingConvertedNestedBuilds(); + DefaultGradleLightBuild rootBuild = builds.isEmpty() ? null : builds.get(0); + List nestedBuilds = builds.size() > 1 ? builds.subList(1, builds.size()) : Collections.emptyList(); + Map models = pollAllPendingConvertedModels(); - return new GradleModelHolderState(rootBuild, nestedBuilds, models); + return new GradleModelHolderState(rootBuild, new ArrayList<>(nestedBuilds), models); } - private @Nullable DefaultGradleLightBuild pollPendingConvertedRootBuild() { - return GradleExecutorServiceUtil.poolPendingResult(myConvertedRootBuild); - } - - private @NotNull Collection pollPendingConvertedNestedBuilds() { - Collection builds = GradleExecutorServiceUtil.poolPendingResult(myConvertedNestedBuilds); + private @NotNull List pollPendingConvertedNestedBuilds() { + List builds = GradleExecutorServiceUtil.poolPendingResult(myConvertedBuilds); return builds == null ? Collections.emptyList() : builds; } @@ -133,45 +129,6 @@ public class GradleDaemonModelHolder { return modelMap; } - private static @NotNull Collection convertNestedGradleBuilds( - @NotNull Collection nestedGradleBuilds - ) { - List nestedBuilds = new ArrayList<>(); - for (GradleBuild gradleBuild : nestedGradleBuilds) { - DefaultGradleLightBuild build = DefaultGradleLightBuild.convertGradleBuild(gradleBuild); - nestedBuilds.add(build); - } - setupNestedBuildHierarchy(nestedBuilds, nestedGradleBuilds); - return nestedBuilds; - } - - private static void setupNestedBuildHierarchy( - @NotNull Collection builds, - @NotNull Collection gradleBuilds - ) { - Set updatedBuilds = new HashSet<>(); - Map rootDirsToBuilds = new HashMap<>(); - for (DefaultGradleLightBuild build : builds) { - BuildIdentifier buildIdentifier = build.getBuildIdentifier(); - rootDirsToBuilds.put(buildIdentifier.getRootDir(), build); - } - - for (GradleBuild gradleBuild : gradleBuilds) { - BuildIdentifier buildIdentifier = gradleBuild.getBuildIdentifier(); - DefaultGradleLightBuild build = rootDirsToBuilds.get(buildIdentifier.getRootDir()); - if (build == null) { - continue; - } - - for (GradleBuild includedGradleBuild : gradleBuild.getIncludedBuilds()) { - DefaultGradleLightBuild buildToUpdate = rootDirsToBuilds.get(includedGradleBuild.getBuildIdentifier().getRootDir()); - if (buildToUpdate != null && updatedBuilds.add(buildToUpdate)) { - buildToUpdate.setParentBuildIdentifier(new DefaultBuildIdentifier(buildIdentifier.getRootDir())); - } - } - } - } - private static class ConvertedModel { private final @NotNull GradleModelId myId; diff --git a/plugins/gradle/tooling-extension-impl/src/org/jetbrains/plugins/gradle/model/DefaultGradleLightBuild.java b/plugins/gradle/tooling-extension-impl/src/org/jetbrains/plugins/gradle/model/DefaultGradleLightBuild.java index e600064ac1c0..94c6dbc185ed 100644 --- a/plugins/gradle/tooling-extension-impl/src/org/jetbrains/plugins/gradle/model/DefaultGradleLightBuild.java +++ b/plugins/gradle/tooling-extension-impl/src/org/jetbrains/plugins/gradle/model/DefaultGradleLightBuild.java @@ -23,7 +23,7 @@ public final class DefaultGradleLightBuild implements GradleLightBuild, Serializ private final @NotNull DefaultGradleLightProject myRootProject; private final @NotNull List myProjects; - private @Nullable DefaultBuildIdentifier myParentBuildIdentifier = null; + private @Nullable DefaultGradleLightBuild myParentBuild = null; public DefaultGradleLightBuild(@NotNull GradleBuild gradleBuild) { BasicGradleProject rootGradleProject = gradleBuild.getRootProject(); @@ -66,12 +66,12 @@ public final class DefaultGradleLightBuild implements GradleLightBuild, Serializ } @Override - public @Nullable DefaultBuildIdentifier getParentBuildIdentifier() { - return myParentBuildIdentifier; + public @Nullable DefaultGradleLightBuild getParentBuild() { + return myParentBuild; } - public void setParentBuildIdentifier(@Nullable DefaultBuildIdentifier parentBuildIdentifier) { - myParentBuildIdentifier = parentBuildIdentifier; + public void setParentBuild(@Nullable DefaultGradleLightBuild parentBuild) { + myParentBuild = parentBuild; } @Override @@ -82,10 +82,6 @@ public final class DefaultGradleLightBuild implements GradleLightBuild, Serializ '}'; } - public static @NotNull DefaultGradleLightBuild convertGradleBuild(@NotNull GradleBuild gradleBuild) { - return new DefaultGradleLightBuild(gradleBuild); - } - public static void replicateModelHierarchy( @NotNull ModelA rootModelA, @NotNull Function<@NotNull ModelA, @Nullable ModelB> getModel, @@ -115,4 +111,40 @@ public final class DefaultGradleLightBuild implements GradleLightBuild, Serializ } } } + + /** + * @return {@code gradleBuilds} converted to {@link DefaultGradleLightBuild} instances. + * Original order is preserved: if a root build is a first element of {@code gradleBuilds}, + * then the first element of returned list is also a root build. + */ + public static @NotNull List convertGradleBuilds( + @NotNull Collection gradleBuilds + ) { + Map gradleBuildsToConverted = new HashMap<>(); + List convertedBuilds = new ArrayList<>(); + // TODO traverse builds via graph to avoid separated parent build field initialization + for (GradleBuild gradleBuild : gradleBuilds) { + DefaultGradleLightBuild build = new DefaultGradleLightBuild(gradleBuild); + gradleBuildsToConverted.put(gradleBuild, build); + convertedBuilds.add(build); + } + setHierarchy(gradleBuilds, gradleBuildsToConverted); + return convertedBuilds; + } + + private static void setHierarchy( + @NotNull Collection gradleBuilds, + Map gradleBuildsToConverted + ) { + for (GradleBuild gradleBuild : gradleBuilds) { + DefaultGradleLightBuild build = gradleBuildsToConverted.get(gradleBuild); + assert build != null; + + for (GradleBuild includedGradleBuild : gradleBuild.getIncludedBuilds()) { + DefaultGradleLightBuild buildToUpdate = gradleBuildsToConverted.get(includedGradleBuild); + assert buildToUpdate != null; + buildToUpdate.setParentBuild(build); + } + } + } }