diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index b32b766..92e7cd3 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -112,3 +112,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the referenc (SHA-256 `e931927e83134f57197aba082345ea96909291469d01a4f146836418a76bb3d1`). Class-file SHA-256 is `a03e8eb8566ae830829d59a379c67f355519bc82de24265baabd84720f068ce1`, stored as `src/test/resources/compat/ReferenceScalaSaw.class.b64`. + +### `ReferenceScalaMicroMaterialConsumer` + +`scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala` reads +`MicroMaterialRegistry$.MODULE$` and calls the instance methods ProjRed links against (`getMaterial(int)`, +`materialID`, `materialName`), plus the `scala.Tuple2` array `getIdMap` that extrautilities links against. It reads +`_1` and `_2` off the raw tuples rather than going through accessors, so the array element type is exercised too. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the reference dev jar built at `52ff8ff` +(SHA-256 `122d89cf0a070cfd2325bbbcf4bb4550d6130bb7da1ffba1a3b2e2efea6ec7e5`). Class-file SHA-256 is +`e66be14e6422277cc9567acf8e96a26e419abf5ec6582085c8c6e32cc60aa599`, stored as +`src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64`. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala new file mode 100644 index 0000000..344e25f --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala @@ -0,0 +1,21 @@ +package codechicken.multipart.compat + +import codechicken.microblock.MicroMaterialRegistry +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial + +/** Compiled against the reference dev jar. Reads MicroMaterialRegistry$.MODULE$ and calls the instance methods ProjRed + * links against, plus the Tuple2-array getIdMap that extrautilities links against. + */ +class ReferenceScalaMicroMaterialConsumer { + def materialAt(id: Int): IMicroMaterial = MicroMaterialRegistry.getMaterial(id) + + def idOf(name: String): Int = MicroMaterialRegistry.materialID(name) + + def nameOf(id: Int): String = MicroMaterialRegistry.materialName(id) + + /** Exercises the raw scala.Tuple2 array descriptor rather than the accessors. */ + def idMapEntry(id: Int): String = { + val entry = MicroMaterialRegistry.getIdMap.apply(id) + entry._1 + "=" + (entry._2 eq materialAt(id)) + } +} diff --git a/src/test/java/codechicken/microblock/MicroMaterialRegistryBinaryCompatibilityTest.java b/src/test/java/codechicken/microblock/MicroMaterialRegistryBinaryCompatibilityTest.java new file mode 100644 index 0000000..e7336a4 --- /dev/null +++ b/src/test/java/codechicken/microblock/MicroMaterialRegistryBinaryCompatibilityTest.java @@ -0,0 +1,75 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.Scanner; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; + +/** + * Loads a Scala class compiled against the reference dev jar. It reads MicroMaterialRegistry$.MODULE$ and calls the + * instance methods ProjRed links against, plus the scala.Tuple2 array getIdMap that extrautilities links against. + * + * Registration is global, so this registers its own uniquely named material and only asserts facts about it. + */ +class MicroMaterialRegistryBinaryCompatibilityTest { + + private static final String NAME = "test:binarycompat"; + + @BeforeAll + static void register() { + // The other test class may already have registered the placeholder; re-registering would hit the error path. + if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) { + MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key()); + } + if (MicroMaterialRegistry.getMaterial(NAME) == null) { + MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, NAME); + } + MicroMaterialRegistry$.MODULE$.setupIDMap(); + } + + @Test + void referenceScalaConsumerStillLinksAgainstTheSingletonAndIdMap() throws Exception { + Class fixtureClass = new FixtureClassLoader(MicroMaterialRegistry.class.getClassLoader()) + .define(loadFixture()); + Object fixture = fixtureClass.getDeclaredConstructor().newInstance(); + + int id = (Integer) fixtureClass.getMethod("idOf", String.class).invoke(fixture, NAME); + assertEquals(NAME, fixtureClass.getMethod("nameOf", int.class).invoke(fixture, id)); + + Object material = fixtureClass.getMethod("materialAt", int.class).invoke(fixture, id); + assertSame(MicroMaterialRegistry.getMaterial(id), material); + assertSame(MicroMaterialRegistry.getMaterial(id), (IMicroMaterial) material); + + // Reads the raw scala.Tuple2 array rather than the accessors. + assertEquals(NAME + "=true", fixtureClass.getMethod("idMapEntry", int.class).invoke(fixture, id)); + } + + private static byte[] loadFixture() { + InputStream input = Objects.requireNonNull( + MicroMaterialRegistryBinaryCompatibilityTest.class + .getResourceAsStream("/compat/ReferenceScalaMicroMaterialConsumer.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); + } + } +} diff --git a/src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java b/src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java new file mode 100644 index 0000000..6bae44f --- /dev/null +++ b/src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java @@ -0,0 +1,242 @@ +package codechicken.microblock; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.lang.reflect.Proxy; + +import net.minecraft.block.Block.SoundType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.IIcon; +import net.minecraft.util.MovingObjectPosition; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.data.MCDataOutputWrapper; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import codechicken.microblock.MicroMaterialRegistry.IMicroHighlightRenderer; +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; + +/** + * The registry is global mutable state, so everything is registered once in {@link #registerAndFreezeIdMap()} and the + * cases below only read it. Error paths are not covered: they call the microblock logger, which is null until + * MicroblockProxy.preInit runs, so they cannot execute headless. + */ +class MicroMaterialRegistryCharacterizationTest { + + private static final TestMaterial STONE = new TestMaterial(false, 3); + private static final TestMaterial GLASS = new TestMaterial(true, 1); + + @BeforeAll + static void registerAndFreezeIdMap() { + // Registration is global and re-registering hits the logger-backed error path, which cannot run headless. + if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) { + MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key()); + } + MicroMaterialRegistry.registerMaterial(STONE, "test:stone"); + MicroMaterialRegistry.registerMaterial(GLASS, "test:glass"); + MicroMaterialRegistry.remapName("test:oldglass", "test:glass"); + // setupIDMap is package private in Scala, so it has no static forwarder and must go through the singleton. + MicroMaterialRegistry$.MODULE$.setupIDMap(); + } + + @Test + void idMapIsSortedByNameAndDrivesBothLookups() { + String[] names = new String[MicroMaterialRegistry.getIdMap().length]; + for (int i = 0; i < names.length; i++) { + names[i] = MicroMaterialRegistry.getIdMap()[i]._1(); + } + + String[] sorted = names.clone(); + java.util.Arrays.sort(sorted); + assertArrayEquals(sorted, names); + + for (int i = 0; i < names.length; i++) { + assertEquals(i, MicroMaterialRegistry.materialID(names[i])); + assertEquals(names[i], MicroMaterialRegistry.materialName(i)); + assertSame(MicroMaterialRegistry.getIdMap()[i]._2(), MicroMaterialRegistry.getMaterial(i)); + } + } + + @Test + void getMaterialByNameResolvesRemapsAndReturnsNullWhenUnknown() { + assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:glass")); + assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:oldglass")); + assertNull(MicroMaterialRegistry.getMaterial("test:nosuchmaterial")); + } + + @Test + void materialIdResolvesRemappedNames() { + assertEquals(MicroMaterialRegistry.materialID("test:glass"), MicroMaterialRegistry.materialID("test:oldglass")); + } + + @Test + void missingIdPointsAtTheMissingMaterialRatherThanZero() { + assertEquals( + MicroMaterialRegistry.materialID(MissingMicroMaterial.key()), + MicroMaterialRegistry.getMissingId()); + assertSame( + MissingMicroMaterial$.MODULE$, + MicroMaterialRegistry.getMaterial(MicroMaterialRegistry.getMissingId())); + } + + @Test + void materialIdsRoundTripThroughTheIdWriter() throws Exception { + for (int id = 0; id < MicroMaterialRegistry.getIdMap().length; id++) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MCDataOutput output = new MCDataOutputWrapper(new DataOutputStream(bytes)); + MicroMaterialRegistry.writeMaterialID(output, id); + + byte[] encoded = bytes.toByteArray(); + assertEquals(1, encoded.length, "A small id map must still use the byte carrier"); + assertEquals(id, MicroMaterialRegistry.readMaterialID(input(encoded))); + } + } + + @Test + void highlightRendererShortCircuitsTheDefaultRenderer() { + RecordingRenderer accepting = new RecordingRenderer(true); + MicroMaterialRegistry.registerHighlightRenderer(accepting); + + // A renderer that claims the highlight stops the default client renderer being reached at all. + assertTrue(MicroMaterialRegistry.renderHighlight(null, null, null, 2, 0)); + assertEquals(1, accepting.calls); + } + + /** + * MissingMicroMaterial overrides only isTransparent and loadIcons, so it inherits canRenderInPass and isSolid from + * the trait. A Java class cannot inherit them today, so this is the only way to observe the supplied values. + */ + @Test + void materialInterfaceSuppliesRenderPassAndSolidity() { + assertTrue(MissingMicroMaterial.canRenderInPass(0)); + assertFalse(MissingMicroMaterial.canRenderInPass(1)); + assertFalse(MissingMicroMaterial.canRenderInPass(-1)); + + assertFalse(MissingMicroMaterial.isTransparent()); + assertTrue(MissingMicroMaterial.isSolid()); + } + + private static MCDataInput input(byte[] bytes) { + DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes)); + return (MCDataInput) Proxy.newProxyInstance( + MCDataInput.class.getClassLoader(), + new Class[] { MCDataInput.class }, + (proxy, method, arguments) -> { + switch (method.getName()) { + case "readUByte": + return (short) data.readUnsignedByte(); + case "readUShort": + return data.readUnsignedShort(); + case "readInt": + return data.readInt(); + default: + throw new AssertionError("Unexpected read method: " + method.getName()); + } + }); + } + + private static final class RecordingRenderer implements IMicroHighlightRenderer { + + private final boolean handled; + private int calls; + + private RecordingRenderer(boolean handled) { + this.handled = handled; + } + + @Override + public boolean renderHighlight(EntityPlayer player, MovingObjectPosition hit, CommonMicroClass mcrClass, + int size, int material) { + calls++; + return handled; + } + } + + /** Mirrors the forwarders Scala emits, so this file compiles against the trait and against the Java interface. */ + private static final class TestMaterial implements IMicroMaterial { + + private final boolean transparent; + private final int cutterStrength; + + private TestMaterial(boolean transparent, int cutterStrength) { + this.transparent = transparent; + this.cutterStrength = cutterStrength; + } + + @Override + public IIcon getBreakingIcon(int side) { + return null; + } + + @Override + public void loadIcons() {} + + @Override + public void renderMicroFace(Vector3 pos, int pass, Cuboid6 bounds) {} + + @Override + public boolean canRenderInPass(int pass) { + return pass == 0; + } + + @Override + public boolean isTransparent() { + return transparent; + } + + @Override + public int getLightValue() { + return 0; + } + + @Override + public float getStrength(EntityPlayer player) { + return 1f; + } + + @Override + public String getLocalizedName() { + return "Test Material"; + } + + @Override + public ItemStack getItem() { + return null; + } + + @Override + public int getCutterStrength() { + return cutterStrength; + } + + @Override + public SoundType getSound() { + return null; + } + + @Override + public boolean isSolid() { + return !isTransparent(); + } + + @Override + public float explosionResistance(Entity entity) { + return 1f; + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64 b/src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64 new file mode 100644 index 0000000..3dd7191 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64 @@ -0,0 +1,42 @@ +yv66vgAAADQAXAEAQGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFN +aWNyb01hdGVyaWFsQ29uc3VtZXIHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQApUmVmZXJlbmNl +U2NhbGFNaWNyb01hdGVyaWFsQ29uc3VtZXIuc2NhbGEBAB5Mc2NhbGEvcmVmbGVjdC9TY2FsYVNp +Z25hdHVyZTsBAAVieXRlcwEB5AYBBTNBIQECARMJGSMrGjRmZRZ0Ny1aKmRDMgxXKlsyc182C0cv +GjpqQzIcdU5cOnZbFhQoQkECBQMZGXcuHDliaSoRUUFCAQpbVmRHLxs5YmVSVBFhQgEMRz4kV20Z +NWpHLixnbgEBFAUBUQFDQQYPGwVhISJBBwILTRwXDVwxCgU9YSFBQiFvc0orZw1DAxIBEQUhIwEE +PVM6TEdPEAsCJ0ERQQNBBwIFISlhAwFDAS8FUVEuGTtmZSYMRy4ROxUFYUEDQ0ENJh0JUSJFBAIc +QTkRQWRIBwI7KRFhBEMBB3lJfHcOHiAKAx1JISEJBAIVNUw3TV04Y1k+HDcuAwIkSQUpUipbMnNf +NgtHLxo6akMyFFZtWjV0aUpMKEJBEQcTCTFzRQEIShsmHCdvXCdiaRYUGC4ZNwsFDSIDIkIVFgEE +URNBQTVlIQlZMSYDAi0ZCRkRShw7CQs5AkERQRgCCSUkd0oaCwNVQUJRIU0XQQJJCkFBXDFuS0IR +MUcOCAMXUUohIQ4HAg1BE1gNWjNnEwk5BEgBBFRpSkxnbhoGA2sxQVFBDwEFAm0KYUFcMW5LPjNH +QwEaPREVSRMIMQErERVxBAEiAUADKUlHLVQxcQs6JCgvHwsDZQFDUSFLH0ECKQIBAAptYXRlcmlh +bEF0AQBAKEkpTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElN +aWNyb01hdGVyaWFsOwEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lz +dHJ5JAcACwEAB01PRFVMRSQBAC9MY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb01hdGVyaWFs +UmVnaXN0cnkkOwwADQAOCQAMAA8BAAtnZXRNYXRlcmlhbAwAEQAKCgAMABIBAAR0aGlzAQBCTGNv +ZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFNaWNyb01hdGVyaWFsQ29u +c3VtZXI7AQACaWQBAAFJAQAEaWRPZgEAFShMamF2YS9sYW5nL1N0cmluZzspSQEACm1hdGVyaWFs +SUQMABoAGQoADAAbAQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm5hbWVPZgEAFShJKUxq +YXZhL2xhbmcvU3RyaW5nOwEADG1hdGVyaWFsTmFtZQwAIQAgCgAMACIBAAppZE1hcEVudHJ5AQAI +Z2V0SWRNYXABABEoKVtMc2NhbGEvVHVwbGUyOwwAJQAmCgAMACcBACZzY2FsYS9jb2xsZWN0aW9u +L211dGFibGUvU3RyaW5nQnVpbGRlcgcAKQEABjxpbml0PgEAAygpVgwAKwAsCgAqAC0BAAxzY2Fs +YS9UdXBsZTIHAC8BAAJfMQEAFCgpTGphdmEvbGFuZy9PYmplY3Q7DAAxADIKADAAMwEAEGphdmEv +bGFuZy9TdHJpbmcHADUBAAZhcHBlbmQBADwoTGphdmEvbGFuZy9PYmplY3Q7KUxzY2FsYS9jb2xs +ZWN0aW9uL211dGFibGUvU3RyaW5nQnVpbGRlcjsMADcAOAoAKgA5AQABPQgAOwEAAl8yDAA9ADIK +ADAAPgwACQAKCgACAEABABpzY2FsYS9ydW50aW1lL0JveGVzUnVuVGltZQcAQgEADGJveFRvQm9v +bGVhbgEAFihaKUxqYXZhL2xhbmcvQm9vbGVhbjsMAEQARQoAQwBGAQAIdG9TdHJpbmcBABQoKUxq +YXZhL2xhbmcvU3RyaW5nOwwASABJCgAqAEoBAAVlbnRyeQEADkxzY2FsYS9UdXBsZTI7CgAEAC0B +ADtjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeSRJTWljcm9NYXRl +cmlhbAcATwEALGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5BwBR +AQAOSU1pY3JvTWF0ZXJpYWwBAARDb2RlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAPTGluZU51bWJl +clRhYmxlAQANU3RhY2tNYXBUYWJsZQEAClNvdXJjZUZpbGUBAAxJbm5lckNsYXNzZXMBABlSdW50 +aW1lVmlzaWJsZUFubm90YXRpb25zAQAIU2NhbGFTaWcAIQACAAQAAAAAAAUAAQAJAAoAAQBUAAAA +PAACAAIAAAAIsgAQG7YAE7AAAAACAFUAAAAWAAIAAAAIABQAFQAAAAAACAAWABcAAQBWAAAABgAB +AAAACgABABgAGQABAFQAAAA8AAIAAgAAAAiyABArtgAcrAAAAAIAVQAAABYAAgAAAAgAFAAVAAAA +AAAIAB0AHgABAFYAAAAGAAEAAAAMAAEAHwAgAAEAVAAAADwAAgACAAAACLIAEBu2ACOwAAAAAgBV +AAAAFgACAAAACAAUABUAAAAAAAgAFgAXAAEAVgAAAAYAAQAAAA4AAQAkACAAAQBUAAAApwAEAAMA +AAA6sgAQtgAoGzJNuwAqWbcALiy2ADTAADa2ADoSPLYAOiy2AD8qG7YAQaYABwSnAAQDuABHtgA6 +tgBLsAAAAAMAVQAAACAAAwAAADoAFAAVAAAAAAA6ABYAFwABAAkAMABMAE0AAgBWAAAACgACAAAA +EgAJABMAVwAAACUAAv8ALwADBwACAQcAMAABBwAq/wAAAAMHAAIBBwAwAAIHACoBAAEAKwAsAAEA +VAAAAC8AAQABAAAABSq3AE6xAAAAAgBVAAAADAABAAAABQAUABUAAABWAAAABgABAAAAFQAEAFgA +AAACAAUAWQAAAAoAAQBQAFIAUwYJAFoAAAALAAEABgABAAdzAAgAWwAAAAMFAAA=