1
0
forked from Clones/Controlify

test framework

This commit is contained in:
isXander
2023-02-27 19:48:24 +00:00
parent 0d5307ba43
commit caacdf3aad
12 changed files with 411 additions and 8 deletions

View File

@ -48,3 +48,25 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
path: build/libs/*.jar 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

View File

@ -31,8 +31,26 @@ repositories {
maven("https://maven.flashyreese.me/snapshots") maven("https://maven.flashyreese.me/snapshots")
} }
val testmod by sourceSets.registering {
compileClasspath += sourceSets.main.get().compileClasspath
runtimeClasspath += sourceSets.main.get().runtimeClasspath
}
loom { loom {
accessWidenerPath.set(file("src/main/resources/controlify.accesswidener")) 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() val minecraftVersion = libs.versions.minecraft.get()
@ -73,6 +91,9 @@ dependencies {
modImplementation(files("libs/iris-5d0efad3.jar")) modImplementation(files("libs/iris-5d0efad3.jar"))
modRuntimeOnly("org.anarres:jcpp:1.4.14") modRuntimeOnly("org.anarres:jcpp:1.4.14")
modRuntimeOnly("io.github.douira:glsl-transformer:2.0.0-pre9") modRuntimeOnly("io.github.douira:glsl-transformer:2.0.0-pre9")
// testmod
"testmodImplementation"(sourceSets.main.get().output)
} }
tasks { tasks {

View File

@ -36,7 +36,7 @@ public class Controlify implements ControlifyApi {
private InGameInputHandler inGameInputHandler; private InGameInputHandler inGameInputHandler;
public InGameButtonGuide inGameButtonGuide; public InGameButtonGuide inGameButtonGuide;
private VirtualMouseHandler virtualMouseHandler; private VirtualMouseHandler virtualMouseHandler;
private InputMode currentInputMode; private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE;
private ControllerHIDService controllerHIDService; private ControllerHIDService controllerHIDService;
private final ControlifyConfig config = new ControlifyConfig(this); private final ControlifyConfig config = new ControlifyConfig(this);
@ -290,9 +290,4 @@ public class Controlify implements ControlifyApi {
if (instance == null) instance = new Controlify(); if (instance == null) instance = new Controlify();
return instance; return instance;
} }
@Override
public @NotNull ControlifyBindingsApi bindingsApi() {
return ControllerBindings.Api.INSTANCE;
}
} }

View File

@ -19,8 +19,6 @@ public interface ControlifyApi {
@NotNull InputMode currentInputMode(); @NotNull InputMode currentInputMode();
void setInputMode(@NotNull InputMode mode); void setInputMode(@NotNull InputMode mode);
@NotNull ControlifyBindingsApi bindingsApi();
static ControlifyApi get() { static ControlifyApi get() {
return Controlify.instance(); return Controlify.instance();
} }

View File

@ -1,6 +1,7 @@
package dev.isxander.controlify.api.bind; package dev.isxander.controlify.api.bind;
import dev.isxander.controlify.bindings.BindingSupplier; import dev.isxander.controlify.bindings.BindingSupplier;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.bindings.GamepadBinds; import dev.isxander.controlify.bindings.GamepadBinds;
import net.minecraft.client.KeyMapping; import net.minecraft.client.KeyMapping;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -29,4 +30,8 @@ public interface ControlifyBindingsApi {
* @return the binding supplier to fetch the binding for a specific controller. * @return the binding supplier to fetch the binding for a specific controller.
*/ */
BindingSupplier registerBind(GamepadBinds bind, ResourceLocation id, KeyMapping override, BooleanSupplier toggleOverride); BindingSupplier registerBind(GamepadBinds bind, ResourceLocation id, KeyMapping override, BooleanSupplier toggleOverride);
static ControlifyBindingsApi get() {
return ControllerBindings.Api.INSTANCE;
}
} }

View File

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

View File

@ -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<Minecraft> 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 <T> CompletableFuture<T> submit(Function<Minecraft, T> function) {
return Minecraft.getInstance().submit(() -> function.apply(Minecraft.getInstance()));
}
public static <T> T submitAndWait(Function<Minecraft, T> function) {
return submit(function).join();
}
public static Controller<?, ?> createFakeController() {
return new Controller<>() {
private final ControllerBindings<ControllerState> 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<ControllerState> 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() {
}
};
}
}

View File

@ -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<Test> preLoadTests = Lists.newArrayList();
List<Test> 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;
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
{
"package": "dev.isxander.controlify.test.mixins",
"required": true,
"minVersion": "0.8",
"compatibilityLevel": "JAVA_17",
"mixins": [
]
}

View File

@ -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": "*"
}
}