test: characterize tile loading contracts

This commit is contained in:
Algent committed 2026-09-05 00:04:06 +02:00
1 parent 533dc6a11e
commit 92273bd047
2 files changed
+197 -1

No files matched your search

@@ -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<TileMultipart, List<TMultiPart>> load) {
World world = world();
clear(world, LOADING_POS);
List<String> 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
@@ -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<TMultiPart> input = new ArrayList<>(Arrays.asList(part));
Seq<TMultiPart> 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<String> 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<String> 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<TMultiPart>) 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<TMultiPart> 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<Method> 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<TMultiPart> seq(TMultiPart... parts) {
return JavaConversions.asScalaBuffer(Arrays.asList(parts)).toList();
}
private static class LoadingTile extends TileMultipart {
private final List<String> events;
private LoadingTile(List<String> 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<String> events;
private Part(String name, List<String> 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);
}
}
}