diff --git a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java index 057758a7..421b37c4 100644 --- a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java @@ -38,22 +38,18 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.ProjectDependency; -import org.gradle.api.attributes.Usage; import org.gradle.api.plugins.JavaPlugin; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.RemapConfigurationSettings; import net.fabricmc.loom.api.processor.SpecContext; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.util.AsyncCache; -import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.fmj.FabricModJson; import net.fabricmc.loom.util.fmj.FabricModJsonFactory; import net.fabricmc.loom.util.fmj.FabricModJsonHelpers; -import net.fabricmc.loom.util.gradle.GradleUtils; /** * @param modDependencies External mods that are depended on @@ -65,20 +61,24 @@ public record SpecContextImpl( List localMods, List compileRuntimeMods) implements SpecContext { public static SpecContextImpl create(Project project) { - AsyncCache> fmjCache = new AsyncCache>(); + return create(new SpecContextProjectView.Impl(project, LoomGradleExtension.get(project))); + } + + @VisibleForTesting + public static SpecContextImpl create(SpecContextProjectView projectView) { + AsyncCache> fmjCache = new AsyncCache<>(); return new SpecContextImpl( - getDependentMods(project, fmjCache), - FabricModJsonHelpers.getModsInProject(project), - getCompileRuntimeMods(project, fmjCache) + getDependentMods(projectView, fmjCache), + projectView.getMods(), + getCompileRuntimeMods(projectView, fmjCache) ); } // Reruns a list of mods found on both the compile and/or runtime classpaths - private static List getDependentMods(Project project, AsyncCache> fmjCache) { - final LoomGradleExtension extension = LoomGradleExtension.get(project); + private static List getDependentMods(SpecContextProjectView projectView, AsyncCache> fmjCache) { var futures = new ArrayList>>(); - for (RemapConfigurationSettings entry : extension.getRemapConfigurations()) { + for (RemapConfigurationSettings entry : projectView.extension().getRemapConfigurations()) { final Set artifacts = entry.getSourceConfiguration().get().resolve(); for (File artifact : artifacts) { @@ -90,10 +90,9 @@ public record SpecContextImpl( } } - // TODO provide a project isolated way of doing this. - if (!extension.isProjectIsolationActive() && !GradleUtils.getBooleanProperty(project, Constants.Properties.DISABLE_PROJECT_DEPENDENT_MODS)) { + if (!projectView.disableProjectDependantMods()) { // Add all the dependent projects - for (Project dependentProject : getDependentProjects(project).toList()) { + for (Project dependentProject : getDependentProjects(projectView).toList()) { futures.add(fmjCache.get(dependentProject.getPath(), () -> FabricModJsonHelpers.getModsInProject(dependentProject))); } } @@ -101,19 +100,19 @@ public record SpecContextImpl( return sorted(AsyncCache.joinList(futures)); } - private static Stream getDependentProjects(Project project) { - final Stream runtimeProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)); - final Stream compileProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)); + private static Stream getDependentProjects(SpecContextProjectView projectView) { + final Stream runtimeProjects = projectView.getLoomProjectDependencies(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + final Stream compileProjects = projectView.getLoomProjectDependencies(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME); return Stream.concat(runtimeProjects, compileProjects) .distinct(); } // Returns a list of mods that are on both to compile and runtime classpath - private static List getCompileRuntimeMods(Project project, AsyncCache> fmjCache) { - var mods = new ArrayList<>(getCompileRuntimeModsFromRemapConfigs(project, fmjCache)); + private static List getCompileRuntimeMods(SpecContextProjectView projectView, AsyncCache> fmjCache) { + var mods = new ArrayList<>(getCompileRuntimeModsFromRemapConfigs(projectView, fmjCache)); - for (Project dependentProject : getCompileRuntimeProjectDependencies(project).toList()) { + for (Project dependentProject : getCompileRuntimeProjectDependencies(projectView).toList()) { List projectMods = fmjCache.getBlocking(dependentProject.getPath(), () -> { return FabricModJsonHelpers.getModsInProject(dependentProject); }); @@ -127,49 +126,49 @@ public record SpecContextImpl( } // Returns a list of jar mods that are found on the compile and runtime remapping configurations - private static List getCompileRuntimeModsFromRemapConfigs(Project project, AsyncCache> fmjCache) { - final LoomGradleExtension extension = LoomGradleExtension.get(project); - + private static List getCompileRuntimeModsFromRemapConfigs(SpecContextProjectView projectView, AsyncCache> fmjCache) { // A set of mod ids from all remap configurations that are considered for dependency transforms. final Set runtimeModIds = getModIds( - project, + projectView, fmjCache, - extension.getRuntimeRemapConfigurations().stream() + projectView.extension().getRuntimeRemapConfigurations().stream() .filter(settings -> settings.getApplyDependencyTransforms().get()) ); // A set of mod ids that are found on one or more remap configurations that target the common source set. // Null when split source sets are not enabled, meaning all mods are common. - final Set commonModIds = extension.areEnvironmentSourceSetsSplit() ? getModIds( - project, + final Set commonRuntimeModIds = projectView.extension().areEnvironmentSourceSetsSplit() ? getModIds( + projectView, fmjCache, - extension.getRuntimeRemapConfigurations().stream() + projectView.extension().getRuntimeRemapConfigurations().stream() .filter(settings -> settings.getSourceSet().map(sourceSet -> !sourceSet.getName().equals(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME)).get()) .filter(settings -> settings.getApplyDependencyTransforms().get())) : null; - return getMods( - project, + Stream compileMods = getMods( + projectView, fmjCache, - extension.getCompileRemapConfigurations().stream() - .filter(settings -> settings.getApplyDependencyTransforms().get())) + projectView.extension().getCompileRemapConfigurations().stream() + .filter(settings -> settings.getApplyDependencyTransforms().get())); + + return compileMods // Only check based on the modid, as there may be differing versions used between the compile and runtime classpath. // We assume that the version used at runtime will be binary compatible with the version used to compile against. // It's not perfect but better than silently not supplying the mod, and this could happen with regular API that you compile against anyway. .filter(fabricModJson -> runtimeModIds.contains(fabricModJson.getId())) .sorted(Comparator.comparing(FabricModJson::getId)) - .map(fabricModJson -> new ModHolder(fabricModJson, commonModIds == null || commonModIds.contains(fabricModJson.getId()))) + .map(fabricModJson -> new ModHolder(fabricModJson, commonRuntimeModIds == null || commonRuntimeModIds.contains(fabricModJson.getId()))) .toList(); } - private static Stream getMods(Project project, AsyncCache> fmjCache, Stream stream) { - return stream.flatMap(resolveArtifacts(project, true)) + private static Stream getMods(SpecContextProjectView projectView, AsyncCache> fmjCache, Stream stream) { + return stream.flatMap(projectView.resolveArtifacts(true)) .map(modFromZip(fmjCache)) .filter(Objects::nonNull); } - private static Set getModIds(Project project, AsyncCache> fmjCache, Stream stream) { - return getMods(project, fmjCache, stream) + private static Set getModIds(SpecContextProjectView projectView, AsyncCache> fmjCache, Stream stream) { + return getMods(projectView, fmjCache, stream) .map(FabricModJson::getId) .collect(Collectors.toSet()); } @@ -185,43 +184,19 @@ public record SpecContextImpl( }; } - private static Function> resolveArtifacts(Project project, boolean runtime) { - final Usage usage = project.getObjects().named(Usage.class, runtime ? Usage.JAVA_RUNTIME : Usage.JAVA_API); - - return settings -> { - final Configuration configuration = settings.getSourceConfiguration().get().copyRecursive(); - configuration.setCanBeConsumed(false); - configuration.attributes(attributes -> attributes.attribute(Usage.USAGE_ATTRIBUTE, usage)); - return configuration.resolve().stream().map(File::toPath); - }; - } - // Returns a list of Loom Projects found in both the runtime and compile classpath - private static Stream getCompileRuntimeProjectDependencies(Project project) { - final LoomGradleExtension extension = LoomGradleExtension.get(project); - - // TODO provide a project isolated way of doing this. - if (extension.isProjectIsolationActive() - || GradleUtils.getBooleanProperty(project, Constants.Properties.DISABLE_PROJECT_DEPENDENT_MODS)) { + private static Stream getCompileRuntimeProjectDependencies(SpecContextProjectView projectView) { + if (projectView.disableProjectDependantMods()) { return Stream.empty(); } - final Stream runtimeProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)); - final List compileProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)).toList(); + final Stream runtimeProjects = projectView.getLoomProjectDependencies(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + final List compileProjects = projectView.getLoomProjectDependencies(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME).toList(); return runtimeProjects .filter(compileProjects::contains); // Use the intersection of the two configurations. } - // Returns a list of Loom Projects found in the provided Configuration - private static Stream getLoomProjectDependencies(Project project, Configuration configuration) { - return configuration.getAllDependencies() - .withType(ProjectDependency.class) - .stream() - .map((d) -> project.project(d.getPath())) - .filter(GradleUtils::isLoomProject); - } - // Sort to ensure stable caching private static List sorted(List mods) { return mods.stream().sorted(Comparator.comparing(FabricModJson::getId)).toList(); diff --git a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextProjectView.java b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextProjectView.java new file mode 100644 index 00000000..c8686a20 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextProjectView.java @@ -0,0 +1,94 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.processors; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.attributes.Usage; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.RemapConfigurationSettings; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.fmj.FabricModJson; +import net.fabricmc.loom.util.fmj.FabricModJsonHelpers; +import net.fabricmc.loom.util.gradle.GradleUtils; + +// Used to abstract out the Gradle API usage to ease unit testing. +public interface SpecContextProjectView { + LoomGradleExtension extension(); + + // Returns a list of Loom Projects found in the specified Configuration + Stream getLoomProjectDependencies(String name); + + Function> resolveArtifacts(boolean runtime); + + List getMods(); + + boolean disableProjectDependantMods(); + + record Impl(Project project, LoomGradleExtension extension) implements SpecContextProjectView { + @Override + public Stream getLoomProjectDependencies(String name) { + final Configuration configuration = project.getConfigurations().getByName(name); + return configuration.getAllDependencies() + .withType(ProjectDependency.class) + .stream() + .map((d) -> project.project(d.getPath())) + .filter(GradleUtils::isLoomProject); + } + + @Override + public Function> resolveArtifacts(boolean runtime) { + final Usage usage = project.getObjects().named(Usage.class, runtime ? Usage.JAVA_RUNTIME : Usage.JAVA_API); + + return settings -> { + final Configuration configuration = settings.getSourceConfiguration().get().copyRecursive(); + configuration.setCanBeConsumed(false); + configuration.attributes(attributes -> attributes.attribute(Usage.USAGE_ATTRIBUTE, usage)); + return configuration.resolve().stream().map(File::toPath); + }; + } + + @Override + public List getMods() { + return FabricModJsonHelpers.getModsInProject(project); + } + + @Override + public boolean disableProjectDependantMods() { + final LoomGradleExtension extension = LoomGradleExtension.get(project); + // TODO provide a project isolated way of doing this. + return extension.isProjectIsolationActive() + || GradleUtils.getBooleanProperty(project, Constants.Properties.DISABLE_PROJECT_DEPENDENT_MODS); + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/SpecContextTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/SpecContextTest.groovy new file mode 100644 index 00000000..3a07991d --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/SpecContextTest.groovy @@ -0,0 +1,239 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit + +import java.nio.file.Path +import java.util.function.Function +import java.util.stream.Stream + +import groovy.transform.CompileStatic +import org.gradle.api.NamedDomainObjectList +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import spock.lang.Specification +import spock.lang.TempDir + +import net.fabricmc.loom.LoomGradleExtension +import net.fabricmc.loom.api.RemapConfigurationSettings +import net.fabricmc.loom.api.fmj.FabricModJsonV1Spec +import net.fabricmc.loom.configuration.processors.SpecContextImpl +import net.fabricmc.loom.configuration.processors.SpecContextProjectView +import net.fabricmc.loom.test.util.GradleTestUtil +import net.fabricmc.loom.util.ZipUtils +import net.fabricmc.loom.util.fmj.gen.FabricModJsonV1Generator + +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when + +@SuppressWarnings('ExplicitCallToModMethod') +class SpecContextTest extends Specification { + @TempDir + Path tempDir + + Project project + LoomGradleExtension extension + SpecContextProjectView projectView + NamedDomainObjectList remapConfigurations + + RemapConfigurationSettings implementation + RemapConfigurationSettings runtimeOnly + RemapConfigurationSettings compileOnly + + Map> runtimeArtifacts = [:] + Map> apiArtifacts = [:] + + void setup() { + project = GradleTestUtil.mockProject() + extension = LoomGradleExtension.get(project) + projectView = mock(SpecContextProjectView.class) + remapConfigurations = project.getObjects().namedDomainObjectList(RemapConfigurationSettings.class) + + when(projectView.extension()).thenReturn(extension) + when(extension.getRemapConfigurations()).thenReturn(remapConfigurations) + when(projectView.resolveArtifacts(true)).thenReturn(resolve(runtimeArtifacts)) + when(projectView.resolveArtifacts(false)).thenReturn(resolve(apiArtifacts)) + + implementation = createConfigurationSettings("implementation") + runtimeOnly = createConfigurationSettings("runtimeOnly") + compileOnly = createConfigurationSettings("compileOnly") + remapConfigurations.addAll([ + implementation, + runtimeOnly, + compileOnly + ]) + + when(extension.getCompileRemapConfigurations()).thenReturn([implementation, compileOnly]) + when(extension.getRuntimeRemapConfigurations()).thenReturn([implementation, runtimeOnly]) + } + + def "Empty"() { + setup: + dependencies( + implementation: [], + runtimeOnly: [], + compileOnly: [] + ) + + when: + def specContext = SpecContextImpl.create(projectView) + + then: + specContext.modDependencies().size() == 0 + specContext.localMods().size() == 0 + specContext.modDependenciesCompileRuntime().size() == 0 + specContext.modDependenciesCompileRuntimeClient().size() == 0 + specContext.allMods().size() == 0 + } + + def "implementation dependency"() { + setup: + dependencies( + implementation: [mod("test1")], + runtimeOnly: [], + compileOnly: [] + ) + + when: + def specContext = SpecContextImpl.create(projectView) + + then: + specContext.modDependencies().size() == 1 + specContext.localMods().size() == 0 + specContext.modDependenciesCompileRuntime().size() == 1 + specContext.modDependenciesCompileRuntimeClient().size() == 0 + specContext.allMods().size() == 1 + } + + def "runtime only dependency"() { + setup: + dependencies( + implementation: [], + runtimeOnly: [mod("test1")], + compileOnly: [] + ) + + when: + def specContext = SpecContextImpl.create(projectView) + + then: + specContext.modDependencies().size() == 1 + specContext.localMods().size() == 0 + specContext.modDependenciesCompileRuntime().size() == 0 + specContext.modDependenciesCompileRuntimeClient().size() == 0 + specContext.allMods().size() == 1 + } + + def "compile only dependency"() { + setup: + dependencies( + implementation: [], + runtimeOnly: [], + compileOnly: [mod("test1")] + ) + + when: + def specContext = SpecContextImpl.create(projectView) + + then: + specContext.modDependencies().size() == 1 + specContext.localMods().size() == 0 + specContext.modDependenciesCompileRuntime().size() == 0 + specContext.modDependenciesCompileRuntimeClient().size() == 0 + specContext.allMods().size() == 1 + } + + // TODO I believe this test is testing broken behaviour + def "compile only runtime only dependency"() { + setup: + def test1 = mod("test1") + dependencies( + implementation: [], + runtimeOnly: [test1], + compileOnly: [test1] + ) + + when: + def specContext = SpecContextImpl.create(projectView) + + then: + specContext.modDependencies().size() == 2 + specContext.localMods().size() == 0 + specContext.modDependenciesCompileRuntime().size() == 0 + specContext.modDependenciesCompileRuntimeClient().size() == 0 + specContext.allMods().size() == 2 + } + + private void dependencies(Map> files) { + configureDependencies(files.implementation, this.implementation) + configureDependencies(files.runtimeOnly, this.runtimeOnly) + configureDependencies(files.compileOnly, this.compileOnly) + + runtimeArtifacts[this.implementation].addAll(files.implementation) + runtimeArtifacts[this.runtimeOnly].addAll(files.runtimeOnly) + apiArtifacts[this.implementation].addAll(files.implementation) + apiArtifacts[this.compileOnly].addAll(files.compileOnly) + } + + private static void configureDependencies(List files, RemapConfigurationSettings settings) { + def configuration = mock(Configuration.class) + when(configuration.resolve()).thenReturn(files*.toFile() as Set) + + def provider = mock(NamedDomainObjectProvider.class) + when(provider.get()).thenReturn(configuration) + + when(settings.getSourceConfiguration()).thenReturn(provider) + } + + private Path mod(String modId) { + def zip = tempDir.resolve("${modId}.zip") + + def spec = project.objects.newInstance(FabricModJsonV1Spec.class) + spec.modId.set(modId) + spec.version.set("1.0.0") + def json = FabricModJsonV1Generator.INSTANCE.generate(spec) + ZipUtils.add(zip, "fabric.mod.json", json) + + return zip + } + + private RemapConfigurationSettings createConfigurationSettings(String name) { + def settings = project.getObjects().newInstance(RemapConfigurationSettings.class, name) + settings.applyDependencyTransforms.set(true) + + runtimeArtifacts.put(settings, []) + apiArtifacts.put(settings, []) + + return settings + } + + @CompileStatic + private static Function> resolve(Map> artifacts) { + return { settings -> + def paths = artifacts.get(settings) + return paths != null ? paths.stream() : Stream.empty() + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy index 264d3cd8..c1faaad2 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy @@ -25,6 +25,7 @@ package net.fabricmc.loom.test.util import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.dsl.RepositoryHandler import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty @@ -47,6 +48,7 @@ import net.fabricmc.loom.util.download.Download import static org.mockito.ArgumentMatchers.any import static org.mockito.Mockito.* +import static org.mockito.Mockito.mock class GradleTestUtil { static Property mockProperty(T value) { @@ -64,17 +66,19 @@ class GradleTestUtil { } static Project mockProject() { - def mock = mock(Project.class) - def serviceRegistry = TestServiceFactory.createServiceRegistry(mock) + def project = mock(Project.class) + def serviceRegistry = TestServiceFactory.createServiceRegistry(project) def objectFactory = serviceRegistry.get(ObjectFactory) def providerFactory = serviceRegistry.get(ProviderFactory) def extensions = mockExtensionContainer() - when(mock.getExtensions()).thenReturn(extensions) - when(mock.getObjects()).thenReturn(objectFactory) - when(mock.provider(any())).thenAnswer { + def configurationContainer = mock(ConfigurationContainer.class) + when(project.getExtensions()).thenReturn(extensions) + when(project.getObjects()).thenReturn(objectFactory) + when(project.provider(any())).thenAnswer { providerFactory.provider(it.getArgument(0)) } - return mock + when(project.getConfigurations()).thenReturn(configurationContainer) + return project } static ExtensionContainer mockExtensionContainer() {