From 82da4522bda24d114008695901bc5a84e2bd881f Mon Sep 17 00:00:00 2001 From: Algent Date: Thu, 3 Sep 2026 21:53:10 +0200 Subject: [PATCH] test: characterize corner microblock traits --- .../CornerMicroblockTraitsFunctionalTest.java | 128 ++++++++++++++++++ src/test/fixtures/README.md | 12 ++ .../ReferenceScalaCornerMicroblock.scala | 23 ++++ ...rMicroblockTraitsCharacterizationTest.java | 93 +++++++++++++ .../ReferenceScalaCornerMicroblock.class.b64 | 1 + 5 files changed, 257 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/CornerMicroblockTraitsFunctionalTest.java create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCornerMicroblock.scala create mode 100644 src/test/java/codechicken/microblock/CornerMicroblockTraitsCharacterizationTest.java create mode 100644 src/test/resources/compat/ReferenceScalaCornerMicroblock.class.b64 diff --git a/src/functionalTest/java/codechicken/multipart/test/CornerMicroblockTraitsFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/CornerMicroblockTraitsFunctionalTest.java new file mode 100644 index 0000000..ce9f8e8 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/CornerMicroblockTraitsFunctionalTest.java @@ -0,0 +1,128 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import net.minecraft.nbt.NBTTagCompound; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutputWrapper; +import codechicken.lib.vec.Cuboid6; +import codechicken.microblock.CornerMicroClass; +import codechicken.microblock.CornerMicroClass$; +import codechicken.microblock.CornerMicroblock; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockGenerator; + +class CornerMicroblockTraitsFunctionalTest { + + @Test + void generatedCornerKeepsClassIdentityAndAllFiftySixBounds() { + Microblock part = MicroblockGenerator.create(CornerMicroClass$.MODULE$, 0, false); + CornerMicroblock cornerPart = (CornerMicroblock) part; + assertSame(CornerMicroClass$.MODULE$, cornerPart.microClass()); + assertSame(CornerMicroClass$.MODULE$, part.microClass()); + assertEquals("mcr_cnr", part.getType()); + int[] axisBits = { 4, 1, 2 }; + for (int size = 1; size <= 7; size++) { + for (int corner = 0; corner < 8; corner++) { + part.setShape(size, corner + 7); + assertEquals(size, part.getSize()); + assertEquals(corner + 7, cornerPart.getSlot()); + assertEquals(1 << (corner + 7), cornerPart.getSlotMask()); + Cuboid6 bounds = cornerPart.getBounds(); + assertSame(CornerMicroClass.aBounds()[size * 16 + corner], bounds); + double distance = size / 8D; + double[] min = { bounds.min.x, bounds.min.y, bounds.min.z }; + double[] max = { bounds.max.x, bounds.max.y, bounds.max.z }; + for (int axis = 0; axis < 3; axis++) { + boolean positive = (corner & axisBits[axis]) != 0; + assertEquals(positive ? 1 - distance : 0, min[axis], 1e-12); + assertEquals(positive ? 1 : distance, max[axis], 1e-12); + } + } + } + } + + @Test + void boundsUseSignedShapeIndexAndTheLiveReplaceableArray() { + Microblock part = MicroblockGenerator.create(CornerMicroClass$.MODULE$, 0, false); + CornerMicroblock corner = (CornerMicroblock) part; + for (int shape : new int[] { 0, 8, 127 }) { + part.shape_$eq((byte) shape); + assertNull(corner.getBounds()); + } + for (int shape = -128; shape < 0; shape++) { + part.shape_$eq((byte) shape); + assertThrows(ArrayIndexOutOfBoundsException.class, corner::getBounds); + } + Cuboid6[] original = CornerMicroClass.aBounds(); + Cuboid6[] replacement = new Cuboid6[256]; + Cuboid6 sentinel = new Cuboid6(0.1, 0.2, 0.3, 0.4, 0.5, 0.6); + replacement[35] = sentinel; + part.setShape(2, 10); + try { + CornerMicroClass.aBounds_$eq(replacement); + assertSame(sentinel, corner.getBounds()); + replacement[35] = null; + assertNull(corner.getBounds()); + CornerMicroClass.aBounds_$eq(new Cuboid6[1]); + assertThrows(ArrayIndexOutOfBoundsException.class, corner::getBounds); + } finally { + CornerMicroClass.aBounds_$eq(original); + } + assertSame(original[35], corner.getBounds()); + } + + @Test + void generatedCornersKeepThePackedShapeInNbtAndDescriptions() { + Microblock part = MicroblockGenerator.create(CornerMicroClass$.MODULE$, 0, false); + Microblock loaded = MicroblockGenerator.create(CornerMicroClass$.MODULE$, 0, false); + ByteArrayOutputStream prefixBytes = new ByteArrayOutputStream(); + MicroMaterialRegistry.writeMaterialID(new MCDataOutputWrapper(new DataOutputStream(prefixBytes)), 0); + byte[] prefix = prefixBytes.toByteArray(); + for (int size : new int[] { 1, 4, 7 }) { + for (int corner = 0; corner < 8; corner++) { + byte shape = (byte) (size * 16 + corner); + part.setShape(size, corner + 7); + NBTTagCompound tag = new NBTTagCompound(); + part.save(tag); + assertEquals(shape, tag.getByte("shape")); + assertEquals(MicroMaterialRegistry.materialName(0), tag.getString("material")); + loaded.load(tag); + assertEquals(shape, loaded.shape()); + assertEquals(0, loaded.material()); + assertEquals(corner + 7, ((CornerMicroblock) loaded).getSlot()); + assertSame(part.getBounds(), loaded.getBounds()); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + part.writeDesc(new MCDataOutputWrapper(new DataOutputStream(bytes))); + byte[] expected = Arrays.copyOf(prefix, prefix.length + 1); + expected[prefix.length] = shape; + assertArrayEquals(expected, bytes.toByteArray()); + AtomicInteger reads = new AtomicInteger(); + MCDataInput input = (MCDataInput) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] { MCDataInput.class }, + (proxy, method, args) -> { + assertEquals("readByte", method.getName()); + reads.incrementAndGet(); + return shape; + }); + loaded.setShape(2, 7); + loaded.readDesc(input); + assertEquals(1, reads.get()); + assertEquals(shape, loaded.shape()); + assertEquals(corner + 7, ((CornerMicroblock) loaded).getSlot()); + } + } + } +} diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 49de826..ca8189c 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -192,3 +192,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchange `3848f0ba68344b103614e6aaa85dfe300a1b3c41b94cb171224b411a46ecdf3b`; class-file SHA-256 is `96283797af5f81137e6fa354fbce5886e0de4f00655a6ab89a21de94bfadcbde`, stored as `src/test/resources/compat/ReferenceScalaFaceMicroblockClient.class.b64`. Do not regenerate it against the port. + +### `ReferenceScalaCornerMicroblock` + +`scala/codechicken/multipart/compat/ReferenceScalaCornerMicroblock.scala` freezes a concrete corner implementor +whose forwarders call `CornerMicroblock$class`. Observable shape accessors pin virtual dispatch, byte truncation, +out-of-range input behavior and the seven-slot offset without requiring Forge initialization. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchanged dev jar at `cfde822` +(SHA-256 `0c01bf316fe3706155dda05f6cb56a084658e5133b7590503e69a263a3fdfb85`). Source SHA-256 is +`6c0ff48c4bc6ff5508a429d53f13b718062173cc4a117095c192aed9cf0a0311`; class-file SHA-256 is +`5a6c3cf1ce593eb189af59f6db26a04d85902972073de83a77d8f0d920642cfa`, stored as +`src/test/resources/compat/ReferenceScalaCornerMicroblock.class.b64`. Do not regenerate it against the port. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCornerMicroblock.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCornerMicroblock.scala new file mode 100644 index 0000000..99bb77f --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCornerMicroblock.scala @@ -0,0 +1,23 @@ +package codechicken.multipart.compat + +import codechicken.microblock.{CornerMicroblock, Microblock} + +/** Frozen corner-trait forwarders with observable virtual shape access. */ +class ReferenceScalaCornerMicroblock extends Microblock(29) with CornerMicroblock { + var writes = 0 + var writtenShape: Byte = 0 + var reads = 0 + var overrideShape = false + var selectedShape = 0 + + override def shape_$eq(value: Byte) { + writes += 1 + writtenShape = value + super.shape_$eq(value) + } + + override def getShape = { + reads += 1 + if (overrideShape) selectedShape else super.getShape + } +} diff --git a/src/test/java/codechicken/microblock/CornerMicroblockTraitsCharacterizationTest.java b/src/test/java/codechicken/microblock/CornerMicroblockTraitsCharacterizationTest.java new file mode 100644 index 0000000..a99ce31 --- /dev/null +++ b/src/test/java/codechicken/microblock/CornerMicroblockTraitsCharacterizationTest.java @@ -0,0 +1,93 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.*; + +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.Test; + +/** Executes corner-trait forwarders compiled against the untouched Scala implementation. */ +class CornerMicroblockTraitsCharacterizationTest { + + @Test + void packsSupportedCornersThroughTheVirtualShapeSetterExactlyOnce() throws Exception { + Fixture f = new Fixture(); + assertEquals(29, f.part.material()); + assertEquals(0, f.part.shape()); + int writes = 0; + for (int size = 1; size <= 7; size++) { + for (int corner = 0; corner < 8; corner++) { + f.part.setShape(size, corner + 7); + byte expected = (byte) (size * 16 + corner); + assertEquals(expected, f.part.shape()); + assertEquals(expected, f.get("writtenShape")); + assertEquals(++writes, f.get("writes")); + assertEquals(size, f.part.getSize()); + assertEquals(corner + 7, f.corner.getSlot()); + assertEquals(1 << (corner + 7), f.corner.getSlotMask()); + assertEquals(29, f.part.material()); + } + } + } + + @Test + void preservesTruncationAndOutOfRangeSlotBitsWithoutValidation() throws Exception { + Fixture f = new Fixture(); + int[][] cases = { { 1, 6, -1, -1, 22 }, { 8, 7, -128, -8, 7 }, { 16, 7, 0, 0, 7 }, { 0, 23, 16, 1, 7 }, + { -1, 14, -9, -1, 14 }, { Integer.MAX_VALUE, Integer.MIN_VALUE, -7, -1, 16 } }; + for (int[] value : cases) { + f.part.setShape(value[0], value[1]); + assertEquals((byte) value[2], f.part.shape()); + assertEquals(value[3], f.part.getSize()); + assertEquals(value[4], f.corner.getSlot()); + } + assertEquals(cases.length, f.get("writes")); + } + + @Test + void slotDecodingUsesTheVirtualGetterAndKeepsIntegerOverflow() throws Exception { + Fixture f = new Fixture(); + f.set("overrideShape", boolean.class, true); + int[][] cases = { { -8, -1 }, { 0, 7 }, { 25, 32 }, { Integer.MAX_VALUE, Integer.MIN_VALUE + 6 } }; + int reads = 0; + for (int[] value : cases) { + f.set("selectedShape", int.class, value[0]); + assertEquals(value[1], f.corner.getSlot()); + assertEquals(++reads, f.get("reads")); + assertEquals(1 << value[1], f.corner.getSlotMask()); + assertEquals(++reads, f.get("reads")); + } + assertEquals(0, f.get("writes")); + assertEquals(0, f.part.shape()); + } + + private static final class Fixture extends ClassLoader { + + final Microblock part; + final CornerMicroblock corner; + + Fixture() throws Exception { + super(Microblock.class.getClassLoader()); + InputStream input = Objects + .requireNonNull(getClass().getResourceAsStream("/compat/ReferenceScalaCornerMicroblock.class.b64")); + byte[] bytes; + try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) { + bytes = Base64.getMimeDecoder().decode(scanner.next()); + } + part = (Microblock) defineClass(null, bytes, 0, bytes.length).getConstructor().newInstance(); + corner = (CornerMicroblock) part; + } + + Object get(String name) throws Exception { + return part.getClass().getMethod(name).invoke(part); + } + + void set(String name, Class type, Object value) throws Exception { + part.getClass().getMethod(name + "_$eq", type).invoke(part, value); + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaCornerMicroblock.class.b64 b/src/test/resources/compat/ReferenceScalaCornerMicroblock.class.b64 new file mode 100644 index 0000000..74a09d8 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaCornerMicroblock.class.b64 @@ -0,0 +1 @@ +yv66vgAAADQAmQEAO2NvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFDb3JuZXJNaWNyb2Jsb2NrBwABAQAhY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrBwADAQAnY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db3JuZXJNaWNyb2Jsb2NrBwAFAQAkUmVmZXJlbmNlU2NhbGFDb3JuZXJNaWNyb2Jsb2NrLnNjYWxhAQAeTHNjYWxhL3JlZmxlY3QvU2NhbGFTaWduYXR1cmU7AQAFYnl0ZXMBAzYGAW0zQSEBAgETCXEiKxo0ZmUWdDctWipkQzIMN2kcOm9LSmwVbhk6cEUyfDdtGwYDBxEJYWFZOG5hBiQoQkEDBwMlaVcPHDtqYQYUSE9DAQgDLRl3DlozZFEmHDcuGjgEAU0ZAUEDCREFLXFRIgEHCwU1MRFBQzdqR0p8Jw1cOGRXJhFxAgQCCxsmHCdvHDJtXw5cB0NBBhITCRFCQgEJRF9KdFdNXSdqR0p8Jw1cOGRXIilBAwFDASsFMUEoGzhqaXoiEkEGCQMvAWkRQQEFCDMBAQ0RIgEbAxk5KC8bO2ZnVgkxBAUCHT81CVFEQwEfAxUZOC0ZN2ITCQFTREECSl1SRHFBCQFBAhMFMSUBBnhlJiRYbV0wJUtGJCIBShQRBXEpE0IBFB4FESlmLhs7CQ8hChMRIWEBNwUZARBKGQkNKQIBFSkDHAMdOSgvGztmZwJCcQEMAUECEwVRJgEHeGUmJEgvGjhUUQYEWC1GAS8hCWFyJgMCMTsJISEpHztmER0RBAExQQUCTQoBYx46amlSLGduFTViYRZ8RiVaORUFESIEYgIVMgMDBQ1BDAUHbQEBCxUCGAIbXRQYDh47Zl1OQxcNXTMhER1BBAExQQUCaQlRQV0zYklORHFBDwFBAhMFMShBBXNLBiQ3bxgTZmNSEUEFEAUIUWUKCRExARwRGXEEASlRBTcFMSEvWjFlZwJCcQERAUECEwURKUEHcG0WFCgvGzNmJyIMBy9aCwIFQhFBZFEFAwl2EXFBUThwWRYMZw5DBEcBAQdJEUEkAiM9NFhNHTpqSRYcBi4ZOWY/EioXDwYCJREiOQEmUkEBAgQRBUICJgFBAyYhKQEIcG0WFCgvGzNmJyIMBy8aEQkPMQMBGSFDATUFaTEvGjdmR1IsR20VNWJhFkRxQRQBQQITBXEqQQl0SzIsNw1eM2UnIgwHL1owJUtGJCIBCikJDyFqFREhYQE3ITEhCwFRIQptCWFiXTNtSw4kWA1aKmlDQiwHBUMDVQERBVMrQQV0UQYEWG0YE2ZjUhFBRRYFBi9OAw1BTAEGbQZkVy8aBQYzAiEJRVcBCU8WJDgLWzFxS1IJMQQBAAZ3cml0ZXMBAAFJAQAMd3JpdHRlblNoYXBlAQABQgEABXJlYWRzAQANb3ZlcnJpZGVTaGFwZQEAAVoBAA1zZWxlY3RlZFNoYXBlAQAIc2V0U2hhcGUBAAUoSUkpVgEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29ybmVyTWljcm9ibG9jayRjbGFzcwcAFQEALihMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db3JuZXJNaWNyb2Jsb2NrO0lJKVYMABMAFwoAFgAYAQAEdGhpcwEAPUxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvY29tcGF0L1JlZmVyZW5jZVNjYWxhQ29ybmVyTWljcm9ibG9jazsBAARzaXplAQAEc2xvdAEACm1pY3JvQ2xhc3MBACwoKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0Nvcm5lck1pY3JvQ2xhc3MkOwEAVShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db3JuZXJNaWNyb2Jsb2NrOylMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db3JuZXJNaWNyb0NsYXNzJDsMAB4AIAoAFgAhAQAJZ2V0Qm91bmRzAQAfKClMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwEASChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db3JuZXJNaWNyb2Jsb2NrOylMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwwAIwAlCgAWACYBAAdnZXRTbG90AQADKClJAQAsKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0Nvcm5lck1pY3JvYmxvY2s7KUkMACgAKgoAFgArAQALZ2V0U2xvdE1hc2sBAC1jb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvYmxvY2skY2xhc3MHAC4BACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jazspSQwALQAwCgAvADEBABhnZXRQYXJ0aWFsT2NjbHVzaW9uQm94ZXMBABIoKUxqYXZhL3V0aWwvTGlzdDsBADsoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jazspTGphdmEvdXRpbC9MaXN0OwwAMwA1CgAvADYBAAtpdGVtQ2xhc3NJRAwAOAAwCgAvADkBADtjb2RlY2hpY2tlbiRtaWNyb2Jsb2NrJFRNaWNyb09jY2x1c2lvbiQkc3VwZXIkb2NjbHVzaW9uVGVzdAEAJShMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVoBACBjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydAcAPQEADW9jY2x1c2lvblRlc3QMAD8APAoAPgBAAQAFbnBhcnQBACJMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7AQAsY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb24kY2xhc3MHAEQBAE0oTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uO0xjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspWgwAPwBGCgBFAEcBABdlZGdlQ29ybmVyT2NjbHVzaW9uVGVzdAEAUyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247TGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uOylaAQB7KExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjtMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247TGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uOylaDABJAEsKAEUATAEABGVkZ2UBAChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247AQAGY29ybmVyDAALAAwJAAIAUQEACndyaXRlc18kZXEBAAQoSSlWAQADeCQxAQADKClCDAANAA4JAAIAVwEAEHdyaXR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