From 3e68e6759943ecef6d9b2356967b69da4de70dbc Mon Sep 17 00:00:00 2001 From: Algent Date: Sat, 15 Aug 2026 15:28:17 +0200 Subject: [PATCH] test: characterize the marker interfaces TSlottedPart, IRandomDisplayTick, INeighborTileChange, TRandomUpdateTick, ISidedHollowConnect and IMicroMaterialRender carry no implementation, so their characterization is their shape: every member abstract and public, no superinterface, and the exact member names. Two of them extend TMultiPart in Scala source but emit a bare interface, and IMicroMaterialRender is satisfied entirely by TMultiPart's Scala-style world/x/y/z accessors, so the names are load-bearing. TRandomUpdateTick's onWorldJoin auto-registration lives in its $class helper. A part that does not declare onWorldJoin resolves it to TMultiPart's no-op; that is pinned so the port cannot quietly turn it into an interface default, which the superclass would shadow. --- .../MarkerInterfaceCharacterizationTest.java | 124 +++++++++++++++ .../MarkerInterfaceCharacterizationTest.java | 141 ++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 src/test/java/codechicken/microblock/MarkerInterfaceCharacterizationTest.java create mode 100644 src/test/java/codechicken/multipart/MarkerInterfaceCharacterizationTest.java diff --git a/src/test/java/codechicken/microblock/MarkerInterfaceCharacterizationTest.java b/src/test/java/codechicken/microblock/MarkerInterfaceCharacterizationTest.java new file mode 100644 index 0000000..7a5dfad --- /dev/null +++ b/src/test/java/codechicken/microblock/MarkerInterfaceCharacterizationTest.java @@ -0,0 +1,124 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.Cuboid6; + +/** + * Both interfaces carry no implementation, so their characterization is their shape. The accessor names matter most: + * {@code IMicroMaterialRender} is satisfied by {@code TMultiPart}'s Scala-style {@code world/x/y/z}, so renaming any of + * them to a bean accessor would silently unimplement it for every part. + */ +class MarkerInterfaceCharacterizationTest { + + @Test + void sidedHollowConnectDeclaresOnlyTheSizeQuery() { + assertBareInterface(ISidedHollowConnect.class, "getHollowSize(int)int"); + } + + @Test + void microMaterialRenderKeepsScalaStyleAccessorNames() { + assertBareInterface( + IMicroMaterialRender.class, + "world()net.minecraft.world.World", + "x()int", + "y()int", + "z()int", + "getRenderBounds()codechicken.lib.vec.Cuboid6"); + } + + @Test + void markersAreImplementableFromJava() { + Cuboid6 bounds = new Cuboid6(0, 0, 0, 0.5, 0.5, 0.5); + MarkedRender render = new MarkedRender(bounds); + + assertTrue(render instanceof ISidedHollowConnect); + assertTrue(render instanceof IMicroMaterialRender); + assertEquals(8, render.getHollowSize(2)); + assertNull(render.world()); + assertEquals(1, render.x()); + assertEquals(2, render.y()); + assertEquals(3, render.z()); + assertSame(bounds, render.getRenderBounds()); + } + + private static void assertBareInterface(Class type, String... expectedMembers) { + assertTrue(type.isInterface(), type.getName() + " must stay an interface"); + assertEquals( + 0, + type.getInterfaces().length, + type.getName() + " must carry no superinterface, as the Scala trait emits none"); + + Set actual = new TreeSet<>(); + for (Method method : type.getDeclaredMethods()) { + assertTrue(Modifier.isAbstract(method.getModifiers()), method + " must stay abstract"); + assertTrue(Modifier.isPublic(method.getModifiers()), method + " must stay public"); + actual.add(signature(method)); + } + assertEquals(new TreeSet<>(Arrays.asList(expectedMembers)), actual); + } + + private static String signature(Method method) { + StringBuilder out = new StringBuilder(method.getName()).append('('); + Class[] parameters = method.getParameterTypes(); + for (int i = 0; i < parameters.length; i++) { + if (i > 0) { + out.append(','); + } + out.append(parameters[i].getName()); + } + return out.append(')').append(method.getReturnType().getName()).toString(); + } + + private static final class MarkedRender implements ISidedHollowConnect, IMicroMaterialRender { + + private final Cuboid6 bounds; + + MarkedRender(Cuboid6 bounds) { + this.bounds = bounds; + } + + @Override + public int getHollowSize(int side) { + return 8; + } + + @Override + public World world() { + return null; + } + + @Override + public int x() { + return 1; + } + + @Override + public int y() { + return 2; + } + + @Override + public int z() { + return 3; + } + + @Override + public Cuboid6 getRenderBounds() { + return bounds; + } + } +} diff --git a/src/test/java/codechicken/multipart/MarkerInterfaceCharacterizationTest.java b/src/test/java/codechicken/multipart/MarkerInterfaceCharacterizationTest.java new file mode 100644 index 0000000..9e5acc4 --- /dev/null +++ b/src/test/java/codechicken/multipart/MarkerInterfaceCharacterizationTest.java @@ -0,0 +1,141 @@ +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.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Random; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; + +/** + * The marker interfaces carry no implementation, so their characterization is their shape: every member abstract, no + * superinterface, and the exact names downstream parts implement. + */ +class MarkerInterfaceCharacterizationTest { + + @Test + void slottedPartIsABareInterfaceDespiteExtendingTMultiPartInSource() { + assertBareInterface(TSlottedPart.class, "getSlotMask()int"); + } + + @Test + void randomDisplayTickDeclaresOnlyTheTickCallback() { + assertBareInterface(IRandomDisplayTick.class, "randomDisplayTick(java.util.Random)void"); + } + + @Test + void neighborTileChangeDeclaresBothCallbacks() { + assertBareInterface( + INeighborTileChange.class, + "weakTileChanges()boolean", + "onNeighborTileChanged(int,boolean)void"); + } + + @Test + void randomUpdateTickDeclaresOnWorldJoinAbstract() { + assertBareInterface(TRandomUpdateTick.class, "randomUpdate()void", "onWorldJoin()void"); + } + + /** + * The auto-registration lives in the {@code $class} helper, which only Scala implementors reach. A part that does + * not declare {@code onWorldJoin} resolves it to {@link TMultiPart}, which does nothing -- notably it does not + * reach {@link TickScheduler}, which would need a world. + */ + @Test + void randomUpdateTickImplementorWithoutOwnOnWorldJoinInheritsTheNoOp() throws Exception { + SilentPart part = new SilentPart(); + + part.onWorldJoin(); + + assertEquals(TMultiPart.class, SilentPart.class.getMethod("onWorldJoin").getDeclaringClass()); + } + + @Test + void markersAreImplementableAlongsideTMultiPart() { + MarkedPart part = new MarkedPart(); + + assertTrue(part instanceof TSlottedPart); + assertTrue(part instanceof IRandomDisplayTick); + assertTrue(part instanceof INeighborTileChange); + assertEquals(1 << 6, part.getSlotMask()); + assertFalse(part.weakTileChanges()); + + part.randomDisplayTick(new Random(0)); + part.onNeighborTileChanged(3, false); + assertEquals(3, part.lastTileChangeSide); + } + + private static void assertBareInterface(Class type, String... expectedMembers) { + assertTrue(type.isInterface(), type.getName() + " must stay an interface"); + assertEquals( + 0, + type.getInterfaces().length, + type.getName() + " must carry no superinterface, as the Scala trait emits none"); + + Set actual = new TreeSet<>(); + for (Method method : type.getDeclaredMethods()) { + assertTrue(Modifier.isAbstract(method.getModifiers()), method + " must stay abstract"); + assertTrue(Modifier.isPublic(method.getModifiers()), method + " must stay public"); + actual.add(signature(method)); + } + assertEquals(new TreeSet<>(Arrays.asList(expectedMembers)), actual); + } + + private static String signature(Method method) { + StringBuilder out = new StringBuilder(method.getName()).append('('); + Class[] parameters = method.getParameterTypes(); + for (int i = 0; i < parameters.length; i++) { + if (i > 0) { + out.append(','); + } + out.append(parameters[i].getName()); + } + return out.append(')').append(method.getReturnType().getName()).toString(); + } + + private static class SilentPart extends TMultiPart implements TRandomUpdateTick { + + @Override + public String getType() { + return "test:silent"; + } + + @Override + public void randomUpdate() {} + } + + private static final class MarkedPart extends TMultiPart + implements TSlottedPart, IRandomDisplayTick, INeighborTileChange { + + int lastTileChangeSide = -1; + + @Override + public String getType() { + return "test:marked"; + } + + @Override + public int getSlotMask() { + return 1 << 6; + } + + @Override + public void randomDisplayTick(Random random) {} + + @Override + public boolean weakTileChanges() { + return false; + } + + @Override + public void onNeighborTileChanged(int side, boolean weak) { + lastTileChangeSide = side; + } + } +}