1
0
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:
Jonathan Coates
2022-11-18 23:57:25 +00:00
committed by GitHub
parent b58b9b7df3
commit 8f92417a2f
43 changed files with 1634 additions and 329 deletions

View File

@@ -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 );
}
*/
}
}

View File

@@ -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()));
}
}