From db62d21eb2c95c7ff9fbb30e9f04e7fa16bf3eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malcolm=20Nihl=C3=A9n?= Date: Sun, 3 Aug 2025 17:18:43 +0200 Subject: [PATCH] chore: add gametests --- build.gradle | 23 ++ docs/testing.md | 55 +++ .../ScriptsChunkLoadersGameTest.java | 354 ++++++++++++++++++ .../data/scl_tests/structure/basic.nbt | Bin 0 -> 647 bytes .../data/scl_tests/structure/empty.nbt | Bin 0 -> 587 bytes src/gametest/resources/fabric.mod.json | 12 + 6 files changed, 444 insertions(+) create mode 100644 docs/testing.md create mode 100644 src/gametest/java/io/nihlen/scriptschunkloaders/ScriptsChunkLoadersGameTest.java create mode 100644 src/gametest/resources/data/scl_tests/structure/basic.nbt create mode 100644 src/gametest/resources/data/scl_tests/structure/empty.nbt create mode 100644 src/gametest/resources/fabric.mod.json diff --git a/build.gradle b/build.gradle index fc8d305..1722e02 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,30 @@ loom { sourceSet sourceSets.client } } +} + + +fabricApi { + configureTests { + createSourceSet = true + enableGameTests = true + enableClientGameTests = false + modId = "scl_tests" + eula = true + } +} + +// `loom/runs` has to appear after the `fabricApi` block since it depends on +// `sourceSets.gametest`, but `loom/splitEnvironmentSourceSets()` must be before. +loom { + runs { + gameTestClient { + inherit client + configName = "GameTest Minecraft Client" + source = sourceSets.gametest + } + } } dependencies { diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..7347b9a --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,55 @@ +# Testing + +This project uses the [Minecraft GameTest framework](https://minecraft.wiki/w/GameTest) +along with [Fabric's GameTest integration](https://docs.fabricmc.net/develop/automatic-testing#writing-game-tests) +for testing. + +File structure +``` +src/ + gametest/ # Module for all things GameTest. + java/ + io.nihlen.scriptschunkloaders/ # Package in which we put our tests. + ScriptsChunkLoadersGameTest.java # We can have multiple test files. Also add to fabric.mod.json. + resources/ + data.scl_tests/ + structure/ # Structures referenced by the tests live here. + basic.nbt + empty.nbt + fabric.mod.json # The GameTests are in their own mod which is defined here. + main/ # The ordinary mod files. +``` + +You can launch Minecraft with the GameTest mod loaded by running the "GameTest +Minecraft Client" configuration. + +To design a new structure for a test, make sure to first define the structure in +your test code: +```java +@GameTest(structure = "scl_tests:my_new_structure") +public void my_new_test(TestContext context) { + // Your test goes here +} +``` + +Then start the game. Open a world and run the command +`/test create scl_tests:scripts_chunk_loaders_game_test_my_new_test` +(autocomplete will help you). + +A test area will be generated in which your can design your test. To save the +structure, right click the Test Block and click "Save Structure". Your structure +will be saved in +`run/saves//generated/scl_tests/structures/my_new_structure.nbt`. +Move this into `src/gametest/resources/data/scl_tests/structure` to ensure it's +commited into Git. + +## Hotswapping tests + +To hotswap tests, make sure to have your IDE configured to use the JetBrains +Runtime and add `-XX:+AllowEnhancedClassRedefinition` to your VM Arguments for +the "GameTest Minecraft Client" configuration. You can then start the game using +the debugger and enjoy hotswapping of existing tests. + +You can read more about hotswapping in the [Fabric documentation](https://docs.fabricmc.net/develop/getting-started/launching-the-game#hotswapping-classes). + + diff --git a/src/gametest/java/io/nihlen/scriptschunkloaders/ScriptsChunkLoadersGameTest.java b/src/gametest/java/io/nihlen/scriptschunkloaders/ScriptsChunkLoadersGameTest.java new file mode 100644 index 0000000..819b992 --- /dev/null +++ b/src/gametest/java/io/nihlen/scriptschunkloaders/ScriptsChunkLoadersGameTest.java @@ -0,0 +1,354 @@ +package io.nihlen.scriptschunkloaders; + +import net.fabricmc.fabric.api.gametest.v1.GameTest; + +import net.minecraft.component.DataComponentTypes; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.test.TestContext; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; + +import java.util.Objects; +import java.util.function.Function; + +/* +* Tests: +* - Registers with default name: +* - Minecart +* - Minecart with Hopper +* - Minecart with Chest +* - Minecart with Furnace +* - Minecart with TNT +* - Minecart with Command Block +* +* - Registers with first slot custom item name: +* - Minecart with Hopper +* - Minecart with Chest +* +* - Registers with default name when custom item in other slot: +* - Minecart with Hopper +* - Minecart with Chest +* +* - Registers with first slot no name item: +* - Minecart with Hopper +* - Minecart with Chest +* +* - Minecart registers and unregisters +* - Minecart registers, unregisters and registers again +* +* - Minecart does not register with empty dispenser +* */ + +public class ScriptsChunkLoadersGameTest { + String defaultName = "Chunk Loader"; + String customItemName = "My Custom Item"; + + Function getCustomName = entity -> { + var customName = entity.getCustomName(); + if (Objects.isNull(customName)) return null; + return customName.getString(); + }; + + private ItemStack createNamedItem() { + ItemStack item = new ItemStack(Items.PAPER); + item.set(DataComponentTypes.CUSTOM_NAME, Text.literal(customItemName)); + return item; + } + + private ItemStack createUnnamedItem() { + return new ItemStack(Items.EMERALD); + } + + private void clearTest(TestContext context) { + context.killAllEntities(); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultName_minecart(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultName_hopperMinecart(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.HOPPER_MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.HOPPER_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultName_chestMinecart(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.CHEST_MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.CHEST_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultName_furnaceMinecart(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.FURNACE_MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.FURNACE_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultName_tntMinecart(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.TNT_MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.TNT_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultName_commandBlockMinecart(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.COMMAND_BLOCK_MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.COMMAND_BLOCK_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + +// @GameTest(structure = "scl_tests:basic") +// public void registersWithFirstItemName_hopperMinecart(TestContext context) { +// clearTest(context); +// +// clearTest(context); +// +// var entity = context.spawnEntity(EntityType.HOPPER_MINECART, 2, 1, 2); +// entity.setInventoryStack(0, createNamedItem()); +// +// context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); +// context.waitAndRun(4, () -> { +// context.expectEntityWithData( +// new BlockPos(2, 1, 2), +// EntityType.HOPPER_MINECART, +// getCustomName, +// customItemName +// ); +// context.complete(); +// }); +// } + + @GameTest(structure = "scl_tests:basic") + public void registersWithFirstItemName_chestMinecart(TestContext context) { + clearTest(context); + + context.killAllEntities(); + var entity = context.spawnEntity(EntityType.CHEST_MINECART, 2, 1, 2); + entity.setInventoryStack(0, createNamedItem()); + + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.CHEST_MINECART, + getCustomName, + customItemName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultNameOtherSlot_hopperMinecart(TestContext context) { + clearTest(context); + + context.killAllEntities(); + var entity = context.spawnEntity(EntityType.HOPPER_MINECART, 2, 1, 2); + entity.setInventoryStack(1, createNamedItem()); + + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.HOPPER_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultNameOtherSlot_chestMinecart(TestContext context) { + clearTest(context); + + context.killAllEntities(); + var entity = context.spawnEntity(EntityType.CHEST_MINECART, 2, 1, 2); + entity.setInventoryStack(1, createNamedItem()); + + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.CHEST_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultNameUnnamedItem_hopperMinecart(TestContext context) { + clearTest(context); + + context.killAllEntities(); + var entity = context.spawnEntity(EntityType.HOPPER_MINECART, 2, 1, 2); + entity.setInventoryStack(0, createUnnamedItem()); + + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.HOPPER_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registersWithDefaultNameUnnamedItem_chestMinecart(TestContext context) { + clearTest(context); + + context.killAllEntities(); + var entity = context.spawnEntity(EntityType.CHEST_MINECART, 2, 1, 2); + entity.setInventoryStack(0, createUnnamedItem()); + + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.CHEST_MINECART, + getCustomName, + defaultName + ); + context.complete(); + }); + } + + @GameTest(structure = "scl_tests:basic") + public void registers_and_unregisters(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + + context.waitAndRun(4, () -> { + context.expectEntityWithData(new BlockPos(2, 1, 2), EntityType.MINECART, getCustomName, defaultName); + + context.waitAndRun(4, () -> { + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + + context.waitAndRun(4, () -> { + context.expectEntityWithData(new BlockPos(2, 1, 2), EntityType.MINECART, getCustomName, null); + context.complete(); + }); + }); + }); + } + + /** + * Covers #34 + */ + @GameTest(structure = "scl_tests:basic") + public void registers_unregisters_and_registers(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData(new BlockPos(2, 1, 2), EntityType.MINECART, getCustomName, defaultName); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + + context.waitAndRun(4, () -> { + context.expectEntityWithData(new BlockPos(2, 1, 2), EntityType.MINECART, getCustomName, null); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + + context.waitAndRun(4, () -> { + context.expectEntityWithData(new BlockPos(2, 1, 2), EntityType.MINECART, getCustomName, defaultName); + context.complete(); + }); + }); + }); + } + + @GameTest(structure = "scl_tests:empty") + public void doesNotRegisterWithEmptyDispenser(TestContext context) { + clearTest(context); + + context.spawnEntity(EntityType.MINECART, 2, 1, 2); + context.putAndRemoveRedstoneBlock(new BlockPos(1, 1, 1), 1); + context.waitAndRun(4, () -> { + context.expectEntityWithData( + new BlockPos(2, 1, 2), + EntityType.MINECART, + getCustomName, + null + ); + context.complete(); + }); + } +} \ No newline at end of file diff --git a/src/gametest/resources/data/scl_tests/structure/basic.nbt b/src/gametest/resources/data/scl_tests/structure/basic.nbt new file mode 100644 index 0000000000000000000000000000000000000000..543e390d26c2f0c2121ba8f2d87ca1a252d2dcf6 GIT binary patch literal 647 zcmb2|=3oGW|8s8~%)0F$aO~sFnSn_+w`}=xDgEWn>2Y2QFJw=6yH!`oJFVO~V`2a8 z?dim@o>9FsG zc6C2vC(E>Dbf42-sI^S(=jE=I#~68;dl`Duq}XSyJvkGOBn_e`8^h83_v{y*oVf-= zTqsKC>*$|(=k>Uutk@{#eu^O?Tr1V)2~lhZ(?c8oW^*DX-4Xy8lVlV z2HFW>2i7!1GHzx{v%1@VsiyE?j&J6^9SPf(nZ&DZHeLEtYo=^MS;I_5pgy)UJToK> zxD&(<=rlw!Y-UJfJ;OCa6l{dgH%^nKl2e*)&bq|ob?^-XP^Y90aXOc;Gii=8hz_3R zs5@h==GlakKnJjSr?JnFO-MU%24*wU3|5%UAh&{SF1=)<{&M=tWv{$H{a)^FaJ8bw z)aJ{}iaQhQ-nTtTJMwmx-MicO);n$XoIh)?|L*yQcaO#jUo+Y07Pryu|Jt7)_N3kB z?KkAzJO9+`@HvmRZFy#YXjd=$eRuhFe?IQsV`yZyy?xnd&7Y5t%G6J&ll6CaKmU95 zuTqJy?{ifUZE7}9~vYla?!D?_?_VeOpX<`S~G(<9PW=dl{!!_e& zjeXSbqr96<*ZzIfc5~LXx~*rfZTYr3wXESLBTyI53`qmygtP-X4Ur6+8PXVm#)ukd zCxn5GFurMhVM|$nR9beFwFd8Ipbxf`Z6HqP+}+psvJU6?X0vZi+_r3Mvo6p9vNydt^7prY5*d~~E8pyY{?Ywr{IR{e`@HSGt$$vsdNea^ v*UWEQzP`42