From 92273bd047ffb91cf74932166db3d3aa0255b007 Mon Sep 17 00:00:00 2001 From: Algent Date: Sat, 5 Sep 2026 00:04:06 +0200 Subject: [PATCH] test: characterize tile loading contracts --- .../TileMultipartLifecycleFunctionalTest.java | 34 +++- .../multipart/TileMultipartLoadingTest.java | 164 ++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/test/java/codechicken/multipart/TileMultipartLoadingTest.java diff --git a/src/functionalTest/java/codechicken/multipart/test/TileMultipartLifecycleFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/TileMultipartLifecycleFunctionalTest.java index be1c8da..bb51636 100644 --- a/src/functionalTest/java/codechicken/multipart/test/TileMultipartLifecycleFunctionalTest.java +++ b/src/functionalTest/java/codechicken/multipart/test/TileMultipartLifecycleFunctionalTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.BiConsumer; import net.minecraft.block.Block; import net.minecraft.server.MinecraftServer; @@ -20,6 +21,7 @@ import codechicken.multipart.TMultiPart; import codechicken.multipart.TSlottedPart; import codechicken.multipart.TileMultipart; import codechicken.multipart.scalatraits.TSlottedTile; +import scala.collection.JavaConversions; /** Consumer-visible multipart ordering and lifecycle behavior that requires a real world and generated tile. */ class TileMultipartLifecycleFunctionalTest { @@ -28,6 +30,7 @@ class TileMultipartLifecycleFunctionalTest { private static final BlockCoord MOVE_FROM = new BlockCoord(34, 200, 32); private static final BlockCoord MOVE_TO = new BlockCoord(36, 200, 32); private static final BlockCoord TRAVERSAL_POS = new BlockCoord(38, 200, 32); + private static final BlockCoord LOADING_POS = new BlockCoord(40, 200, 32); @Test void addAndRemovePreserveOrderSlotsAndPartCallbackOrder() { @@ -157,6 +160,35 @@ class TileMultipartLifecycleFunctionalTest { } } + @Test + void legacyLoadingRebuildsGeneratedSlotsAndNotifiesAfterBinding() { + checkLoading((tile, parts) -> tile.loadParts(JavaConversions.asScalaBuffer(parts).toList())); + } + + private static void checkLoading(BiConsumer> load) { + World world = world(); + clear(world, LOADING_POS); + List events = new ArrayList<>(); + RecordingPart old = new RecordingPart("old", 2, events); + RecordingPart first = new RecordingPart("first", 7, events); + RecordingPart second = new RecordingPart("second", 8, events); + try { + TileMultipart tile = TileMultipart.addPart(world, LOADING_POS, old); + events.clear(); + load.accept(tile, Arrays.asList(first, second)); + assertEquals(Arrays.asList("first.bind", "second.bind", "first.changed:all", "second.changed:all"), events); + assertEquals(Arrays.asList(first, second), tile.jPartList()); + assertNull(tile.partMap(2)); + assertSame(first, tile.partMap(7)); + assertSame(second, tile.partMap(8)); + assertSame(tile, first.tile()); + assertSame(tile, second.tile()); + assertSame(tile, old.tile(), "Loading is not removal of previously bound parts"); + } finally { + clear(world, LOADING_POS); + } + } + private static World world() { World world = MinecraftServer.getServer().worldServers[0]; world.getChunkFromBlockCoords(ADD_REMOVE_POS.x, ADD_REMOVE_POS.z); @@ -214,7 +246,7 @@ class TileMultipartLifecycleFunctionalTest { @Override public void onPartChanged(TMultiPart part) { - events.add(name + ".changed:" + ((RecordingPart) part).name); + events.add(name + ".changed:" + (part == null ? "all" : ((RecordingPart) part).name)); } @Override diff --git a/src/test/java/codechicken/multipart/TileMultipartLoadingTest.java b/src/test/java/codechicken/multipart/TileMultipartLoadingTest.java new file mode 100644 index 0000000..edc234c --- /dev/null +++ b/src/test/java/codechicken/multipart/TileMultipartLoadingTest.java @@ -0,0 +1,164 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import scala.collection.JavaConversions; +import scala.collection.Seq; + +/** Storage assignment and reconstruction contracts used by GuideNH and Schematica. */ +class TileMultipartLoadingTest { + + @Test + void legacySetterSharesStorageWithoutBindingOrChangingTickState() { + TileMultipart tile = new TileMultipart(); + Part part = new Part("part", new ArrayList<>()); + List input = new ArrayList<>(Arrays.asList(part)); + Seq sequence = JavaConversions.asScalaBuffer(input); + tile.partList_$eq(sequence); + + assertSame(sequence, tile.partList()); + assertNull(part.tile()); + assertFalse(tile.canUpdate()); + input.clear(); + assertTrue(tile.jPartList().isEmpty()); + tile.partList_$eq(null); + assertNull(tile.partList()); + } + + @Test + void legacyLoadingRebuildsThroughHooksButDoesNotRemoveOldPartsOrResetTicking() { + List events = new ArrayList<>(); + LoadingTile tile = new LoadingTile(events); + Part old = new Part("old", events); + tile.addPart_do(old); + events.clear(); + Part first = new Part("first", events); + Part second = new Part("second", events); + tile.loadParts(seq(first, second)); + + assertEquals(Arrays.asList("clear", "cache:first", "bind:first", "cache:second", "bind:second"), events); + assertEquals(Arrays.asList(first, second), tile.jPartList()); + assertSame(tile, first.tile()); + assertSame(tile, second.tile()); + assertSame(tile, old.tile()); + assertTrue(tile.canUpdate()); + events.clear(); + tile.loadParts(seq()); + assertEquals(Arrays.asList("clear"), events); + assertTrue(tile.jPartList().isEmpty()); + assertTrue(tile.canUpdate(), "Loading does not reset the existing ticking flag"); + } + + @Test + void legacyLoadingFailuresLeaveClearedOrPartiallyLoadedState() { + List events = new ArrayList<>(); + LoadingTile tile = new LoadingTile(events); + Part old = new Part("old", events); + tile.addPart_do(old); + assertThrows(NullPointerException.class, () -> tile.loadParts((scala.collection.Iterable) null)); + assertTrue(tile.jPartList().isEmpty()); + assertSame(tile, old.tile()); + + Part first = new Part("first", events); + Part last = new Part("last", events); + assertThrows(NullPointerException.class, () -> tile.loadParts(seq(first, null, last))); + assertEquals(Arrays.asList(first, null), tile.jPartList()); + assertSame(tile, first.tile()); + assertNull(last.tile()); + } + + @Test + void legacyReflectionResolvesTheSetterAndOneAssignableLoader() throws Exception { + TileMultipart tile = new TileMultipart(); + Part part = new Part("part", new ArrayList<>()); + Seq input = seq(part); + TileMultipart.class.getMethod("partList_$eq", Seq.class).invoke(tile, input); + assertSame(input, tile.partList()); + assertNull(part.tile()); + Method exact = TileMultipart.class.getMethod("loadParts", scala.collection.Iterable.class); + List matches = new ArrayList<>(); + for (Method method : TileMultipart.class.getMethods()) { + if (method.getName().equals("loadParts") && method.getParameterCount() == 1 + && method.getParameterTypes()[0].isAssignableFrom(input.getClass())) { + matches.add(method); + } + } + assertEquals(Arrays.asList(exact), matches); + matches.get(0).invoke(tile, input); + assertSame(tile, part.tile()); + assertEquals(Arrays.asList(part), tile.jPartList()); + } + + private static Seq seq(TMultiPart... parts) { + return JavaConversions.asScalaBuffer(Arrays.asList(parts)).toList(); + } + + private static class LoadingTile extends TileMultipart { + + private final List events; + + private LoadingTile(List events) { + this.events = events; + } + + @Override + public void clearParts() { + events.add("clear"); + super.clearParts(); + } + + @Override + public void bindPart(TMultiPart part) { + events.add("cache:" + (part == null ? "null" : part.getType())); + } + } + + private static class Part extends TMultiPart { + + private final String name; + private final List events; + + private Part(String name, List events) { + this.name = name; + this.events = events; + } + + @Override + public String getType() { + return name; + } + + @Override + public void bind(TileMultipart tile) { + events.add("bind:" + name); + super.bind(tile); + } + + @Override + public void onAdded() { + events.add("added:" + name); + } + + @Override + public void onRemoved() { + events.add("removed:" + name); + } + + @Override + public void onWorldJoin() { + events.add("join:" + name); + } + } +}