IDEA-390110 [gradle] new: add suppressFailures option to GradleModelFetchRequest

(cherry picked from commit bb602a8ad4a96558f6e10d60d6b8ea8edc5c8ea7)
IJ-CR-214557

GitOrigin-RevId: d0066d771c112b24a2e5017e307c6e3c45e89692
This commit is contained in:
Sergei Vorobyov
2026-08-11 10:29:34 +00:00
committed by intellij-monorepo-bot
parent 715a95f180
commit dd487f45e3
4 changed files with 130 additions and 69 deletions
@@ -4,6 +4,7 @@ package com.intellij.gradle.toolingExtension.modelAction
import org.gradle.api.Action
import org.gradle.tooling.model.gradle.BasicGradleProject
import org.gradle.tooling.model.gradle.GradleBuild
import org.jetbrains.annotations.ApiStatus.Experimental
import org.jetbrains.annotations.ApiStatus.NonExtendable
import org.jetbrains.plugins.gradle.model.ProjectImportModelProvider.GradleModelConsumer
import org.gradle.tooling.model.Model as GradleModel
@@ -81,6 +82,18 @@ interface GradleModelController {
parameterInitializer: Action<in Parameter>,
): GradleModelFetchRequest<Model>
/**
* Suppresses failures for this model fetch request.
*
* When suppressed:
* - With the resilient Gradle API: failures returned by [org.gradle.tooling.BuildController.fetch] are not forwarded.
* - Without the resilient Gradle API: exceptions thrown by [org.gradle.tooling.BuildController.getModel] are caught and discarded.
*
* Use this when the model may not be available in all Gradle configurations.
*/
@Experimental
fun suppressFailures(): GradleModelFetchRequest<Model>
fun execute(modelConsumer: GradleModelConsumer)
enum class GradleExecutionMode {
@@ -54,7 +54,7 @@ class GradleModelControllerImpl(
error("Strict Gradle model fetch unexpectedly returned invalid model for ${modelClass.name}")
}
sendModelFetchFailures(null, result)
sendModelFetchFailures(null, result, false)
return model
}
@@ -63,7 +63,7 @@ class GradleModelControllerImpl(
return buildController.findModel(modelClass)
}
return buildController.fetch(modelClass)
.also { sendModelFetchFailures(null, it) }
.also { sendModelFetchFailures(null, it, false) }
.getModel()
}
@@ -71,23 +71,19 @@ class GradleModelControllerImpl(
target: Target,
modelClass: Class<Model>,
modelParameter: GradleModelParameter<*>?,
suppressFailures: Boolean,
): Model? {
if (modelParameter != null) {
return fetchModelOrNull(target, modelClass, modelParameter.parameterClass, modelParameter.parameterInitializer)
return fetchModelOrNull(target, modelClass, modelParameter.parameterClass, modelParameter.parameterInitializer, suppressFailures)
}
return fetchModelOrNull(target, modelClass)
return fetchModelOrNull(target, modelClass, suppressFailures)
}
override fun <Target : GradleModel, Model : Any> fetchModelOrNull(
target: Target,
modelClass: Class<Model>,
): Model? {
if (!isResilientModelFetchApiUsed()) {
return buildController.findModel(target, modelClass)
}
return buildController.fetch(target, modelClass)
.also { sendModelFetchFailures(target, it) }
.getModel()
return fetchModelOrNull(target, modelClass, false)
}
override fun <Target : GradleModel, Model : Any, Parameter : Any> fetchModelOrNull(
@@ -95,12 +91,39 @@ class GradleModelControllerImpl(
modelClass: Class<Model>,
modelParameterClass: Class<Parameter>,
modelParameterInitializer: Action<in Parameter>,
): Model? {
return fetchModelOrNull(target, modelClass, modelParameterClass, modelParameterInitializer, false)
}
private fun <Target : GradleModel, Model : Any> fetchModelOrNull(
target: Target,
modelClass: Class<Model>,
suppressFailures: Boolean,
): Model? {
if (!isResilientModelFetchApiUsed()) {
return buildController.findModel(target, modelClass, modelParameterClass, modelParameterInitializer)
return handleModelFetchFailures(suppressFailures) {
buildController.findModel(target, modelClass)
}
}
return buildController.fetch(target, modelClass)
.also { sendModelFetchFailures(target, it, suppressFailures) }
.getModel()
}
private fun <Target : GradleModel, Model : Any, Parameter : Any> fetchModelOrNull(
target: Target,
modelClass: Class<Model>,
modelParameterClass: Class<Parameter>,
modelParameterInitializer: Action<in Parameter>,
suppressFailures: Boolean,
): Model? {
if (!isResilientModelFetchApiUsed()) {
return handleModelFetchFailures(suppressFailures) {
buildController.findModel(target, modelClass, modelParameterClass, modelParameterInitializer)
}
}
return buildController.fetch(target, modelClass, modelParameterClass, modelParameterInitializer)
.also { sendModelFetchFailures(target, it) }
.also { sendModelFetchFailures(target, it, suppressFailures) }
.getModel()
}
@@ -109,14 +132,35 @@ class GradleModelControllerImpl(
private fun <Model : Any> fetchModels(request: GradleModelFetchRequestImpl<Model>, modelConsumer: GradleModelConsumer) {
fun <Target : GradleModel> fetchTargetModelsInParallel(targets: Collection<Target>, consumer: (Target, Model, Class<Model>) -> Unit) {
val buildActions = targets.map { target ->
BuildAction { innerBuildController ->
val innerController = GradleModelControllerImpl(innerBuildController)
val model = innerController.fetchModelOrNull(target, request.modelClass, request.modelParameter, request.suppressFailures)
target to model
}
}
val models = buildController.run(buildActions)
for ((target, model) in models) {
consumer(target, model ?: continue, request.modelClass)
}
}
fun <Target : GradleModel> fetchTargetModelsInSequence(targets: Collection<Target>, consumer: (Target, Model, Class<Model>) -> Unit) {
for (target in targets) {
val model = fetchModelOrNull(target, request.modelClass, request.modelParameter, request.suppressFailures)
consumer(target, model ?: continue, request.modelClass)
}
}
fun <Target : GradleModel> fetchModels(targets: Collection<Target>, consumer: (Target, Model, Class<Model>) -> Unit) {
when (request.executionMode) {
GradleExecutionMode.DEFAULT -> when (isParallelModelFetchEnabled()) {
true -> fetchTargetModelsInParallel(targets, request.modelClass, request.modelParameter, consumer)
else -> fetchTargetModelsInSequence(targets, request.modelClass, request.modelParameter, consumer)
true -> fetchTargetModelsInParallel(targets, consumer)
else -> fetchTargetModelsInSequence(targets, consumer)
}
GradleExecutionMode.PARALLEL -> fetchTargetModelsInParallel(targets, request.modelClass, request.modelParameter, consumer)
GradleExecutionMode.SEQUENTIAL -> fetchTargetModelsInSequence(targets, request.modelClass, request.modelParameter, consumer)
GradleExecutionMode.PARALLEL -> fetchTargetModelsInParallel(targets, consumer)
GradleExecutionMode.SEQUENTIAL -> fetchTargetModelsInSequence(targets, consumer)
}
}
@@ -134,39 +178,6 @@ class GradleModelControllerImpl(
}
}
private fun <Target : GradleModel, Model : Any> fetchTargetModelsInParallel(
targets: Collection<Target>,
modelClass: Class<Model>,
modelParameter: GradleModelParameter<*>?,
modelConsumer: (Target, Model, Class<Model>) -> Unit,
) {
val buildActions = targets.map { target ->
BuildAction { innerBuildController ->
val innerController = GradleModelControllerImpl(innerBuildController)
val model = innerController.fetchModelOrNull(target, modelClass, modelParameter)
target to (model ?: return@BuildAction null)
}
}
val models = buildController.run(buildActions)
for (model in models) {
if (model != null) {
modelConsumer(model.first, model.second, modelClass)
}
}
}
private fun <Target : GradleModel, Model : Any> fetchTargetModelsInSequence(
targets: Collection<Target>,
modelClass: Class<Model>,
modelParameter: GradleModelParameter<*>?,
modelConsumer: (Target, Model, Class<Model>) -> Unit,
) {
for (target in targets) {
val model = fetchModelOrNull(target, modelClass, modelParameter) ?: continue
modelConsumer(target, model, modelClass)
}
}
private fun collectAllProjectModels(buildModels: Collection<GradleBuild>): Collection<BasicGradleProject> {
val projectModels = ArrayList<BasicGradleProject>()
for (buildModel in buildModels) {
@@ -196,8 +207,12 @@ class GradleModelControllerImpl(
return buildModel.projects.filter { it.parent == null }
}
private fun sendModelFetchFailures(target: GradleModel?, result: FetchModelResult<*>) {
val failures = result.failures.takeIf { it.isNotEmpty() } ?: return
private fun <Model : Any> handleModelFetchFailures(suppressFailures: Boolean, action: () -> Model?): Model? {
return runCatching(action).getOrElse { if (!suppressFailures) throw it else null }
}
private fun sendModelFetchFailures(target: GradleModel?, result: FetchModelResult<*>, suppressFailures: Boolean) {
val failures = result.failures.takeIf { it.isNotEmpty() && !suppressFailures } ?: return
val targetPath = when (target) {
is BasicGradleProject -> target.projectDirectory
is GradleBuild -> target.buildIdentifier.rootDir
@@ -215,6 +230,7 @@ class GradleModelControllerImpl(
val executionMode: GradleExecutionMode = GradleExecutionMode.DEFAULT,
val targetLevel: GradleModelLevel = GradleModelLevel.PROJECT,
val projectTraversal: GradleTraversalMode = GradleTraversalMode.DIRECT,
val suppressFailures: Boolean = false,
) : GradleModelFetchRequest<Model> {
override fun modelLevel(targetLevel: GradleModelLevel): GradleModelFetchRequest<Model> =
@@ -232,6 +248,9 @@ class GradleModelControllerImpl(
): GradleModelFetchRequest<Model> =
copy(modelParameter = GradleModelParameter(parameterClass, parameterInitializer))
override fun suppressFailures(): GradleModelFetchRequest<Model> =
copy(suppressFailures = true)
override fun execute(modelConsumer: GradleModelConsumer): Unit =
modelController.fetchModels(this, modelConsumer)
}
@@ -283,6 +283,32 @@ class GradleModelControllerTest(val isResilientSyncEnabled: Boolean) {
modelConsumer.assertProjectModels(emptyList())
}
@Test
fun `suppressed model fetch request suppresses all failures`() {
val rootProject = MockGradleProject("root")
val subProject = MockGradleProject("sub-project", rootProject)
val projectModels = listOf(rootProject, subProject)
val buildModel = MockGradleBuild(rootProject, projectModels)
val buildController = TestBuildController().apply {
registerModelFailure(rootProject, TestModel::class.java, TestModelFetchException())
}
val modelConsumer = TestModelConsumer()
GradleModelControllerImpl(buildController)
.fetchRequest(listOf(buildModel), TestModel::class.java)
.suppressFailures()
.execute(modelConsumer)
buildController.assertRunActionCounts(emptyList())
buildController.assertModelRequests(projectModels.map {
TestModelRequest(it, TestModel::class.java)
})
modelConsumer.assertBuildModels(emptyList())
modelConsumer.assertProjectModels(emptyList())
}
@Test
fun `resilient model fetch api propagates project directory as target path in failure result`() {
val projectDirectory = File("root-dir")
@@ -53,21 +53,34 @@ internal class TestBuildController : BuildController by notImplemented(BuildCont
assertEqualsOrdered(expectedTargetPaths, actualTargetPaths)
}
override fun <T> findModel(target: Model?, modelType: Class<T>): T? {
modelRequests.add(TestModelRequest(target!!, modelType))
override fun <T> getModel(target: Model, modelType: Class<T>): T {
return findModel(target, modelType) ?: error("No model of type ${modelType.name} for $target")
}
override fun <T, P : Any> getModel(
target: Model,
modelType: Class<T>,
parameterType: Class<P>,
parameterInitializer: Action<in P>,
): T {
return findModel(target, modelType, parameterType, parameterInitializer) ?: error("No model of type ${modelType.name} for $target")
}
override fun <T> findModel(target: Model, modelType: Class<T>): T? {
modelRequests.add(TestModelRequest(target, modelType))
throwModelFailure(target, modelType)
@Suppress("UNCHECKED_CAST")
return models[target to modelType] as T?
}
override fun <T, P : Any> findModel(
target: Model?,
target: Model,
modelType: Class<T>,
parameterType: Class<P>?,
parameterInitializer: Action<in P>?,
parameterType: Class<P>,
parameterInitializer: Action<in P>,
): T? {
val parameter = createParameter(parameterType!!, parameterInitializer!!)
modelRequests.add(TestModelRequest(target!!, modelType, parameterType, parameter))
val parameter = createParameter(parameterType, parameterInitializer)
modelRequests.add(TestModelRequest(target, modelType, parameterType, parameter))
throwModelFailure(target, modelType)
@Suppress("UNCHECKED_CAST")
return models[target to modelType] as T?
@@ -87,26 +100,16 @@ internal class TestBuildController : BuildController by notImplemented(BuildCont
return parameter
}
override fun <T : Any> fetch(modelType: Class<T>): FetchModelResult<T> =
fetchModelResult { findModel(modelType) }
override fun <T : Any> fetch(target: Model, modelType: Class<T>): FetchModelResult<T> =
fetchModelResult { findModel(target, modelType) }
override fun <T : Any, P : Any> fetch(
modelType: Class<T>,
parameterType: Class<P>?,
parameterInitializer: Action<in P>?,
): FetchModelResult<T> =
fetchModelResult { findModel(modelType, parameterType, parameterInitializer) }
override fun <T : Any, P : Any> fetch(
target: Model?,
modelType: Class<T>,
parameterType: Class<P>?,
parameterInitializer: Action<in P>?,
): FetchModelResult<T> =
fetchModelResult { findModel(target, modelType, parameterType, parameterInitializer) }
fetchModelResult { findModel(target!!, modelType, parameterType!!, parameterInitializer!!) }
override fun <T> run(actions: Collection<BuildAction<out T>>): List<T> {
runActionCounts.add(actions.size)