diff --git a/src/functionalTest/java/codechicken/multipart/test/MultipartHelperFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MultipartHelperFunctionalTest.java new file mode 100644 index 0000000..e26a623 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MultipartHelperFunctionalTest.java @@ -0,0 +1,129 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.List; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.multipart.MultiPartRegistry; +import codechicken.multipart.MultipartHelper; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import codechicken.multipart.handler.MultipartSaveLoad; +import scala.collection.JavaConversions; + +/** + * The half of MultipartHelper a plain JVM test cannot reach. Tile construction runs the ASM generator, the NBT path + * needs the save/load hooks, and the packet path needs a loaded chunk. + */ +class MultipartHelperFunctionalTest { + + @Test + void buildsATileAroundTheGivenParts() { + TMultiPart torch = MultiPartRegistry.loadPart("mc_torch", null); + assertNotNull(torch); + + TileMultipart tile = MultipartHelper.createTileFromParts(Collections.singletonList(torch)); + + assertNotNull(tile); + assertEquals(1, tile.jPartList().size()); + assertSame(torch, tile.jPartList().get(0)); + assertSame(tile, torch.tile()); + } + + /** + * Multipart tiles all save themselves under the id "savedMultipart", which is the whole reason this entry point + * exists: the tile has to be rebuilt from NBT without waiting for the ChunkLoad event. + */ + @Test + void rebuildsATileFromItsOwnNbtAndPublishesTheLoadingWorld() { + World world = world(); + TMultiPart torch = MultiPartRegistry.loadPart("mc_torch", null); + TileMultipart saved = MultipartHelper.createTileFromParts(Collections.singletonList(torch)); + + NBTTagCompound tag = new NBTTagCompound(); + saved.writeToNBT(tag); + assertEquals("savedMultipart", tag.getString("id")); + + MultipartSaveLoad.loadingWorld_$eq(null); + TileEntity loaded = MultipartHelper.createTileFromNBT(world, tag); + + TileMultipart tile = assertInstanceOf(TileMultipart.class, loaded); + assertEquals(1, tile.jPartList().size()); + assertEquals("mc_torch", tile.jPartList().get(0).getType()); + assertSame(world, MultipartSaveLoad.loadingWorld()); + } + + /** The id guard runs before anything else, so a foreign tag must not disturb the loading world. */ + @Test + void leavesTheLoadingWorldAloneForATagItDoesNotOwn() { + World world = world(); + MultipartSaveLoad.loadingWorld_$eq(world); + + NBTTagCompound tag = new NBTTagCompound(); + tag.setString("id", "somethingElse"); + + assertNull(MultipartHelper.createTileFromNBT(null, tag)); + assertSame(world, MultipartSaveLoad.loadingWorld()); + } + + @Test + void registersATileConverterOntoTheSaveLoadList() { + MarkerConverter converter = new MarkerConverter(); + + MultipartHelper.registerTileConverter(converter); + + List converters = JavaConversions.seqAsJavaList(MultipartSaveLoad.converters()); + assertTrue(converters.contains(converter), "registerTileConverter must append to MultipartSaveLoad.converters"); + } + + /** Needs a loaded chunk. With no players watching, the send itself is a no-op, but the packet is still built. */ + @Test + void sendsADescPacketForATileInALoadedChunk() { + World world = world(); + TMultiPart torch = MultiPartRegistry.loadPart("mc_torch", null); + TileMultipart tile = MultipartHelper.createTileFromParts(Collections.singletonList(torch)); + tile.setWorldObj(world); + tile.xCoord = 0; + tile.yCoord = 64; + tile.zCoord = 0; + + assertNotNull(world.getChunkFromBlockCoords(0, 0)); + assertDoesNotThrow(() -> MultipartHelper.sendDescPacket(world, tile)); + } + + private static World world() { + World world = MinecraftServer.getServer().worldServers[0]; + assertNotNull(world); + return world; + } + + /** Matches only its own tile class, so registering it cannot affect any real chunk load. */ + private static final class MarkerTile extends TileEntity { + } + + private static final class MarkerConverter extends MultipartHelper.IPartTileConverter { + + MarkerConverter() { + super(MarkerTile.class); + } + + @Override + public TMultiPart convertOne(MarkerTile tile) { + return null; + } + } +} diff --git a/src/test/java/codechicken/multipart/MultipartHelperCharacterizationTest.java b/src/test/java/codechicken/multipart/MultipartHelperCharacterizationTest.java new file mode 100644 index 0000000..db983a3 --- /dev/null +++ b/src/test/java/codechicken/multipart/MultipartHelperCharacterizationTest.java @@ -0,0 +1,184 @@ +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.assertNotNull; +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.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +/** + * Covers the part of MultipartHelper that does not need a world: the id guard on createTileFromNBT and the whole of + * IPartTileConverter. registerTileConverter, createTileFromParts and sendDescPacket need the save/load hooks, the ASM + * generator or a chunk, so the Forge server suite covers those instead. + */ +class MultipartHelperCharacterizationTest { + + /** + * MultipartSaveLoad cannot class-initialize headless -- it reflects into TileEntity's static maps through + * ObfMapping. That makes it a usable probe: returning null rather than raising a LinkageError is what proves the id + * guard short-circuits before the loadingWorld assignment. + */ + @Test + void aTagThatIsNotASavedMultipartReturnsNullBeforeTouchingTheLoader() { + assertNull(MultipartHelper.createTileFromNBT(null, new NBTTagCompound())); + + NBTTagCompound wrongId = new NBTTagCompound(); + wrongId.setString("id", "somethingElse"); + assertNull(MultipartHelper.createTileFromNBT(null, wrongId)); + } + + /** The other half of the same probe: the matching branch does reach the loader. */ + @Test + void aSavedMultipartTagReachesTheLoader() { + NBTTagCompound tag = new NBTTagCompound(); + tag.setString("id", "savedMultipart"); + + assertThrows(LinkageError.class, () -> MultipartHelper.createTileFromNBT(null, tag)); + } + + @Test + void aConverterMatchesOnlyInstancesOfItsOwnTileClass() { + CountingConverter converter = new CountingConverter(TileMultipart.class); + + assertSame(TileMultipart.class, converter.clazz()); + assertTrue(converter.canConvert(new TileMultipart())); + assertFalse(converter.canConvert(new PlainTile())); + assertFalse(converter.canConvert(null)); + } + + @Test + void convertMultiYieldsNothingWhenConvertOneReturnsNull() { + CountingConverter converter = new CountingConverter(TileMultipart.class); + converter.result = null; + + assertTrue(drain(converter.convertMulti(new TileMultipart())).isEmpty()); + } + + @Test + void convertMultiWrapsTheSinglePartConvertOneReturns() { + CountingConverter converter = new CountingConverter(TileMultipart.class); + NamedPart part = new NamedPart("only"); + converter.result = part; + + List parts = drain(converter.convertMulti(new TileMultipart())); + + assertEquals(1, parts.size()); + assertSame(part, parts.get(0)); + } + + @Test + void convertCastsTheTileAndRoutesThroughConvertMulti() { + CountingConverter converter = new CountingConverter(TileMultipart.class); + TileMultipart tile = new TileMultipart(); + converter.result = new NamedPart("routed"); + + List parts = drain(converter.convert(tile)); + + assertEquals(1, converter.convertMultiCalls); + assertSame(tile, converter.lastConvertMultiArgument); + assertEquals(1, parts.size()); + } + + @Test + void convertFailsOnATileTheConverterDoesNotAccept() { + CountingConverter converter = new CountingConverter(TileMultipart.class); + + assertThrows(ClassCastException.class, () -> converter.convert(new PlainTile())); + } + + /** + * guidenh reflects on both of these names and on nothing else here, so the ABI diff cannot see this break. The two + * statics are the members other jars link against. + */ + @Test + void theNamesAndStaticsDownstreamReflectsOnStillResolve() throws Exception { + Method createTileFromNBT = MultipartHelper.class + .getDeclaredMethod("createTileFromNBT", World.class, NBTTagCompound.class); + assertSame(TileEntity.class, createTileFromNBT.getReturnType()); + assertPublicStatic(createTileFromNBT); + + Method sendDescPacket = MultipartHelper.class + .getDeclaredMethod("sendDescPacket", World.class, TileEntity.class); + assertSame(void.class, sendDescPacket.getReturnType()); + assertPublicStatic(sendDescPacket); + + Class companion = Class.forName("codechicken.multipart.MultipartHelper$"); + Field module = companion.getField("MODULE$"); + assertSame(companion, module.getType()); + assertPublicStatic(module); + assertNotNull(module.get(null)); + } + + private static void assertPublicStatic(Method method) { + assertTrue(Modifier.isPublic(method.getModifiers()), method + " must stay public"); + assertTrue(Modifier.isStatic(method.getModifiers()), method + " must stay static"); + } + + private static void assertPublicStatic(Field field) { + assertTrue(Modifier.isPublic(field.getModifiers()), field + " must stay public"); + assertTrue(Modifier.isStatic(field.getModifiers()), field + " must stay static"); + } + + private static List drain(Iterable parts) { + List drained = new ArrayList<>(); + for (Iterator iterator = parts.iterator(); iterator.hasNext();) { + drained.add(iterator.next()); + } + return drained; + } + + private static class CountingConverter extends MultipartHelper.IPartTileConverter { + + TMultiPart result; + int convertMultiCalls; + TileMultipart lastConvertMultiArgument; + + CountingConverter(Class clazz) { + super(clazz); + } + + @Override + public Iterable convertMulti(TileMultipart tile) { + convertMultiCalls++; + lastConvertMultiArgument = tile; + return super.convertMulti(tile); + } + + @Override + public TMultiPart convertOne(TileMultipart tile) { + return result; + } + } + + private static final class PlainTile extends TileEntity { + } + + private static final class NamedPart extends TMultiPart { + + private final String type; + + NamedPart(String type) { + this.type = type; + } + + @Override + public String getType() { + return type; + } + } +}