From 3f8ddfda535f505226579de46df9d78704f2281f Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Wed, 15 Jul 2026 09:28:47 +0200 Subject: [PATCH 1/6] feat(framework): introduce server/worlds/bots/events facets Add the four facet interfaces that carve the ILightkeeperFramework god-interface into cohesive slices, per the D2/D3 2.0 API restructure: - IServerControl: command execution, console output, platform, filesystem access, process lifecycle, tick counter, captured server errors. - IWorlds: main world, create (defaults/spec), template load, builder. - IBots: join synthetic players (with/without UUID), builder. - IEvents: capture Bukkit events (thin today; grows in S6/S7). These interfaces are the new canonical API surface. Wiring the accessors and routing implementations through them lands in the follow-up refactor commit. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P15FjGQzyNmX9T6Qmyq8Dn --- .../lightkeeper/framework/IBots.java | 42 ++++++ .../lightkeeper/framework/IEvents.java | 18 +++ .../lightkeeper/framework/IServerControl.java | 135 ++++++++++++++++++ .../lightkeeper/framework/IWorlds.java | 59 ++++++++ 4 files changed, 254 insertions(+) create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IBots.java create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IEvents.java create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IServerControl.java create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IWorlds.java diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IBots.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IBots.java new file mode 100644 index 00000000..46d59c83 --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IBots.java @@ -0,0 +1,42 @@ +package nl.pim16aap2.lightkeeper.framework; + +import java.util.UUID; + +/** + * Bots facet of the framework: join synthetic players into a world, or configure one through a builder. + * + *

Obtained from {@link ILightkeeperFramework#bots()}. + */ +public interface IBots +{ + /** + * Joins a synthetic player into a world at spawn. + * + * @param name + * Player name. + * @param world + * Target world. + * @return The joined player handle. + */ + PlayerHandle join(String name, WorldHandle world); + + /** + * Joins a synthetic player into a world at spawn. + * + * @param name + * Player name. + * @param uuid + * Player UUID. + * @param world + * Target world. + * @return The joined player handle. + */ + PlayerHandle join(String name, UUID uuid, WorldHandle world); + + /** + * Creates a player builder. + * + * @return A new player builder. + */ + IPlayerBuilder builder(); +} diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IEvents.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IEvents.java new file mode 100644 index 00000000..e2f61f2b --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IEvents.java @@ -0,0 +1,18 @@ +package nl.pim16aap2.lightkeeper.framework; + +/** + * Events facet of the framework: capture Bukkit events for later inspection. + * + *

Obtained from {@link ILightkeeperFramework#events()}. Thin today; it grows as the event-driven APIs expand. + */ +public interface IEvents +{ + /** + * Starts capturing Bukkit events of the specified type. + * + * @param eventClassName + * The full class name of the event to capture (e.g. "org.bukkit.event.player.PlayerMoveEvent"). + * @return A handle to manage the capture session. + */ + EventCaptureHandle capture(String eventClassName); +} diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IServerControl.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IServerControl.java new file mode 100644 index 00000000..b6735b39 --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IServerControl.java @@ -0,0 +1,135 @@ +package nl.pim16aap2.lightkeeper.framework; + +import nl.pim16aap2.lightkeeper.protocol.CommandSource; + +import java.nio.file.Path; +import java.util.List; + +/** + * Server-control facet of the framework: command execution, console output, platform, filesystem access, process + * lifecycle, tick counter, and captured server errors. + * + *

Obtained from {@link ILightkeeperFramework#server()}. + */ +public interface IServerControl +{ + /** + * Executes a command from the requested source. + * + * @param source + * The command source. + * @param command + * The command text. + * @return Command result. + */ + CommandResult executeCommand(CommandSource source, String command); + + /** + * Gets a snapshot of captured Minecraft server output lines. + * + * @return Captured server output lines ordered oldest-to-newest. + */ + List output(); + + /** + * Gets the server platform (e.g. PAPER, SPIGOT). + * + * @return Server platform. + */ + Platform platform(); + + /** + * Gets the Minecraft server's working directory. + * + *

Filesystem contract: reading is safe at any time; writing (e.g. seeding database files or patching + * plugin configurations) is only safe while the server is stopped — between {@link #stop()} and + * {@link #start()} — because the server reads most files once at boot and may overwrite them on shutdown. + * + * @return The server directory. + */ + Path directory(); + + /** + * Gets the data directory of a plugin inside the server's {@code plugins} directory. + * + *

The directory may not exist yet, e.g. before the plugin's first boot. The filesystem contract of + * {@link #directory()} applies. + * + * @param pluginName + * The plugin's name, as used for its data directory (usually the {@code name} from its + * {@code plugin.yml}). + * @return The plugin data directory. + */ + Path pluginDataDirectory(String pluginName); + + /** + * Crashes the Minecraft server immediately by force-killing the process. + * + *

All fixtures created before the crash are invalidated: player and world handles obtained earlier no + * longer refer to live server state. In shared-server mode the server must be started again via + * {@link #start()} or {@link #restart()} before the next test method runs, otherwise that method fails fast; + * annotate the test with {@code @FreshServer} to receive a new server per method instead. + */ + void crash(); + + /** + * Stops the Minecraft server gracefully via the console {@code stop} command, force-killing it only when it + * does not exit within the shutdown timeout. + * + *

Synthetic players are removed before the server shuts down so they quit cleanly. All fixtures created + * before the stop are invalidated, exactly as documented on {@link #crash()}. While the server is stopped, the + * server directory may be modified freely — see {@link #directory()}. + * + * @throws IllegalStateException + * If the server is not running. + */ + void stop(); + + /** + * Starts the Minecraft server after a {@link #stop()} or {@link #crash()} call. + * + *

Only worlds configured in the runtime manifest are preloaded again. Players and worlds created at runtime + * before the server went down are not recreated; tests must re-establish their own fixtures + * after starting. + * + * @throws IllegalStateException + * If the server is already running. + */ + void start(); + + /** + * Restarts the Minecraft server: a graceful {@link #stop()} when it is running, followed by {@link #start()}. + * + *

Also valid when the server is already down (after {@link #stop()} or {@link #crash()}), in which case it + * only starts the server. See {@link #start()} for what is — and is not — restored. + */ + void restart(); + + /** + * Gets the agent's monotonic tick counter, for correlating against {@link CapturedEventSnapshot#tick()} + * stamps. + * + *

This is an agent-relative counter (ticks since the agent enabled), not the server's absolute game tick: + * it resets on every server start, so values are only comparable within one server session. + * + * @return The monotonic, session-relative server tick. + */ + long currentTick(); + + /** + * Gets a handle over the always-on server-error capture. + * + *

The agent captures every WARN-or-worse log event inside the server as a structured snapshot — with the + * real throwable class, message, and cause chain — from the moment the agent plugin loads (before any + * plugin's {@code onEnable}). Failures inside the logging system itself (reported via Log4j's status logger) + * and stack traces written to the server process's raw stderr file descriptor are captured as well. + * + *

In shared-server mode the capture buffer is cleared automatically at the end of every test method, so + * each test observes only the errors of its own window; the first test's window also covers server boot. Known + * gaps: exceptions that are caught and never logged are invisible, as are log events emitted before the agent + * plugin loads (use {@link #output()} for pre-boot inspection). + * + * @return A handle exposing the captured server errors. + */ + ServerErrorsHandle errors(); +} diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IWorlds.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IWorlds.java new file mode 100644 index 00000000..166785c0 --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/IWorlds.java @@ -0,0 +1,59 @@ +package nl.pim16aap2.lightkeeper.framework; + +/** + * Worlds facet of the framework: create worlds from defaults, specs, or provisioned templates, or configure one + * through a builder. + * + *

Obtained from {@link ILightkeeperFramework#worlds()}. + */ +public interface IWorlds +{ + /** + * Gets the main world handle. + * + * @return The main world handle. + */ + WorldHandle main(); + + /** + * Creates a new world using framework defaults. + * + * @return The created world handle. + */ + WorldHandle create(); + + /** + * Creates a new world from a world spec. + * + * @param worldSpec + * The world specification. + * @return The created world handle. + */ + WorldHandle create(WorldSpec worldSpec); + + /** + * Loads a world from a pre-provisioned template folder. + * + *

Templates are world folders provisioned into the server directory by the Maven plugin's + * {@code } configuration (typically with {@code loadOnStartup=false}). The name is validated against + * the runtime manifest's provisioned-template list before touching the server: a typo fails loudly + * instead of silently creating a fresh world. + * + *

A provisioned world that is already loaded (e.g. one with {@code loadOnStartup=true}) is returned as-is + * rather than reloaded. + * + * @param templateName + * The provisioned world folder's name. + * @return A handle for the loaded world. + * @throws IllegalArgumentException + * If no template with that name was provisioned; the message lists the available templates. + */ + WorldHandle fromTemplate(String templateName); + + /** + * Creates a world builder. + * + * @return A new world builder. + */ + IWorldBuilder builder(); +} From dd180601c99ba9abbdc1f918b2a44339cf62719c Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Wed, 15 Jul 2026 09:29:01 +0200 Subject: [PATCH 2/6] refactor(framework): move implementations behind facets + deprecate v1 delegates Complete the D2/D3 evolve-in-place restructure: the facet accessors become the single canonical codepath and every flat v1 method becomes a deprecated delegate. - ILightkeeperFramework gains abstract server()/worlds()/bots()/events() accessors; each former v1 method is now a @Deprecated(forRemoval = true) default that delegates into the matching facet. waitUntil() stays plain (no facet mapping); currentServerTick() is deprecated on the public interface but DefaultLightkeeperFramework keeps its concrete impl because the identical signature is also on the untouched IFrameworkGatewayView seam. - New package-private facade impls (ServerControlFacade/WorldsFacade/BotsFacade/ EventsFacade) own the moved method bodies via constructor-injected internals; the deleted v1 impls no longer exist on DefaultLightkeeperFramework, which retains only lifecycle/close/ensureOpen plumbing and the gateway seam. - Route in-repo production consumers (LightkeeperFrameworkAssert, FailureDiagnosticsWriter) through the facet accessors so the deprecated names have no remaining production callers. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P15FjGQzyNmX9T6Qmyq8Dn --- .../framework/ILightkeeperFramework.java | 225 +++++++---- .../LightkeeperFrameworkAssert.java | 15 +- .../framework/internal/BotsFacade.java | 114 ++++++ .../internal/DefaultLightkeeperFramework.java | 359 ++++-------------- .../framework/internal/EventsFacade.java | 34 ++ .../internal/FailureDiagnosticsWriter.java | 4 +- .../internal/ServerControlFacade.java | 195 ++++++++++ .../framework/internal/WorldsFacade.java | 121 ++++++ 8 files changed, 694 insertions(+), 373 deletions(-) create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacade.java create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacade.java create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java create mode 100644 lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacade.java diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/ILightkeeperFramework.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/ILightkeeperFramework.java index fc5d22b5..d12d8f0e 100644 --- a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/ILightkeeperFramework.java +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/ILightkeeperFramework.java @@ -9,22 +9,65 @@ /** * LightKeeper end-to-end test framework entrypoint. + * + *

The framework surface is organised into facets, each returned by an accessor: {@link #server()}, + * {@link #worlds()}, {@link #bots()}, and {@link #events()}. Prefer these facet accessors: the flat v1 methods + * on this interface are deprecated for removal and delegate into the facets. */ public interface ILightkeeperFramework extends AutoCloseable { + /** + * Gets the server-control facet: command execution, console output, platform, filesystem access, process + * lifecycle, tick counter, and captured server errors. + * + * @return The server-control facet. + */ + IServerControl server(); + + /** + * Gets the worlds facet: create worlds from defaults, specs, or provisioned templates, or via a builder. + * + * @return The worlds facet. + */ + IWorlds worlds(); + + /** + * Gets the bots facet: join synthetic players into a world, or configure one via a builder. + * + * @return The bots facet. + */ + IBots bots(); + + /** + * Gets the events facet: capture Bukkit events for later inspection. + * + * @return The events facet. + */ + IEvents events(); + /** * Gets the main world handle. * * @return The main world handle. + * @deprecated Use {@link IWorlds#main()} via {@link #worlds()}. */ - WorldHandle mainWorld(); + @Deprecated(forRemoval = true) + default WorldHandle mainWorld() + { + return worlds().main(); + } /** * Creates a new world using framework defaults. * * @return The created world handle. + * @deprecated Use {@link IWorlds#create()} via {@link #worlds()}. */ - WorldHandle newWorld(); + @Deprecated(forRemoval = true) + default WorldHandle newWorld() + { + return worlds().create(); + } /** * Creates a new world from a world spec. @@ -32,27 +75,27 @@ public interface ILightkeeperFramework extends AutoCloseable * @param worldSpec * The world specification. * @return The created world handle. + * @deprecated Use {@link IWorlds#create(WorldSpec)} via {@link #worlds()}. */ - WorldHandle newWorld(WorldSpec worldSpec); + @Deprecated(forRemoval = true) + default WorldHandle newWorld(WorldSpec worldSpec) + { + return worlds().create(worldSpec); + } /** * Loads a world from a pre-provisioned template folder. * - *

Templates are world folders provisioned into the server directory by the Maven plugin's - * {@code } configuration (typically with {@code loadOnStartup=false}). The name is validated - * against the runtime manifest's provisioned-template list before touching the server: a typo - * fails loudly instead of silently creating a fresh world. - * - *

A provisioned world that is already loaded (e.g. one with {@code loadOnStartup=true}) is returned - * as-is rather than reloaded. - * * @param templateName * The provisioned world folder's name. * @return A handle for the loaded world. - * @throws IllegalArgumentException - * If no template with that name was provisioned; the message lists the available templates. + * @deprecated Use {@link IWorlds#fromTemplate(String)} via {@link #worlds()}. */ - WorldHandle newWorldFromTemplate(String templateName); + @Deprecated(forRemoval = true) + default WorldHandle newWorldFromTemplate(String templateName) + { + return worlds().fromTemplate(templateName); + } /** * Creates a synthetic player in a world at spawn. @@ -62,8 +105,13 @@ public interface ILightkeeperFramework extends AutoCloseable * @param world * Target world. * @return Created player handle. + * @deprecated Use {@link IBots#join(String, WorldHandle)} via {@link #bots()}. */ - PlayerHandle createPlayer(String name, WorldHandle world); + @Deprecated(forRemoval = true) + default PlayerHandle createPlayer(String name, WorldHandle world) + { + return bots().join(name, world); + } /** * Creates a synthetic player in a world at spawn. @@ -75,22 +123,37 @@ public interface ILightkeeperFramework extends AutoCloseable * @param world * Target world. * @return Created player handle. + * @deprecated Use {@link IBots#join(String, UUID, WorldHandle)} via {@link #bots()}. */ - PlayerHandle createPlayer(String name, UUID uuid, WorldHandle world); + @Deprecated(forRemoval = true) + default PlayerHandle createPlayer(String name, UUID uuid, WorldHandle world) + { + return bots().join(name, uuid, world); + } /** * Creates a player builder. * * @return A new player builder. + * @deprecated Use {@link IBots#builder()} via {@link #bots()}. */ - IPlayerBuilder buildPlayer(); + @Deprecated(forRemoval = true) + default IPlayerBuilder buildPlayer() + { + return bots().builder(); + } /** * Creates a world builder. * * @return A new world builder. + * @deprecated Use {@link IWorlds#builder()} via {@link #worlds()}. */ - IWorldBuilder buildWorld(); + @Deprecated(forRemoval = true) + default IWorldBuilder buildWorld() + { + return worlds().builder(); + } /** * Executes a command from the requested source. @@ -100,8 +163,13 @@ public interface ILightkeeperFramework extends AutoCloseable * @param command * The command text. * @return Command result. + * @deprecated Use {@link IServerControl#executeCommand(CommandSource, String)} via {@link #server()}. */ - CommandResult executeCommand(CommandSource source, String command); + @Deprecated(forRemoval = true) + default CommandResult executeCommand(CommandSource source, String command) + { + return server().executeCommand(source, command); + } /** * Waits until a condition is true or timeout expires. @@ -117,84 +185,101 @@ public interface ILightkeeperFramework extends AutoCloseable * Gets a snapshot of captured Minecraft server output lines. * * @return Captured server output lines ordered oldest-to-newest. + * @deprecated Use {@link IServerControl#output()} via {@link #server()}. */ - List serverOutput(); + @Deprecated(forRemoval = true) + default List serverOutput() + { + return server().output(); + } /** * Gets the server platform (e.g. PAPER, SPIGOT). * * @return Server platform. + * @deprecated Use {@link IServerControl#platform()} via {@link #server()}. */ - Platform platform(); + @Deprecated(forRemoval = true) + default Platform platform() + { + return server().platform(); + } /** * Gets the Minecraft server's working directory. * - *

Filesystem contract: reading is safe at any time; writing (e.g. seeding database files or patching - * plugin configurations) is only safe while the server is stopped — between {@link #stopServer()} and - * {@link #startServer()} — because the server reads most files once at boot and may overwrite them on - * shutdown. - * * @return The server directory. + * @deprecated Use {@link IServerControl#directory()} via {@link #server()}. */ - Path serverDirectory(); + @Deprecated(forRemoval = true) + default Path serverDirectory() + { + return server().directory(); + } /** * Gets the data directory of a plugin inside the server's {@code plugins} directory. * - *

The directory may not exist yet, e.g. before the plugin's first boot. The filesystem contract of - * {@link #serverDirectory()} applies. - * * @param pluginName * The plugin's name, as used for its data directory (usually the {@code name} from its * {@code plugin.yml}). * @return The plugin data directory. + * @deprecated Use {@link IServerControl#pluginDataDirectory(String)} via {@link #server()}. */ - Path pluginDataDirectory(String pluginName); + @Deprecated(forRemoval = true) + default Path pluginDataDirectory(String pluginName) + { + return server().pluginDataDirectory(pluginName); + } /** * Crashes the Minecraft server immediately by force-killing the process. * - *

All fixtures created before the crash are invalidated: player and world handles obtained earlier no - * longer refer to live server state. In shared-server mode the server must be started again via - * {@link #startServer()} or {@link #restartServer()} before the next test method runs, otherwise that method - * fails fast; annotate the test with {@code @FreshServer} to receive a new server per method instead. + * @deprecated Use {@link IServerControl#crash()} via {@link #server()}. */ - void crashServer(); + @Deprecated(forRemoval = true) + default void crashServer() + { + server().crash(); + } /** - * Stops the Minecraft server gracefully via the console {@code stop} command, force-killing it only when it - * does not exit within the shutdown timeout. - * - *

Synthetic players are removed before the server shuts down so they quit cleanly. All fixtures created - * before the stop are invalidated, exactly as documented on {@link #crashServer()}. While the server is - * stopped, the server directory may be modified freely — see {@link #serverDirectory()}. + * Stops the Minecraft server gracefully, force-killing it only when it does not exit within the shutdown + * timeout. * * @throws IllegalStateException * If the server is not running. + * @deprecated Use {@link IServerControl#stop()} via {@link #server()}. */ - void stopServer(); + @Deprecated(forRemoval = true) + default void stopServer() + { + server().stop(); + } /** * Starts the Minecraft server after a {@link #stopServer()} or {@link #crashServer()} call. * - *

Only worlds configured in the runtime manifest are preloaded again. Players and worlds created at - * runtime before the server went down are not recreated; tests must re-establish their own - * fixtures after starting. - * * @throws IllegalStateException * If the server is already running. + * @deprecated Use {@link IServerControl#start()} via {@link #server()}. */ - void startServer(); + @Deprecated(forRemoval = true) + default void startServer() + { + server().start(); + } /** - * Restarts the Minecraft server: a graceful {@link #stopServer()} when it is running, followed by - * {@link #startServer()}. + * Restarts the Minecraft server. * - *

Also valid when the server is already down (after {@link #stopServer()} or {@link #crashServer()}), in - * which case it only starts the server. See {@link #startServer()} for what is — and is not — restored. + * @deprecated Use {@link IServerControl#restart()} via {@link #server()}. */ - void restartServer(); + @Deprecated(forRemoval = true) + default void restartServer() + { + server().restart(); + } /** * Starts capturing Bukkit events of the specified type. @@ -202,36 +287,38 @@ public interface ILightkeeperFramework extends AutoCloseable * @param eventClassName * The full class name of the event to capture (e.g. "org.bukkit.event.player.PlayerMoveEvent"). * @return A handle to manage the capture session. + * @deprecated Use {@link IEvents#capture(String)} via {@link #events()}. */ - EventCaptureHandle captureEvents(String eventClassName); + @Deprecated(forRemoval = true) + default EventCaptureHandle captureEvents(String eventClassName) + { + return events().capture(eventClassName); + } /** * Gets the agent's monotonic tick counter, for correlating against {@link CapturedEventSnapshot#tick()} * stamps. * - *

This is an agent-relative counter (ticks since the agent enabled), not the server's absolute game - * tick: it resets on every server start, so values are only comparable within one server session. - * * @return The monotonic, session-relative server tick. + * @deprecated Use {@link IServerControl#currentTick()} via {@link #server()}. */ - long currentServerTick(); + @Deprecated(forRemoval = true) + default long currentServerTick() + { + return server().currentTick(); + } /** * Gets a handle over the always-on server-error capture. * - *

The agent captures every WARN-or-worse log event inside the server as a structured snapshot — with the - * real throwable class, message, and cause chain — from the moment the agent plugin loads (before any - * plugin's {@code onEnable}). Failures inside the logging system itself (reported via Log4j's status - * logger) and stack traces written to the server process's raw stderr file descriptor are captured as well. - * - *

In shared-server mode the capture buffer is cleared automatically at the end of every test method, so - * each test observes only the errors of its own window; the first test's window also covers server boot. - * Known gaps: exceptions that are caught and never logged are invisible, as are log events emitted before - * the agent plugin loads (use {@link #serverOutput()} for pre-boot inspection). - * * @return A handle exposing the captured server errors. + * @deprecated Use {@link IServerControl#errors()} via {@link #server()}. */ - ServerErrorsHandle serverErrors(); + @Deprecated(forRemoval = true) + default ServerErrorsHandle serverErrors() + { + return server().errors(); + } @Override void close(); diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/assertions/LightkeeperFrameworkAssert.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/assertions/LightkeeperFrameworkAssert.java index 240ba6bb..17bc4c24 100644 --- a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/assertions/LightkeeperFrameworkAssert.java +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/assertions/LightkeeperFrameworkAssert.java @@ -1,6 +1,7 @@ package nl.pim16aap2.lightkeeper.framework.assertions; import nl.pim16aap2.lightkeeper.framework.ILightkeeperFramework; +import nl.pim16aap2.lightkeeper.framework.IServerControl; import nl.pim16aap2.lightkeeper.framework.Platform; import nl.pim16aap2.lightkeeper.framework.ServerErrorSnapshot; import org.assertj.core.api.AbstractAssert; @@ -30,7 +31,7 @@ public final class LightkeeperFrameworkAssert */ public ListAssert serverOutput() { - return Assertions.assertThat(nonNullActual().serverOutput()); + return Assertions.assertThat(nonNullActual().server().output()); } /** @@ -41,8 +42,8 @@ public ListAssert serverOutput() public LightkeeperFrameworkAssert isPaper() { final var actual = nonNullActual(); - if (actual.platform() != Platform.PAPER) - failWithMessage("Expected server platform to be PAPER but was %s.", actual.platform()); + if (actual.server().platform() != Platform.PAPER) + failWithMessage("Expected server platform to be PAPER but was %s.", actual.server().platform()); return this; } @@ -54,15 +55,15 @@ public LightkeeperFrameworkAssert isPaper() public LightkeeperFrameworkAssert isSpigot() { final var actual = nonNullActual(); - if (actual.platform() != Platform.SPIGOT) - failWithMessage("Expected server platform to be SPIGOT but was %s.", actual.platform()); + if (actual.server().platform() != Platform.SPIGOT) + failWithMessage("Expected server platform to be SPIGOT but was %s.", actual.server().platform()); return this; } /** * Asserts that no error-severity server errors were captured. * - *

Backed by structured capture (see {@link ILightkeeperFramework#serverErrors()}): entries are matched + *

Backed by structured capture (see {@link IServerControl#errors()}): entries are matched * on their captured severity, not on console-line substrings, so a log message merely containing the word * "exception" no longer trips this assertion. Warnings are never counted. * @@ -90,7 +91,7 @@ public LightkeeperFrameworkAssert hasNoServerErrors() public LightkeeperFrameworkAssert hasNoServerErrors(Predicate allowedErrors) { Objects.requireNonNull(allowedErrors, "allowedErrors may not be null."); - final List failingErrors = nonNullActual().serverErrors().getCaptured().stream() + final List failingErrors = nonNullActual().server().errors().getCaptured().stream() .filter(error -> error.severity() == ServerErrorSnapshot.Severity.ERROR) .filter(error -> !allowedErrors.test(error)) .toList(); diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacade.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacade.java new file mode 100644 index 00000000..12a9f868 --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacade.java @@ -0,0 +1,114 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.FrameworkHandleFactory; +import nl.pim16aap2.lightkeeper.framework.IBots; +import nl.pim16aap2.lightkeeper.framework.IPlayerBuilder; +import nl.pim16aap2.lightkeeper.framework.PlayerHandle; +import nl.pim16aap2.lightkeeper.framework.WorldHandle; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * Default {@link IBots} implementation. + * + *

Wraps the shared framework internals handed to it by {@link DefaultLightkeeperFramework}: it creates + * synthetic players through the agent client and tracks them in the player registry, deferring the shared + * open-state gate to the owning framework. + */ +final class BotsFacade implements IBots +{ + private static final System.Logger LOG = System.getLogger(BotsFacade.class.getName()); + + private final DefaultLightkeeperFramework framework; + private final UdsAgentClient agentClient; + private final PlayerScopeRegistry playerScopeRegistry; + + BotsFacade( + DefaultLightkeeperFramework framework, + UdsAgentClient agentClient, + PlayerScopeRegistry playerScopeRegistry) + { + this.framework = Objects.requireNonNull(framework, "framework may not be null."); + this.agentClient = Objects.requireNonNull(agentClient, "agentClient may not be null."); + this.playerScopeRegistry = Objects.requireNonNull(playerScopeRegistry, "playerScopeRegistry may not be null."); + } + + @Override + public PlayerHandle join(String name, WorldHandle world) + { + return join(name, UUID.randomUUID(), world); + } + + @Override + public PlayerHandle join(String name, UUID uuid, WorldHandle world) + { + framework.ensureOpen(); + final String trimmedName = DefaultLightkeeperFramework.validatePlayerName(name); + Objects.requireNonNull(uuid, "uuid may not be null."); + Objects.requireNonNull(world, "world may not be null."); + final String worldName = Objects.requireNonNull(world.name(), "world.name may not be null."); + + final AgentPlayerData createdPlayer = agentClient.createPlayer( + trimmedName, + uuid, + worldName, + null, + null, + null, + null, + null + ); + final PlayerHandle handle = registerAndWrap(createdPlayer); + LOG.log( + System.Logger.Level.INFO, + () -> "LK_FRAMEWORK: Created player '%s' (%s) in world '%s'." + .formatted(createdPlayer.name(), createdPlayer.uniqueId(), worldName) + ); + return handle; + } + + @Override + public IPlayerBuilder builder() + { + framework.ensureOpen(); + return new DefaultPlayerBuilder(framework); + } + + /** + * Creates a synthetic player from fully-resolved builder inputs. + * + *

Called by {@link DefaultPlayerBuilder} through {@link DefaultLightkeeperFramework}; the open-state gate is + * enforced by the builder before this runs. + */ + PlayerHandle createFromBuilder( + String name, + UUID uuid, + WorldHandle worldHandle, + @Nullable Double x, + @Nullable Double y, + @Nullable Double z, + @Nullable Double health, + Set permissions) + { + final AgentPlayerData createdPlayer = agentClient.createPlayer( + name, + uuid, + worldHandle.name(), + x, + y, + z, + health, + permissions + ); + return registerAndWrap(createdPlayer); + } + + private PlayerHandle registerAndWrap(AgentPlayerData createdPlayer) + { + playerScopeRegistry.register(createdPlayer.uniqueId()); + return FrameworkHandleFactory.playerHandle(framework, createdPlayer.uniqueId(), createdPlayer.name()); + } +} diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java index 77f96848..3d5699a7 100644 --- a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java @@ -3,24 +3,20 @@ import nl.pim16aap2.lightkeeper.framework.BlockPos; import nl.pim16aap2.lightkeeper.framework.CapturedEventSnapshot; import nl.pim16aap2.lightkeeper.framework.ChatComponentSnapshot; -import nl.pim16aap2.lightkeeper.framework.CommandResult; import nl.pim16aap2.lightkeeper.framework.Condition; import nl.pim16aap2.lightkeeper.framework.EntitySnapshot; -import nl.pim16aap2.lightkeeper.framework.EventCaptureHandle; -import nl.pim16aap2.lightkeeper.framework.FrameworkHandleFactory; +import nl.pim16aap2.lightkeeper.framework.IBots; +import nl.pim16aap2.lightkeeper.framework.IEvents; import nl.pim16aap2.lightkeeper.framework.ILightkeeperFramework; -import nl.pim16aap2.lightkeeper.framework.IPlayerBuilder; -import nl.pim16aap2.lightkeeper.framework.IWorldBuilder; +import nl.pim16aap2.lightkeeper.framework.IServerControl; +import nl.pim16aap2.lightkeeper.framework.IWorlds; import nl.pim16aap2.lightkeeper.framework.InventorySnapshot; import nl.pim16aap2.lightkeeper.framework.MenuSnapshot; -import nl.pim16aap2.lightkeeper.framework.Platform; import nl.pim16aap2.lightkeeper.framework.PlayerHandle; import nl.pim16aap2.lightkeeper.framework.ServerErrorSnapshot; -import nl.pim16aap2.lightkeeper.framework.ServerErrorsHandle; import nl.pim16aap2.lightkeeper.framework.Vec3; import nl.pim16aap2.lightkeeper.framework.WorldHandle; import nl.pim16aap2.lightkeeper.framework.WorldSpec; -import nl.pim16aap2.lightkeeper.protocol.CommandSource; import nl.pim16aap2.lightkeeper.protocol.DropResult; import nl.pim16aap2.lightkeeper.protocol.GetServerErrors; import nl.pim16aap2.lightkeeper.protocol.MutatePlayerPermission; @@ -45,14 +41,18 @@ /** * Default LightKeeper framework implementation. + * + *

Public API surface is delegated to the facet facades ({@link ServerControlFacade}, {@link WorldsFacade}, + * {@link BotsFacade}, {@link EventsFacade}); this class owns the shared runtime internals and the lifecycle/close + * plumbing, and implements the internal {@link IFrameworkGateway} seam consumed by handles. */ public final class DefaultLightkeeperFramework implements ILightkeeperFramework, IFrameworkGateway { private static final System.Logger LOG = System.getLogger(DefaultLightkeeperFramework.class.getName()); - private static final Duration STARTUP_TIMEOUT = Duration.ofMinutes(2); - private static final Duration AGENT_CONNECT_TIMEOUT = Duration.ofSeconds(45); - private static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(45); + static final Duration STARTUP_TIMEOUT = Duration.ofMinutes(2); + static final Duration AGENT_CONNECT_TIMEOUT = Duration.ofSeconds(45); + static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(45); private static final String DEFAULT_WORLD_NAME_PREFIX = "lk_world_"; static final WorldSpec.WorldType DEFAULT_WORLD_TYPE = WorldSpec.WorldType.NORMAL; static final WorldSpec.WorldEnvironment DEFAULT_WORLD_ENVIRONMENT = WorldSpec.WorldEnvironment.NORMAL; @@ -64,8 +64,8 @@ public final class DefaultLightkeeperFramework implements ILightkeeperFramework, private final PlayerScopeRegistry playerScopeRegistry; private final AtomicBoolean closed = new AtomicBoolean(false); /** - * Whether the server was taken down — via {@link #crashServer()} or {@link #stopServer()} — and has not been - * started again yet. + * Whether the server was taken down — via {@link IServerControl#crash()} or {@link IServerControl#stop()} — and + * has not been started again yet. */ private final AtomicBoolean serverDown = new AtomicBoolean(false); /** @@ -74,6 +74,11 @@ public final class DefaultLightkeeperFramework implements ILightkeeperFramework, */ private final AtomicLong stderrScanWatermark = new AtomicLong(0L); + private final ServerControlFacade serverControlFacade; + private final WorldsFacade worldsFacade; + private final BotsFacade botsFacade; + private final EventsFacade eventsFacade; + @Inject DefaultLightkeeperFramework( RuntimeManifest runtimeManifest, @@ -86,6 +91,12 @@ public final class DefaultLightkeeperFramework implements ILightkeeperFramework, Objects.requireNonNull(minecraftServerProcess, "minecraftServerProcess may not be null."); this.agentClient = Objects.requireNonNull(agentClient, "agentClient may not be null."); this.playerScopeRegistry = Objects.requireNonNull(playerScopeRegistry, "playerScopeRegistry may not be null."); + + this.serverControlFacade = new ServerControlFacade( + this, runtimeManifest, minecraftServerProcess, agentClient, playerScopeRegistry); + this.worldsFacade = new WorldsFacade(this, runtimeManifest, agentClient); + this.botsFacade = new BotsFacade(this, agentClient, playerScopeRegistry); + this.eventsFacade = new EventsFacade(this, agentClient); } /** @@ -146,123 +157,27 @@ public static DefaultLightkeeperFramework start(Path runtimeManifestPath) } @Override - public WorldHandle mainWorld() - { - ensureOpen(); - return FrameworkHandleFactory.worldHandle(this, agentClient.mainWorld()); - } - - @Override - public WorldHandle newWorld() - { - return newWorld(defaultWorldSpec()); - } - - @Override - public WorldHandle newWorld(WorldSpec worldSpec) + public IServerControl server() { - ensureOpen(); - final WorldSpec validatedWorldSpec = validateWorldSpec(worldSpec); - final String worldName = agentClient.newWorld(validatedWorldSpec); - LOG.log( - System.Logger.Level.INFO, - () -> "LK_FRAMEWORK: Created world '" + worldName + "'." - ); - return FrameworkHandleFactory.worldHandle(this, worldName); - } - - @Override - public WorldHandle newWorldFromTemplate(String templateName) - { - ensureOpen(); - final String trimmedTemplateName = - Objects.requireNonNull(templateName, "templateName may not be null.").trim(); - if (trimmedTemplateName.isEmpty()) - throw new IllegalArgumentException("templateName may not be blank."); - final RuntimeManifest.ProvisionedWorld template = runtimeManifest.provisionedWorlds().stream() - .filter(provisionedWorld -> provisionedWorld.name().equals(trimmedTemplateName)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException( - "No world template named '%s' was provisioned. Provisioned templates: %s".formatted( - trimmedTemplateName, - runtimeManifest.provisionedWorlds().stream() - .map(RuntimeManifest.ProvisionedWorld::name) - .toList()))); - - // Pass the template's own provisioned spec: folders that lack complete world data are generated with - // the configured settings, while folders with real world data load from their own files and ignore it. - final String worldName = agentClient.newWorld(toWorldSpec(template)); - LOG.log( - System.Logger.Level.INFO, - () -> "LK_FRAMEWORK: Loaded world '" + worldName + "' from a provisioned template." - ); - return FrameworkHandleFactory.worldHandle(this, worldName); - } - - private static WorldSpec toWorldSpec(RuntimeManifest.ProvisionedWorld provisionedWorld) - { - return new WorldSpec( - provisionedWorld.name(), - WorldSpec.WorldType.valueOf(provisionedWorld.worldType()), - WorldSpec.WorldEnvironment.valueOf(provisionedWorld.environment()), - provisionedWorld.seed() - ); - } - - @Override - public PlayerHandle createPlayer(String name, WorldHandle world) - { - return createPlayer(name, UUID.randomUUID(), world); - } - - @Override - public PlayerHandle createPlayer(String name, UUID uuid, WorldHandle world) - { - ensureOpen(); - final String trimmedName = validatePlayerName(name); - Objects.requireNonNull(uuid, "uuid may not be null."); - Objects.requireNonNull(world, "world may not be null."); - final String worldName = Objects.requireNonNull(world.name(), "world.name may not be null."); - - final AgentPlayerData createdPlayer = agentClient.createPlayer( - trimmedName, - uuid, - worldName, - null, - null, - null, - null, - null - ); - playerScopeRegistry.register(createdPlayer.uniqueId()); - LOG.log( - System.Logger.Level.INFO, - () -> "LK_FRAMEWORK: Created player '%s' (%s) in world '%s'." - .formatted(createdPlayer.name(), createdPlayer.uniqueId(), worldName) - ); - return FrameworkHandleFactory.playerHandle(this, createdPlayer.uniqueId(), createdPlayer.name()); + return serverControlFacade; } @Override - public IPlayerBuilder buildPlayer() + public IWorlds worlds() { - ensureOpen(); - return new DefaultPlayerBuilder(this); + return worldsFacade; } @Override - public IWorldBuilder buildWorld() + public IBots bots() { - ensureOpen(); - return new DefaultWorldBuilder(this); + return botsFacade; } @Override - public CommandResult executeCommand(CommandSource source, String command) + public IEvents events() { - ensureOpen(); - final boolean success = agentClient.executeCommand(source, command); - return new CommandResult(success, success ? "Command succeeded." : "Command failed."); + return eventsFacade; } @Override @@ -525,15 +440,6 @@ public List playerChatComponents(UUID playerId) return agentClient.playerChatComponents(playerId); } - @Override - public EventCaptureHandle captureEvents(String eventClassName) - { - ensureOpen(); - Objects.requireNonNull(eventClassName, "eventClassName may not be null."); - agentClient.registerEventListener(eventClassName); - return FrameworkHandleFactory.eventCaptureHandle(this, eventClassName); - } - @Override public InventorySnapshot playerInventory(UUID playerId) { @@ -670,20 +576,6 @@ public void unregisterEventListener(String eventClassName) agentClient.unregisterEventListener(eventClassName); } - @Override - public List serverOutput() - { - ensureOpen(); - return minecraftServerProcess.snapshotOutputLines(); - } - - @Override - public ServerErrorsHandle serverErrors() - { - ensureOpen(); - return FrameworkHandleFactory.serverErrorsHandle(this); - } - @Override public List capturedServerErrors() { @@ -736,126 +628,6 @@ private static ServerErrorSnapshot toServerErrorSnapshot(ServerErrorEntry entry) ); } - @Override - public Platform platform() - { - ensureOpen(); - return agentClient.serverPlatform(); - } - - @Override - public Path serverDirectory() - { - ensureOpen(); - return Path.of(runtimeManifest.serverDirectory()); - } - - @Override - public Path pluginDataDirectory(String pluginName) - { - ensureOpen(); - final String trimmedPluginName = - Objects.requireNonNull(pluginName, "pluginName may not be null.").trim(); - if (trimmedPluginName.isEmpty()) - throw new IllegalArgumentException("pluginName may not be blank."); - // The plugin name is authored by the test writer (trusted input); this check catches accidental path - // fragments early rather than acting as a security boundary. - if (trimmedPluginName.contains("/") || trimmedPluginName.contains("\\") || trimmedPluginName.contains("..")) - throw new IllegalArgumentException( - "pluginName must be a plain directory name, got '%s'.".formatted(trimmedPluginName)); - return serverDirectory().resolve("plugins").resolve(trimmedPluginName); - } - - @Override - public void crashServer() - { - ensureOpen(); - LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Crashing Minecraft server."); - playerScopeRegistry.invalidateAll(); - agentClient.close(); - minecraftServerProcess.kill(); - serverDown.set(true); - } - - @Override - public void stopServer() - { - ensureOpen(); - if (!minecraftServerProcess.isRunning()) - throw new IllegalStateException("Cannot stop the server because it is not running."); - doStopServer(); - } - - /** - * Graceful-stop implementation without the running-state precondition. - * - *

Tolerates a server that died on its own after the caller's running check: player cleanup swallows - * per-player failures, the client close is idempotent, and the process stop handles dead processes. This is - * what lets {@link #restartServer()} avoid a check-then-act race on the running state. - */ - private void doStopServer() - { - LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Stopping Minecraft server gracefully."); - try - { - // Remove synthetic players through the live agent first so they quit cleanly instead of being - // persisted as offline players in the world's playerdata by the server's shutdown save. - playerScopeRegistry.cleanupAll(agentClient::removePlayer); - agentClient.close(); - } - finally - { - serverDown.set(true); - minecraftServerProcess.stop(SHUTDOWN_TIMEOUT); - } - } - - @Override - public void startServer() - { - ensureOpen(); - if (minecraftServerProcess.isRunning()) - throw new IllegalStateException("Cannot start the server because it is already running."); - - LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Starting Minecraft server."); - minecraftServerProcess.start(STARTUP_TIMEOUT); - try - { - agentClient.rehandshake( - AGENT_CONNECT_TIMEOUT, - runtimeManifest.agentAuthToken(), - runtimeManifest.runtimeProtocolVersion(), - Objects.requireNonNullElse(runtimeManifest.agentJarSha256(), "") - ); - preloadConfiguredWorlds(); - } - catch (Exception exception) - { - // Return to a clean 'down' state so a retry of startServer() remains possible; leaving the process - // running here would wedge the start path (running + down means only restartServer() could recover). - try - { - minecraftServerProcess.stop(SHUTDOWN_TIMEOUT); - } - catch (Exception stopException) - { - exception.addSuppressed(stopException); - } - throw exception; - } - serverDown.set(false); - } - - @Override - public void restartServer() - { - ensureOpen(); - LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Restarting Minecraft server."); - if (minecraftServerProcess.isRunning()) - doStopServer(); - startServer(); - } - private boolean clickBlock(UUID playerId, BlockPos position, String blockFace, BlockClickOperation operation) { ensureOpen(); @@ -920,7 +692,13 @@ public void endMethodScope(String methodExecutionId) clearServerErrors(); } - private void preloadConfiguredWorlds() + /** + * Preloads the worlds configured in the runtime manifest with {@code loadOnStartup=true}. + * + *

Shared by the initial boot path ({@link #start(Path)}) and the server-control restart path + * ({@link IServerControl#start()}). + */ + void preloadConfiguredWorlds() { for (final RuntimeManifest.ProvisionedWorld provisionedWorld : runtimeManifest.provisionedWorlds()) { @@ -934,6 +712,22 @@ private void preloadConfiguredWorlds() } } + /** + * Marks the shared Minecraft server as taken down (crashed or stopped) and not yet restarted. + */ + void markServerDown() + { + serverDown.set(true); + } + + /** + * Marks the shared Minecraft server as running again after a successful start. + */ + void markServerUp() + { + serverDown.set(false); + } + void ensureOpen() { if (closed.get()) @@ -955,18 +749,14 @@ private static RuntimeManifest readRuntimeManifest(Path runtimeManifestPath) } } - private static WorldSpec validateWorldSpec(WorldSpec worldSpec) + static WorldSpec toWorldSpec(RuntimeManifest.ProvisionedWorld provisionedWorld) { - Objects.requireNonNull(worldSpec, "worldSpec may not be null."); - final String worldName = Objects.requireNonNull(worldSpec.name(), "worldSpec.name may not be null.").trim(); - if (worldName.isEmpty()) - throw new IllegalArgumentException("worldSpec.name may not be blank."); - - final WorldSpec.WorldType worldType = - Objects.requireNonNull(worldSpec.worldType(), "worldSpec.worldType may not be null."); - final WorldSpec.WorldEnvironment worldEnvironment = - Objects.requireNonNull(worldSpec.environment(), "worldSpec.environment may not be null."); - return new WorldSpec(worldName, worldType, worldEnvironment, worldSpec.seed()); + return new WorldSpec( + provisionedWorld.name(), + WorldSpec.WorldType.valueOf(provisionedWorld.worldType()), + WorldSpec.WorldEnvironment.valueOf(provisionedWorld.environment()), + provisionedWorld.seed() + ); } static String createDefaultWorldName() @@ -974,16 +764,6 @@ static String createDefaultWorldName() return DEFAULT_WORLD_NAME_PREFIX + UUID.randomUUID().toString().replace("-", ""); } - private static WorldSpec defaultWorldSpec() - { - return new WorldSpec( - createDefaultWorldName(), - DEFAULT_WORLD_TYPE, - DEFAULT_WORLD_ENVIRONMENT, - DEFAULT_WORLD_SEED - ); - } - static String validatePlayerName(String name) { final String trimmedName = Objects.requireNonNull(name, "name may not be null.").trim(); @@ -996,7 +776,7 @@ static String validatePlayerName(String name) WorldHandle createWorldFromBuilder(WorldSpec worldSpec) { - return newWorld(worldSpec); + return worldsFacade.create(worldSpec); } PlayerHandle createPlayerFromBuilder( @@ -1009,17 +789,6 @@ PlayerHandle createPlayerFromBuilder( @Nullable Double health, java.util.Set permissions) { - final AgentPlayerData createdPlayer = agentClient.createPlayer( - name, - uuid, - worldHandle.name(), - x, - y, - z, - health, - permissions - ); - playerScopeRegistry.register(createdPlayer.uniqueId()); - return FrameworkHandleFactory.playerHandle(this, createdPlayer.uniqueId(), createdPlayer.name()); + return botsFacade.createFromBuilder(name, uuid, worldHandle, x, y, z, health, permissions); } } diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacade.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacade.java new file mode 100644 index 00000000..c120e04e --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacade.java @@ -0,0 +1,34 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.EventCaptureHandle; +import nl.pim16aap2.lightkeeper.framework.FrameworkHandleFactory; +import nl.pim16aap2.lightkeeper.framework.IEvents; + +import java.util.Objects; + +/** + * Default {@link IEvents} implementation. + * + *

Wraps the shared framework internals handed to it by {@link DefaultLightkeeperFramework}: it registers event + * listeners through the agent client, deferring the shared open-state gate to the owning framework. + */ +final class EventsFacade implements IEvents +{ + private final DefaultLightkeeperFramework framework; + private final UdsAgentClient agentClient; + + EventsFacade(DefaultLightkeeperFramework framework, UdsAgentClient agentClient) + { + this.framework = Objects.requireNonNull(framework, "framework may not be null."); + this.agentClient = Objects.requireNonNull(agentClient, "agentClient may not be null."); + } + + @Override + public EventCaptureHandle capture(String eventClassName) + { + framework.ensureOpen(); + Objects.requireNonNull(eventClassName, "eventClassName may not be null."); + agentClient.registerEventListener(eventClassName); + return FrameworkHandleFactory.eventCaptureHandle(framework, eventClassName); + } +} diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriter.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriter.java index 11e5659a..099b77a1 100644 --- a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriter.java +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriter.java @@ -140,7 +140,7 @@ private static void writeSection(Path bundleDirectory, String fileName, ContentS private static String renderServerErrors(ILightkeeperFramework framework) { - final List capturedErrors = framework.serverErrors().getCaptured(); + final List capturedErrors = framework.server().errors().getCaptured(); if (capturedErrors.isEmpty()) return "No captured server errors." + System.lineSeparator(); @@ -152,7 +152,7 @@ private static String renderServerErrors(ILightkeeperFramework framework) private static String renderServerOutput(ILightkeeperFramework framework) { - return String.join(System.lineSeparator(), framework.serverOutput()) + System.lineSeparator(); + return String.join(System.lineSeparator(), framework.server().output()) + System.lineSeparator(); } /** diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java new file mode 100644 index 00000000..a38fbd20 --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java @@ -0,0 +1,195 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.CommandResult; +import nl.pim16aap2.lightkeeper.framework.FrameworkHandleFactory; +import nl.pim16aap2.lightkeeper.framework.IServerControl; +import nl.pim16aap2.lightkeeper.framework.Platform; +import nl.pim16aap2.lightkeeper.framework.ServerErrorsHandle; +import nl.pim16aap2.lightkeeper.protocol.CommandSource; +import nl.pim16aap2.lightkeeper.runtime.RuntimeManifest; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +/** + * Default {@link IServerControl} implementation. + * + *

Wraps the shared framework internals handed to it by {@link DefaultLightkeeperFramework}: it drives the + * server process, the agent client, and the player registry, and defers the shared open-state gate, + * server-down flag, and world preload to the owning framework. + */ +final class ServerControlFacade implements IServerControl +{ + private static final System.Logger LOG = System.getLogger(ServerControlFacade.class.getName()); + + private final DefaultLightkeeperFramework framework; + private final RuntimeManifest runtimeManifest; + private final MinecraftServerProcess minecraftServerProcess; + private final UdsAgentClient agentClient; + private final PlayerScopeRegistry playerScopeRegistry; + + ServerControlFacade( + DefaultLightkeeperFramework framework, + RuntimeManifest runtimeManifest, + MinecraftServerProcess minecraftServerProcess, + UdsAgentClient agentClient, + PlayerScopeRegistry playerScopeRegistry) + { + this.framework = Objects.requireNonNull(framework, "framework may not be null."); + this.runtimeManifest = Objects.requireNonNull(runtimeManifest, "runtimeManifest may not be null."); + this.minecraftServerProcess = + Objects.requireNonNull(minecraftServerProcess, "minecraftServerProcess may not be null."); + this.agentClient = Objects.requireNonNull(agentClient, "agentClient may not be null."); + this.playerScopeRegistry = Objects.requireNonNull(playerScopeRegistry, "playerScopeRegistry may not be null."); + } + + @Override + public CommandResult executeCommand(CommandSource source, String command) + { + framework.ensureOpen(); + final boolean success = agentClient.executeCommand(source, command); + return new CommandResult(success, success ? "Command succeeded." : "Command failed."); + } + + @Override + public List output() + { + framework.ensureOpen(); + return minecraftServerProcess.snapshotOutputLines(); + } + + @Override + public Platform platform() + { + framework.ensureOpen(); + return agentClient.serverPlatform(); + } + + @Override + public Path directory() + { + framework.ensureOpen(); + return Path.of(runtimeManifest.serverDirectory()); + } + + @Override + public Path pluginDataDirectory(String pluginName) + { + framework.ensureOpen(); + final String trimmedPluginName = + Objects.requireNonNull(pluginName, "pluginName may not be null.").trim(); + if (trimmedPluginName.isEmpty()) + throw new IllegalArgumentException("pluginName may not be blank."); + // The plugin name is authored by the test writer (trusted input); this check catches accidental path + // fragments early rather than acting as a security boundary. + if (trimmedPluginName.contains("/") || trimmedPluginName.contains("\\") || trimmedPluginName.contains("..")) + throw new IllegalArgumentException( + "pluginName must be a plain directory name, got '%s'.".formatted(trimmedPluginName)); + return directory().resolve("plugins").resolve(trimmedPluginName); + } + + @Override + public void crash() + { + framework.ensureOpen(); + LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Crashing Minecraft server."); + playerScopeRegistry.invalidateAll(); + agentClient.close(); + minecraftServerProcess.kill(); + framework.markServerDown(); + } + + @Override + public void stop() + { + framework.ensureOpen(); + if (!minecraftServerProcess.isRunning()) + throw new IllegalStateException("Cannot stop the server because it is not running."); + doStop(); + } + + /** + * Graceful-stop implementation without the running-state precondition. + * + *

Tolerates a server that died on its own after the caller's running check: player cleanup swallows + * per-player failures, the client close is idempotent, and the process stop handles dead processes. This is + * what lets {@link #restart()} avoid a check-then-act race on the running state. + */ + private void doStop() + { + LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Stopping Minecraft server gracefully."); + try + { + // Remove synthetic players through the live agent first so they quit cleanly instead of being + // persisted as offline players in the world's playerdata by the server's shutdown save. + playerScopeRegistry.cleanupAll(agentClient::removePlayer); + agentClient.close(); + } + finally + { + framework.markServerDown(); + minecraftServerProcess.stop(DefaultLightkeeperFramework.SHUTDOWN_TIMEOUT); + } + } + + @Override + public void start() + { + framework.ensureOpen(); + if (minecraftServerProcess.isRunning()) + throw new IllegalStateException("Cannot start the server because it is already running."); + + LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Starting Minecraft server."); + minecraftServerProcess.start(DefaultLightkeeperFramework.STARTUP_TIMEOUT); + try + { + agentClient.rehandshake( + DefaultLightkeeperFramework.AGENT_CONNECT_TIMEOUT, + runtimeManifest.agentAuthToken(), + runtimeManifest.runtimeProtocolVersion(), + Objects.requireNonNullElse(runtimeManifest.agentJarSha256(), "") + ); + framework.preloadConfiguredWorlds(); + } + catch (Exception exception) + { + // Return to a clean 'down' state so a retry of start() remains possible; leaving the process running + // here would wedge the start path (running + down means only restart() could recover). + try + { + minecraftServerProcess.stop(DefaultLightkeeperFramework.SHUTDOWN_TIMEOUT); + } + catch (Exception stopException) + { + exception.addSuppressed(stopException); + } + throw exception; + } + framework.markServerUp(); + } + + @Override + public void restart() + { + framework.ensureOpen(); + LOG.log(System.Logger.Level.INFO, "LK_FRAMEWORK: Restarting Minecraft server."); + if (minecraftServerProcess.isRunning()) + doStop(); + start(); + } + + @Override + public long currentTick() + { + framework.ensureOpen(); + return agentClient.getServerTick(); + } + + @Override + public ServerErrorsHandle errors() + { + framework.ensureOpen(); + return FrameworkHandleFactory.serverErrorsHandle(framework); + } +} diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacade.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacade.java new file mode 100644 index 00000000..3604b533 --- /dev/null +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacade.java @@ -0,0 +1,121 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.FrameworkHandleFactory; +import nl.pim16aap2.lightkeeper.framework.IWorldBuilder; +import nl.pim16aap2.lightkeeper.framework.IWorlds; +import nl.pim16aap2.lightkeeper.framework.WorldHandle; +import nl.pim16aap2.lightkeeper.framework.WorldSpec; +import nl.pim16aap2.lightkeeper.runtime.RuntimeManifest; + +import java.util.Objects; + +/** + * Default {@link IWorlds} implementation. + * + *

Wraps the shared framework internals handed to it by {@link DefaultLightkeeperFramework}: it validates world + * specs and delegates world creation to the agent client, deferring the shared open-state gate to the owning + * framework. + */ +final class WorldsFacade implements IWorlds +{ + private static final System.Logger LOG = System.getLogger(WorldsFacade.class.getName()); + + private final DefaultLightkeeperFramework framework; + private final RuntimeManifest runtimeManifest; + private final UdsAgentClient agentClient; + + WorldsFacade( + DefaultLightkeeperFramework framework, + RuntimeManifest runtimeManifest, + UdsAgentClient agentClient) + { + this.framework = Objects.requireNonNull(framework, "framework may not be null."); + this.runtimeManifest = Objects.requireNonNull(runtimeManifest, "runtimeManifest may not be null."); + this.agentClient = Objects.requireNonNull(agentClient, "agentClient may not be null."); + } + + @Override + public WorldHandle main() + { + framework.ensureOpen(); + return FrameworkHandleFactory.worldHandle(framework, agentClient.mainWorld()); + } + + @Override + public WorldHandle create() + { + return create(defaultWorldSpec()); + } + + @Override + public WorldHandle create(WorldSpec worldSpec) + { + framework.ensureOpen(); + final WorldSpec validatedWorldSpec = validateWorldSpec(worldSpec); + final String worldName = agentClient.newWorld(validatedWorldSpec); + LOG.log( + System.Logger.Level.INFO, + () -> "LK_FRAMEWORK: Created world '" + worldName + "'." + ); + return FrameworkHandleFactory.worldHandle(framework, worldName); + } + + @Override + public WorldHandle fromTemplate(String templateName) + { + framework.ensureOpen(); + final String trimmedTemplateName = + Objects.requireNonNull(templateName, "templateName may not be null.").trim(); + if (trimmedTemplateName.isEmpty()) + throw new IllegalArgumentException("templateName may not be blank."); + final RuntimeManifest.ProvisionedWorld template = runtimeManifest.provisionedWorlds().stream() + .filter(provisionedWorld -> provisionedWorld.name().equals(trimmedTemplateName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "No world template named '%s' was provisioned. Provisioned templates: %s".formatted( + trimmedTemplateName, + runtimeManifest.provisionedWorlds().stream() + .map(RuntimeManifest.ProvisionedWorld::name) + .toList()))); + + // Pass the template's own provisioned spec: folders that lack complete world data are generated with + // the configured settings, while folders with real world data load from their own files and ignore it. + final String worldName = agentClient.newWorld(DefaultLightkeeperFramework.toWorldSpec(template)); + LOG.log( + System.Logger.Level.INFO, + () -> "LK_FRAMEWORK: Loaded world '" + worldName + "' from a provisioned template." + ); + return FrameworkHandleFactory.worldHandle(framework, worldName); + } + + @Override + public IWorldBuilder builder() + { + framework.ensureOpen(); + return new DefaultWorldBuilder(framework); + } + + private static WorldSpec validateWorldSpec(WorldSpec worldSpec) + { + Objects.requireNonNull(worldSpec, "worldSpec may not be null."); + final String worldName = Objects.requireNonNull(worldSpec.name(), "worldSpec.name may not be null.").trim(); + if (worldName.isEmpty()) + throw new IllegalArgumentException("worldSpec.name may not be blank."); + + final WorldSpec.WorldType worldType = + Objects.requireNonNull(worldSpec.worldType(), "worldSpec.worldType may not be null."); + final WorldSpec.WorldEnvironment worldEnvironment = + Objects.requireNonNull(worldSpec.environment(), "worldSpec.environment may not be null."); + return new WorldSpec(worldName, worldType, worldEnvironment, worldSpec.seed()); + } + + private static WorldSpec defaultWorldSpec() + { + return new WorldSpec( + DefaultLightkeeperFramework.createDefaultWorldName(), + DefaultLightkeeperFramework.DEFAULT_WORLD_TYPE, + DefaultLightkeeperFramework.DEFAULT_WORLD_ENVIRONMENT, + DefaultLightkeeperFramework.DEFAULT_WORLD_SEED + ); + } +} From 07ec7e2ff810502c8c921f468e33b24c131eebf2 Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Wed, 15 Jul 2026 09:29:14 +0200 Subject: [PATCH 3/6] test(framework): migrate in-repo consumers to the facet API + pin the split Finish the D2/D3 restructure on the test side so the deprecated names have no remaining callers outside their dedicated delegate coverage. - Migrate every framework unit test and integration test that called a v1 method to the facet form (framework.worlds().main(), server().currentTick(), bots().join(...), events().capture(...), etc.), including the mock-based consumers that now stub framework.server() and its facet methods. - Add DeprecatedDelegateTest: a facet-stub ILightkeeperFramework proves every deprecated default delegates to the matching facet method (deprecation warnings suppressed only here). - Add ServerControlFacadeTest/WorldsFacadeTest/BotsFacadeTest/EventsFacadeTest: per-facade delegation + null/blank validation coverage. - Extend FrameworkApiVisibilityTest to pin the facade impls non-public with non-public constructors, consistent with the existing API-visibility pins. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P15FjGQzyNmX9T6Qmyq8Dn --- .../framework/DeprecatedDelegateTest.java | 443 ++++++++++++++++++ .../framework/FrameworkApiVisibilityTest.java | 24 + .../assertions/HandleAssertionsTest.java | 23 +- .../framework/internal/BotsFacadeTest.java | 135 ++++++ ...efaultLightkeeperFrameworkGatewayTest.java | 20 +- ...aultLightkeeperFrameworkLifecycleTest.java | 34 +- ...tLightkeeperFrameworkServerErrorsTest.java | 4 +- .../framework/internal/EventsFacadeTest.java | 70 +++ .../FailureDiagnosticsWriterTest.java | 31 +- .../internal/ServerControlFacadeTest.java | 126 +++++ .../framework/internal/WorldsFacadeTest.java | 129 +++++ .../maven/test/LightkeeperBlockStateIT.java | 4 +- .../maven/test/LightkeeperBotIT.java | 26 +- .../test/LightkeeperChatAndCancelIT.java | 18 +- .../maven/test/LightkeeperDiagnosticsIT.java | 2 +- .../maven/test/LightkeeperEntityQueryIT.java | 14 +- .../maven/test/LightkeeperExtensionIT.java | 6 +- .../maven/test/LightkeeperFrameworkIT.java | 4 +- .../maven/test/LightkeeperFreshServerIT.java | 2 +- .../test/LightkeeperMethodFreshServerIT.java | 2 +- .../LightkeeperMixedFreshLifecycleIT.java | 8 +- .../maven/test/LightkeeperPermissionsIT.java | 8 +- .../test/LightkeeperServerErrorCaptureIT.java | 14 +- .../test/LightkeeperServerLifecycleIT.java | 20 +- .../test/LightkeeperSharedLifecycleIT.java | 4 +- .../test/LightkeeperWorldTemplateIT.java | 6 +- 26 files changed, 1063 insertions(+), 114 deletions(-) create mode 100644 lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/DeprecatedDelegateTest.java create mode 100644 lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java create mode 100644 lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacadeTest.java create mode 100644 lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacadeTest.java create mode 100644 lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/DeprecatedDelegateTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/DeprecatedDelegateTest.java new file mode 100644 index 00000000..8edf656c --- /dev/null +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/DeprecatedDelegateTest.java @@ -0,0 +1,443 @@ +package nl.pim16aap2.lightkeeper.framework; + +import nl.pim16aap2.lightkeeper.protocol.CommandSource; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Pins that every deprecated v1 method on {@link ILightkeeperFramework} delegates to its facet replacement. + * + *

Uses a hand-written facet-stub implementation so the deprecated default methods run against mocked facets; + * this keeps the delegation contract under test without touching {@code DefaultLightkeeperFramework}. + */ +@SuppressWarnings("removal") // Dedicated delegate test: it intentionally exercises the deprecated-for-removal API. +class DeprecatedDelegateTest +{ + @Test + void mainWorld_shouldDelegateToWorldsMain() + { + // setup + final IWorlds worlds = mock(IWorlds.class); + final WorldHandle expected = worldHandle(); + when(worlds.main()).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithWorlds(worlds); + + // execute + final WorldHandle result = framework.mainWorld(); + + // verify + assertThat(result).isSameAs(expected); + verify(worlds).main(); + } + + @Test + void newWorld_shouldDelegateToWorldsCreate() + { + // setup + final IWorlds worlds = mock(IWorlds.class); + final WorldHandle expected = worldHandle(); + when(worlds.create()).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithWorlds(worlds); + + // execute + final WorldHandle result = framework.newWorld(); + + // verify + assertThat(result).isSameAs(expected); + verify(worlds).create(); + } + + @Test + void newWorld_shouldDelegateToWorldsCreateWithSpec() + { + // setup + final IWorlds worlds = mock(IWorlds.class); + final WorldSpec spec = + new WorldSpec("w", WorldSpec.WorldType.NORMAL, WorldSpec.WorldEnvironment.NORMAL, 0L); + final WorldHandle expected = worldHandle(); + when(worlds.create(spec)).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithWorlds(worlds); + + // execute + final WorldHandle result = framework.newWorld(spec); + + // verify + assertThat(result).isSameAs(expected); + verify(worlds).create(spec); + } + + @Test + void newWorldFromTemplate_shouldDelegateToWorldsFromTemplate() + { + // setup + final IWorlds worlds = mock(IWorlds.class); + final WorldHandle expected = worldHandle(); + when(worlds.fromTemplate("template-a")).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithWorlds(worlds); + + // execute + final WorldHandle result = framework.newWorldFromTemplate("template-a"); + + // verify + assertThat(result).isSameAs(expected); + verify(worlds).fromTemplate("template-a"); + } + + @Test + void buildWorld_shouldDelegateToWorldsBuilder() + { + // setup + final IWorlds worlds = mock(IWorlds.class); + final IWorldBuilder expected = mock(IWorldBuilder.class); + when(worlds.builder()).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithWorlds(worlds); + + // execute + final IWorldBuilder result = framework.buildWorld(); + + // verify + assertThat(result).isSameAs(expected); + verify(worlds).builder(); + } + + @Test + void createPlayer_shouldDelegateToBotsJoin() + { + // setup + final IBots bots = mock(IBots.class); + final WorldHandle world = worldHandle(); + final PlayerHandle expected = playerHandle(); + when(bots.join("bot", world)).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithBots(bots); + + // execute + final PlayerHandle result = framework.createPlayer("bot", world); + + // verify + assertThat(result).isSameAs(expected); + verify(bots).join("bot", world); + } + + @Test + void createPlayer_shouldDelegateToBotsJoinWithUuid() + { + // setup + final IBots bots = mock(IBots.class); + final WorldHandle world = worldHandle(); + final UUID uuid = UUID.randomUUID(); + final PlayerHandle expected = playerHandle(); + when(bots.join("bot", uuid, world)).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithBots(bots); + + // execute + final PlayerHandle result = framework.createPlayer("bot", uuid, world); + + // verify + assertThat(result).isSameAs(expected); + verify(bots).join("bot", uuid, world); + } + + @Test + void buildPlayer_shouldDelegateToBotsBuilder() + { + // setup + final IBots bots = mock(IBots.class); + final IPlayerBuilder expected = mock(IPlayerBuilder.class); + when(bots.builder()).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithBots(bots); + + // execute + final IPlayerBuilder result = framework.buildPlayer(); + + // verify + assertThat(result).isSameAs(expected); + verify(bots).builder(); + } + + @Test + void executeCommand_shouldDelegateToServerExecuteCommand() + { + // setup + final IServerControl server = mock(IServerControl.class); + final CommandResult expected = new CommandResult(true, "ok"); + when(server.executeCommand(CommandSource.CONSOLE, "time set day")).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + final CommandResult result = framework.executeCommand(CommandSource.CONSOLE, "time set day"); + + // verify + assertThat(result).isSameAs(expected); + verify(server).executeCommand(CommandSource.CONSOLE, "time set day"); + } + + @Test + void serverOutput_shouldDelegateToServerOutput() + { + // setup + final IServerControl server = mock(IServerControl.class); + final List expected = List.of("line one", "line two"); + when(server.output()).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + final List result = framework.serverOutput(); + + // verify + assertThat(result).isSameAs(expected); + verify(server).output(); + } + + @Test + void platform_shouldDelegateToServerPlatform() + { + // setup + final IServerControl server = mock(IServerControl.class); + when(server.platform()).thenReturn(Platform.PAPER); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + final Platform result = framework.platform(); + + // verify + assertThat(result).isEqualTo(Platform.PAPER); + verify(server).platform(); + } + + @Test + void serverDirectory_shouldDelegateToServerDirectory() + { + // setup + final IServerControl server = mock(IServerControl.class); + final Path expected = Path.of("/tmp/lightkeeper/server"); + when(server.directory()).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + final Path result = framework.serverDirectory(); + + // verify + assertThat(result).isSameAs(expected); + verify(server).directory(); + } + + @Test + void pluginDataDirectory_shouldDelegateToServerPluginDataDirectory() + { + // setup + final IServerControl server = mock(IServerControl.class); + final Path expected = Path.of("/tmp/lightkeeper/server/plugins/AnimatedArchitecture"); + when(server.pluginDataDirectory("AnimatedArchitecture")).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + final Path result = framework.pluginDataDirectory("AnimatedArchitecture"); + + // verify + assertThat(result).isSameAs(expected); + verify(server).pluginDataDirectory("AnimatedArchitecture"); + } + + @Test + void crashServer_shouldDelegateToServerCrash() + { + // setup + final IServerControl server = mock(IServerControl.class); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + framework.crashServer(); + + // verify + verify(server).crash(); + } + + @Test + void stopServer_shouldDelegateToServerStop() + { + // setup + final IServerControl server = mock(IServerControl.class); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + framework.stopServer(); + + // verify + verify(server).stop(); + } + + @Test + void startServer_shouldDelegateToServerStart() + { + // setup + final IServerControl server = mock(IServerControl.class); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + framework.startServer(); + + // verify + verify(server).start(); + } + + @Test + void restartServer_shouldDelegateToServerRestart() + { + // setup + final IServerControl server = mock(IServerControl.class); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + framework.restartServer(); + + // verify + verify(server).restart(); + } + + @Test + void currentServerTick_shouldDelegateToServerCurrentTick() + { + // setup + final IServerControl server = mock(IServerControl.class); + when(server.currentTick()).thenReturn(123L); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + final long result = framework.currentServerTick(); + + // verify + assertThat(result).isEqualTo(123L); + verify(server).currentTick(); + } + + @Test + void serverErrors_shouldDelegateToServerErrors() + { + // setup + final IServerControl server = mock(IServerControl.class); + final ServerErrorsHandle expected = + FrameworkHandleFactory.serverErrorsHandle(mock(IFrameworkGatewayView.class)); + when(server.errors()).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithServer(server); + + // execute + final ServerErrorsHandle result = framework.serverErrors(); + + // verify + assertThat(result).isSameAs(expected); + verify(server).errors(); + } + + @Test + void captureEvents_shouldDelegateToEventsCapture() + { + // setup + final IEvents events = mock(IEvents.class); + final EventCaptureHandle expected = FrameworkHandleFactory.eventCaptureHandle( + mock(IFrameworkGatewayView.class), "org.bukkit.event.player.PlayerJoinEvent"); + when(events.capture("org.bukkit.event.player.PlayerJoinEvent")).thenReturn(expected); + final ILightkeeperFramework framework = frameworkWithEvents(events); + + // execute + final EventCaptureHandle result = framework.captureEvents("org.bukkit.event.player.PlayerJoinEvent"); + + // verify + assertThat(result).isSameAs(expected); + verify(events).capture("org.bukkit.event.player.PlayerJoinEvent"); + } + + private static ILightkeeperFramework frameworkWithServer(IServerControl server) + { + return new FacetStubFramework(server, mock(IWorlds.class), mock(IBots.class), mock(IEvents.class)); + } + + private static ILightkeeperFramework frameworkWithWorlds(IWorlds worlds) + { + return new FacetStubFramework(mock(IServerControl.class), worlds, mock(IBots.class), mock(IEvents.class)); + } + + private static ILightkeeperFramework frameworkWithBots(IBots bots) + { + return new FacetStubFramework(mock(IServerControl.class), mock(IWorlds.class), bots, mock(IEvents.class)); + } + + private static ILightkeeperFramework frameworkWithEvents(IEvents events) + { + return new FacetStubFramework(mock(IServerControl.class), mock(IWorlds.class), mock(IBots.class), events); + } + + private static WorldHandle worldHandle() + { + return FrameworkHandleFactory.worldHandle(mock(IFrameworkGatewayView.class), "w"); + } + + private static PlayerHandle playerHandle() + { + return FrameworkHandleFactory.playerHandle(mock(IFrameworkGatewayView.class), UUID.randomUUID(), "bot"); + } + + /** + * Minimal {@link ILightkeeperFramework} whose facet accessors return the supplied facets and whose deprecated + * default methods therefore delegate into them. + */ + private static final class FacetStubFramework implements ILightkeeperFramework + { + private final IServerControl server; + private final IWorlds worlds; + private final IBots bots; + private final IEvents events; + + private FacetStubFramework(IServerControl server, IWorlds worlds, IBots bots, IEvents events) + { + this.server = Objects.requireNonNull(server); + this.worlds = Objects.requireNonNull(worlds); + this.bots = Objects.requireNonNull(bots); + this.events = Objects.requireNonNull(events); + } + + @Override + public IServerControl server() + { + return server; + } + + @Override + public IWorlds worlds() + { + return worlds; + } + + @Override + public IBots bots() + { + return bots; + } + + @Override + public IEvents events() + { + return events; + } + + @Override + public void waitUntil(Condition condition, Duration timeout) + { + throw new UnsupportedOperationException("Not used by the delegate test."); + } + + @Override + public void close() + { + } + } +} diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/FrameworkApiVisibilityTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/FrameworkApiVisibilityTest.java index aabf68c2..f4aa1b99 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/FrameworkApiVisibilityTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/FrameworkApiVisibilityTest.java @@ -4,6 +4,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -48,4 +50,26 @@ void constructors_shouldNotBePublicForHandleTypes() throws Exception assertThat(isBlockRefConstructorPublic).isFalse(); assertThat(isEntityQueryConstructorPublic).isFalse(); } + + @Test + void facetImplementations_shouldNotBePublicAndHaveNonPublicConstructors() throws Exception + { + // setup + final List> facadeClasses = List.of( + Class.forName("nl.pim16aap2.lightkeeper.framework.internal.ServerControlFacade"), + Class.forName("nl.pim16aap2.lightkeeper.framework.internal.WorldsFacade"), + Class.forName("nl.pim16aap2.lightkeeper.framework.internal.BotsFacade"), + Class.forName("nl.pim16aap2.lightkeeper.framework.internal.EventsFacade")); + + // execute + final boolean allTypesNonPublic = facadeClasses.stream() + .noneMatch(facadeClass -> Modifier.isPublic(facadeClass.getModifiers())); + final boolean allConstructorsNonPublic = facadeClasses.stream() + .flatMap(facadeClass -> Arrays.stream(facadeClass.getDeclaredConstructors())) + .noneMatch(constructor -> Modifier.isPublic(constructor.getModifiers())); + + // verify + assertThat(allTypesNonPublic).isTrue(); + assertThat(allConstructorsNonPublic).isTrue(); + } } diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/assertions/HandleAssertionsTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/assertions/HandleAssertionsTest.java index 56d1e0c3..4a02f748 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/assertions/HandleAssertionsTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/assertions/HandleAssertionsTest.java @@ -6,6 +6,7 @@ import nl.pim16aap2.lightkeeper.framework.BlockStateSnapshot; import nl.pim16aap2.lightkeeper.framework.ChatComponentSnapshot; import nl.pim16aap2.lightkeeper.framework.ILightkeeperFramework; +import nl.pim16aap2.lightkeeper.framework.IServerControl; import nl.pim16aap2.lightkeeper.framework.InventorySnapshot; import nl.pim16aap2.lightkeeper.framework.MenuHandle; import nl.pim16aap2.lightkeeper.framework.MenuItemSnapshot; @@ -141,10 +142,12 @@ void lightkeeperFrameworkAssert_shouldExposeServerOutputAndValidateNoErrors() // setup — a captured WARNING must not fail hasNoServerErrors() final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of(serverError( ServerErrorSnapshot.Severity.WARNING, "WARN", "a warning, not an error"))); - when(framework.serverOutput()).thenReturn(List.of("Server started", "Done")); + when(serverControl.output()).thenReturn(List.of("Server started", "Done")); // execute + verify LightkeeperAssertions.assertThat(framework) @@ -159,7 +162,9 @@ void lightkeeperFrameworkAssert_shouldFailWhenServerErrorsWereCaptured() // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of(new ServerErrorSnapshot( 1L, ServerErrorSnapshot.Severity.ERROR, @@ -186,7 +191,9 @@ void lightkeeperFrameworkAssert_shouldIgnoreAllowlistedServerErrors() // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of(serverError( ServerErrorSnapshot.Severity.ERROR, "ERROR", "known moving_piston complaint"))); @@ -201,7 +208,9 @@ void lightkeeperFrameworkAssert_shouldTruncateOverlongStackTraceInFailureMessage // setup — more stack trace lines than the 15-line rendering cap final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); final List stackTrace = IntStream.range(0, 20) .mapToObj(i -> "\tat net.example.SomePlugin.frame" + i + "(SomePlugin.java:" + i + ")") .toList(); @@ -229,7 +238,9 @@ void lightkeeperFrameworkAssert_shouldRenderQuestionMarkWhenThreadNameIsEmpty() // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of(new ServerErrorSnapshot( 1L, ServerErrorSnapshot.Severity.ERROR, "ERROR", "net.example.SomePlugin", "", "boom", null, null, List.of()))); diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java new file mode 100644 index 00000000..0117720e --- /dev/null +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java @@ -0,0 +1,135 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.FrameworkHandleFactory; +import nl.pim16aap2.lightkeeper.framework.IPlayerBuilder; +import nl.pim16aap2.lightkeeper.framework.PlayerHandle; +import nl.pim16aap2.lightkeeper.framework.WorldHandle; +import nl.pim16aap2.lightkeeper.runtime.RuntimeManifest; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BotsFacadeTest +{ + @Test + void join_shouldCreatePlayerRegisterItAndReturnHandle() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + final PlayerScopeRegistry playerScopeRegistry = mock(PlayerScopeRegistry.class); + final UUID uuid = UUID.randomUUID(); + when(agentClient.createPlayer("lkbot001", uuid, "world", null, null, null, null, null)) + .thenReturn(new AgentPlayerData(uuid, "lkbot001")); + final DefaultLightkeeperFramework framework = framework(agentClient, playerScopeRegistry); + final WorldHandle world = FrameworkHandleFactory.worldHandle(framework, "world"); + + // execute + final PlayerHandle result = framework.bots().join("lkbot001", uuid, world); + + // verify + assertThat(result.name()).isEqualTo("lkbot001"); + assertThat(result.uniqueId()).isEqualTo(uuid); + verify(playerScopeRegistry).register(uuid); + } + + @Test + void join_shouldDefaultToRandomUuidWhenNotProvided() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + final PlayerScopeRegistry playerScopeRegistry = mock(PlayerScopeRegistry.class); + final UUID generatedUuid = UUID.randomUUID(); + when(agentClient.createPlayer( + eq("lkbot002"), any(UUID.class), eq("world"), isNull(), isNull(), isNull(), isNull(), isNull())) + .thenReturn(new AgentPlayerData(generatedUuid, "lkbot002")); + final DefaultLightkeeperFramework framework = framework(agentClient, playerScopeRegistry); + final WorldHandle world = FrameworkHandleFactory.worldHandle(framework, "world"); + + // execute + final PlayerHandle result = framework.bots().join("lkbot002", world); + + // verify + assertThat(result.name()).isEqualTo("lkbot002"); + verify(playerScopeRegistry).register(generatedUuid); + } + + @Test + void join_shouldThrowExceptionWhenNameIsBlank() + { + // setup + final DefaultLightkeeperFramework framework = + framework(mock(UdsAgentClient.class), new PlayerScopeRegistry()); + final WorldHandle world = FrameworkHandleFactory.worldHandle(framework, "world"); + + // execute + verify + assertThatThrownBy(() -> framework.bots().join(" ", UUID.randomUUID(), world)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("blank"); + } + + @Test + void join_shouldThrowExceptionWhenNameExceedsSixteenCharacters() + { + // setup + final DefaultLightkeeperFramework framework = + framework(mock(UdsAgentClient.class), new PlayerScopeRegistry()); + final WorldHandle world = FrameworkHandleFactory.worldHandle(framework, "world"); + + // execute + verify + assertThatThrownBy(() -> framework.bots().join("this_name_is_way_too_long", UUID.randomUUID(), world)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("16"); + } + + @Test + void builder_shouldReturnPlayerBuilder() + { + // setup + final DefaultLightkeeperFramework framework = + framework(mock(UdsAgentClient.class), new PlayerScopeRegistry()); + + // execute + final IPlayerBuilder builder = framework.bots().builder(); + + // verify + assertThat(builder).isNotNull(); + } + + private static DefaultLightkeeperFramework framework( + UdsAgentClient agentClient, PlayerScopeRegistry playerScopeRegistry) + { + return new DefaultLightkeeperFramework( + runtimeManifest(), mock(MinecraftServerProcess.class), agentClient, playerScopeRegistry); + } + + private static RuntimeManifest runtimeManifest() + { + return new RuntimeManifest( + "paper", + "1.21.11", + 1L, + "cache-key", + "/tmp/lightkeeper/server", + "/tmp/lightkeeper/server/server.jar", + 1024, + "/tmp/lightkeeper/agent.sock", + "auth-token", + "/tmp/lightkeeper/agent.jar", + "agent-sha256", + 1, + "agent-cache-identity", + null, + List.of() + ); + } +} diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkGatewayTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkGatewayTest.java index 8b75db17..e6f6f5e0 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkGatewayTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkGatewayTest.java @@ -156,7 +156,7 @@ void serverDirectory_shouldReturnPathFromManifest() ); // execute - final Path serverDirectory = framework.serverDirectory(); + final Path serverDirectory = framework.server().directory(); // verify assertThat(serverDirectory).isEqualTo(Path.of("/tmp/lightkeeper/server")); @@ -174,7 +174,7 @@ void pluginDataDirectory_shouldResolvePluginsSubdirectory() ); // execute - final Path pluginDataDirectory = framework.pluginDataDirectory("AnimatedArchitecture"); + final Path pluginDataDirectory = framework.server().pluginDataDirectory("AnimatedArchitecture"); // verify assertThat(pluginDataDirectory) @@ -193,7 +193,7 @@ void pluginDataDirectory_shouldThrowExceptionWhenNameIsBlank() ); // execute + verify - assertThatThrownBy(() -> framework.pluginDataDirectory(" ")) + assertThatThrownBy(() -> framework.server().pluginDataDirectory(" ")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("blank"); } @@ -210,7 +210,7 @@ void pluginDataDirectory_shouldThrowExceptionWhenNameContainsParentTraversal() ); // execute + verify - assertThatThrownBy(() -> framework.pluginDataDirectory("../evil")) + assertThatThrownBy(() -> framework.server().pluginDataDirectory("../evil")) .isInstanceOf(IllegalArgumentException.class); } @@ -226,7 +226,7 @@ void pluginDataDirectory_shouldThrowExceptionWhenNameContainsSlash() ); // execute + verify - assertThatThrownBy(() -> framework.pluginDataDirectory("a/b")) + assertThatThrownBy(() -> framework.server().pluginDataDirectory("a/b")) .isInstanceOf(IllegalArgumentException.class); } @@ -248,7 +248,7 @@ void newWorldFromTemplate_shouldCreateWorldFromProvisionedTemplate() ); // execute - final WorldHandle result = framework.newWorldFromTemplate("template-a"); + final WorldHandle result = framework.worlds().fromTemplate("template-a"); // verify assertThat(result.name()).isEqualTo("template-a-instance"); @@ -271,7 +271,7 @@ void newWorldFromTemplate_shouldThrowExceptionWhenTemplateIsNotProvisioned() ); // execute + verify - assertThatThrownBy(() -> framework.newWorldFromTemplate("template-x")) + assertThatThrownBy(() -> framework.worlds().fromTemplate("template-x")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("template-a") .hasMessageContaining("template-b"); @@ -289,7 +289,7 @@ void newWorldFromTemplate_shouldThrowExceptionWhenTemplateNameIsBlank() ); // execute + verify - assertThatThrownBy(() -> framework.newWorldFromTemplate(" ")) + assertThatThrownBy(() -> framework.worlds().fromTemplate(" ")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("blank"); } @@ -307,7 +307,7 @@ void newWorldFromTemplate_shouldThrowNullPointerExceptionWhenTemplateNameIsNull( ); // execute + verify - assertThatThrownBy(() -> framework.newWorldFromTemplate(null)) + assertThatThrownBy(() -> framework.worlds().fromTemplate(null)) .isInstanceOf(NullPointerException.class); } @@ -601,7 +601,7 @@ void currentServerTick_shouldReturnValueFromAgentClient() ); // execute - final long result = framework.currentServerTick(); + final long result = framework.server().currentTick(); // verify assertThat(result).isEqualTo(123L); diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java index b2e99ed1..cf8e5f92 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java @@ -83,7 +83,7 @@ void serverOutput_shouldReturnServerProcessOutputSnapshot() ); // execute - final List serverOutput = framework.serverOutput(); + final List serverOutput = framework.server().output(); // verify assertThat(serverOutput).containsExactly("line one", "line two"); @@ -105,7 +105,7 @@ void crashServer_shouldInvalidatePlayersAndKillProcess() ); // execute - framework.crashServer(); + framework.server().crash(); // verify verify(playerScopeRegistry, times(1)).invalidateAll(); @@ -130,7 +130,7 @@ void stopServer_shouldCleanupPlayersCloseClientAndStopProcessInOrder() ); // execute - framework.stopServer(); + framework.server().stop(); // verify final InOrder inOrder = inOrder(playerScopeRegistry, agentClient, minecraftServerProcess); @@ -156,7 +156,7 @@ void stopServer_shouldThrowExceptionAndNotInteractWhenProcessIsNotRunning() ); // execute + verify - assertThatThrownBy(framework::stopServer) + assertThatThrownBy(() -> framework.server().stop()) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("not running"); verify(playerScopeRegistry, never()).cleanupAll(any()); @@ -185,7 +185,7 @@ void stopServer_shouldStillStopProcessAndPropagateWhenCleanupThrows() ); // execute + verify: the client close is skipped (mirroring close()), but the process is still stopped - assertThatThrownBy(framework::stopServer).isSameAs(cleanupFailure); + assertThatThrownBy(() -> framework.server().stop()).isSameAs(cleanupFailure); verify(agentClient, never()).close(); verify(minecraftServerProcess, times(1)).stop(java.time.Duration.ofSeconds(45)); } @@ -205,7 +205,7 @@ void startServer_shouldThrowExceptionWhenProcessIsRunning() ); // execute + verify - assertThatThrownBy(framework::startServer) + assertThatThrownBy(() -> framework.server().start()) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("already running"); } @@ -226,10 +226,10 @@ void startServer_shouldStartProcessRehandshakePreloadWorldsAndAllowMethodScopeAf agentClient, playerScopeRegistry ); - framework.crashServer(); + framework.server().crash(); // execute - framework.startServer(); + framework.server().start(); // verify verify(minecraftServerProcess, times(1)).start(java.time.Duration.ofMinutes(2)); @@ -270,10 +270,10 @@ void startServer_shouldStopProcessAndRethrowWhenRehandshakeFails() agentClient, playerScopeRegistry ); - framework.crashServer(); + framework.server().crash(); // execute + verify: the failure propagates and the process is stopped again so a retry stays possible - assertThatThrownBy(framework::startServer).isSameAs(rehandshakeFailure); + assertThatThrownBy(() -> framework.server().start()).isSameAs(rehandshakeFailure); verify(minecraftServerProcess, times(1)).start(java.time.Duration.ofMinutes(2)); verify(minecraftServerProcess, times(1)).stop(java.time.Duration.ofSeconds(45)); assertThatThrownBy(() -> framework.beginMethodScope("method-after-failed-start")) @@ -297,7 +297,7 @@ void restartServer_shouldStartProcessAndRehandshake() ); // execute - framework.restartServer(); + framework.server().restart(); // verify verify(minecraftServerProcess, times(1)).start(java.time.Duration.ofMinutes(2)); @@ -329,7 +329,7 @@ void restartServer_shouldStopThenStartWhenProcessIsRunning() ); // execute - framework.restartServer(); + framework.server().restart(); // verify final InOrder inOrder = inOrder(minecraftServerProcess); @@ -354,7 +354,7 @@ void restartServer_shouldSkipStopWhenServerIsAlreadyDown() ); // execute - framework.restartServer(); + framework.server().restart(); // verify verify(minecraftServerProcess, never()).stop(any()); @@ -375,7 +375,7 @@ void beginMethodScope_shouldThrowAfterCrashWithoutRestart() agentClient, playerScopeRegistry ); - framework.crashServer(); + framework.server().crash(); // execute + verify assertThatThrownBy(() -> framework.beginMethodScope("method-2")) @@ -399,8 +399,8 @@ void beginMethodScope_shouldSucceedAfterCrashAndRestart() agentClient, playerScopeRegistry ); - framework.crashServer(); - framework.restartServer(); + framework.server().crash(); + framework.server().restart(); // execute framework.beginMethodScope("method-2"); @@ -423,7 +423,7 @@ void beginMethodScope_shouldThrowAfterStopWithoutStart() agentClient, playerScopeRegistry ); - framework.stopServer(); + framework.server().stop(); // execute + verify assertThatThrownBy(() -> framework.beginMethodScope("method-3")) diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkServerErrorsTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkServerErrorsTest.java index 41c3bc1c..805d7be4 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkServerErrorsTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkServerErrorsTest.java @@ -52,7 +52,7 @@ void serverErrors_shouldReturnHandleBoundToFramework() when(minecraftServerProcess.snapshotStderrLinesFrom(0L)).thenReturn(List.of()); // execute - final ServerErrorsHandle handle = framework.serverErrors(); + final ServerErrorsHandle handle = framework.server().errors(); // verify assertThat(handle.getCaptured()).isEmpty(); @@ -184,7 +184,7 @@ void endMethodScope_shouldNotClearServerErrorsAfterCrash() // setup final DefaultLightkeeperFramework framework = framework(); framework.beginMethodScope("method-1"); - framework.crashServer(); + framework.server().crash(); // execute framework.endMethodScope("method-1"); diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacadeTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacadeTest.java new file mode 100644 index 00000000..5121c6f9 --- /dev/null +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/EventsFacadeTest.java @@ -0,0 +1,70 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.EventCaptureHandle; +import nl.pim16aap2.lightkeeper.runtime.RuntimeManifest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class EventsFacadeTest +{ + @Test + void capture_shouldRegisterListenerAndReturnHandle() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + final DefaultLightkeeperFramework framework = framework(agentClient); + + // execute + final EventCaptureHandle handle = + framework.events().capture("org.bukkit.event.player.PlayerJoinEvent"); + + // verify + assertThat(handle).isNotNull(); + verify(agentClient).registerEventListener("org.bukkit.event.player.PlayerJoinEvent"); + } + + @Test + @SuppressWarnings("NullAway") // Intentionally crosses the non-null API boundary to verify fail-fast validation. + void capture_shouldThrowNullPointerExceptionWhenEventClassNameIsNull() + { + // setup + final DefaultLightkeeperFramework framework = framework(mock(UdsAgentClient.class)); + + // execute + verify + assertThatThrownBy(() -> framework.events().capture(null)) + .isInstanceOf(NullPointerException.class); + } + + private static DefaultLightkeeperFramework framework(UdsAgentClient agentClient) + { + return new DefaultLightkeeperFramework( + runtimeManifest(), mock(MinecraftServerProcess.class), agentClient, new PlayerScopeRegistry()); + } + + private static RuntimeManifest runtimeManifest() + { + return new RuntimeManifest( + "paper", + "1.21.11", + 1L, + "cache-key", + "/tmp/lightkeeper/server", + "/tmp/lightkeeper/server/server.jar", + 1024, + "/tmp/lightkeeper/agent.sock", + "auth-token", + "/tmp/lightkeeper/agent.jar", + "agent-sha256", + 1, + "agent-cache-identity", + null, + List.of() + ); + } +} diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriterTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriterTest.java index 9112aaa0..9b1b1b4e 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriterTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/FailureDiagnosticsWriterTest.java @@ -1,6 +1,7 @@ package nl.pim16aap2.lightkeeper.framework.internal; import nl.pim16aap2.lightkeeper.framework.ILightkeeperFramework; +import nl.pim16aap2.lightkeeper.framework.IServerControl; import nl.pim16aap2.lightkeeper.framework.ServerErrorSnapshot; import nl.pim16aap2.lightkeeper.framework.ServerErrorsHandle; import org.junit.jupiter.api.Test; @@ -24,7 +25,9 @@ void write_shouldCreateBundleWithOutcomeErrorsAndOutput(@TempDir Path tempDirect // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of(new ServerErrorSnapshot( 123L, ServerErrorSnapshot.Severity.ERROR, @@ -36,7 +39,7 @@ void write_shouldCreateBundleWithOutcomeErrorsAndOutput(@TempDir Path tempDirect "boom", List.of("at example.Foo.bar(Foo.java:1)") ))); - when(framework.serverOutput()).thenReturn(List.of("line one", "line two")); + when(serverControl.output()).thenReturn(List.of("line one", "line two")); final RuntimeException failure = new RuntimeException("test assertion failed"); // execute @@ -69,9 +72,11 @@ void write_shouldReportPassedOutcomeWhenNoFailureGiven(@TempDir Path tempDirecto // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of()); - when(framework.serverOutput()).thenReturn(List.of()); + when(serverControl.output()).thenReturn(List.of()); // execute final Path writtenDirectory = FailureDiagnosticsWriter.write( @@ -94,9 +99,11 @@ void write_shouldReportAbortedOutcomeForAssumptionAborts(@TempDir Path tempDirec // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of()); - when(framework.serverOutput()).thenReturn(List.of()); + when(serverControl.output()).thenReturn(List.of()); // execute final Path writtenDirectory = FailureDiagnosticsWriter.write( @@ -120,8 +127,10 @@ void write_shouldRecordSectionFailureWithoutLosingOtherSections(@TempDir Path te { // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); - when(framework.serverErrors()).thenThrow(new IllegalStateException("agent connection is gone")); - when(framework.serverOutput()).thenReturn(List.of("still available")); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenThrow(new IllegalStateException("agent connection is gone")); + when(serverControl.output()).thenReturn(List.of("still available")); // execute final Path writtenDirectory = FailureDiagnosticsWriter.write( @@ -161,9 +170,11 @@ void write_shouldSanitizeUnsafeCharactersInDirectoryNames(@TempDir Path tempDire // setup final ILightkeeperFramework framework = mock(ILightkeeperFramework.class); final ServerErrorsHandle serverErrorsHandle = mock(ServerErrorsHandle.class); - when(framework.serverErrors()).thenReturn(serverErrorsHandle); + final IServerControl serverControl = mock(IServerControl.class); + when(framework.server()).thenReturn(serverControl); + when(serverControl.errors()).thenReturn(serverErrorsHandle); when(serverErrorsHandle.getCaptured()).thenReturn(List.of()); - when(framework.serverOutput()).thenReturn(List.of()); + when(serverControl.output()).thenReturn(List.of()); // execute final Path writtenDirectory = FailureDiagnosticsWriter.write( diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacadeTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacadeTest.java new file mode 100644 index 00000000..81ec60cc --- /dev/null +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacadeTest.java @@ -0,0 +1,126 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.CommandResult; +import nl.pim16aap2.lightkeeper.framework.Platform; +import nl.pim16aap2.lightkeeper.framework.ServerErrorsHandle; +import nl.pim16aap2.lightkeeper.protocol.CommandSource; +import nl.pim16aap2.lightkeeper.protocol.GetServerErrors; +import nl.pim16aap2.lightkeeper.runtime.RuntimeManifest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ServerControlFacadeTest +{ + @Test + void executeCommand_shouldDelegateToAgentClientAndReportSuccess() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + when(agentClient.executeCommand(CommandSource.CONSOLE, "time set day")).thenReturn(true); + final DefaultLightkeeperFramework framework = framework(agentClient); + + // execute + final CommandResult result = framework.server().executeCommand(CommandSource.CONSOLE, "time set day"); + + // verify + assertThat(result.success()).isTrue(); + assertThat(result.message()).isEqualTo("Command succeeded."); + } + + @Test + void executeCommand_shouldReportFailureWhenAgentClientReturnsFalse() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + when(agentClient.executeCommand(CommandSource.CONSOLE, "broken")).thenReturn(false); + final DefaultLightkeeperFramework framework = framework(agentClient); + + // execute + final CommandResult result = framework.server().executeCommand(CommandSource.CONSOLE, "broken"); + + // verify + assertThat(result.success()).isFalse(); + assertThat(result.message()).isEqualTo("Command failed."); + } + + @Test + void output_shouldReturnServerProcessOutputSnapshot() + { + // setup + final MinecraftServerProcess minecraftServerProcess = mock(MinecraftServerProcess.class); + when(minecraftServerProcess.snapshotOutputLines()).thenReturn(List.of("line one", "line two")); + final DefaultLightkeeperFramework framework = new DefaultLightkeeperFramework( + runtimeManifest(), minecraftServerProcess, mock(UdsAgentClient.class), new PlayerScopeRegistry()); + + // execute + final List output = framework.server().output(); + + // verify + assertThat(output).containsExactly("line one", "line two"); + } + + @Test + void platform_shouldReturnValueFromAgentClient() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + when(agentClient.serverPlatform()).thenReturn(Platform.PAPER); + final DefaultLightkeeperFramework framework = framework(agentClient); + + // execute + final Platform platform = framework.server().platform(); + + // verify + assertThat(platform).isEqualTo(Platform.PAPER); + } + + @Test + void errors_shouldReturnHandleBackedByAgentClient() + { + // setup — the handle must delegate back through the framework that created it + final MinecraftServerProcess minecraftServerProcess = mock(MinecraftServerProcess.class); + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + when(agentClient.getServerErrors()).thenReturn(new GetServerErrors.Response(List.of(), 0L, true)); + when(minecraftServerProcess.snapshotStderrLinesFrom(0L)).thenReturn(List.of()); + final DefaultLightkeeperFramework framework = new DefaultLightkeeperFramework( + runtimeManifest(), minecraftServerProcess, agentClient, new PlayerScopeRegistry()); + + // execute + final ServerErrorsHandle handle = framework.server().errors(); + + // verify + assertThat(handle.getCaptured()).isEmpty(); + } + + private static DefaultLightkeeperFramework framework(UdsAgentClient agentClient) + { + return new DefaultLightkeeperFramework( + runtimeManifest(), mock(MinecraftServerProcess.class), agentClient, new PlayerScopeRegistry()); + } + + private static RuntimeManifest runtimeManifest() + { + return new RuntimeManifest( + "paper", + "1.21.11", + 1L, + "cache-key", + "/tmp/lightkeeper/server", + "/tmp/lightkeeper/server/server.jar", + 1024, + "/tmp/lightkeeper/agent.sock", + "auth-token", + "/tmp/lightkeeper/agent.jar", + "agent-sha256", + 1, + "agent-cache-identity", + null, + List.of() + ); + } +} diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java new file mode 100644 index 00000000..29bf5548 --- /dev/null +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java @@ -0,0 +1,129 @@ +package nl.pim16aap2.lightkeeper.framework.internal; + +import nl.pim16aap2.lightkeeper.framework.IWorldBuilder; +import nl.pim16aap2.lightkeeper.framework.WorldHandle; +import nl.pim16aap2.lightkeeper.framework.WorldSpec; +import nl.pim16aap2.lightkeeper.runtime.RuntimeManifest; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class WorldsFacadeTest +{ + @Test + void main_shouldReturnHandleForAgentMainWorld() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + when(agentClient.mainWorld()).thenReturn("world"); + final DefaultLightkeeperFramework framework = framework(agentClient); + + // execute + final WorldHandle result = framework.worlds().main(); + + // verify + assertThat(result.name()).isEqualTo("world"); + } + + @Test + void create_shouldCreateWorldWithFrameworkDefaults() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + when(agentClient.newWorld(any(WorldSpec.class))).thenReturn("created"); + final DefaultLightkeeperFramework framework = framework(agentClient); + + // execute + final WorldHandle result = framework.worlds().create(); + + // verify + assertThat(result.name()).isEqualTo("created"); + final ArgumentCaptor specCaptor = ArgumentCaptor.forClass(WorldSpec.class); + verify(agentClient).newWorld(specCaptor.capture()); + assertThat(specCaptor.getValue().worldType()).isEqualTo(WorldSpec.WorldType.NORMAL); + assertThat(specCaptor.getValue().environment()).isEqualTo(WorldSpec.WorldEnvironment.NORMAL); + assertThat(specCaptor.getValue().seed()).isEqualTo(0L); + assertThat(specCaptor.getValue().name()).isNotBlank(); + } + + @Test + void create_shouldValidateAndTrimWorldSpecName() + { + // setup + final UdsAgentClient agentClient = mock(UdsAgentClient.class); + when(agentClient.newWorld(new WorldSpec( + "myworld", WorldSpec.WorldType.FLAT, WorldSpec.WorldEnvironment.NETHER, 5L))) + .thenReturn("myworld-instance"); + final DefaultLightkeeperFramework framework = framework(agentClient); + + // execute + final WorldHandle result = framework.worlds().create(new WorldSpec( + " myworld ", WorldSpec.WorldType.FLAT, WorldSpec.WorldEnvironment.NETHER, 5L)); + + // verify + assertThat(result.name()).isEqualTo("myworld-instance"); + verify(agentClient).newWorld(new WorldSpec( + "myworld", WorldSpec.WorldType.FLAT, WorldSpec.WorldEnvironment.NETHER, 5L)); + } + + @Test + void create_shouldThrowExceptionWhenWorldSpecNameIsBlank() + { + // setup + final DefaultLightkeeperFramework framework = framework(mock(UdsAgentClient.class)); + + // execute + verify + assertThatThrownBy(() -> framework.worlds().create(new WorldSpec( + " ", WorldSpec.WorldType.NORMAL, WorldSpec.WorldEnvironment.NORMAL, 0L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("blank"); + } + + @Test + void builder_shouldReturnWorldBuilder() + { + // setup + final DefaultLightkeeperFramework framework = framework(mock(UdsAgentClient.class)); + + // execute + final IWorldBuilder builder = framework.worlds().builder(); + + // verify + assertThat(builder).isNotNull(); + } + + private static DefaultLightkeeperFramework framework(UdsAgentClient agentClient) + { + return new DefaultLightkeeperFramework( + runtimeManifest(), mock(MinecraftServerProcess.class), agentClient, new PlayerScopeRegistry()); + } + + private static RuntimeManifest runtimeManifest() + { + return new RuntimeManifest( + "paper", + "1.21.11", + 1L, + "cache-key", + "/tmp/lightkeeper/server", + "/tmp/lightkeeper/server/server.jar", + 1024, + "/tmp/lightkeeper/agent.sock", + "auth-token", + "/tmp/lightkeeper/agent.jar", + "agent-sha256", + 1, + "agent-cache-identity", + null, + List.of() + ); + } +} diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBlockStateIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBlockStateIT.java index 722029a2..2bd5e8eb 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBlockStateIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBlockStateIT.java @@ -25,7 +25,7 @@ void blockState_shouldPlaceAndMatchLeverStateEndToEnd(ILightkeeperFramework fram { // setup — a lever is an attachable block: place its supporting block first so the physics update // applied by the placement does not pop it off. - final WorldHandle world = framework.newWorld(new WorldSpec( + final WorldHandle world = framework.worlds().create(new WorldSpec( "lk_blockstate_" + UUID.randomUUID().toString().replace("-", ""), WorldSpec.WorldType.FLAT, WorldSpec.WorldEnvironment.NORMAL, @@ -57,7 +57,7 @@ void setBlockAt_shouldRejectMalformedBlockDataWithTypedError(ILightkeeperFramewo { // setup — the material key parses client-side; the agent's block-data parser is the authority that // rejects it, surfacing as a typed INVALID_ARGUMENT error. - final WorldHandle world = framework.mainWorld(); + final WorldHandle world = framework.worlds().main(); final BlockSpec bogusSpec = BlockSpec.parse("minecraft:definitely_not_a_block[foo=bar]"); // execute diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBotIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBotIT.java index 20602bfe..f80fec1b 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBotIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperBotIT.java @@ -31,10 +31,10 @@ class LightkeeperBotIT void playerBots_shouldExecuteCommandsAndInteractWithMenusAndBlocks(ILightkeeperFramework framework) { // setup - final var world = framework.mainWorld(); + final var world = framework.worlds().main(); - final var player = framework.createPlayer("lkplayer001", world); - final var secondPlayer = framework.buildPlayer() + final var player = framework.bots().join("lkplayer001", world); + final var secondPlayer = framework.bots().builder() .withName("lkplayer002") .atLocation(world, 0, 100, 0) .withHealth(10) @@ -46,7 +46,7 @@ void playerBots_shouldExecuteCommandsAndInteractWithMenusAndBlocks(ILightkeeperF ) .build(); - final var thirdPlayer = framework.buildPlayer() + final var thirdPlayer = framework.bots().builder() .withRandomName() .atSpawn(world) .build(); @@ -111,18 +111,18 @@ void playerBots_shouldExecuteCommandsAndInteractWithMenusAndBlocks(ILightkeeperF void frameworkApi_shouldCoverBuilderAndCommandAndHandleVariants(ILightkeeperFramework framework) { // setup - final var buildWorldResult = framework.buildWorld() + final var buildWorldResult = framework.worlds().builder() .withName("lk_world_" + UUID.randomUUID().toString().replace("-", "")) .withSeed(123L) .build(); - final var consoleCommandResult = framework.executeCommand(CommandSource.CONSOLE, "time set day"); - final var builtPlayer = framework.buildPlayer() + final var consoleCommandResult = framework.server().executeCommand(CommandSource.CONSOLE, "time set day"); + final var builtPlayer = framework.bots().builder() .withName("lkplayer003") .atSpawn(buildWorldResult) .withPermissions(TEST_GUI_PERMISSION) .build(); final UUID explicitUuid = UUID.fromString("1479d27d-1260-48f5-8044-b6aad4c8ea0f"); - final var explicitPlayer = framework.createPlayer("lkplayer004", explicitUuid, buildWorldResult); + final var explicitPlayer = framework.bots().join("lkplayer004", explicitUuid, buildWorldResult); // execute builtPlayer.executeCommand("lktestgui"); @@ -164,8 +164,8 @@ void frameworkApi_shouldCoverBuilderAndCommandAndHandleVariants(ILightkeeperFram void lktestgui_shouldFailWhenPlayerHasNoPermission(ILightkeeperFramework framework) { // setup - final var world = framework.mainWorld(); - final var playerWithoutPermission = framework.buildPlayer() + final var world = framework.worlds().main(); + final var playerWithoutPermission = framework.bots().builder() .withName("lkplayer005") .atSpawn(world) .build(); @@ -185,8 +185,8 @@ void lktestgui_shouldFailWhenPlayerHasNoPermission(ILightkeeperFramework framewo void runtimeActions_shouldTeleportManageChunksAndCaptureEvents(ILightkeeperFramework framework) { // setup - final var world = framework.mainWorld(); - final var player = framework.buildPlayer() + final var world = framework.worlds().main(); + final var player = framework.bots().builder() .withRandomName() .atLocation(world, 0, 100, 0) .build(); @@ -194,7 +194,7 @@ void runtimeActions_shouldTeleportManageChunksAndCaptureEvents(ILightkeeperFrame final int chunkZ = 24; // execute - try (var eventCapture = framework.captureEvents("org.bukkit.event.player.PlayerTeleportEvent")) + try (var eventCapture = framework.events().capture("org.bukkit.event.player.PlayerTeleportEvent")) { world.loadChunk(chunkX, chunkZ); final boolean loadedAfterLoad = world.isChunkLoaded(chunkX, chunkZ); diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperChatAndCancelIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperChatAndCancelIT.java index 077cfe64..001f3681 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperChatAndCancelIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperChatAndCancelIT.java @@ -22,11 +22,11 @@ void chat_shouldFireCapturedChatEventWithTickStamp(ILightkeeperFramework framewo throws Exception { // setup - final var world = framework.mainWorld(); - final var player = framework.createPlayer("lkchat001", world); - final long tickBefore = framework.currentServerTick(); + final var world = framework.worlds().main(); + final var player = framework.bots().join("lkchat001", world); + final long tickBefore = framework.server().currentTick(); - try (var chatCapture = framework.captureEvents(CHAT_EVENT)) + try (var chatCapture = framework.events().capture(CHAT_EVENT)) { // execute player.chat("hello lightkeeper"); @@ -38,7 +38,7 @@ void chat_shouldFireCapturedChatEventWithTickStamp(ILightkeeperFramework framewo assertThat(chatEvent.value("getMessage")) .isEqualTo(new IProtocolValue.PString("hello lightkeeper")); assertThat(chatEvent.tick()).isGreaterThanOrEqualTo(tickBefore); - assertThat(framework.currentServerTick()).isGreaterThanOrEqualTo(chatEvent.tick()); + assertThat(framework.server().currentTick()).isGreaterThanOrEqualTo(chatEvent.tick()); } } @@ -47,10 +47,10 @@ void cancelNext_shouldCancelExactlyTheNextChatEvent(ILightkeeperFramework framew throws Exception { // setup - final var world = framework.mainWorld(); - final var player = framework.createPlayer("lkchat002", world); + final var world = framework.worlds().main(); + final var player = framework.bots().join("lkchat002", world); - try (var chatCapture = framework.captureEvents(CHAT_EVENT)) + try (var chatCapture = framework.events().capture(CHAT_EVENT)) { // execute chatCapture.cancelNext(1); @@ -73,7 +73,7 @@ void cancelNext_shouldRejectNonCancellableEventClassWithTypedError(ILightkeeperF throws Exception { // setup — PlayerJoinEvent does not implement Cancellable, so arming must fail loudly - try (var joinCapture = framework.captureEvents("org.bukkit.event.player.PlayerJoinEvent")) + try (var joinCapture = framework.events().capture("org.bukkit.event.player.PlayerJoinEvent")) { // execute final Throwable thrown = diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperDiagnosticsIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperDiagnosticsIT.java index f35201f8..31d4e63a 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperDiagnosticsIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperDiagnosticsIT.java @@ -49,7 +49,7 @@ void diagnostics_shouldEnableAlwaysModeForThisTest(ILightkeeperFramework framewo System.setProperty("lightkeeper.diagnosticsDirectory", REPORTS_ROOT.toString()); // execute + verify: a trivially passing interaction; the bundle itself is written in afterEach - assertThat(framework.mainWorld()).hasNonBlankName(); + assertThat(framework.worlds().main()).hasNonBlankName(); } @Test diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperEntityQueryIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperEntityQueryIT.java index ab0d4c59..bc2aba41 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperEntityQueryIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperEntityQueryIT.java @@ -32,13 +32,13 @@ void entities_shouldCountAndSnapshotSummonedDisplayEntity(ILightkeeperFramework // binary-exact floats so the float-to-double comparison below is exact. The target chunk must be // force-loaded: a playerless world unloads its chunks (and their entities) right after the summon // command's temporary chunk ticket expires, and entity queries only see loaded chunks. - final WorldHandle world = framework.newWorld(new WorldSpec( + final WorldHandle world = framework.worlds().create(new WorldSpec( "lk_entityquery_" + UUID.randomUUID().toString().replace("-", ""), WorldSpec.WorldType.FLAT, WorldSpec.WorldEnvironment.NORMAL, 99L )); - final CommandResult forceloadResult = framework.executeCommand( + final CommandResult forceloadResult = framework.server().executeCommand( CommandSource.CONSOLE, "execute in minecraft:%s run forceload add 8 8".formatted(world.name())); assertThat(forceloadResult.success()).isTrue(); @@ -50,7 +50,7 @@ void entities_shouldCountAndSnapshotSummonedDisplayEntity(ILightkeeperFramework ).formatted(world.name()); // execute - final CommandResult summonResult = framework.executeCommand(CommandSource.CONSOLE, summonCommand); + final CommandResult summonResult = framework.server().executeCommand(CommandSource.CONSOLE, summonCommand); // verify assertThat(summonResult.success()).isTrue(); @@ -66,7 +66,7 @@ void entities_shouldCountAndSnapshotSummonedDisplayEntity(ILightkeeperFramework assertThat(snapshot.customName()).isEqualTo("lk-display"); assertThat(snapshot.pdcKeys()).isEmpty(); assertThat(snapshot.tick()).isPositive(); - assertThat(framework.currentServerTick()).isGreaterThanOrEqualTo(snapshot.tick()); + assertThat(framework.server().currentTick()).isGreaterThanOrEqualTo(snapshot.tick()); final EntitySnapshot.Transform transform = snapshot.transform(); assertThat(transform).isNotNull(); @@ -99,13 +99,13 @@ void snapshot_shouldReturnNullTransformForNonDisplayEntity(ILightkeeperFramework // setup — armor stands are regular (non-display) entities: Marker:1b and NoGravity:1b pin it in // place so the position assertion is exact. The chunk is force-loaded for the same reason as above: // playerless worlds unload chunks (and their entities) otherwise. - final WorldHandle world = framework.newWorld(new WorldSpec( + final WorldHandle world = framework.worlds().create(new WorldSpec( "lk_entityquery_" + UUID.randomUUID().toString().replace("-", ""), WorldSpec.WorldType.FLAT, WorldSpec.WorldEnvironment.NORMAL, 99L )); - final CommandResult forceloadResult = framework.executeCommand( + final CommandResult forceloadResult = framework.server().executeCommand( CommandSource.CONSOLE, "execute in minecraft:%s run forceload add 4 4".formatted(world.name())); assertThat(forceloadResult.success()).isTrue(); @@ -114,7 +114,7 @@ void snapshot_shouldReturnNullTransformForNonDisplayEntity(ILightkeeperFramework .formatted(world.name()); // execute - final CommandResult summonResult = framework.executeCommand(CommandSource.CONSOLE, summonCommand); + final CommandResult summonResult = framework.server().executeCommand(CommandSource.CONSOLE, summonCommand); // verify assertThat(summonResult.success()).isTrue(); diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperExtensionIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperExtensionIT.java index 1fad2b74..46fd09eb 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperExtensionIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperExtensionIT.java @@ -17,7 +17,7 @@ class LightkeeperExtensionIT void mainWorld_shouldInjectFrameworkFromExtension(ILightkeeperFramework framework) { // execute - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(mainWorld).hasNonBlankName(); @@ -27,11 +27,11 @@ void mainWorld_shouldInjectFrameworkFromExtension(ILightkeeperFramework framewor void newWorld_shouldCreateIsolatedWorldWhenRequestedFromFramework(ILightkeeperFramework framework) { // setup - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); final BlockPos position = new BlockPos(2, 70, 2); // execute - final var world = framework.newWorld(); + final var world = framework.worlds().create(); world.setBlockAt(position, "STONE"); framework.waitUntil(() -> "minecraft:stone".equals(world.blockTypeAt(position)), Duration.ofSeconds(20)); diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFrameworkIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFrameworkIT.java index 701b6c72..2cf35777 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFrameworkIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFrameworkIT.java @@ -29,7 +29,7 @@ void start_shouldExposeValidRuntimeManifestAndMainWorld() // execute try (ILightkeeperFramework framework = Lightkeeper.start(runtimeManifestPath)) { - final WorldHandle worldHandle = framework.mainWorld(); + final WorldHandle worldHandle = framework.worlds().main(); final String expectedServerType = System.getProperty("lightkeeper.expectedServerType", "paper"); // verify @@ -60,7 +60,7 @@ void newWorld_shouldCreateWorldAndSetBlockWhenExecuteCommandIsUsed() // execute try (ILightkeeperFramework framework = Lightkeeper.start(runtimeManifestPath)) { - final WorldHandle worldHandle = framework.newWorld(worldSpec); + final WorldHandle worldHandle = framework.worlds().create(worldSpec); worldHandle.setBlockAt(position, "STONE"); framework.waitUntil( () -> "minecraft:stone".equals(worldHandle.blockTypeAt(position)), diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFreshServerIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFreshServerIT.java index bb3a38b4..09a3d022 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFreshServerIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperFreshServerIT.java @@ -17,7 +17,7 @@ class LightkeeperFreshServerIT void mainWorld_shouldStartFreshServerWhenClassIsAnnotatedWithFreshServer(ILightkeeperFramework framework) { // execute - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(mainWorld).hasNonBlankName(); diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMethodFreshServerIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMethodFreshServerIT.java index 2d46c02e..11fe3838 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMethodFreshServerIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMethodFreshServerIT.java @@ -16,7 +16,7 @@ class LightkeeperMethodFreshServerIT void mainWorld_shouldStartFreshServerWhenMethodIsAnnotated(ILightkeeperFramework framework) { // execute - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(mainWorld).hasNonBlankName(); diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMixedFreshLifecycleIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMixedFreshLifecycleIT.java index d1c1de8c..b2bcefba 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMixedFreshLifecycleIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperMixedFreshLifecycleIT.java @@ -32,7 +32,7 @@ void frameworkLifecycle_shouldUseSharedFrameworkForFirstUnannotatedMethod(ILight firstSharedFramework = framework; // execute - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(mainWorld).hasNonBlankName(); @@ -46,7 +46,7 @@ void frameworkLifecycle_shouldReuseSharedFrameworkForSecondUnannotatedMethod(ILi assertThat(firstSharedFramework).isNotNull(); // execute - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(framework).isSameAs(firstSharedFramework); @@ -63,7 +63,7 @@ void frameworkLifecycle_shouldUseFreshFrameworkForAnnotatedMethod(ILightkeeperFr // execute freshFramework = framework; - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(framework).isNotSameAs(firstSharedFramework); @@ -80,7 +80,7 @@ void frameworkLifecycle_shouldCreateNewSharedFrameworkAfterAnnotatedMethod(ILigh // execute secondSharedFramework = framework; - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(secondSharedFramework).isNotNull(); diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperPermissionsIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperPermissionsIT.java index 0932e359..dc7558f1 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperPermissionsIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperPermissionsIT.java @@ -19,8 +19,8 @@ class LightkeeperPermissionsIT void permissions_shouldGateCommandsThroughRuntimeGrantAndRevoke(ILightkeeperFramework framework) { // setup - final var world = framework.mainWorld(); - final var player = framework.createPlayer("lkperm001", world); + final var world = framework.worlds().main(); + final var player = framework.bots().join("lkperm001", world); final PermissionControl permissions = player.permissions(); // execute @@ -57,8 +57,8 @@ void permissions_shouldGateCommandsThroughRuntimeGrantAndRevoke(ILightkeeperFram void permissions_shouldOverrideSpawnTimePermissions(ILightkeeperFramework framework) { // setup - final var world = framework.mainWorld(); - final var player = framework.buildPlayer() + final var world = framework.worlds().main(); + final var player = framework.bots().builder() .withName("lkperm002") .atSpawn(world) .withPermissions(TEST_GUI_PERMISSION) diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerErrorCaptureIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerErrorCaptureIT.java index 832464c3..629b5fb7 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerErrorCaptureIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerErrorCaptureIT.java @@ -47,10 +47,10 @@ class LightkeeperServerErrorCaptureIT void serverErrors_shouldCaptureProvokedSevereWithThrowableMetadata(ILightkeeperFramework framework) { // setup - final ServerErrorsHandle serverErrors = framework.serverErrors(); + final ServerErrorsHandle serverErrors = framework.server().errors(); // execute - framework.executeCommand(CommandSource.CONSOLE, "lktesterror severe"); + framework.server().executeCommand(CommandSource.CONSOLE, "lktesterror severe"); framework.waitUntil(() -> containsMessage(serverErrors, SEVERE_MARKER), CAPTURE_TIMEOUT); // verify — the entry carries the real throwable, not scraped console text @@ -76,7 +76,7 @@ void serverErrors_shouldCaptureProvokedSevereWithThrowableMetadata(ILightkeeperF void serverErrors_shouldBeClearedAutomaticallyBetweenTestMethods(ILightkeeperFramework framework) { // verify — the severe error provoked by the previous test must not leak into this test's window - assertThat(framework.serverErrors().getCaptured()) + assertThat(framework.server().errors().getCaptured()) .noneSatisfy(error -> assertThat(error.message()).contains(SEVERE_MARKER)); assertThat(framework).hasNoServerErrors(); } @@ -86,10 +86,10 @@ void serverErrors_shouldBeClearedAutomaticallyBetweenTestMethods(ILightkeeperFra void serverErrors_shouldBufferWarningsWithoutFailingHealthCheck(ILightkeeperFramework framework) { // setup - final ServerErrorsHandle serverErrors = framework.serverErrors(); + final ServerErrorsHandle serverErrors = framework.server().errors(); // execute - framework.executeCommand(CommandSource.CONSOLE, "lktesterror warning"); + framework.server().executeCommand(CommandSource.CONSOLE, "lktesterror warning"); framework.waitUntil(() -> containsMessage(serverErrors, WARNING_MARKER), CAPTURE_TIMEOUT); // verify — the warning is available for diagnostics but does not gate the health check @@ -104,8 +104,8 @@ void serverErrors_shouldBufferWarningsWithoutFailingHealthCheck(ILightkeeperFram void serverErrors_clearShouldDiscardProvokedError(ILightkeeperFramework framework) { // setup - final ServerErrorsHandle serverErrors = framework.serverErrors(); - framework.executeCommand(CommandSource.CONSOLE, "lktesterror severe"); + final ServerErrorsHandle serverErrors = framework.server().errors(); + framework.server().executeCommand(CommandSource.CONSOLE, "lktesterror severe"); framework.waitUntil(() -> containsMessage(serverErrors, SEVERE_MARKER), CAPTURE_TIMEOUT); // execute diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerLifecycleIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerLifecycleIT.java index 7508c042..6f0c092f 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerLifecycleIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperServerLifecycleIT.java @@ -18,30 +18,30 @@ class LightkeeperServerLifecycleIT void serverLifecycle_shouldSupportGracefulStopStartAndRestart(ILightkeeperFramework framework) { // setup - final Path serverDirectory = framework.serverDirectory(); + final Path serverDirectory = framework.server().directory(); assertThat(serverDirectory).isDirectory(); - assertThat(framework.pluginDataDirectory("LightkeeperSpigotTestPlugin")) + assertThat(framework.server().pluginDataDirectory("LightkeeperSpigotTestPlugin")) .isEqualTo(serverDirectory.resolve("plugins").resolve("LightkeeperSpigotTestPlugin")); - framework.createPlayer("lklife001", framework.mainWorld()); + framework.bots().join("lklife001", framework.worlds().main()); // execute: graceful stop - framework.stopServer(); + framework.server().stop(); // verify: while stopped, the server directory remains accessible for seeding and inspection assertThat(serverDirectory.resolve("server.properties")).exists(); // execute: start again after the graceful stop - framework.startServer(); + framework.server().start(); // verify: the started server is fully usable - assertThat(framework.mainWorld()).hasNonBlankName(); - assertThat(framework.createPlayer("lklife002", framework.mainWorld())).hasName("lklife002"); + assertThat(framework.worlds().main()).hasNonBlankName(); + assertThat(framework.bots().join("lklife002", framework.worlds().main())).hasName("lklife002"); // execute: graceful restart while the server is running - framework.restartServer(); + framework.server().restart(); // verify: usable again after the restart - assertThat(framework.mainWorld()).hasNonBlankName(); - assertThat(framework.createPlayer("lklife003", framework.mainWorld())).hasName("lklife003"); + assertThat(framework.worlds().main()).hasNonBlankName(); + assertThat(framework.bots().join("lklife003", framework.worlds().main())).hasName("lklife003"); } } diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperSharedLifecycleIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperSharedLifecycleIT.java index cfc119c9..b44363a4 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperSharedLifecycleIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperSharedLifecycleIT.java @@ -28,7 +28,7 @@ void frameworkLifecycle_shouldProvideSharedFrameworkOnFirstMethod(ILightkeeperFr initialFramework = framework; // execute - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(mainWorld).hasNonBlankName(); @@ -39,7 +39,7 @@ void frameworkLifecycle_shouldProvideSharedFrameworkOnFirstMethod(ILightkeeperFr void frameworkLifecycle_shouldReuseSharedFrameworkOnSecondMethod(ILightkeeperFramework framework) { // execute - final var mainWorld = framework.mainWorld(); + final var mainWorld = framework.worlds().main(); // verify assertThat(initialFramework).isNotNull(); diff --git a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperWorldTemplateIT.java b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperWorldTemplateIT.java index 4e98f073..db9fdf44 100644 --- a/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperWorldTemplateIT.java +++ b/lightkeeper-integration-tests/src/test/java/nl/pim16aap2/lightkeeper/maven/test/LightkeeperWorldTemplateIT.java @@ -20,10 +20,10 @@ class LightkeeperWorldTemplateIT void newWorldFromTemplate_shouldLoadProvisionedTemplateWorld(ILightkeeperFramework framework) { // setup - final Path templateDirectory = framework.serverDirectory().resolve(TEMPLATE_NAME); + final Path templateDirectory = framework.server().directory().resolve(TEMPLATE_NAME); // execute - final WorldHandle templateWorld = framework.newWorldFromTemplate(TEMPLATE_NAME); + final WorldHandle templateWorld = framework.worlds().fromTemplate(TEMPLATE_NAME); // verify assertThat(templateWorld).hasNonBlankName(); @@ -39,7 +39,7 @@ void newWorldFromTemplate_shouldRejectUnknownTemplateName(ILightkeeperFramework final String unknownTemplate = "lightkeeper-no-such-template"; // execute - final Throwable thrown = catchThrowable(() -> framework.newWorldFromTemplate(unknownTemplate)); + final Throwable thrown = catchThrowable(() -> framework.worlds().fromTemplate(unknownTemplate)); // verify assertThat(thrown) From 2c3554aeca33da20dfc6f2edd556b972d24aadbd Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Wed, 15 Jul 2026 09:29:24 +0200 Subject: [PATCH 4/6] docs(readme): document the facet API and v1 deprecation Bring the README in line with the D2/D3 2.0 API: the Quick Start and Vault examples now use framework.worlds().main() and framework.bots().builder(), the Core Features bullets reference the facet forms (worlds().fromTemplate(...), server().stop()/start()/restart()/crash(), server().directory()/ pluginDataDirectory(...)), and a note flags the flat v1 names as deprecated-for-removal in favour of the server()/worlds()/bots()/events() facets. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P15FjGQzyNmX9T6Qmyq8Dn --- README.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2989e37c..9d67fbda 100644 --- a/README.md +++ b/README.md @@ -196,9 +196,10 @@ class MyPluginIT @Test void playerBots_shouldInteractWithTheServer(ILightkeeperFramework framework) { + // The API is organised into facets: framework.server(), .worlds(), .bots(), .events(). // setup - final WorldHandle world = framework.mainWorld(); - final PlayerHandle player = framework.buildPlayer() + final WorldHandle world = framework.worlds().main(); + final PlayerHandle player = framework.bots().builder() .withName("lk_tester") .atSpawn(world) .withPermissions("minecraft.command.time") @@ -218,6 +219,10 @@ class MyPluginIT } ``` +> The framework surface is organised into facets — `framework.server()`, `.worlds()`, `.bots()`, and +> `.events()`. The older flat method names (`mainWorld()`, `buildPlayer()`, `stopServer()`, …) are deprecated +> for removal; use the facet accessors instead. + ## Core Features - Real server E2E tests (not mocks) @@ -233,7 +238,7 @@ class MyPluginIT - Menu interaction and assertions; menu actions auto-wait for an open menu, and `clickItem("name")` clicks by item display name - World templates: provision world folders via `` and load them with - `newWorldFromTemplate("name")` — typos fail loudly instead of silently creating a fresh world + `worlds().fromTemplate("name")` — typos fail loudly instead of silently creating a fresh world - Received-message assertions with AssertJ string chaining - Explicit retrying assertions: `eventually(timeout, () -> assertThat(...))` re-runs a live probe until it passes, and reports the attempt count, elapsed time, and last failure on timeout @@ -253,10 +258,10 @@ class MyPluginIT burst so every snapshot shares one server tick - Diagnostics-on-failure: failed tests automatically get a bundle (test outcome, captured server errors, server console output) under `target/lightkeeper-reports/` -- Graceful server lifecycle control from tests (`stopServer()`, `startServer()`, `restartServer()`), plus - `crashServer()` for hard-kill scenarios -- Server directory access (`serverDirectory()`, `pluginDataDirectory(name)`) for seeding files while the - server is stopped +- Graceful server lifecycle control from tests (`server().stop()`, `server().start()`, `server().restart()`), + plus `server().crash()` for hard-kill scenarios +- Server directory access (`server().directory()`, `server().pluginDataDirectory(name)`) for seeding files + while the server is stopped ## World and Plugin Provisioning @@ -345,9 +350,9 @@ class VaultIT void vaultInfo_shouldReportVaultVersion(ILightkeeperFramework framework) { // setup - final PlayerHandle player = framework.buildPlayer() + final PlayerHandle player = framework.bots().builder() .withName("vault_tester") - .atSpawn(framework.mainWorld()) + .atSpawn(framework.worlds().main()) .withPermissions("vault.admin") .build(); From 2a0e1cbb0f9da07304df9eb4c614d121e5bfe1db Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Wed, 15 Jul 2026 09:53:04 +0200 Subject: [PATCH 5/6] fix: address pre-PR deep-review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Goal: fold the Opus review round into the facet pass. Collapses the duplicated tick read — ServerControlFacade.currentTick() now delegates to the framework's gateway-mandated currentServerTick(), restoring a single source of truth — and rewrites the shared-server-down error message (plus its test expectations) to steer users to server().crash()/ stop()/start()/restart() instead of the v1 names this branch deprecates. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P15FjGQzyNmX9T6Qmyq8Dn --- .../framework/internal/DefaultLightkeeperFramework.java | 6 +++--- .../lightkeeper/framework/internal/ServerControlFacade.java | 5 +++-- .../internal/DefaultLightkeeperFrameworkLifecycleTest.java | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java index 3d5699a7..f1df8a60 100644 --- a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFramework.java @@ -675,9 +675,9 @@ public void beginMethodScope(String methodExecutionId) ensureOpen(); if (serverDown.get()) throw new IllegalStateException( - "A previous test took down the shared Minecraft server via crashServer() or stopServer() " - + "without starting it again. Call startServer() (or restartServer()) before the test ends, " - + "or annotate the test with @FreshServer so each method receives a fresh server."); + "A previous test took down the shared Minecraft server via server().crash() or server().stop() " + + "without starting it again. Call server().start() (or server().restart()) before the test " + + "ends, or annotate the test with @FreshServer so each method receives a fresh server."); playerScopeRegistry.beginMethodScope(methodExecutionId); } diff --git a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java index a38fbd20..2d2d9e66 100644 --- a/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java +++ b/lightkeeper-framework-junit/src/main/java/nl/pim16aap2/lightkeeper/framework/internal/ServerControlFacade.java @@ -182,8 +182,9 @@ public void restart() @Override public long currentTick() { - framework.ensureOpen(); - return agentClient.getServerTick(); + // Delegates to the framework's gateway method: the tick read must have exactly one implementation, and + // the gateway seam (IFrameworkGatewayView) forces that body to live on the framework class. + return framework.currentServerTick(); } @Override diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java index cf8e5f92..6bd75cc8 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/DefaultLightkeeperFrameworkLifecycleTest.java @@ -380,8 +380,8 @@ void beginMethodScope_shouldThrowAfterCrashWithoutRestart() // execute + verify assertThatThrownBy(() -> framework.beginMethodScope("method-2")) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("crashServer") - .hasMessageContaining("restartServer"); + .hasMessageContaining("server().crash()") + .hasMessageContaining("server().restart()"); verify(playerScopeRegistry, never()).beginMethodScope("method-2"); } @@ -428,7 +428,7 @@ void beginMethodScope_shouldThrowAfterStopWithoutStart() // execute + verify assertThatThrownBy(() -> framework.beginMethodScope("method-3")) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("stopServer"); + .hasMessageContaining("server().stop()"); verify(playerScopeRegistry, never()).beginMethodScope("method-3"); } From 00431c2e9129110a4b00d6d060f487fdefb360ac Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Wed, 15 Jul 2026 10:26:55 +0200 Subject: [PATCH 6/6] test: cover facade fail-fast null validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Goal: address CodeRabbit's round-one findings on the facet PR — the join(uuid/world) and create(worldSpec) fail-fast paths were validated in the facades but not pinned by tests. Adds the three missing NullPointerException tests following the existing NullAway-suppressed boundary-crossing pattern. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P15FjGQzyNmX9T6Qmyq8Dn --- .../framework/internal/BotsFacadeTest.java | 29 +++++++++++++++++++ .../framework/internal/WorldsFacadeTest.java | 13 +++++++++ 2 files changed, 42 insertions(+) diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java index 0117720e..e71fa808 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/BotsFacadeTest.java @@ -105,6 +105,35 @@ void builder_shouldReturnPlayerBuilder() assertThat(builder).isNotNull(); } + @Test + @SuppressWarnings("NullAway") // Intentionally crosses the non-null API boundary to verify fail-fast validation. + void join_shouldThrowNullPointerExceptionWhenUuidIsNull() + { + // setup + final DefaultLightkeeperFramework framework = + framework(mock(UdsAgentClient.class), mock(PlayerScopeRegistry.class)); + final WorldHandle world = FrameworkHandleFactory.worldHandle(framework, "world"); + + // execute + verify + assertThatThrownBy(() -> framework.bots().join("lkbot001", null, world)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("uuid"); + } + + @Test + @SuppressWarnings("NullAway") // Intentionally crosses the non-null API boundary to verify fail-fast validation. + void join_shouldThrowNullPointerExceptionWhenWorldIsNull() + { + // setup + final DefaultLightkeeperFramework framework = + framework(mock(UdsAgentClient.class), mock(PlayerScopeRegistry.class)); + + // execute + verify + assertThatThrownBy(() -> framework.bots().join("lkbot001", UUID.randomUUID(), null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("world"); + } + private static DefaultLightkeeperFramework framework( UdsAgentClient agentClient, PlayerScopeRegistry playerScopeRegistry) { diff --git a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java index 29bf5548..e433647d 100644 --- a/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java +++ b/lightkeeper-framework-junit/src/test/java/nl/pim16aap2/lightkeeper/framework/internal/WorldsFacadeTest.java @@ -100,6 +100,19 @@ void builder_shouldReturnWorldBuilder() assertThat(builder).isNotNull(); } + @Test + @SuppressWarnings("NullAway") // Intentionally crosses the non-null API boundary to verify fail-fast validation. + void create_shouldThrowNullPointerExceptionWhenWorldSpecIsNull() + { + // setup + final DefaultLightkeeperFramework framework = framework(mock(UdsAgentClient.class)); + + // execute + verify + assertThatThrownBy(() -> framework.worlds().create(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("worldSpec"); + } + private static DefaultLightkeeperFramework framework(UdsAgentClient agentClient) { return new DefaultLightkeeperFramework(