From 8294240659e75dbf1f1b22c9bf12d26aaf974792 Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 14 Aug 2026 20:03:33 +0200 Subject: [PATCH] test: characterize TCuboidPart Freezes the observable behavior of TCuboidPart, JCuboidPart and the TCuboidPart$class bridge before the Java port, per the test-first gate in JAVA_MIGRATION.md. All cases pass against the untouched Scala. Records two asymmetries worth preserving: getSubParts wraps the bounds in an IndexedCuboid6, whose copy constructor makes it a copy carrying a boxed Integer 0 as data, while getCollisionBoxes aliases the bounds instance itself. Cuboid6 has no equals, so geometry is compared component-wise. Also adds a frozen Scala consumer compiled with 2.11.5 against the reference dev jar at 246daff. Its forwarders call all four $class statics, so it detects the linkage break that a freshly recompiled test would miss. ForgeRelocationFMP, OpenComputers, ProjRed and ProjectBlue all depend on those descriptors. One case exists specifically to guard the port: TMultiPart supplies empty getSubParts and getCollisionBoxes, and on the JVM a superclass method beats an interface default, so a Java interface cannot reproduce Scala trait linearization on its own. --- src/test/fixtures/README.md | 21 ++- .../compat/ReferenceScalaCuboidPart.scala | 13 ++ .../CuboidPartCharacterizationTest.java | 129 ++++++++++++++++++ .../TCuboidPartBinaryCompatibilityTest.java | 68 +++++++++ .../compat/ReferenceScalaCuboidPart.class.b64 | 37 +++++ 5 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCuboidPart.scala create mode 100644 src/test/java/codechicken/multipart/CuboidPartCharacterizationTest.java create mode 100644 src/test/java/codechicken/multipart/TCuboidPartBinaryCompatibilityTest.java create mode 100644 src/test/resources/compat/ReferenceScalaCuboidPart.class.b64 diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 5dd9394..f64e95d 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -16,6 +16,21 @@ When a port changes descriptors that the ABI inventory shows are load-bearing, f reference dev jar here as a class file and load it directly in a test. Recompiling a consumer against the port would hide exactly the linkage failure the fixture exists to catch. -None are currently retained. `ReferenceScalaPartialOcclusion` was removed together with the `JPartialOcclusion$class` -bridge once the inventory showed no downstream consumer of that helper. The trait helpers that do need this treatment -are listed in `JAVA_MIGRATION_ABI_INVENTORY.md`. +`ReferenceScalaPartialOcclusion` was removed together with the `JPartialOcclusion$class` bridge once the inventory +showed no downstream consumer of that helper. The trait helpers that do need this treatment are listed in +`JAVA_MIGRATION_ABI_INVENTORY.md`. + +### `ReferenceScalaCuboidPart` + +`scala/codechicken/multipart/compat/ReferenceScalaCuboidPart.scala` mixes in `TCuboidPart` without overriding anything +the trait supplies, so its compiled forwarders call all four `TCuboidPart$class` statics: `$init$`, `getSubParts`, +`getCollisionBoxes` and `drawBreaking`. Four shipping mods depend on those descriptors. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the reference dev jar built at `246daff` +(SHA-256 `0ca7103905c8e2ada010bfc0362a257f19a67f7af4e9fb9dd00a7379c274f69a`). Class-file SHA-256 is +`3aba21aa01c4d689e2e1fb33b872424c7e7b5f4a8254abf2c86e9b11bb9fabe9`, stored as +`src/test/resources/compat/ReferenceScalaCuboidPart.class.b64`. + +`TCuboidPartBinaryCompatibilityTest` exercises `$init$`, `getSubParts` and `getCollisionBoxes`. The `drawBreaking` +forwarder is present in the fixture but cannot be invoked without a client render context, so its bridge descriptor is +covered by the ABI baseline rather than by execution. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCuboidPart.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCuboidPart.scala new file mode 100644 index 0000000..899788a --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaCuboidPart.scala @@ -0,0 +1,13 @@ +package codechicken.multipart.compat + +import codechicken.lib.vec.Cuboid6 +import codechicken.multipart.TCuboidPart + +/** Mixes in TCuboidPart without overriding anything it supplies, so the compiled forwarders call + * TCuboidPart$class.$init$, getSubParts, getCollisionBoxes and drawBreaking directly. + */ +class ReferenceScalaCuboidPart extends TCuboidPart { + def getType = "test:reference-cuboid" + + def getBounds = new Cuboid6(0, 0, 0, 0.5, 0.5, 0.5) +} diff --git a/src/test/java/codechicken/multipart/CuboidPartCharacterizationTest.java b/src/test/java/codechicken/multipart/CuboidPartCharacterizationTest.java new file mode 100644 index 0000000..e67a3a3 --- /dev/null +++ b/src/test/java/codechicken/multipart/CuboidPartCharacterizationTest.java @@ -0,0 +1,129 @@ +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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.raytracer.IndexedCuboid6; +import codechicken.lib.vec.Cuboid6; + +class CuboidPartCharacterizationTest { + + @Test + void subPartsHoldOneIndexedCopyOfTheBounds() { + TestPart part = new TestPart(box(0, 0, 0, 0.5, 0.5, 0.5)); + + List subParts = drain(part.getSubParts()); + + assertEquals(1, subParts.size()); + IndexedCuboid6 only = assertInstanceOf(IndexedCuboid6.class, subParts.get(0)); + assertEquals(Integer.valueOf(0), only.data); + assertGeometry(part.getBounds(), only); + assertNotSame(part.getBounds(), only); + } + + @Test + void collisionBoxesAliasTheBoundsInstance() { + TestPart part = new TestPart(box(0, 0, 0, 1, 1, 1)); + + List boxes = drain(part.getCollisionBoxes()); + + assertEquals(1, boxes.size()); + assertSame(part.getBounds(), boxes.get(0)); + } + + @Test + void resultsFollowTheCurrentBounds() { + TestPart part = new TestPart(box(0, 0, 0, 1, 1, 1)); + part.bounds = box(0.25, 0.25, 0.25, 0.75, 0.75, 0.75); + + assertGeometry(part.getBounds(), drain(part.getSubParts()).get(0)); + assertSame(part.getBounds(), drain(part.getCollisionBoxes()).get(0)); + } + + @Test + @SuppressWarnings("unchecked") + void deprecatedBridgeMatchesTheInstanceMethods() { + TestPart part = new TestPart(box(0.1, 0.2, 0.3, 0.4, 0.5, 0.6)); + + List bridgeSubParts = drain(TCuboidPart$class.getSubParts(part)); + List bridgeBoxes = drain(TCuboidPart$class.getCollisionBoxes(part)); + + assertEquals(1, bridgeSubParts.size()); + assertEquals(Integer.valueOf(0), bridgeSubParts.get(0).data); + assertGeometry(part.getBounds(), bridgeSubParts.get(0)); + assertEquals(1, bridgeBoxes.size()); + assertSame(part.getBounds(), bridgeBoxes.get(0)); + } + + /** + * TMultiPart supplies empty defaults. A cuboid part must not fall back to them: on the JVM a superclass method + * beats an interface default, so this is the guard against the port silently losing the cuboid behavior. + */ + @Test + void cuboidBehaviorWinsOverTheEmptyTMultiPartDefaults() { + TMultiPart bare = new TMultiPart() { + + @Override + public String getType() { + return "test:bare"; + } + }; + + assertFalse(bare.getSubParts().iterator().hasNext()); + assertFalse(bare.getCollisionBoxes().iterator().hasNext()); + + TestPart part = new TestPart(box(0, 0, 0, 1, 1, 1)); + assertTrue(part.getSubParts().iterator().hasNext()); + assertTrue(part.getCollisionBoxes().iterator().hasNext()); + } + + private static void assertGeometry(Cuboid6 expected, Cuboid6 actual) { + assertEquals(expected.min.x, actual.min.x); + assertEquals(expected.min.y, actual.min.y); + assertEquals(expected.min.z, actual.min.z); + assertEquals(expected.max.x, actual.max.x); + assertEquals(expected.max.y, actual.max.y); + assertEquals(expected.max.z, actual.max.z); + } + + private static List drain(Iterable values) { + List result = new ArrayList<>(); + for (Iterator iterator = values.iterator(); iterator.hasNext();) { + result.add(iterator.next()); + } + return result; + } + + private static Cuboid6 box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return new Cuboid6(minX, minY, minZ, maxX, maxY, maxZ); + } + + private static final class TestPart extends JCuboidPart { + + private Cuboid6 bounds; + + private TestPart(Cuboid6 bounds) { + this.bounds = bounds; + } + + @Override + public Cuboid6 getBounds() { + return bounds; + } + + @Override + public String getType() { + return "test:cuboid"; + } + } +} diff --git a/src/test/java/codechicken/multipart/TCuboidPartBinaryCompatibilityTest.java b/src/test/java/codechicken/multipart/TCuboidPartBinaryCompatibilityTest.java new file mode 100644 index 0000000..78f4043 --- /dev/null +++ b/src/test/java/codechicken/multipart/TCuboidPartBinaryCompatibilityTest.java @@ -0,0 +1,68 @@ +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.assertTrue; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Iterator; +import java.util.Objects; +import java.util.Scanner; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.raytracer.IndexedCuboid6; +import codechicken.lib.vec.Cuboid6; + +/** + * Loads a Scala class compiled against the reference dev jar, whose forwarders call TCuboidPart$class directly. + * Recompiling the fixture against the port would hide the linkage failure this exists to catch. + */ +class TCuboidPartBinaryCompatibilityTest { + + @Test + void referenceScalaImplementorStillLinksThroughTheBridge() throws Exception { + byte[] bytecode = loadFixture(); + Class fixtureClass = new FixtureClassLoader(TCuboidPart.class.getClassLoader()).define(bytecode); + Object fixture = fixtureClass.getDeclaredConstructor().newInstance(); + + assertTrue(fixture instanceof TCuboidPart); + assertTrue(fixture instanceof TMultiPart); + TCuboidPart part = (TCuboidPart) fixture; + + Cuboid6 bounds = part.getBounds(); + assertEquals(0.5, bounds.max.x); + + Iterator subParts = part.getSubParts().iterator(); + IndexedCuboid6 only = subParts.next(); + assertEquals(Integer.valueOf(0), only.data); + assertEquals(0.5, only.max.x); + assertFalse(subParts.hasNext()); + + Iterator boxes = part.getCollisionBoxes().iterator(); + assertEquals(0.5, boxes.next().max.x); + assertFalse(boxes.hasNext()); + } + + private static byte[] loadFixture() { + InputStream input = Objects.requireNonNull( + TCuboidPartBinaryCompatibilityTest.class + .getResourceAsStream("/compat/ReferenceScalaCuboidPart.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/resources/compat/ReferenceScalaCuboidPart.class.b64 b/src/test/resources/compat/ReferenceScalaCuboidPart.class.b64 new file mode 100644 index 0000000..22eb03a --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaCuboidPart.class.b64 @@ -0,0 +1,37 @@ +yv66vgAAADQAQAEANWNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFD +dWJvaWRQYXJ0BwABAQAgY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQHAAMBACFjb2Rl +Y2hpY2tlbi9tdWx0aXBhcnQvVEN1Ym9pZFBhcnQHAAUBAB5SZWZlcmVuY2VTY2FsYUN1Ym9pZFBh +cnQuc2NhbGEBAB5Mc2NhbGEvcmVmbGVjdC9TY2FsYVNpZ25hdHVyZTsBAAVieXRlcwEBNQYBMTJB +IQECARMJQSIrGjRmZRZ0Ny1aKmRDMgw3KR4ycFMSBBZNHTsLBQ0hEUFCMnBbQgxHTwMCBg0FSVEu +Hjd1U0IMJw8eBgIPBVkxbRwzZkciTDdtWzNvBwEZMgEBBg8hCVlBIkQBBRMJaUFBAQZVG1ZkRy8b +KWJlUgQiYUMICgVBISFhAytEawp8Fw4aKWJlUkRRQQUBBQJNCWEBUDVvU1J0RCMBCxEFVQFRIgEC +CQtdAUERAQ0CDx0sRw9WPXFLVgkRBAUCGz81CTFEAwIdOwUhQS4ZOGgVBXESAQI2Ym0GTCEBSQ4D +DU0jKC8bOGgRFREDASIBJAMlOVcNHiJwazokNy9GASUhCSkjJkQBJxUJOQMmQQJ3Sw5UISELBAIH +MUwnLQMCLE0JOTEpHjJwUxI0BAEAC2dldFN1YlBhcnRzAQAWKClMamF2YS9sYW5nL0l0ZXJhYmxl +OwEAJ2NvZGVjaGlja2VuL211bHRpcGFydC9UQ3Vib2lkUGFydCRjbGFzcwcADQEAOShMY29kZWNo +aWNrZW4vbXVsdGlwYXJ0L1RDdWJvaWRQYXJ0OylMamF2YS9sYW5nL0l0ZXJhYmxlOwwACwAPCgAO +ABABAAR0aGlzAQA3TGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFD +dWJvaWRQYXJ0OwEAEWdldENvbGxpc2lvbkJveGVzDAAUAA8KAA4AFQEADGRyYXdCcmVha2luZwEA +LyhMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvUmVuZGVyQmxvY2tzOylWAQAiTGNwdy9t +b2RzL2ZtbC9yZWxhdW5jaGVyL1NpZGVPbmx5OwEABXZhbHVlAQAeTGNwdy9tb2RzL2ZtbC9yZWxh +dW5jaGVyL1NpZGU7AQAGQ0xJRU5UAQBSKExjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVEN1Ym9pZFBh +cnQ7TG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL1JlbmRlckJsb2NrczspVgwAFwAdCgAO +AB4BAAxyZW5kZXJCbG9ja3MBACxMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvUmVuZGVy +QmxvY2tzOwEAB2dldFR5cGUBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEAFXRlc3Q6cmVmZXJlbmNl +LWN1Ym9pZAgAJAEACWdldEJvdW5kcwEAHygpTGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjsB +ABtjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDYHACgGP+AAAAAAAAABAAY8aW5pdD4BAAkoRERE +REREKVYMACwALQoAKQAuAQADKClWDAAsADAKAAQAMQEABiRpbml0JAEAJihMY29kZWNoaWNrZW4v +bXVsdGlwYXJ0L1RDdWJvaWRQYXJ0OylWDAAzADQKAA4ANQEABENvZGUBABJMb2NhbFZhcmlhYmxl +VGFibGUBAA9MaW5lTnVtYmVyVGFibGUBAAlTaWduYXR1cmUBAEIoKUxqYXZhL2xhbmcvSXRlcmFi +bGU8TGNvZGVjaGlja2VuL2xpYi9yYXl0cmFjZXIvSW5kZXhlZEN1Ym9pZDY7PjsBADUoKUxqYXZh +L2xhbmcvSXRlcmFibGU8TGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjs+OwEAGVJ1bnRpbWVW +aXNpYmxlQW5ub3RhdGlvbnMBAApTb3VyY2VGaWxlAQAIU2NhbGFTaWcAIQACAAQAAQAGAAAABgAB +AAsADAACADcAAAAvAAEAAQAAAAUquAARsAAAAAIAOAAAAAwAAQAAAAUAEgATAAAAOQAAAAYAAQAA +AAkAOgAAAAIAOwABABQADAACADcAAAAvAAEAAQAAAAUquAAWsAAAAAIAOAAAAAwAAQAAAAUAEgAT +AAAAOQAAAAYAAQAAAAkAOgAAAAIAPAABABcAGAACADcAAAA6AAIAAgAAAAYqK7gAH7EAAAACADgA +AAAWAAIAAAAGABIAEwAAAAAABgAgACEAAQA5AAAABgABAAAACQA9AAAADQABABkAAQAaZQAbABwA +AQAiACMAAQA3AAAALQABAAEAAAADEiWwAAAAAgA4AAAADAABAAAAAwASABMAAAA5AAAABgABAAAA +CgABACYAJwABADcAAAA+AA4AAQAAABS7AClZDg4OFAAqFAAqFAAqtwAvsAAAAAIAOAAAAAwAAQAA +ABQAEgATAAAAOQAAAAYAAQAAAAwAAQAsADAAAQA3AAAAMwABAAEAAAAJKrcAMiq4ADaxAAAAAgA4 +AAAADAABAAAACQASABMAAAA5AAAABgABAAAACQADAD4AAAACAAcAPQAAAAsAAQAIAAEACXMACgA/ +AAAAAwUAAA==