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 0000000..543e390 Binary files /dev/null and b/src/gametest/resources/data/scl_tests/structure/basic.nbt differ diff --git a/src/gametest/resources/data/scl_tests/structure/empty.nbt b/src/gametest/resources/data/scl_tests/structure/empty.nbt new file mode 100644 index 0000000..c0467ab Binary files /dev/null and b/src/gametest/resources/data/scl_tests/structure/empty.nbt differ diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json new file mode 100644 index 0000000..1eac9a1 --- /dev/null +++ b/src/gametest/resources/fabric.mod.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": 1, + "id": "scl_tests", + "version": "0.0.0", + "name": "Script's Chunk Loaders Tests", + "environment": "*", + "entrypoints": { + "fabric-gametest": [ + "io.nihlen.scriptschunkloaders.ScriptsChunkLoadersGameTest" + ] + } +} \ No newline at end of file