Skip to content

Commit

Permalink
WIP: Apply patch from FabricMC#1622
Browse files Browse the repository at this point in the history
  • Loading branch information
turtton committed Jun 27, 2024
1 parent 3ede25f commit c5aa3dc
Show file tree
Hide file tree
Showing 20 changed files with 1,798 additions and 1 deletion.
10 changes: 9 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,19 @@ sourceSets {
testmod
}

// These modules are not included in the fat jar, maven will resolve them via the pom.
def devOnlyModules = [
"fabric-gametest-api-v1"
]

dependencies {
afterEvaluate {
subprojects.each {
api project(path: ":${it.name}", configuration: "namedElements")
include project("${it.name}:")

if (!(it.name in devOnlyModules)) {
include project("${it.name}:")
}

testmodImplementation project("${it.name}:").sourceSets.testmod.output
}
Expand Down
24 changes: 24 additions & 0 deletions fabric-gametest-api-v1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
archivesBaseName = "fabric-gametest-api-v1"
version = getSubprojectVersion(project)

moduleDependencies(project, [
'fabric-api-base',
'fabric-resource-loader-v0'
])

loom {
runs {
gametest {
server()
name "Game Test"
vmArg "-Dfabric-api.gametest"
vmArg "-Dfabric-api.gametest.report-file=${project.buildDir}/junit.xml"
runDir "build/gametest"

// Specific to fabric api
source sourceSets.testmod
ideConfigGenerated true
}
}
}
test.dependsOn runGametest
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.gametest.v1;

import java.lang.reflect.Method;

import net.minecraft.test.TestContext;

import net.fabricmc.fabric.impl.gametest.FabricGameTestHelper;

/**
* This interface can be optionally implemented on your test class.
*/
public interface FabricGameTest {
/**
* Use in {@link net.minecraft.test.GameTest} structureName to use an empty 8x8 structure for the test.
*/
String EMPTY_STRUCTURE = "fabric-gametest-api-v1:empty";

/**
* Override this method to implement custom logic to invoke the test method.
* This can be used to run code before or after each test.
* You can also pass in custom objects into the test method if desired.
* The structure will have been placed in the world before this method is invoked.
*
* @param context The vanilla test context
* @param method The test method to invoke
*/
default void invokeTestMethod(TestContext context, Method method) {
FabricGameTestHelper.invokeTestMethod(context, method, this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.gametest;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.function.Consumer;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.ServerResourceManager;
import net.minecraft.server.MinecraftServer;
import net.minecraft.test.GameTestBatch;
import net.minecraft.test.TestContext;
import net.minecraft.test.TestFailureLogger;
import net.minecraft.test.TestFunction;
import net.minecraft.test.TestFunctions;
import net.minecraft.test.TestServer;
import net.minecraft.test.TestUtil;
import net.minecraft.test.XmlReportingTestCompletionListener;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.registry.DynamicRegistryManager;
import net.minecraft.world.level.storage.LevelStorage;

import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;

public final class FabricGameTestHelper {
public static final boolean ENABLED = System.getProperty("fabric-api.gametest") != null;

private static final Logger LOGGER = LogManager.getLogger();

private FabricGameTestHelper() {
}

public static void runHeadlessServer(LevelStorage.Session session, ResourcePackManager resourcePackManager, ServerResourceManager serverResourceManager, DynamicRegistryManager.Impl registryManager) {
String reportPath = System.getProperty("fabric-api.gametest.report-file");

if (reportPath != null) {
try {
TestFailureLogger.setCompletionListener(new XmlReportingTestCompletionListener(new File(reportPath)));
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
}

LOGGER.info("Starting test server");
MinecraftServer server = TestServer.startServer(thread -> {
TestServer testServer = new TestServer(thread, session, resourcePackManager, serverResourceManager, getBatches(), BlockPos.ORIGIN, registryManager);
return testServer;
});
}

public static Consumer<TestContext> getTestMethodInvoker(Method method) {
return testContext -> {
Class<?> testClass = method.getDeclaringClass();

Constructor<?> constructor;

try {
constructor = testClass.getConstructor();
} catch (NoSuchMethodException e) {
throw new RuntimeException("Test class (%s) provided by (%s) must have a public default or no args constructor".formatted(testClass.getSimpleName(), FabricGameTestModInitializer.getModIdForTestClass(testClass)));
}

Object testObject;

try {
testObject = constructor.newInstance();
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Failed to create instance of test class (%s)".formatted(testClass.getCanonicalName()), e);
}

if (testObject instanceof FabricGameTest fabricGameTest) {
fabricGameTest.invokeTestMethod(testContext, method);
} else {
invokeTestMethod(testContext, method, testObject);
}
};
}

public static void invokeTestMethod(TestContext testContext, Method method, Object testObject) {
try {
method.invoke(testObject, testContext);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to invoke test method (%s) in (%s)".formatted(method.getName(), method.getDeclaringClass().getCanonicalName()), e);
}
}

private static Collection<GameTestBatch> getBatches() {
return TestUtil.createBatches(getTestFunctions());
}

private static Collection<TestFunction> getTestFunctions() {
return TestFunctions.getTestFunctions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.gametest;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.ApiStatus;

import net.minecraft.test.TestFunctions;

import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.entrypoint.EntrypointContainer;

@ApiStatus.Internal
public final class FabricGameTestModInitializer implements ModInitializer {
private static final String ENTRYPOINT_KEY = "fabric-gametest";
private static final Map<Class<?>, String> GAME_TEST_IDS = new HashMap<>();
private static final Logger LOGGER = LogManager.getLogger();

@Override
public void onInitialize() {
List<EntrypointContainer<Object>> entrypointContainers = FabricLoader.getInstance()
.getEntrypointContainers(ENTRYPOINT_KEY, Object.class);

for (EntrypointContainer<Object> container : entrypointContainers) {
Class<?> testClass = container.getEntrypoint().getClass();
String modid = container.getProvider().getMetadata().getId();

if (GAME_TEST_IDS.containsKey(testClass)) {
throw new UnsupportedOperationException("Test class (%s) has already been registered with mod (%s)".formatted(testClass.getCanonicalName(), modid));
}

GAME_TEST_IDS.put(testClass, modid);
TestFunctions.register(testClass);

LOGGER.debug("Registered test class {} for mod {}", testClass.getCanonicalName(), modid);
}
}

public static String getModIdForTestClass(Class<?> testClass) {
if (!GAME_TEST_IDS.containsKey(testClass)) {
throw new UnsupportedOperationException("The test class (%s) was not registered using the '%s' entrypoint".formatted(testClass.getCanonicalName(), ENTRYPOINT_KEY));
}

return GAME_TEST_IDS.get(testClass);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.mixin.gametest;

import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.mojang.brigadier.CommandDispatcher;

import net.minecraft.SharedConstants;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.command.TestCommand;

@Mixin(CommandManager.class)
public abstract class CommandManagerMixin {
@Shadow
@Final
private CommandDispatcher<ServerCommandSource> dispatcher;

@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/command/WorldBorderCommand;register(Lcom/mojang/brigadier/CommandDispatcher;)V", shift = At.Shift.AFTER))
private void construct(CommandManager.RegistrationEnvironment environment, CallbackInfo info) {
// Registered by vanilla when isDevelopment is enabled.
if (!SharedConstants.isDevelopment) {
TestCommand.register(this.dispatcher);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.mixin.gametest;

import java.util.function.BooleanSupplier;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import net.minecraft.SharedConstants;
import net.minecraft.server.MinecraftServer;
import net.minecraft.test.TestManager;

@Mixin(MinecraftServer.class)
public abstract class MinecraftServerMixin {
@Inject(method = "tickWorlds", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;updatePlayerLatency()V", shift = At.Shift.AFTER))
private void tickWorlds(BooleanSupplier shouldKeepTicking, CallbackInfo callbackInfo) {
// Called by vanilla when isDevelopment is enabled.
if (!SharedConstants.isDevelopment) {
TestManager.INSTANCE.tick();
}
}
}
Loading

0 comments on commit c5aa3dc

Please sign in to comment.