From caacdf3aadf28b0942d05d7ab04c5e320b708038 Mon Sep 17 00:00:00 2001 From: isXander Date: Mon, 27 Feb 2023 19:48:24 +0000 Subject: [PATCH] test framework --- .github/workflows/gradle.yml | 22 +++ build.gradle.kts | 21 +++ .../dev/isxander/controlify/Controlify.java | 7 +- .../controlify/api/ControlifyApi.java | 2 - .../api/bind/ControlifyBindingsApi.java | 5 + .../isxander/controlify/test/Assertions.java | 45 ++++++ .../controlify/test/ClientTestHelper.java | 132 ++++++++++++++++++ .../test/ControlifyAutoTestClient.java | 93 ++++++++++++ .../controlify/test/ControlifyTests.java | 45 ++++++ .../dev/isxander/controlify/test/Test.java | 20 +++ .../resources/controlify-test.mixins.json | 8 ++ src/testmod/resources/fabric.mod.json | 19 +++ 12 files changed, 411 insertions(+), 8 deletions(-) create mode 100644 src/testmod/java/dev/isxander/controlify/test/Assertions.java create mode 100644 src/testmod/java/dev/isxander/controlify/test/ClientTestHelper.java create mode 100644 src/testmod/java/dev/isxander/controlify/test/ControlifyAutoTestClient.java create mode 100644 src/testmod/java/dev/isxander/controlify/test/ControlifyTests.java create mode 100644 src/testmod/java/dev/isxander/controlify/test/Test.java create mode 100644 src/testmod/resources/controlify-test.mixins.json create mode 100644 src/testmod/resources/fabric.mod.json diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 43d638f..98a62fa 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -48,3 +48,25 @@ jobs: - uses: actions/upload-artifact@v3 with: path: build/libs/*.jar + + client_test: + runs-on: ubuntu-latest + name: In-game test + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-java@v3 + with: + distribution: microsoft + java-version: 17 + - name: Run auto test client + uses: modmuss50/xvfb-action@v1 + with: + run: ./gradlew runTestmod --stracktrace --warning-mode=fail --no-daemon + - name: Upload test screenshots + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-screenshots + path: run/screenshots diff --git a/build.gradle.kts b/build.gradle.kts index 57db546..d81dc4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,8 +31,26 @@ repositories { maven("https://maven.flashyreese.me/snapshots") } +val testmod by sourceSets.registering { + compileClasspath += sourceSets.main.get().compileClasspath + runtimeClasspath += sourceSets.main.get().runtimeClasspath +} + loom { accessWidenerPath.set(file("src/main/resources/controlify.accesswidener")) + + runs { + register("testmod") { + client() + ideConfigGenerated(true) + name("Test Mod") + source(testmod.get()) + } + + named("server") { ideConfigGenerated(false) } + } + + createRemapConfigurations(testmod.get()) } val minecraftVersion = libs.versions.minecraft.get() @@ -73,6 +91,9 @@ dependencies { modImplementation(files("libs/iris-5d0efad3.jar")) modRuntimeOnly("org.anarres:jcpp:1.4.14") modRuntimeOnly("io.github.douira:glsl-transformer:2.0.0-pre9") + + // testmod + "testmodImplementation"(sourceSets.main.get().output) } tasks { diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index d8a6bce..22bf635 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -36,7 +36,7 @@ public class Controlify implements ControlifyApi { private InGameInputHandler inGameInputHandler; public InGameButtonGuide inGameButtonGuide; private VirtualMouseHandler virtualMouseHandler; - private InputMode currentInputMode; + private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE; private ControllerHIDService controllerHIDService; private final ControlifyConfig config = new ControlifyConfig(this); @@ -290,9 +290,4 @@ public class Controlify implements ControlifyApi { if (instance == null) instance = new Controlify(); return instance; } - - @Override - public @NotNull ControlifyBindingsApi bindingsApi() { - return ControllerBindings.Api.INSTANCE; - } } diff --git a/src/main/java/dev/isxander/controlify/api/ControlifyApi.java b/src/main/java/dev/isxander/controlify/api/ControlifyApi.java index 360135e..d68289c 100644 --- a/src/main/java/dev/isxander/controlify/api/ControlifyApi.java +++ b/src/main/java/dev/isxander/controlify/api/ControlifyApi.java @@ -19,8 +19,6 @@ public interface ControlifyApi { @NotNull InputMode currentInputMode(); void setInputMode(@NotNull InputMode mode); - @NotNull ControlifyBindingsApi bindingsApi(); - static ControlifyApi get() { return Controlify.instance(); } diff --git a/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindingsApi.java b/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindingsApi.java index ab6f800..7492ec8 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindingsApi.java +++ b/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindingsApi.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.api.bind; import dev.isxander.controlify.bindings.BindingSupplier; +import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.bindings.GamepadBinds; import net.minecraft.client.KeyMapping; import net.minecraft.resources.ResourceLocation; @@ -29,4 +30,8 @@ public interface ControlifyBindingsApi { * @return the binding supplier to fetch the binding for a specific controller. */ BindingSupplier registerBind(GamepadBinds bind, ResourceLocation id, KeyMapping override, BooleanSupplier toggleOverride); + + static ControlifyBindingsApi get() { + return ControllerBindings.Api.INSTANCE; + } } diff --git a/src/testmod/java/dev/isxander/controlify/test/Assertions.java b/src/testmod/java/dev/isxander/controlify/test/Assertions.java new file mode 100644 index 0000000..f770d47 --- /dev/null +++ b/src/testmod/java/dev/isxander/controlify/test/Assertions.java @@ -0,0 +1,45 @@ +package dev.isxander.controlify.test; + +public class Assertions { + public static void assertEquals(Object expected, Object actual) { + assertTrue(expected.equals(actual), "Expected " + expected + " but got " + actual); + } + + public static void assertEquals(Object expected, Object actual, String message) { + assertTrue(expected.equals(actual), message); + } + + public static void assertNotNull(Object object) { + assertTrue(object != null, "Expected object to not be null"); + } + + public static void assertNotNull(Object object, String message) { + assertTrue(object != null, message); + } + + public static void assertNull(Object object) { + assertTrue(object == null, "Expected object to be null"); + } + + public static void assertNull(Object object, String message) { + assertTrue(object == null, message); + } + + public static void assertTrue(boolean condition) { + assertTrue(condition, "Condition not met"); + } + + public static void assertTrue(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } + + public static void assertFalse(boolean condition) { + assertTrue(!condition, "Condition not met"); + } + + public static void assertFalse(boolean condition, String message) { + assertTrue(!condition, message); + } +} diff --git a/src/testmod/java/dev/isxander/controlify/test/ClientTestHelper.java b/src/testmod/java/dev/isxander/controlify/test/ClientTestHelper.java new file mode 100644 index 0000000..1830970 --- /dev/null +++ b/src/testmod/java/dev/isxander/controlify/test/ClientTestHelper.java @@ -0,0 +1,132 @@ +package dev.isxander.controlify.test; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import dev.isxander.controlify.bindings.ControllerBindings; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerConfig; +import dev.isxander.controlify.controller.ControllerState; +import dev.isxander.controlify.controller.ControllerType; +import net.minecraft.client.Minecraft; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Predicate; + +public class ClientTestHelper { + public static void waitForLoadingComplete() { + waitFor("Loading to complete", client -> client.getOverlay() == null, Duration.ofMinutes(5)); + } + + public static void waitFor(String what, Predicate condition, Duration timeout) { + final LocalDateTime end = LocalDateTime.now().plus(timeout); + + while (true) { + boolean result = submitAndWait(condition::test); + + if (result) break; + + if (LocalDateTime.now().isAfter(end)) { + throw new RuntimeException("Timed out waiting for " + what + " to complete. (timeout: " + timeout + ")"); + } + + waitFor(Duration.ofSeconds(1)); + } + } + + public static void waitFor(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private static CompletableFuture submit(Function function) { + return Minecraft.getInstance().submit(() -> function.apply(Minecraft.getInstance())); + } + + public static T submitAndWait(Function function) { + return submit(function).join(); + } + + public static Controller createFakeController() { + return new Controller<>() { + private final ControllerBindings bindings = new ControllerBindings<>(this); + private final ControllerConfig config = new ControllerConfig() { + @Override + public void setDeadzone(int axis, float deadzone) { + + } + + @Override + public float getDeadzone(int axis) { + return 0; + } + }; + + @Override + public String uid() { + return "FAKE"; + } + + @Override + public String guid() { + return "FAKE"; + } + + @Override + public ControllerBindings bindings() { + return bindings; + } + + @Override + public ControllerConfig config() { + return config; + } + + @Override + public ControllerConfig defaultConfig() { + return config; + } + + @Override + public void resetConfig() { + + } + + @Override + public void setConfig(Gson gson, JsonElement json) { + + } + + @Override + public ControllerType type() { + return ControllerType.UNKNOWN; + } + + @Override + public String name() { + return "FAKE CONTROLLER"; + } + + @Override + public ControllerState state() { + return ControllerState.EMPTY; + } + + @Override + public ControllerState prevState() { + return ControllerState.EMPTY; + } + + @Override + public void updateState() { + + } + }; + } + +} diff --git a/src/testmod/java/dev/isxander/controlify/test/ControlifyAutoTestClient.java b/src/testmod/java/dev/isxander/controlify/test/ControlifyAutoTestClient.java new file mode 100644 index 0000000..343d6e2 --- /dev/null +++ b/src/testmod/java/dev/isxander/controlify/test/ControlifyAutoTestClient.java @@ -0,0 +1,93 @@ +package dev.isxander.controlify.test; + +import com.google.common.collect.Lists; +import net.fabricmc.api.ClientModInitializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.List; +import static dev.isxander.controlify.test.ClientTestHelper.*; + +public class ControlifyAutoTestClient implements ClientModInitializer { + private static final Logger LOGGER = LoggerFactory.getLogger("Controlify Auto Test"); + + @Override + public void onInitializeClient() { + var thread = new Thread(() -> { + try { + runTests(ControlifyTests.class); + } catch (Throwable t) { + t.printStackTrace(); + System.exit(1); + } + }); + thread.setName("Controlify Auto Test Thread"); + thread.start(); + } + + private void runTests(Class testClass) throws Exception { + List preLoadTests = Lists.newArrayList(); + List postLoadTests = Lists.newArrayList(); + + Object testObject = testClass.getConstructor().newInstance(); + + Method[] methods = testClass.getDeclaredMethods(); + for (var method : methods) { + method.setAccessible(true); + + Test.PreLoad preLoad = method.getAnnotation(Test.PreLoad.class); + if (preLoad != null) { + if (method.getParameterCount() > 0) + throw new RuntimeException("PreLoad test method " + method.getName() + " has parameters!"); + + preLoadTests.add(new Test(() -> { + try { + method.invoke(testObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, preLoad.value())); + } + + Test.PostLoad postLoad = method.getAnnotation(Test.PostLoad.class); + if (postLoad != null) { + if (method.getParameterCount() > 0) + throw new RuntimeException("PostLoad test method " + method.getName() + " has parameters!"); + + postLoadTests.add(new Test(() -> { + try { + method.invoke(testObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, postLoad.value())); + } + } + + boolean success = true; + for (var test : preLoadTests) { + success &= wrapTest(test); + } + + waitForLoadingComplete(); + + for (var test : postLoadTests) { + success &= wrapTest(test); + } + + System.exit(success ? 0 : 1); + } + + private boolean wrapTest(Test test) { + LOGGER.info("\u001b[36mRunning test " + test.name() + "..."); + try { + test.method().run(); + LOGGER.info("\u001b[32mPassed test " + test.name() + "!"); + return true; + } catch (Throwable t) { + LOGGER.error("\u001b[31mFailed test " + test.name() + "!", t); + return false; + } + } +} diff --git a/src/testmod/java/dev/isxander/controlify/test/ControlifyTests.java b/src/testmod/java/dev/isxander/controlify/test/ControlifyTests.java new file mode 100644 index 0000000..eccb4dc --- /dev/null +++ b/src/testmod/java/dev/isxander/controlify/test/ControlifyTests.java @@ -0,0 +1,45 @@ +package dev.isxander.controlify.test; + +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.api.bind.ControlifyBindingsApi; +import dev.isxander.controlify.api.event.ControlifyEvents; +import dev.isxander.controlify.bindings.BindingSupplier; +import dev.isxander.controlify.bindings.GamepadBinds; +import net.minecraft.resources.ResourceLocation; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static dev.isxander.controlify.test.Assertions.*; +import static dev.isxander.controlify.test.ClientTestHelper.*; + +public class ControlifyTests { + BindingSupplier binding = null; + + @Test.PreLoad("Binding registry test") + void bindingRegistryTest() { + var registry = ControlifyBindingsApi.get(); + assertNotNull(registry, "Binding registry is null"); + binding = registry.registerBind(GamepadBinds.A_BUTTON, new ResourceLocation("controlify", "test_bind")); + assertNotNull(binding, "Bind registry failed - BindingSupplier is null"); + } + + @Test.PostLoad("BindingSupplier getter test") + void bindingSupplierGetterTest() { + var controller = createFakeController(); + assertNotNull(binding.get(controller), "Bind registry failed - Bind for fake controller is null"); + } + + @Test.PostLoad("Input mode changed event test") + void checkInputModeChangedEvent() { + var api = ControlifyApi.get(); + + AtomicBoolean called = new AtomicBoolean(false); + + ControlifyEvents.INPUT_MODE_CHANGED.register(mode -> called.set(true)); + api.setInputMode(InputMode.CONTROLLER); + api.setInputMode(InputMode.KEYBOARD_MOUSE); + + assertTrue(called.get(), "Input mode changed event was not called"); + } +} diff --git a/src/testmod/java/dev/isxander/controlify/test/Test.java b/src/testmod/java/dev/isxander/controlify/test/Test.java new file mode 100644 index 0000000..b4a088c --- /dev/null +++ b/src/testmod/java/dev/isxander/controlify/test/Test.java @@ -0,0 +1,20 @@ +package dev.isxander.controlify.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public record Test(Runnable method, String name) { + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface PreLoad { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface PostLoad { + String value(); + } +} diff --git a/src/testmod/resources/controlify-test.mixins.json b/src/testmod/resources/controlify-test.mixins.json new file mode 100644 index 0000000..225dab3 --- /dev/null +++ b/src/testmod/resources/controlify-test.mixins.json @@ -0,0 +1,8 @@ +{ + "package": "dev.isxander.controlify.test.mixins", + "required": true, + "minVersion": "0.8", + "compatibilityLevel": "JAVA_17", + "mixins": [ + ] +} diff --git a/src/testmod/resources/fabric.mod.json b/src/testmod/resources/fabric.mod.json new file mode 100644 index 0000000..357dbc8 --- /dev/null +++ b/src/testmod/resources/fabric.mod.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "id": "controlify-test-client", + "version": "0", + "name": "Controlify Test Client", + "description": "Auto test module for Controlify.", + "environment": "client", + "entrypoints": { + "client": [ + "dev.isxander.controlify.test.ControlifyAutoTestClient" + ] + }, + "mixins": [ + "controlify-test.mixins.json" + ], + "depends": { + "controlify": "*" + } +}