From 61f110ad1f927a3d2b2db6eb4f34f06c13484a4b Mon Sep 17 00:00:00 2001 From: melontini <104443436+melontini@users.noreply.github.com> Date: Sun, 28 Jul 2024 00:56:06 +0700 Subject: [PATCH 1/3] Late check and simple TestRunner --- build.gradle | 20 +- gradle.properties | 6 +- gradle/libs.versions.toml | 7 + gradle/wrapper/gradle-wrapper.properties | 2 +- .../handytests/client/ClientTestContext.java | 153 +++++---- .../client/ClientTestEntrypoint.java | 4 +- .../client/FabricClientTestHelper.java | 320 +++++++++--------- .../handytests/client/HandyClient.java | 46 +-- .../mixin/client/MinecraftClientMixin.java | 77 +++-- .../mixin/server/ServerMainMixin.java | 13 +- .../handytests/server/HandyServer.java | 49 ++- .../handytests/server/ServerTestContext.java | 85 ++--- .../server/ServerTestEntrypoint.java | 4 +- .../handytests/util/TestContext.java | 23 +- .../me/melontini/handytests/util/Utils.java | 41 ++- .../handytests/util/runner/HandyTest.java | 12 + .../handytests/util/runner/TestRunner.java | 55 +++ 17 files changed, 549 insertions(+), 368 deletions(-) create mode 100644 gradle/libs.versions.toml create mode 100644 src/main/java/me/melontini/handytests/util/runner/HandyTest.java create mode 100644 src/main/java/me/melontini/handytests/util/runner/TestRunner.java diff --git a/build.gradle b/build.gradle index 11370b1..7e64337 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { - id 'fabric-loom' version '1.6-SNAPSHOT' + alias libs.plugins.fabric.loom id 'maven-publish' + alias libs.plugins.spotless } def local = !System.getenv().containsKey("GITHUB_RUN_NUMBER") @@ -24,7 +25,7 @@ dependencies { // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation libs.fabric.loader // Fabric API. This is technically optional, but you probably want it anyway. def fabricModules = [ @@ -32,7 +33,16 @@ dependencies { "fabric-lifecycle-events-v1" ] fabricModules.each { - modImplementation include(fabricApi.module(it, project.fabric_version)) + modImplementation include(fabricApi.module(it, libs.fabric.api.get().version)) + } +} + +spotless { + java { + removeUnusedImports() + palantirJavaFormat('2.47.0').style("GOOGLE") + trimTrailingWhitespace() + formatAnnotations() } } @@ -50,13 +60,13 @@ loom { processResources { inputs.property "version", project.version inputs.property "minecraft_version", project.minecraft_version - inputs.property "loader_version", project.loader_version + inputs.property "loader_version", libs.fabric.loader.get().version filteringCharset "UTF-8" filesMatching("fabric.mod.json") { expand "version": project.version, "minecraft_version": project.minecraft_version, - "loader_version": project.loader_version + "loader_version": libs.fabric.loader.get().version } } diff --git a/gradle.properties b/gradle.properties index b96bb41..7efe48e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,11 +4,7 @@ org.gradle.jvmargs=-Xmx1G # check these on https://modmuss50.me/fabric.html minecraft_version=1.20.1 yarn_mappings=1.20.1+build.10 -loader_version=0.15.10 # Mod Properties -mod_version=0.2.0 +mod_version=0.3.0 maven_group=me.melontini archives_base_name=handy-tests -# Dependencies -# check this on https://modmuss50.me/fabric.html -fabric_version=0.92.1+1.20.1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..120a82a --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[libraries] +fabric-loader = { group = "net.fabricmc", name = "fabric-loader", version = "0.16.0" } +fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version = "0.92.1+1.20.1" } + +[plugins] +fabric-loom = { id = "fabric-loom", version = "1.7.2" } +spotless = { id = "com.diffplug.spotless", version = "6.25.0" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22c..a441313 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/me/melontini/handytests/client/ClientTestContext.java b/src/main/java/me/melontini/handytests/client/ClientTestContext.java index b7e709e..d0db35d 100644 --- a/src/main/java/me/melontini/handytests/client/ClientTestContext.java +++ b/src/main/java/me/melontini/handytests/client/ClientTestContext.java @@ -1,86 +1,89 @@ package me.melontini.handytests.client; -import me.melontini.handytests.util.TestContext; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.option.Perspective; +import static me.melontini.handytests.client.FabricClientTestHelper.submit; import java.time.Duration; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; - -import static me.melontini.handytests.client.FabricClientTestHelper.submit; +import me.melontini.handytests.util.TestContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.option.Perspective; public record ClientTestContext(MinecraftClient client) implements TestContext { - public void sendCommand(String command) { - FabricClientTestHelper.submitAndWait(client -> { - client.player.networkHandler.sendCommand(command); - return null; - }); - } - - public void waitForLoadingComplete() { - FabricClientTestHelper.waitForLoadingComplete(); - } - - public void waitForScreen(Class screenClass) { - FabricClientTestHelper.waitForScreen(screenClass); - } - - public T executeForScreen(Class screenClass, BiFunction function) { - return FabricClientTestHelper.submitAndWait(client -> { - if (screenClass.isInstance(client.currentScreen)) { - return function.apply(client, screenClass.cast(client.currentScreen)); - } - throw new IllegalStateException("Expected: %s, got: %s".formatted(screenClass.getName(), client.currentScreen != null ? client.currentScreen.getClass().getName() : "null")); - }); - } - - public void openGameMenu() { - FabricClientTestHelper.openGameMenu(); - } - - public void openInventory() { - FabricClientTestHelper.openInventory(); - } - - public void closeScreen() { - setScreen((client) -> null); - } - - public void setScreen(Function screenSupplier) { - FabricClientTestHelper.setScreen(screenSupplier); - } - - public void takeScreenshot(String name) { - FabricClientTestHelper.takeScreenshot(name); - } - - public void waitForWorldTicks(long ticks) { - FabricClientTestHelper.waitForWorldTicks(ticks); - } - - public void enableDebugHud() { - FabricClientTestHelper.enableDebugHud(); - } - - public void setPerspective(Perspective perspective) { - FabricClientTestHelper.setPerspective(perspective); - } - - @Override - public void waitFor(String what, Predicate predicate, Duration timeout) { - FabricClientTestHelper.waitFor(what, predicate, timeout); - } - - public T submitAndWait(Function function) { - return submit(function).join(); - } - - @Override - public MinecraftClient context() { - return client(); - } + public void sendCommand(String command) { + FabricClientTestHelper.submitAndWait(client -> { + client.player.networkHandler.sendCommand(command); + return null; + }); + } + + public void waitForLoadingComplete() { + FabricClientTestHelper.waitForLoadingComplete(); + } + + public void waitForScreen(Class screenClass) { + FabricClientTestHelper.waitForScreen(screenClass); + } + + public T executeForScreen( + Class screenClass, BiFunction function) { + return FabricClientTestHelper.submitAndWait(client -> { + if (screenClass.isInstance(client.currentScreen)) { + return function.apply(client, screenClass.cast(client.currentScreen)); + } + throw new IllegalStateException("Expected: %s, got: %s" + .formatted( + screenClass.getName(), + client.currentScreen != null ? client.currentScreen.getClass().getName() : "null")); + }); + } + + public void openGameMenu() { + FabricClientTestHelper.openGameMenu(); + } + + public void openInventory() { + FabricClientTestHelper.openInventory(); + } + + public void closeScreen() { + setScreen((client) -> null); + } + + public void setScreen(Function screenSupplier) { + FabricClientTestHelper.setScreen(screenSupplier); + } + + public void takeScreenshot(String name) { + FabricClientTestHelper.takeScreenshot(name); + } + + public void waitForWorldTicks(long ticks) { + FabricClientTestHelper.waitForWorldTicks(ticks); + } + + public void enableDebugHud() { + FabricClientTestHelper.enableDebugHud(); + } + + public void setPerspective(Perspective perspective) { + FabricClientTestHelper.setPerspective(perspective); + } + + @Override + public void waitFor(String what, Predicate predicate, Duration timeout) { + FabricClientTestHelper.waitFor(what, predicate, timeout); + } + + public T submitAndWait(Function function) { + return submit(function).join(); + } + + @Override + public MinecraftClient context() { + return client(); + } } diff --git a/src/main/java/me/melontini/handytests/client/ClientTestEntrypoint.java b/src/main/java/me/melontini/handytests/client/ClientTestEntrypoint.java index 0bdf232..1c387a4 100644 --- a/src/main/java/me/melontini/handytests/client/ClientTestEntrypoint.java +++ b/src/main/java/me/melontini/handytests/client/ClientTestEntrypoint.java @@ -1,5 +1,7 @@ package me.melontini.handytests.client; public interface ClientTestEntrypoint { - void onClientTest(ClientTestContext context); + default void onClientTest(ClientTestContext context) { + context.runAllForEntrypoint(this); + } } diff --git a/src/main/java/me/melontini/handytests/client/FabricClientTestHelper.java b/src/main/java/me/melontini/handytests/client/FabricClientTestHelper.java index 4e010db..d225d9f 100644 --- a/src/main/java/me/melontini/handytests/client/FabricClientTestHelper.java +++ b/src/main/java/me/melontini/handytests/client/FabricClientTestHelper.java @@ -16,6 +16,12 @@ package me.melontini.handytests.client; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Predicate; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.GameMenuScreen; @@ -26,160 +32,166 @@ import net.minecraft.client.option.Perspective; import net.minecraft.client.util.ScreenshotRecorder; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.function.Function; -import java.util.function.Predicate; - // Provides thread safe utils for interacting with a running game. public final class FabricClientTestHelper { - public static void waitForLoadingComplete() { - waitFor("Loading to complete", client -> client.getOverlay() == null, Duration.ofMinutes(5)); - } - - public static void waitForScreen(Class screenClass) { - waitFor("Screen %s".formatted(screenClass.getName()), client -> client.currentScreen != null && client.currentScreen.getClass() == screenClass); - } - - public static void openGameMenu() { - setScreen((client) -> new GameMenuScreen(true)); - waitForScreen(GameMenuScreen.class); - } - - public static void openInventory() { - setScreen((client) -> new InventoryScreen(Objects.requireNonNull(client.player))); - - boolean creative = submitAndWait(client -> Objects.requireNonNull(client.player).isCreative()); - waitForScreen(creative ? CreativeInventoryScreen.class : InventoryScreen.class); - } - - public static void closeScreen() { - setScreen((client) -> null); - } - - public static void setScreen(Function screenSupplier) { - submit(client -> { - client.setScreen(screenSupplier.apply(client)); - return null; - }); - } - - public static void takeScreenshot(String name) { - // Allow time for any screens to open - waitFor(Duration.ofSeconds(1)); - - submitAndWait(client -> { - ScreenshotRecorder.saveScreenshot(FabricLoader.getInstance().getGameDir().toFile(), name + ".png", client.getFramebuffer(), (message) -> { - }); - return null; - }); - } - - /*public static void clickScreenButton(String translationKey) { - final String buttonText = Text.translatable(translationKey).getString(); - - waitFor("Click button" + buttonText, client -> { - final Screen screen = client.currentScreen; - - if (screen == null) { - return false; - } - - final ScreenAccessor screenAccessor = (ScreenAccessor) screen; - - for (Drawable drawable : screenAccessor.getDrawables()) { - if (drawable instanceof PressableWidget pressableWidget && pressMatchingButton(pressableWidget, buttonText)) { - return true; - } - - if (drawable instanceof Widget widget) { - widget.forEachChild(clickableWidget -> pressMatchingButton(clickableWidget, buttonText)); - } - } - - // Was unable to find the button to press - return false; - }); - } - - private static boolean pressMatchingButton(ClickableWidget widget, String text) { - if (widget instanceof ButtonWidget buttonWidget) { - if (text.equals(buttonWidget.getMessage().getString())) { - buttonWidget.onPress(); - return true; - } - } - - if (widget instanceof CyclingButtonWidget buttonWidget) { - CyclingButtonWidgetAccessor accessor = (CyclingButtonWidgetAccessor) buttonWidget; - - if (text.equals(accessor.getOptionText().getString())) { - buttonWidget.onPress(); - return true; - } - } - - return false; - }*/ - - public static void waitForWorldTicks(long ticks) { - // Wait for the world to be loaded and get the start ticks - waitFor("World load", client -> client.world != null && !(client.currentScreen instanceof LevelLoadingScreen), Duration.ofMinutes(30)); - final long startTicks = submitAndWait(client -> client.world.getTime()); - waitFor("World load", client -> Objects.requireNonNull(client.world).getTime() > startTicks + ticks, Duration.ofMinutes(10)); - } - - public static void enableDebugHud() { - submitAndWait(client -> { - client.options.debugEnabled = true; - return null; - }); - } - - public static void setPerspective(Perspective perspective) { - submitAndWait(client -> { - client.options.setPerspective(perspective); - return null; - }); - } - - static void waitFor(String what, Predicate predicate) { - waitFor(what, predicate, Duration.ofSeconds(10)); - } - - static void waitFor(String what, Predicate predicate, Duration timeout) { - final LocalDateTime end = LocalDateTime.now().plus(timeout); - - while (true) { - boolean result = submitAndWait(predicate::test); - - if (result) { - break; - } - - if (LocalDateTime.now().isAfter(end)) { - throw new RuntimeException("Timed out waiting for " + what); - } - - waitFor(Duration.ofSeconds(1)); - } - } - - static void waitFor(Duration duration) { - try { - Thread.sleep(duration.toMillis()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - static CompletableFuture submit(Function function) { - return MinecraftClient.getInstance().submit(() -> function.apply(MinecraftClient.getInstance())); - } - - public static T submitAndWait(Function function) { - return submit(function).join(); - } + public static void waitForLoadingComplete() { + waitFor("Loading to complete", client -> client.getOverlay() == null, Duration.ofMinutes(5)); + } + + public static void waitForScreen(Class screenClass) { + waitFor( + "Screen %s".formatted(screenClass.getName()), + client -> client.currentScreen != null && client.currentScreen.getClass() == screenClass); + } + + public static void openGameMenu() { + setScreen((client) -> new GameMenuScreen(true)); + waitForScreen(GameMenuScreen.class); + } + + public static void openInventory() { + setScreen((client) -> new InventoryScreen(Objects.requireNonNull(client.player))); + + boolean creative = + submitAndWait(client -> Objects.requireNonNull(client.player).isCreative()); + waitForScreen(creative ? CreativeInventoryScreen.class : InventoryScreen.class); + } + + public static void closeScreen() { + setScreen((client) -> null); + } + + public static void setScreen(Function screenSupplier) { + submit(client -> { + client.setScreen(screenSupplier.apply(client)); + return null; + }); + } + + public static void takeScreenshot(String name) { + // Allow time for any screens to open + waitFor(Duration.ofSeconds(1)); + + submitAndWait(client -> { + ScreenshotRecorder.saveScreenshot( + FabricLoader.getInstance().getGameDir().toFile(), + name + ".png", + client.getFramebuffer(), + (message) -> {}); + return null; + }); + } + + /*public static void clickScreenButton(String translationKey) { + final String buttonText = Text.translatable(translationKey).getString(); + + waitFor("Click button" + buttonText, client -> { + final Screen screen = client.currentScreen; + + if (screen == null) { + return false; + } + + final ScreenAccessor screenAccessor = (ScreenAccessor) screen; + + for (Drawable drawable : screenAccessor.getDrawables()) { + if (drawable instanceof PressableWidget pressableWidget && pressMatchingButton(pressableWidget, buttonText)) { + return true; + } + + if (drawable instanceof Widget widget) { + widget.forEachChild(clickableWidget -> pressMatchingButton(clickableWidget, buttonText)); + } + } + + // Was unable to find the button to press + return false; + }); + } + + private static boolean pressMatchingButton(ClickableWidget widget, String text) { + if (widget instanceof ButtonWidget buttonWidget) { + if (text.equals(buttonWidget.getMessage().getString())) { + buttonWidget.onPress(); + return true; + } + } + + if (widget instanceof CyclingButtonWidget buttonWidget) { + CyclingButtonWidgetAccessor accessor = (CyclingButtonWidgetAccessor) buttonWidget; + + if (text.equals(accessor.getOptionText().getString())) { + buttonWidget.onPress(); + return true; + } + } + + return false; + }*/ + + public static void waitForWorldTicks(long ticks) { + // Wait for the world to be loaded and get the start ticks + waitFor( + "World load", + client -> client.world != null && !(client.currentScreen instanceof LevelLoadingScreen), + Duration.ofMinutes(30)); + final long startTicks = submitAndWait(client -> client.world.getTime()); + waitFor( + "World load", + client -> Objects.requireNonNull(client.world).getTime() > startTicks + ticks, + Duration.ofMinutes(10)); + } + + public static void enableDebugHud() { + submitAndWait(client -> { + client.options.debugEnabled = true; + return null; + }); + } + + public static void setPerspective(Perspective perspective) { + submitAndWait(client -> { + client.options.setPerspective(perspective); + return null; + }); + } + + static void waitFor(String what, Predicate predicate) { + waitFor(what, predicate, Duration.ofSeconds(10)); + } + + static void waitFor(String what, Predicate predicate, Duration timeout) { + final LocalDateTime end = LocalDateTime.now().plus(timeout); + + while (true) { + boolean result = submitAndWait(predicate::test); + + if (result) { + break; + } + + if (LocalDateTime.now().isAfter(end)) { + throw new RuntimeException("Timed out waiting for " + what); + } + + waitFor(Duration.ofSeconds(1)); + } + } + + static void waitFor(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + static CompletableFuture submit(Function function) { + return MinecraftClient.getInstance() + .submit(() -> function.apply(MinecraftClient.getInstance())); + } + + public static T submitAndWait(Function function) { + return submit(function).join(); + } } diff --git a/src/main/java/me/melontini/handytests/client/HandyClient.java b/src/main/java/me/melontini/handytests/client/HandyClient.java index 66f1e59..9af9ffc 100644 --- a/src/main/java/me/melontini/handytests/client/HandyClient.java +++ b/src/main/java/me/melontini/handytests/client/HandyClient.java @@ -1,33 +1,41 @@ package me.melontini.handytests.client; -import me.melontini.handytests.server.HandyServer; +import com.mojang.logging.LogUtils; import me.melontini.handytests.util.Utils; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; +import org.lwjgl.glfw.GLFW; +import org.slf4j.Logger; import org.spongepowered.asm.mixin.MixinEnvironment; public class HandyClient implements ClientModInitializer { - @Override - public void onInitializeClient() { - if (!Utils.ENABLED) return; + private static final Logger log = LogUtils.getLogger(); - var thread = new Thread(() -> { - try { - HandyServer.LOGGER.info("Started client test."); - ClientTestContext context = new ClientTestContext(MinecraftClient.getInstance()); - context.waitForWorldTicks(200); + @Override + public void onInitializeClient() { + if (!Utils.ENABLED) return; - FabricLoader.getInstance().invokeEntrypoints("handy:client_test", ClientTestEntrypoint.class, e -> e.onClientTest(context)); - MixinEnvironment.getCurrentEnvironment().audit(); + var thread = new Thread(() -> { + try { + log.info("Started client test."); + ClientTestContext context = new ClientTestContext(MinecraftClient.getInstance()); + context.waitForWorldTicks(200); + GLFW.glfwShowWindow(context.client().getWindow().getHandle()); - MinecraftClient.getInstance().scheduleStop(); - } catch (Throwable t) { - t.printStackTrace(); - System.exit(1); - } - }); - thread.start(); - } + FabricLoader.getInstance() + .invokeEntrypoints( + "handy:client_test", ClientTestEntrypoint.class, e -> e.onClientTest(context)); + MixinEnvironment.getCurrentEnvironment().audit(); + + Utils.runChecks(); + MinecraftClient.getInstance().scheduleStop(); + } catch (Throwable t) { + log.error("Failed client test!", t); + System.exit(1); + } + }); + thread.start(); + } } diff --git a/src/main/java/me/melontini/handytests/mixin/client/MinecraftClientMixin.java b/src/main/java/me/melontini/handytests/mixin/client/MinecraftClientMixin.java index e63ebd1..d4eff4b 100644 --- a/src/main/java/me/melontini/handytests/mixin/client/MinecraftClientMixin.java +++ b/src/main/java/me/melontini/handytests/mixin/client/MinecraftClientMixin.java @@ -1,5 +1,6 @@ package me.melontini.handytests.mixin.client; +import java.util.regex.Pattern; import me.melontini.handytests.util.Utils; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.SharedConstants; @@ -20,40 +21,58 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.regex.Pattern; - @Mixin(value = MinecraftClient.class, priority = 1200) public class MinecraftClientMixin { - @Unique - private static final Pattern RESERVED_WINDOWS_NAMES = Pattern.compile(".*\\.|(?:COM|CLOCK\\$|CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\\..*)?", 2); + @Unique private static final Pattern RESERVED_WINDOWS_NAMES = + Pattern.compile(".*\\.|(?:COM|CLOCK\\$|CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\\..*)?", 2); - @Inject(method = "method_29338", at = @At("TAIL"), require = 0) - private void handy$init(CallbackInfo ci) { - if (!Utils.ENABLED) return; + @Inject(method = "method_29338", at = @At("TAIL"), require = 0) + private void handy$init(CallbackInfo ci) { + if (!Utils.ENABLED) return; - MinecraftClient.getInstance().send(() -> { - try { - MinecraftClient client = MinecraftClient.getInstance(); - String levelName = "handy_test_" + FabricLoader.getInstance().getModContainer("minecraft").orElseThrow().getMetadata().getVersion().getFriendlyString(); + MinecraftClient.getInstance().send(() -> { + try { + MinecraftClient client = MinecraftClient.getInstance(); + String levelName = "handy_test_" + + FabricLoader.getInstance() + .getModContainer("minecraft") + .orElseThrow() + .getMetadata() + .getVersion() + .getFriendlyString(); - for (char c : SharedConstants.INVALID_CHARS_LEVEL_NAME) levelName = levelName.replace(c, '_'); - levelName = levelName.replaceAll("[./\"]", "_"); - if (RESERVED_WINDOWS_NAMES.matcher(levelName).matches()) levelName = "_" + levelName + "_"; + for (char c : SharedConstants.INVALID_CHARS_LEVEL_NAME) + levelName = levelName.replace(c, '_'); + levelName = levelName.replaceAll("[./\"]", "_"); + if (RESERVED_WINDOWS_NAMES.matcher(levelName).matches()) levelName = "_" + levelName + "_"; - if (!client.getLevelStorage().levelExists(levelName)) { - client.createIntegratedServerLoader().createAndStart(levelName, - new LevelInfo(levelName, GameMode.CREATIVE, false, Difficulty.PEACEFUL, true, - new GameRules(), DataConfiguration.SAFE_MODE), - new GeneratorOptions(0, true, false), - registryManager -> registryManager.get(RegistryKeys.WORLD_PRESET).entryOf(WorldPresets.FLAT).value().createDimensionsRegistryHolder()); - } else { - client.createIntegratedServerLoader().start(new TitleScreen(), levelName); - } - } catch (Throwable t) { - CrashReport report = CrashReport.create(t, "Setting tests world"); - MinecraftClient.printCrashReport(report); - } - }); - } + if (!client.getLevelStorage().levelExists(levelName)) { + client + .createIntegratedServerLoader() + .createAndStart( + levelName, + new LevelInfo( + levelName, + GameMode.CREATIVE, + false, + Difficulty.PEACEFUL, + true, + new GameRules(), + DataConfiguration.SAFE_MODE), + new GeneratorOptions(0, true, false), + registryManager -> registryManager + .get(RegistryKeys.WORLD_PRESET) + .entryOf(WorldPresets.FLAT) + .value() + .createDimensionsRegistryHolder()); + } else { + client.createIntegratedServerLoader().start(new TitleScreen(), levelName); + } + } catch (Throwable t) { + CrashReport report = CrashReport.create(t, "Setting tests world"); + MinecraftClient.printCrashReport(report); + } + }); + } } diff --git a/src/main/java/me/melontini/handytests/mixin/server/ServerMainMixin.java b/src/main/java/me/melontini/handytests/mixin/server/ServerMainMixin.java index 552c892..66b66df 100644 --- a/src/main/java/me/melontini/handytests/mixin/server/ServerMainMixin.java +++ b/src/main/java/me/melontini/handytests/mixin/server/ServerMainMixin.java @@ -8,8 +8,13 @@ @Mixin(Main.class) public class ServerMainMixin { - @ModifyExpressionValue(method = "main", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/dedicated/EulaReader;isEulaAgreedTo()Z")) - private static boolean isEulaAgreedTo(boolean original) { - return original || Utils.ENABLED; - } + @ModifyExpressionValue( + method = "main", + at = + @At( + value = "INVOKE", + target = "Lnet/minecraft/server/dedicated/EulaReader;isEulaAgreedTo()Z")) + private static boolean isEulaAgreedTo(boolean original) { + return original || Utils.ENABLED; + } } diff --git a/src/main/java/me/melontini/handytests/server/HandyServer.java b/src/main/java/me/melontini/handytests/server/HandyServer.java index 4f55ec4..e44f69e 100644 --- a/src/main/java/me/melontini/handytests/server/HandyServer.java +++ b/src/main/java/me/melontini/handytests/server/HandyServer.java @@ -4,39 +4,38 @@ import me.melontini.handytests.util.Utils; import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.loader.api.FabricLoader; -import org.apache.commons.lang3.mutable.MutableInt; import org.slf4j.Logger; import org.spongepowered.asm.mixin.MixinEnvironment; public class HandyServer implements DedicatedServerModInitializer { - public static final Logger LOGGER = LogUtils.getLogger(); - @Override - public void onInitializeServer() { - if (!Utils.ENABLED) return; + private static final Logger log = LogUtils.getLogger(); - MutableInt ticks = new MutableInt(0); - ServerTickEvents.END_SERVER_TICK.register(server -> ticks.add(1)); + @Override + public void onInitializeServer() { + if (!Utils.ENABLED) return; - ServerLifecycleEvents.SERVER_STARTING.register(server -> { - var thread = new Thread(() -> { - try { - HandyServer.LOGGER.info("Started server test."); - ServerTestContext context = new ServerTestContext(ticks::intValue, server); - context.waitForOverworldTicks(200); + ServerLifecycleEvents.SERVER_STARTING.register(server -> { + var thread = new Thread(() -> { + try { + log.info("Started server test."); + ServerTestContext context = new ServerTestContext(server); + context.waitForOverworldTicks(200); - FabricLoader.getInstance().invokeEntrypoints("handy:server_test", ServerTestEntrypoint.class, e -> e.onServerTest(context)); - MixinEnvironment.getCurrentEnvironment().audit(); + FabricLoader.getInstance() + .invokeEntrypoints( + "handy:server_test", ServerTestEntrypoint.class, e -> e.onServerTest(context)); + MixinEnvironment.getCurrentEnvironment().audit(); - server.stop(false); - } catch (Throwable t) { - t.printStackTrace(); - System.exit(1); - } - }); - thread.start(); - }); - } + Utils.runChecks(); + server.stop(false); + } catch (Throwable t) { + log.error("Failed server test!", t); + System.exit(1); + } + }); + thread.start(); + }); + } } diff --git a/src/main/java/me/melontini/handytests/server/ServerTestContext.java b/src/main/java/me/melontini/handytests/server/ServerTestContext.java index f4f55db..ae8c2ff 100644 --- a/src/main/java/me/melontini/handytests/server/ServerTestContext.java +++ b/src/main/java/me/melontini/handytests/server/ServerTestContext.java @@ -1,65 +1,66 @@ package me.melontini.handytests.server; - -import me.melontini.handytests.util.TestContext; -import net.minecraft.server.MinecraftServer; - import java.time.Duration; import java.time.LocalDateTime; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import java.util.function.IntSupplier; import java.util.function.Predicate; +import me.melontini.handytests.util.TestContext; +import net.minecraft.server.MinecraftServer; -public record ServerTestContext(IntSupplier ticks, MinecraftServer server) implements TestContext { +public record ServerTestContext(MinecraftServer server) implements TestContext { - public void sendCommand(String command) { - submitAndWait(server -> server.getCommandManager().executeWithPrefix(server.getCommandSource(), command)); - } + public void sendCommand(String command) { + submitAndWait( + server -> server.getCommandManager().executeWithPrefix(server.getCommandSource(), command)); + } - public void waitForOverworldTicks(long ticks) { - waitFor("Overworld load", server -> server.getOverworld() != null, Duration.ofMinutes(30)); - final long startTicks = submitAndWait(server -> server.getOverworld().getTime()); - waitFor("Overworld load", server -> Objects.requireNonNull(server.getOverworld()).getTime() > startTicks + ticks, Duration.ofMinutes(10)); - } + public void waitForOverworldTicks(long ticks) { + waitFor("Overworld load", server -> server.getOverworld() != null, Duration.ofMinutes(30)); + final long startTicks = submitAndWait(server -> server.getOverworld().getTime()); + waitFor( + "Overworld load", + server -> Objects.requireNonNull(server.getOverworld()).getTime() > startTicks + ticks, + Duration.ofMinutes(10)); + } - public void waitFor(String what, Predicate predicate, Duration timeout) { - final LocalDateTime end = LocalDateTime.now().plus(timeout); + public void waitFor(String what, Predicate predicate, Duration timeout) { + final LocalDateTime end = LocalDateTime.now().plus(timeout); - while (true) { - boolean result = submitAndWait(predicate::test); + while (true) { + boolean result = submitAndWait(predicate::test); - if (result) { - break; - } + if (result) { + break; + } - if (LocalDateTime.now().isAfter(end)) { - throw new RuntimeException("Timed out waiting for " + what); - } + if (LocalDateTime.now().isAfter(end)) { + throw new RuntimeException("Timed out waiting for " + what); + } - waitFor(Duration.ofSeconds(1)); - } + waitFor(Duration.ofSeconds(1)); } + } - public T submitAndWait(Function function) { - return submit(function).join(); - } + public T submitAndWait(Function function) { + return submit(function).join(); + } - @Override - public MinecraftServer context() { - return server(); - } + @Override + public MinecraftServer context() { + return server(); + } - static void waitFor(Duration duration) { - try { - Thread.sleep(duration.toMillis()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + static void waitFor(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + throw new RuntimeException(e); } + } - CompletableFuture submit(Function function) { - return server.submit(() -> function.apply(server)); - } + CompletableFuture submit(Function function) { + return server.submit(() -> function.apply(server)); + } } diff --git a/src/main/java/me/melontini/handytests/server/ServerTestEntrypoint.java b/src/main/java/me/melontini/handytests/server/ServerTestEntrypoint.java index 9b2cd7d..5b5bcb9 100644 --- a/src/main/java/me/melontini/handytests/server/ServerTestEntrypoint.java +++ b/src/main/java/me/melontini/handytests/server/ServerTestEntrypoint.java @@ -1,5 +1,7 @@ package me.melontini.handytests.server; public interface ServerTestEntrypoint { - void onServerTest(ServerTestContext context); + default void onServerTest(ServerTestContext context) { + context.runAllForEntrypoint(this); + } } diff --git a/src/main/java/me/melontini/handytests/util/TestContext.java b/src/main/java/me/melontini/handytests/util/TestContext.java index 54b565e..6a1f8f8 100644 --- a/src/main/java/me/melontini/handytests/util/TestContext.java +++ b/src/main/java/me/melontini/handytests/util/TestContext.java @@ -3,14 +3,25 @@ import java.time.Duration; import java.util.function.Function; import java.util.function.Predicate; +import me.melontini.handytests.util.runner.TestRunner; public interface TestContext { - default void waitFor(String what, Predicate predicate) { - this.waitFor(what, predicate, Duration.ofSeconds(10)); - } - void waitFor(String what, Predicate predicate, Duration timeout); - T submitAndWait(Function function); + default void waitFor(String what, Predicate predicate) { + this.waitFor(what, predicate, Duration.ofSeconds(10)); + } - C context(); + void waitFor(String what, Predicate predicate, Duration timeout); + + T submitAndWait(Function function); + + default void addLateCheck(String description, Runnable check) { + Utils.addLateCheck(description, check); + } + + default void runAllForEntrypoint(Object entryponit) { + TestRunner.runTests(entryponit, this); + } + + C context(); } diff --git a/src/main/java/me/melontini/handytests/util/Utils.java b/src/main/java/me/melontini/handytests/util/Utils.java index d5079a3..9d3d641 100644 --- a/src/main/java/me/melontini/handytests/util/Utils.java +++ b/src/main/java/me/melontini/handytests/util/Utils.java @@ -1,5 +1,44 @@ package me.melontini.handytests.util; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + public class Utils { - public static final boolean ENABLED = System.getProperty("handy-tests.auto-test") != null; + + public static final boolean ENABLED = System.getProperty("handy-tests.auto-test") != null; + + private static final AtomicBoolean CRASHED = new AtomicBoolean(); + private static Set CHECKS = new LinkedHashSet<>(); + + public static void runChecks() { + if (!ENABLED) return; + + Set checks; + + synchronized (Utils.class) { + checks = CHECKS; + CHECKS = null; + } + + for (CheckEntry check : checks) { + try { + check.check().run(); + } catch (Throwable throwable) { + throw new IllegalStateException( + "Failed late check: %s".formatted(check.description()), throwable); + } + } + } + + public static synchronized void addLateCheck(String description, Runnable check) { + if (CHECKS == null) throw new IllegalStateException("Game is shutting down!"); + CHECKS.add(new CheckEntry(description, check)); + } + + public static void markCrashed() { + CRASHED.set(true); + } + + record CheckEntry(String description, Runnable check) {} } diff --git a/src/main/java/me/melontini/handytests/util/runner/HandyTest.java b/src/main/java/me/melontini/handytests/util/runner/HandyTest.java new file mode 100644 index 0000000..b5d21c4 --- /dev/null +++ b/src/main/java/me/melontini/handytests/util/runner/HandyTest.java @@ -0,0 +1,12 @@ +package me.melontini.handytests.util.runner; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface HandyTest { + int priority() default 1000; +} diff --git a/src/main/java/me/melontini/handytests/util/runner/TestRunner.java b/src/main/java/me/melontini/handytests/util/runner/TestRunner.java new file mode 100644 index 0000000..ed32110 --- /dev/null +++ b/src/main/java/me/melontini/handytests/util/runner/TestRunner.java @@ -0,0 +1,55 @@ +package me.melontini.handytests.util.runner; + +import com.mojang.logging.LogUtils; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import me.melontini.handytests.util.TestContext; +import org.slf4j.Logger; + +public class TestRunner { + + private static final Logger log = LogUtils.getLogger(); + + public static > void runTests(Object entrypoint, C context) { + List tests = new ArrayList<>(); + for (Method method : entrypoint.getClass().getDeclaredMethods()) { + if (!method.isAnnotationPresent(HandyTest.class)) continue; + if (Modifier.isStatic(method.getModifiers())) + throw new IllegalStateException("static @HandyTest method %s".formatted(method.getName())); + + verifyParams(method.getParameterTypes(), context.getClass()); + method.setAccessible(true); + tests.add(method); + } + tests.sort( + Comparator.comparingInt(value -> value.getAnnotation(HandyTest.class).priority())); + + log.info("Running {} {} tests...", tests.size(), entrypoint.getClass().getName()); + for (int i = 0; i < tests.size(); i++) { + Method test = tests.get(i); + try { + test.invoke(entrypoint, context); + log.info( + "Completed {}#{} [{}/{}]...", + entrypoint.getClass().getName(), + test.getName(), + i, + tests.size()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException( + "Test %s#%s failed!".formatted(entrypoint.getClass().getName(), test.getName()), + e instanceof InvocationTargetException ite ? ite.getCause() : e); + } + } + } + + private static > void verifyParams(Class[] params, Class cls) { + if (params.length != 1 || params[0] != cls) + throw new IllegalStateException( + "Parameters must only contain the %s!".formatted(cls.getName())); + } +} From bd110eb31b657ed61aa556a3f018195bccfb1a8a Mon Sep 17 00:00:00 2001 From: melontini <104443436+melontini@users.noreply.github.com> Date: Sun, 28 Jul 2024 01:05:06 +0700 Subject: [PATCH 2/3] [ci skip] Use test action in build_pr.yml --- .github/workflows/build_pr.yml | 49 +++++----------------------------- 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index 1918863..76bd638 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -22,46 +22,9 @@ jobs: run: | ./gradlew build - client_test: - needs: build - runs-on: ubuntu-latest - steps: - - name: checkout repository - uses: actions/checkout@v4.1.1 - - name: validate gradle wrapper - uses: gradle/wrapper-validation-action@v2.1.1 - - name: setup jdk 17 - uses: actions/setup-java@v4.1.0 - with: - distribution: 'temurin' - java-version: 17 - cache: gradle - - name: make gradle wrapper executable - run: chmod +x ./gradlew - - name: run testmod - uses: modmuss50/xvfb-action@v1 - with: - run: ./gradlew runClient --stacktrace --warning-mode=fail - #- uses: actions/upload-artifact@v4.3.1 - # with: - # name: Test Screenshots - # path: run/screenshots - - server_test: - needs: build - runs-on: ubuntu-latest - steps: - - name: checkout repository - uses: actions/checkout@v4.1.1 - - name: validate gradle wrapper - uses: gradle/wrapper-validation-action@v2.1.1 - - name: setup jdk 17 - uses: actions/setup-java@v4.1.0 - with: - distribution: 'temurin' - java-version: 17 - cache: gradle - - name: make gradle wrapper executable - run: chmod +x ./gradlew - - name: run testmod - run: ./gradlew runServer --stacktrace --warning-mode=fail \ No newline at end of file + run_tests: + uses: constellation-mc/actions/.github/workflows/mc-tests.yml@main + with: + java: 17 + client_task: runClient + server_task: runServer \ No newline at end of file From 2cf038a9ea730c8d0161dcdd4f900e45ce68beb8 Mon Sep 17 00:00:00 2001 From: melontini <104443436+melontini@users.noreply.github.com> Date: Sun, 28 Jul 2024 01:48:05 +0700 Subject: [PATCH 3/3] Minor fixes. --- .../java/me/melontini/handytests/util/runner/TestRunner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/melontini/handytests/util/runner/TestRunner.java b/src/main/java/me/melontini/handytests/util/runner/TestRunner.java index ed32110..e568530 100644 --- a/src/main/java/me/melontini/handytests/util/runner/TestRunner.java +++ b/src/main/java/me/melontini/handytests/util/runner/TestRunner.java @@ -25,6 +25,7 @@ public static > void runTests(Object entrypoint, C cont method.setAccessible(true); tests.add(method); } + tests.sort(Comparator.comparing(Method::getName)); tests.sort( Comparator.comparingInt(value -> value.getAnnotation(HandyTest.class).priority())); @@ -37,7 +38,7 @@ public static > void runTests(Object entrypoint, C cont "Completed {}#{} [{}/{}]...", entrypoint.getClass().getName(), test.getName(), - i, + i + 1, tests.size()); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(