mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-27 03:47:38 +00:00
Add a system for client-side tests (#1219)
- Add a new ClientJavaExec Gradle task, which is used for client-side
tests. This:
- Copies the exec spec from another JavaExec task.
- Sets some additional system properties to configure on gametest framework.
- Runs Java inside an X framebuffer (when available), meaning we
don't need to spin up a new window.
We also configure this task so that only one instance can run at
once, meaning we don't spawn multiple MC windows at once!
- Port our 1.16 client test framework to 1.19. This is mostly the same
as before, but screenshots no longer do a golden test: they /just/
write to a folder. Screenshots are compared manually afterwards.
This is still pretty brittle, and there's a lot of sleeps scattered
around in the code. It's not clear how well this will play on CI.
- Roll our own game test loader, rather than relying on the mod loader
to do it for us. This ensures that loading is consistent between
platforms (we already had to do some hacks for Forge) and makes it
easier to provide custom logic for loading client-only tests.
- Run several client tests (namely those involving monitor rendering)
against Sodium and Iris too. There's some nastiness here to set up
new Loom run configurations and automatically configure Iris to use
Complementary Shaders, but it's not too bad. These tests /don't/ run
on CI, so it doesn't need to be as reliable.
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.gametest.core;
|
||||
|
||||
import net.minecraft.client.CloudStatus;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.ParticleStatus;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.tutorial.TutorialSteps;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.ScreenEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = "cctest", value = Dist.CLIENT)
|
||||
public final class ClientHooks {
|
||||
private static final Logger LOG = LogManager.getLogger(TestHooks.class);
|
||||
|
||||
private static boolean triggered = false;
|
||||
|
||||
private ClientHooks() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onGuiInit(ScreenEvent.Init event) {
|
||||
if (triggered || !(event.getScreen() instanceof TitleScreen)) return;
|
||||
triggered = true;
|
||||
|
||||
ClientHooks.openWorld();
|
||||
}
|
||||
|
||||
private static void openWorld() {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
|
||||
// Clear some options before we get any further.
|
||||
minecraft.options.autoJump().set(false);
|
||||
minecraft.options.cloudStatus().set(CloudStatus.OFF);
|
||||
minecraft.options.particles().set(ParticleStatus.MINIMAL);
|
||||
minecraft.options.tutorialStep = TutorialSteps.NONE;
|
||||
minecraft.options.renderDistance().set(6);
|
||||
minecraft.options.gamma().set(1.0);
|
||||
|
||||
/*
|
||||
if( minecraft.getLevelSource().levelExists( "test" ) )
|
||||
{
|
||||
LOG.info( "World exists, loading it" );
|
||||
Minecraft.getInstance().loadLevel( "test" );
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info( "World does not exist, creating it for the first time" );
|
||||
|
||||
RegistryAccess registries = RegistryAccess.builtinCopy();
|
||||
|
||||
Registry<DimensionType> dimensions = registries.registryOrThrow( Registry.DIMENSION_TYPE_REGISTRY );
|
||||
var biomes = registries.registryOrThrow( Registry.BIOME_REGISTRY );
|
||||
var structures = registries.registryOrThrow( Registry.STRUCTURE_SET_REGISTRY );
|
||||
|
||||
FlatLevelGeneratorSettings flatSettings = FlatLevelGeneratorSettings.getDefault( biomes, structures )
|
||||
.withLayers(
|
||||
Collections.singletonList( new FlatLayerInfo( 4, Blocks.WHITE_CONCRETE ) ),
|
||||
Optional.empty()
|
||||
);
|
||||
flatSettings.setBiome( biomes.getHolderOrThrow( Biomes.DESERT ) );
|
||||
|
||||
WorldGenSettings generator = new WorldGenSettings( 0, false, false, withOverworld(
|
||||
dimensions,
|
||||
DimensionType.defaultDimensions( registries, 0 ),
|
||||
new FlatLevelSource( structures, flatSettings )
|
||||
) );
|
||||
|
||||
LevelSettings settings = new LevelSettings(
|
||||
"test", GameType.CREATIVE, false, Difficulty.PEACEFUL, true,
|
||||
new GameRules(), DataPackConfig.DEFAULT
|
||||
);
|
||||
Minecraft.getInstance().createLevel( "test", settings, registries, generator );
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -6,31 +6,18 @@
|
||||
package dan200.computercraft.gametest.core;
|
||||
|
||||
import dan200.computercraft.export.Exporter;
|
||||
import dan200.computercraft.gametest.api.GameTestHolder;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestRegistry;
|
||||
import net.minecraft.gametest.framework.StructureUtils;
|
||||
import net.minecraft.gametest.framework.TestFunction;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.RegisterClientCommandsEvent;
|
||||
import net.minecraftforge.client.event.ScreenEvent;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.event.RegisterGameTestsEvent;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.server.ServerStartedEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import net.minecraftforge.forgespi.language.ModFileScanData;
|
||||
import net.minecraftforge.gametest.PrefixGameTestTemplate;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Mod("cctest")
|
||||
public class TestMod {
|
||||
@@ -40,85 +27,21 @@ public class TestMod {
|
||||
var bus = MinecraftForge.EVENT_BUS;
|
||||
bus.addListener(EventPriority.LOW, (ServerStartedEvent e) -> TestHooks.onServerStarted(e.getServer()));
|
||||
bus.addListener((RegisterCommandsEvent e) -> CCTestCommand.register(e.getDispatcher()));
|
||||
bus.addListener((RegisterClientCommandsEvent e) -> Exporter.register(e.getDispatcher()));
|
||||
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> TestMod::onInitializeClient);
|
||||
|
||||
var modBus = FMLJavaModLoadingContext.get().getModEventBus();
|
||||
modBus.addListener((RegisterGameTestsEvent event) -> {
|
||||
var holder = Type.getType(GameTestHolder.class);
|
||||
ModList.get().getAllScanData().stream()
|
||||
.map(ModFileScanData::getAnnotations)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(a -> holder.equals(a.annotationType()))
|
||||
.forEach(x -> registerClass(x.clazz().getClassName(), event::register));
|
||||
modBus.addListener((RegisterGameTestsEvent event) -> TestHooks.loadTests(event::register));
|
||||
}
|
||||
|
||||
private static void onInitializeClient() {
|
||||
var bus = MinecraftForge.EVENT_BUS;
|
||||
|
||||
bus.addListener((TickEvent.ServerTickEvent e) -> {
|
||||
if (e.phase == TickEvent.Phase.START) ClientTestHooks.onServerTick(e.getServer());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static Class<?> loadClass(String name) {
|
||||
try {
|
||||
return Class.forName(name, true, TestMod.class.getClassLoader());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerClass(String className, Consumer<Method> fallback) {
|
||||
var klass = loadClass(className);
|
||||
for (var method : klass.getDeclaredMethods()) {
|
||||
var testInfo = method.getAnnotation(GameTest.class);
|
||||
if (testInfo == null) {
|
||||
fallback.accept(method);
|
||||
continue;
|
||||
}
|
||||
|
||||
GameTestRegistry.getAllTestFunctions().add(turnMethodIntoTestFunction(method, testInfo));
|
||||
GameTestRegistry.getAllTestClassNames().add(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom implementation of {@link GameTestRegistry#turnMethodIntoTestFunction(Method)} which makes
|
||||
* {@link GameTest#template()} behave the same as Fabric, namely in that it points to a {@link ResourceLocation},
|
||||
* rather than a test-class-specific structure.
|
||||
* <p>
|
||||
* This effectively acts as a global version of {@link PrefixGameTestTemplate}, just one which doesn't require Forge
|
||||
* to be present.
|
||||
*
|
||||
* @param method The method to register.
|
||||
* @param testInfo The test info.
|
||||
* @return The constructed test function.
|
||||
*/
|
||||
private static TestFunction turnMethodIntoTestFunction(Method method, GameTest testInfo) {
|
||||
var className = method.getDeclaringClass().getSimpleName().toLowerCase(Locale.ROOT);
|
||||
var testName = className + "." + method.getName().toLowerCase(Locale.ROOT);
|
||||
return new TestFunction(
|
||||
testInfo.batch(),
|
||||
testName,
|
||||
testInfo.template().isEmpty() ? testName : testInfo.template(),
|
||||
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps()), testInfo.timeoutTicks(), testInfo.setupTicks(),
|
||||
testInfo.required(), testInfo.requiredSuccesses(), testInfo.attempts(),
|
||||
turnMethodIntoConsumer(method)
|
||||
);
|
||||
}
|
||||
|
||||
private static <T> Consumer<T> turnMethodIntoConsumer(Method method) {
|
||||
return value -> {
|
||||
try {
|
||||
Object instance = null;
|
||||
if (!Modifier.isStatic(method.getModifiers())) {
|
||||
instance = method.getDeclaringClass().getConstructor().newInstance();
|
||||
}
|
||||
|
||||
method.invoke(instance, value);
|
||||
} catch (InvocationTargetException e) {
|
||||
if (e.getCause() instanceof RuntimeException) {
|
||||
throw (RuntimeException) e.getCause();
|
||||
} else {
|
||||
throw new RuntimeException(e.getCause());
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
bus.addListener((ScreenEvent.Opening e) -> {
|
||||
if (ClientTestHooks.onOpenScreen(e.getScreen())) e.setCanceled(true);
|
||||
});
|
||||
bus.addListener((RegisterClientCommandsEvent e) -> Exporter.register(e.getDispatcher()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user