diff --git a/src/functionalTest/java/codechicken/multipart/test/MultipartSaveLoadFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MultipartSaveLoadFunctionalTest.java new file mode 100644 index 0000000..282feef --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MultipartSaveLoadFunctionalTest.java @@ -0,0 +1,258 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Objects; +import java.util.Scanner; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.ChunkPosition; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; + +import org.junit.jupiter.api.Test; + +import codechicken.multipart.MultipartHelper; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import codechicken.multipart.handler.MultipartSaveLoad; +import codechicken.multipart.handler.MultipartSaveLoad$; +import codechicken.multipart.minecraft.ButtonPart; +import codechicken.multipart.minecraft.TorchPart; + +class MultipartSaveLoadFunctionalTest { + + @Test + void referenceScalaConsumerStillLinksToTheCompanionState() throws Exception { + World original = MultipartSaveLoad.loadingWorld(); + World world = world(); + try { + Class fixtureClass = new FixtureClassLoader(MultipartSaveLoad.class.getClassLoader()) + .define(loadFixture()); + Object fixture = fixtureClass.getDeclaredConstructor().newInstance(); + + fixtureClass.getMethod("loadingWorld_$eq", World.class).invoke(fixture, world); + + assertSame(world, MultipartSaveLoad.loadingWorld()); + assertSame(world, MultipartSaveLoad$.MODULE$.loadingWorld()); + assertSame(world, fixtureClass.getMethod("loadingWorld").invoke(fixture)); + assertSame(MultipartSaveLoad.converters(), MultipartSaveLoad$.MODULE$.converters()); + } finally { + MultipartSaveLoad.loadingWorld_$eq(original); + } + } + + @Test + void hooksTheDummyTileAndGeneratedTileNamesIntoVanillaMaps() { + MultipartSaveLoad.hookLoader(); + + NBTTagCompound saved = new NBTTagCompound(); + saved.setString("id", "savedMultipart"); + saved.setInteger("x", 7); + saved.setInteger("y", 8); + saved.setInteger("z", 9); + TileEntity dummy = TileEntity.createAndLoadEntity(saved); + + assertNotNull(dummy); + assertEquals("codechicken.multipart.handler.MultipartSaveLoad$TileNBTContainer", dummy.getClass().getName()); + assertSame(saved, tag(dummy)); + assertEquals(7, dummy.xCoord); + assertEquals(8, dummy.yCoord); + assertEquals(9, dummy.zCoord); + + MultipartSaveLoad.registerTileClass(RegisteredTile.class); + assertEquals("savedMultipart", MultipartSaveLoad.getClassToNameMap().get(RegisteredTile.class)); + NBTTagCompound registered = new NBTTagCompound(); + new RegisteredTile().writeToNBT(registered); + assertEquals("savedMultipart", registered.getString("id")); + } + + @Test + void leavesUnclaimedTilesRemovesEmptyConversionsAndUsesTheFirstMatch() { + World world = world(); + Chunk chunk = new Chunk(world, 40, 40); + UnclaimedTile unclaimed = tile(new UnclaimedTile(), world); + ConvertedTile converted = tile(new ConvertedTile(), world); + DeletedTile deleted = tile(new DeletedTile(), world); + ChunkPosition unclaimedPosition = put(chunk, 1, unclaimed); + ChunkPosition convertedPosition = put(chunk, 2, converted); + ChunkPosition deletedPosition = put(chunk, 3, deleted); + + RecordingConverter first = new RecordingConverter(); + RejectingSecondConverter second = new RejectingSecondConverter(); + MultipartHelper.registerTileConverter(first); + MultipartHelper.registerTileConverter(second); + MultipartHelper.registerTileConverter(new DeletingConverter()); + + MultipartSaveLoad.loadTiles(chunk); + + assertSame(world, MultipartSaveLoad.loadingWorld()); + assertSame(unclaimed, chunk.chunkTileEntityMap.get(unclaimedPosition)); + assertFalse(chunk.chunkTileEntityMap.containsKey(deletedPosition)); + TileMultipart replacement = assertInstanceOf( + TileMultipart.class, + chunk.chunkTileEntityMap.get(convertedPosition)); + assertSame(world, replacement.getWorldObj()); + assertEquals(1, replacement.jPartList().size()); + assertSame(first.part, replacement.jPartList().get(0)); + assertEquals(1, first.calls); + assertEquals(0, second.calls); + } + + @Test + void rebuildsSavedDummyTilesAndLeavesForeignDummiesUntouched() { + World world = world(); + Chunk chunk = new Chunk(world, 41, 41); + TileMultipart savedTile = MultipartHelper + .createTileFromParts(Arrays.asList(new TorchPart(5), new ButtonPart(1))); + NBTTagCompound savedTag = new NBTTagCompound(); + savedTile.writeToNBT(savedTag); + TileEntity savedDummy = dummy(savedTag, world); + ChunkPosition savedPosition = put(chunk, 1, savedDummy); + + NBTTagCompound foreignTag = new NBTTagCompound(); + foreignTag.setString("id", "foreign"); + TileEntity foreignDummy = dummy(foreignTag, world); + ChunkPosition foreignPosition = put(chunk, 2, foreignDummy); + + MultipartSaveLoad.loadTiles(chunk); + + TileMultipart rebuilt = assertInstanceOf(TileMultipart.class, chunk.chunkTileEntityMap.get(savedPosition)); + assertSame(world, rebuilt.getWorldObj()); + assertEquals(2, rebuilt.jPartList().size()); + assertEquals("mc_torch", rebuilt.jPartList().get(0).getType()); + assertEquals("mc_button", rebuilt.jPartList().get(1).getType()); + assertSame(foreignDummy, chunk.chunkTileEntityMap.get(foreignPosition)); + } + + private static TileEntity dummy(NBTTagCompound tag, World world) { + try { + TileEntity dummy = (TileEntity) Class + .forName("codechicken.multipart.handler.MultipartSaveLoad$TileNBTContainer") + .getDeclaredConstructor().newInstance(); + dummy.readFromNBT(tag); + dummy.setWorldObj(world); + return dummy; + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + private static Object tag(TileEntity tile) { + try { + return tile.getClass().getMethod("tag").invoke(tile); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + private static T tile(T tile, World world) { + tile.setWorldObj(world); + return tile; + } + + private static ChunkPosition put(Chunk chunk, int offset, TileEntity tile) { + ChunkPosition position = new ChunkPosition(chunk.xPosition * 16 + offset, 64, chunk.zPosition * 16 + offset); + chunk.chunkTileEntityMap.put(position, tile); + return position; + } + + private static WorldServer world() { + WorldServer world = MinecraftServer.getServer().worldServers[0]; + assertNotNull(world); + assertFalse(world.isRemote); + return world; + } + + private static byte[] loadFixture() { + InputStream input = Objects.requireNonNull( + MultipartSaveLoadFunctionalTest.class + .getResourceAsStream("/compat/ReferenceScalaMultipartSaveLoadConsumer.class.b64")); + try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) { + return Base64.getMimeDecoder().decode(scanner.next()); + } + } + + private static final class FixtureClassLoader extends ClassLoader { + + private FixtureClassLoader(ClassLoader parent) { + super(parent); + } + + private Class define(byte[] bytecode) { + return defineClass(null, bytecode, 0, bytecode.length); + } + } + + private static class RegisteredTile extends TileEntity { + } + + private static final class UnclaimedTile extends TileEntity { + } + + private static final class ConvertedTile extends TileEntity { + } + + private static final class DeletedTile extends TileEntity { + } + + private static final class RecordingConverter extends MultipartHelper.IPartTileConverter { + + private final TMultiPart part = new TorchPart(3); + private int calls; + + private RecordingConverter() { + super(ConvertedTile.class); + } + + @Override + public TMultiPart convertOne(ConvertedTile tile) { + calls++; + return part; + } + } + + private static final class RejectingSecondConverter extends MultipartHelper.IPartTileConverter { + + private int calls; + + private RejectingSecondConverter() { + super(ConvertedTile.class); + } + + @Override + public TMultiPart convertOne(ConvertedTile tile) { + calls++; + return new ButtonPart(1); + } + } + + private static final class DeletingConverter extends MultipartHelper.IPartTileConverter { + + private DeletingConverter() { + super(DeletedTile.class); + } + + @Override + public Iterable convertMulti(DeletedTile tile) { + return Collections.emptyList(); + } + + @Override + public TMultiPart convertOne(DeletedTile tile) { + throw new AssertionError("convertMulti supplies the deletion result"); + } + } +} diff --git a/src/functionalTest/resources/compat/ReferenceScalaMultipartSaveLoadConsumer.class.b64 b/src/functionalTest/resources/compat/ReferenceScalaMultipartSaveLoadConsumer.class.b64 new file mode 100644 index 0000000..5d78cc7 --- /dev/null +++ b/src/functionalTest/resources/compat/ReferenceScalaMultipartSaveLoadConsumer.class.b64 @@ -0,0 +1 @@ +yv66vgAAADQAJQEARGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFNdWx0aXBhcnRTYXZlTG9hZENvbnN1bWVyBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEALVJlZmVyZW5jZVNjYWxhTXVsdGlwYXJ0U2F2ZUxvYWRDb25zdW1lci5zY2FsYQEAHkxzY2FsYS9yZWZsZWN0L1NjYWxhU2lnbmF0dXJlOwEABWJ5dGVzAQE/BgElMkEhAQIBEwk5IysaNGZlFnQ3LVoqZEMyDFcqHjd1U0IMJw9eKmJtFmR1LhkzRF86HFgvXDNzFQkZQSEBBGRfNgQYDR4GAwsZCREiXDttaSYEGE0dOwsDHQkxYlk4ZUsORBduWTZmXQ4BMUMBAQshCVlhIkQBDRUFaREhQjpkQzIMF0JBCA0FGQllLh8qZk0iKREDAUMBJQUxQSgbOGppeiISYQUJAykBaRFBAQUGLQEhCWFGAQ1ZPgxHLRs4aC8+FEgOWgsCMUEREQRJBwI1KRExBEgBBm8+FEgOGgYDO3kJESJcNW9LDhQYTRo7CwN9CTFBXDN1EwkJI0RBA1hfSmRHDUMDJAERBUElAQltXwYkF05cNFhfSmRHbRgTZmNSEVEFCwkDFxlKIWEKBwMJVXMXDh4FBjcJAg0BBwEADGxvYWRpbmdXb3JsZAEAHSgpTG5ldC9taW5lY3JhZnQvd29ybGQvV29ybGQ7AQAwY29kZWNoaWNrZW4vbXVsdGlwYXJ0L2hhbmRsZXIvTXVsdGlwYXJ0U2F2ZUxvYWQkBwALAQAHTU9EVUxFJAEAMkxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvaGFuZGxlci9NdWx0aXBhcnRTYXZlTG9hZCQ7DAANAA4JAAwADwwACQAKCgAMABEBAAR0aGlzAQBGTGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFNdWx0aXBhcnRTYXZlTG9hZENvbnN1bWVyOwEAEGxvYWRpbmdXb3JsZF8kZXEBAB4oTG5ldC9taW5lY3JhZnQvd29ybGQvV29ybGQ7KVYMABUAFgoADAAXAQAFd29ybGQBABtMbmV0L21pbmVjcmFmdC93b3JsZC9Xb3JsZDsBAAY8aW5pdD4BAAMoKVYMABsAHAoABAAdAQAEQ29kZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAD0xpbmVOdW1iZXJUYWJsZQEAClNvdXJjZUZpbGUBABlSdW50aW1lVmlzaWJsZUFubm90YXRpb25zAQAIU2NhbGFTaWcAIQACAAQAAAAAAAMAAQAJAAoAAQAfAAAAMQABAAEAAAAHsgAQtgASsAAAAAIAIAAAAAwAAQAAAAcAEwAUAAAAIQAAAAYAAQAAAAcAAQAVABYAAQAfAAAAPAACAAIAAAAIsgAQK7YAGLEAAAACACAAAAAWAAIAAAAIABMAFAAAAAAACAAZABoAAQAhAAAABgABAAAACgABABsAHAABAB8AAAAvAAEAAQAAAAUqtwAesQAAAAIAIAAAAAwAAQAAAAUAEwAUAAAAIQAAAAYAAQAAAAwAAwAiAAAAAgAFACMAAAALAAEABgABAAdzAAgAJAAAAAMFAAA= diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 92e7cd3..9b93807 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -124,3 +124,13 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the referenc (SHA-256 `122d89cf0a070cfd2325bbbcf4bb4550d6130bb7da1ffba1a3b2e2efea6ec7e5`). Class-file SHA-256 is `e66be14e6422277cc9567acf8e96a26e419abf5ec6582085c8c6e32cc60aa599`, stored as `src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64`. + +### `ReferenceScalaMultipartSaveLoadConsumer` + +`scala/codechicken/multipart/compat/ReferenceScalaMultipartSaveLoadConsumer.scala` reads +`MultipartSaveLoad$.MODULE$` and calls the `loadingWorld` getter and setter that ProjRed links against. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the reference dev jar built at `471c767` +(SHA-256 `1f571035283b5ca9120bf817f00ce961ebcbae258d901fb8ee9147636f71a481`). Class-file SHA-256 is +`27543f97e0973f042c3867c1aa0ae5598324c60c895c8d9a7ea5a6cea2f5ee70`, stored as +`src/functionalTest/resources/compat/ReferenceScalaMultipartSaveLoadConsumer.class.b64`. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMultipartSaveLoadConsumer.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMultipartSaveLoadConsumer.scala new file mode 100644 index 0000000..7cc72e9 --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMultipartSaveLoadConsumer.scala @@ -0,0 +1,15 @@ +package codechicken.multipart.compat + +import codechicken.multipart.handler.MultipartSaveLoad +import net.minecraft.world.World + +/** Compiled against the reference dev jar. Reads MultipartSaveLoad$.MODULE$ and calls the loadingWorld getter/setter + * that ProjectRed links against. + */ +class ReferenceScalaMultipartSaveLoadConsumer { + def loadingWorld: World = MultipartSaveLoad.loadingWorld + + def loadingWorld_=(world: World) { + MultipartSaveLoad.loadingWorld = world + } +} diff --git a/src/test/java/codechicken/multipart/handler/MultipartSaveLoadCharacterizationTest.java b/src/test/java/codechicken/multipart/handler/MultipartSaveLoadCharacterizationTest.java new file mode 100644 index 0000000..723dab1 --- /dev/null +++ b/src/test/java/codechicken/multipart/handler/MultipartSaveLoadCharacterizationTest.java @@ -0,0 +1,129 @@ +package codechicken.multipart.handler; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +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.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.tileentity.TileEntity; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; + +class MultipartSaveLoadCharacterizationTest { + + private static final Set METHODS = new TreeSet<>( + Arrays.asList( + "converters()Lscala/collection/mutable/MutableList;", + "getClassToNameMap()Ljava/util/Map;", + "hookLoader()V", + "loadTiles(Lnet/minecraft/world/chunk/Chunk;)V", + "loadingWorld()Lnet/minecraft/world/World;", + "loadingWorld_$eq(Lnet/minecraft/world/World;)V", + "registerTileClass(Ljava/lang/Class;)V")); + + @Test + void keepsTheStaticFacadeAndLoadBearingCompanion() throws Exception { + Class facade = load("codechicken.multipart.handler.MultipartSaveLoad"); + Class companion = load("codechicken.multipart.handler.MultipartSaveLoad$"); + + assertTrue(Modifier.isPublic(facade.getModifiers())); + assertTrue(Modifier.isFinal(facade.getModifiers())); + assertEquals(METHODS, publicMethodSignatures(facade)); + for (Method method : facade.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + assertTrue(Modifier.isStatic(method.getModifiers()), method.toString()); + } + } + + assertTrue(Modifier.isPublic(companion.getModifiers())); + assertTrue(Modifier.isFinal(companion.getModifiers())); + assertSame(Object.class, companion.getSuperclass()); + assertArrayEquals(new Class[0], companion.getInterfaces()); + assertEquals(METHODS, publicMethodSignatures(companion)); + for (Method method : companion.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + assertFalse(Modifier.isStatic(method.getModifiers()), method.toString()); + } + } + + Field module = companion.getField("MODULE$"); + assertSame(companion, module.getType()); + assertTrue(Modifier.isStatic(module.getModifiers())); + assertTrue(Modifier.isFinal(module.getModifiers())); + } + + @Test + void keepsTheCompanionStateFields() throws Exception { + Class companion = load("codechicken.multipart.handler.MultipartSaveLoad$"); + + assertField(companion, "converters", "scala.collection.mutable.MutableList", true); + assertField(companion, "loadingWorld", "net.minecraft.world.World", false); + assertField(companion, "classToNameMap", "java.util.Map", true); + } + + @Test + void keepsTheNbtContainerShapeAndReadBehavior() throws Exception { + Class type = load("codechicken.multipart.handler.MultipartSaveLoad$TileNBTContainer"); + assertTrue(Modifier.isPublic(type.getModifiers())); + assertTrue(Modifier.isStatic(type.getModifiers())); + assertFalse(Modifier.isFinal(type.getModifiers())); + assertSame(load("codechicken.multipart.handler.MultipartSaveLoad"), type.getEnclosingClass()); + assertSame(TileEntity.class, type.getSuperclass()); + assertArrayEquals(new Class[0], type.getInterfaces()); + assertEquals( + new TreeSet<>( + Arrays.asList( + "readFromNBT(Lnet/minecraft/nbt/NBTTagCompound;)V", + "tag()Lnet/minecraft/nbt/NBTTagCompound;", + "tag_$eq(Lnet/minecraft/nbt/NBTTagCompound;)V")), + publicMethodSignatures(type)); + + Field field = type.getDeclaredField("tag"); + assertSame(NBTTagCompound.class, field.getType()); + assertTrue(Modifier.isPrivate(field.getModifiers())); + assertFalse(Modifier.isFinal(field.getModifiers())); + + TileEntity tile = (TileEntity) type.getDeclaredConstructor().newInstance(); + NBTTagCompound tag = new NBTTagCompound(); + tag.setInteger("x", 12); + tag.setInteger("y", 34); + tag.setInteger("z", 56); + type.getMethod("readFromNBT", NBTTagCompound.class).invoke(tile, tag); + + assertEquals(12, tile.xCoord); + assertEquals(34, tile.yCoord); + assertEquals(56, tile.zCoord); + assertSame(tag, type.getMethod("tag").invoke(tile)); + } + + private static Class load(String name) throws ClassNotFoundException { + return Class.forName(name, false, MultipartSaveLoadCharacterizationTest.class.getClassLoader()); + } + + private static void assertField(Class owner, String name, String typeName, boolean isFinal) throws Exception { + Field field = owner.getDeclaredField(name); + assertEquals(typeName, field.getType().getName()); + assertTrue(Modifier.isPrivate(field.getModifiers())); + assertEquals(isFinal, Modifier.isFinal(field.getModifiers())); + } + + private static Set publicMethodSignatures(Class type) { + Set signatures = new TreeSet<>(); + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + signatures.add(method.getName() + Type.getMethodDescriptor(method)); + } + } + return signatures; + } +}