Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start work on Client Game Test API #4292

Merged
merged 15 commits into from
Dec 16, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Address review comments
Earthcomputer committed Dec 12, 2024
commit beb47a069ea5461bdd0bf50b349acfa8d2e740b8
Original file line number Diff line number Diff line change
@@ -16,7 +16,9 @@

package net.fabricmc.fabric.api.client.gametest.v1;

import java.nio.file.Path;
import java.util.function.Predicate;
import java.util.function.Supplier;

import org.apache.commons.lang3.function.FailableConsumer;
import org.apache.commons.lang3.function.FailableFunction;
@@ -85,7 +87,7 @@ public interface ClientGameTestContext {
* @param screen The screen to open
* @see MinecraftClient#setScreen(Screen)
*/
void setScreen(@Nullable Screen screen);
void setScreen(Supplier<@Nullable Screen> screen);

/**
* Presses the button in the current screen whose label is the given translation key. Fails if the button couldn't
@@ -109,15 +111,15 @@ public interface ClientGameTestContext {
*
* @param name The name of the screenshot
*/
void takeScreenshot(String name);
Path takeScreenshot(String name);

/**
* Takes a screnshot after waiting {@code delay} ticks and saves it in the screenshots directory.
*
* @param name The name of the screenshot
* @param delay The delay in ticks before taking the screenshot
*/
void takeScreenshot(String name, int delay);
Path takeScreenshot(String name, int delay);

/**
* Gets the input handler used to simulate inputs to the client.
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
package net.fabricmc.fabric.api.client.gametest.v1;

/**
* The client game test entrypoint interface. See the package documentation.
* The {@code fabric-client-gametest} entrypoint interface. See the package documentation.
*/
public interface FabricClientGameTest {
/**
Original file line number Diff line number Diff line change
@@ -16,10 +16,12 @@

package net.fabricmc.fabric.impl.client.gametest;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

import com.google.common.base.Preconditions;
import org.apache.commons.lang3.function.FailableConsumer;
@@ -159,9 +161,9 @@ public void waitForScreen(@Nullable Class<? extends Screen> screenClass) {
}

@Override
public void setScreen(@Nullable Screen screen) {
public void setScreen(Supplier<@Nullable Screen> screen) {
ThreadingImpl.checkOnGametestThread("setScreen");
runOnClient(client -> client.setScreen(screen));
runOnClient(client -> client.setScreen(screen.get()));
}

@Override
@@ -239,14 +241,14 @@ private static boolean pressMatchingButton(ClickableWidget widget, String text)
}

@Override
public void takeScreenshot(String name) {
public Path takeScreenshot(String name) {
ThreadingImpl.checkOnGametestThread("takeScreenshot");
Preconditions.checkNotNull(name, "name");
takeScreenshot(name, 1);
return takeScreenshot(name, 1);
}

@Override
public void takeScreenshot(String name, int delay) {
public Path takeScreenshot(String name, int delay) {
ThreadingImpl.checkOnGametestThread("takeScreenshot");
Preconditions.checkNotNull(name, "name");
Preconditions.checkArgument(delay >= 0, "delay cannot be negative");
@@ -256,6 +258,8 @@ public void takeScreenshot(String name, int delay) {
ScreenshotRecorder.saveScreenshot(FabricLoader.getInstance().getGameDir().toFile(), name + ".png", client.getFramebuffer(), (message) -> {
});
});

return FabricLoader.getInstance().getGameDir().resolve("screenshots").resolve(name + ".png");
}

@Override
Original file line number Diff line number Diff line change
@@ -18,17 +18,14 @@

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.TitleScreen;

import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.loader.api.FabricLoader;

public class FabricClientGameTestRunner {
private static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-gametest-api-v1");
private static final String ENTRYPOINT_KEY = "fabric-client-gametest";

public static void start() {
@@ -39,31 +36,35 @@ public static void start() {

ThreadingImpl.runTestThread(() -> {
ClientGameTestContextImpl context = new ClientGameTestContextImpl();
boolean failed = false;

for (FabricClientGameTest gameTest : gameTests) {
context.restoreDefaultGameOptions();
Earthcomputer marked this conversation as resolved.
Show resolved Hide resolved

try {
gameTest.runTest(context);
} catch (Throwable e) {
LOGGER.error("Failed test {}", gameTest.getClass().getName(), e);
failed = true;
} finally {
context.getInput().clearKeysDown();

// Open the title screen to reset the state for the next gametest.
// If the gametest API was used correctly, we should be in the menus somewhere because any test
// world should have been closed at the end of a try-with-resources statement.
context.setScreen(new TitleScreen());
checkFinalGameTestState(context, gameTest.getClass().getName());
}
}

if (failed) {
throw new AssertionError("There were failing client gametests");
context.clickScreenButton("menu.quit");
});
}

private static void checkFinalGameTestState(ClientGameTestContext context, String testClassName) {
if (ThreadingImpl.isServerRunning) {
throw new AssertionError("Client gametest %s finished while a server is still running".formatted(testClassName));
}

context.runOnClient(client -> {
if (client.getNetworkHandler() != null) {
throw new AssertionError("Client gametest %s finished while still connected to a server".formatted(testClassName));
}

context.clickScreenButton("menu.quit");
if (!(client.currentScreen instanceof TitleScreen)) {
throw new AssertionError("Client gametest %s did not finish on the title screen".formatted(testClassName));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -3,23 +3,21 @@
"package": "net.fabricmc.fabric.mixin.client.gametest",
"compatibilityLevel": "JAVA_21",
"mixins": [
Earthcomputer marked this conversation as resolved.
Show resolved Hide resolved
"MinecraftDedicatedServerMixin",
"MinecraftServerMixin"
],
"plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin",
"injectors": {
"defaultRequire": 1
},
"client": [
"CyclingButtonWidgetAccessor",
"GameOptionsAccessor",
"GameOptionsMixin",
"InputUtilMixin",
"KeyBindingAccessor",
"KeyboardAccessor",
"MinecraftClientMixin",
"MinecraftDedicatedServerMixin",
"MinecraftServerMixin",
"MouseAccessor",
"ScreenAccessor",
"WindowMixin"
]
],
"plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin",
"injectors": {
"defaultRequire": 1
}
}
Original file line number Diff line number Diff line change
@@ -103,11 +103,11 @@ public void runTest(ClientGameTestContext context) {
{
context.getInput().pressKey(options -> options.inventoryKey);
context.takeScreenshot("in_game_inventory");
context.setScreen(null);
context.setScreen(() -> null);
}

{
context.setScreen(new GameMenuScreen(true));
context.setScreen(() -> new GameMenuScreen(true));
context.takeScreenshot("game_menu");
context.clickScreenButton("menu.returnToMenu");
context.waitForScreen(TitleScreen.class);
@@ -133,7 +133,7 @@ public void runTest(ClientGameTestContext context) {
waitForWorldTicks(context, 1);
}

context.setScreen(new GameMenuScreen(true));
context.setScreen(() -> new GameMenuScreen(true));
context.takeScreenshot("server_game_menu");
context.clickScreenButton("menu.disconnect");