diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java index e05f7062f..c308f59e7 100644 --- a/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java +++ b/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2024 FabricMC + * Copyright (c) 2024-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 @@ -26,6 +26,7 @@ import org.gradle.api.Action; import org.gradle.api.artifacts.Dependency; +import org.jetbrains.annotations.ApiStatus; /** * A gradle extension with specific functionality related to Fabric API. @@ -58,4 +59,17 @@ public interface FabricApiExtension { * @param action An action to configure specific data generation settings. See {@link DataGenerationSettings} for more information. */ void configureDataGeneration(Action action); + + /** + * Configuration of game and client tests using the default settings. + */ + @ApiStatus.Experimental + void configureTests(); + + /** + * Configuration of game and/or client tests using the specified settings. + * @param action An action to configure specific game test settings. See {@link GameTestSettings} for more information. + */ + @ApiStatus.Experimental + void configureTests(Action action); } diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java b/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java new file mode 100644 index 000000000..5675484ab --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java @@ -0,0 +1,82 @@ +/* + * 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.api.fabricapi; + +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Optional; +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents the settings for game and/or client tests. + */ +@ApiStatus.Experimental +public interface GameTestSettings { + /** + * Contains a boolean property indicating whether a new source set should be created for the tests. + * + *

Default: false + */ + Property getCreateSourceSet(); + + /** + * Contains a string property representing the mod ID associated with the tests. + * + *

This must be set when {@link #getCreateSourceSet()} is set. + */ + @Optional + Property getModId(); + + /** + * Contains a boolean property indicating whether a run configuration will be created for the server side game tests, using Vanilla Game Test framework. + * + *

Default: true + */ + Property getEnableGameTests(); + + /** + * Contains a boolean property indicating whether a run configuration will be created for the client side game tests, using the Fabric API Client Test framework. + * + *

Default: true + */ + Property getEnableClientGameTests(); + + /** + * Contains a boolean property indicating whether the eula has been accepted. By enabling this you agree to the Minecraft EULA located at https://aka.ms/MinecraftEULA. + * + *

This only works when {@link #getEnableClientGameTests()} is enabled. + * + *

Default: false + */ + Property getEula(); + + /** + * Contains a boolean property indicating whether the run directories should be cleared before running the tests. + * + *

This only works when {@link #getEnableClientGameTests()} is enabled. + * + *

Default: true + */ + Property getClearRunDirectory(); +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java index c19a1d8fb..0e7de09e3 100644 --- a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2020-2023 FabricMC + * Copyright (c) 2020-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 @@ -32,6 +32,7 @@ import net.fabricmc.loom.api.fabricapi.DataGenerationSettings; import net.fabricmc.loom.api.fabricapi.FabricApiExtension; +import net.fabricmc.loom.api.fabricapi.GameTestSettings; public abstract class FabricApiExtensionImpl implements FabricApiExtension { @Inject @@ -39,10 +40,12 @@ public abstract class FabricApiExtensionImpl implements FabricApiExtension { private final FabricApiVersions versions; private final FabricApiDataGeneration dataGeneration; + private final FabricApiTesting testing; public FabricApiExtensionImpl() { versions = getObjectFactory().newInstance(FabricApiVersions.class); dataGeneration = getObjectFactory().newInstance(FabricApiDataGeneration.class); + testing = getObjectFactory().newInstance(FabricApiTesting.class); } @Override @@ -64,4 +67,14 @@ public void configureDataGeneration() { public void configureDataGeneration(Action action) { dataGeneration.configureDataGeneration(action); } + + @Override + public void configureTests() { + configureTests(gameTestSettings -> { }); + } + + @Override + public void configureTests(Action action) { + testing.configureTests(action); + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiTesting.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiTesting.java new file mode 100644 index 000000000..c3585db33 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiTesting.java @@ -0,0 +1,144 @@ +/* + * 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.fabricapi; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Consumer; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Delete; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskContainer; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.fabricapi.GameTestSettings; +import net.fabricmc.loom.configuration.ide.RunConfigSettings; +import net.fabricmc.loom.task.AbstractLoomTask; +import net.fabricmc.loom.task.LoomTasks; +import net.fabricmc.loom.util.Constants; + +public abstract class FabricApiTesting extends FabricApiAbstractSourceSet { + @Inject + protected abstract Project getProject(); + + @Inject + public FabricApiTesting() { + } + + @Override + protected String getSourceSetName() { + return "gametest"; + } + + void configureTests(Action action) { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final TaskContainer tasks = getProject().getTasks(); + + GameTestSettings settings = getProject().getObjects().newInstance(GameTestSettings.class); + settings.getCreateSourceSet().convention(false); + settings.getEnableGameTests().convention(true); + settings.getEnableClientGameTests().convention(true); + settings.getEula().convention(false); + settings.getClearRunDirectory().convention(true); + + action.execute(settings); + + if (settings.getCreateSourceSet().get()) { + configureSourceSet(settings.getModId(), true); + } + + Consumer configureBase = run -> { + if (settings.getCreateSourceSet().get()) { + run.source(getSourceSetName()); + } + }; + + if (settings.getEnableGameTests().get()) { + RunConfigSettings gameTest = extension.getRunConfigs().create("gameTest", run -> { + run.inherit(extension.getRunConfigs().getByName("server")); + run.property("fabric-api.gametest"); + run.runDir("build/run/gameTest"); + configureBase.accept(run); + }); + + tasks.named("test", task -> task.dependsOn(LoomTasks.getRunConfigTaskName(gameTest))); + } + + if (settings.getEnableClientGameTests().get()) { + RunConfigSettings clientGameTest = extension.getRunConfigs().create("clientGameTest", run -> { + run.inherit(extension.getRunConfigs().getByName("client")); + run.property("fabric.client.gametest"); + run.runDir("build/run/clientGameTest"); + configureBase.accept(run); + }); + + if (settings.getClearRunDirectory().get()) { + var deleteGameTestRunDir = tasks.register("deleteGameTestRunDir", Delete.class, task -> { + task.setGroup(Constants.TaskGroup.FABRIC); + task.delete(clientGameTest.getRunDir()); + }); + + tasks.named(LoomTasks.getRunConfigTaskName(clientGameTest), task -> task.dependsOn(deleteGameTestRunDir)); + } + + if (settings.getEula().get()) { + var acceptEula = tasks.register("acceptGameTestEula", AcceptEulaTask.class, task -> { + task.getEulaFile().set(getProject().file(clientGameTest.getRunDir() + "/eula.txt")); + + if (settings.getClearRunDirectory().get()) { + // Ensure that the eula is accepted after the run directory is cleared + task.dependsOn(tasks.named("deleteGameTestRunDir")); + } + }); + + tasks.named("configureLaunch", task -> task.dependsOn(acceptEula)); + } + } + } + + public abstract static class AcceptEulaTask extends AbstractLoomTask { + @OutputFile + public abstract RegularFileProperty getEulaFile(); + + @TaskAction + public void acceptEula() throws IOException { + final Path eula = getEulaFile().get().getAsFile().toPath(); + + if (Files.notExists(eula)) { + Files.writeString(eula, """ + #This file was generated by the Fabric Loom Gradle plugin. As the user opted into accepting the EULA. + eula=true + """); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index 44d1b5f64..27ddf5912 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -126,7 +126,7 @@ private void registerIDETasks() { }); } - private static String getRunConfigTaskName(RunConfigSettings config) { + public static String getRunConfigTaskName(RunConfigSettings config) { String configName = config.getName(); return "run" + configName.substring(0, 1).toUpperCase() + configName.substring(1); } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy index 94e9d293a..9f80f5add 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2023 FabricMC + * Copyright (c) 2023-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 @@ -24,6 +24,7 @@ package net.fabricmc.loom.test.integration +import spock.lang.IgnoreIf import spock.lang.Specification import spock.lang.Unroll @@ -31,15 +32,16 @@ import net.fabricmc.loom.test.util.GradleProjectTestTrait import static net.fabricmc.loom.test.LoomTestConstants.PRE_RELEASE_GRADLE import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS +import static org.gradle.testkit.runner.TaskOutcome.FAILED import static org.gradle.testkit.runner.TaskOutcome.SUCCESS class DataGenerationTest extends Specification implements GradleProjectTestTrait { private static String DEPENDENCIES = """ dependencies { - minecraft "com.mojang:minecraft:1.20.2" - mappings "net.fabricmc:yarn:1.20.2+build.4:v2" - modImplementation "net.fabricmc:fabric-loader:0.14.23" - modImplementation "net.fabricmc.fabric-api:fabric-api:0.90.0+1.20.2" + minecraft "com.mojang:minecraft:1.21.4" + mappings "net.fabricmc:yarn:1.21.4+build.4:v2" + modImplementation "net.fabricmc:fabric-loader:0.16.9" + modImplementation "net.fabricmc.fabric-api:fabric-api:0.114.0+1.21.4" } """ @@ -202,4 +204,52 @@ class DataGenerationTest extends Specification implements GradleProjectTestTrait then: result.task(":runDatagen").outcome == SUCCESS } + + @Unroll + def "game tests (gradle #version)"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << ''' + fabricApi { + configureTests() + } + ''' + DEPENDENCIES + when: + def result = gradle.run(task: "runGameTest", expectFailure: true) + + then: + // We expect this to fail because there is nothing to test + // At least we know that Fabric API is attempting to run the tests + result.task(":runGameTest").outcome == FAILED + result.output.contains("No test functions were given!") + + where: + version << STANDARD_TEST_VERSIONS + } + + @Unroll + @IgnoreIf({ System.getenv("CI") != null }) // This test is disabled on CI because it launches a real client and cannot run headless. + def "client game tests (gradle #version)"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << ''' + fabricApi { + configureTests { + createSourceSet = true + modId = "example-test" + eula = true + } + } + ''' + DEPENDENCIES + when: + def result = gradle.run(task: "runClientGameTest") + def eula = new File(gradle.projectDir, "build/run/clientGameTest/eula.txt") + + then: + result.task(":runClientGameTest").outcome == SUCCESS + eula.text.contains("eula=true") + + where: + version << STANDARD_TEST_VERSIONS + } }