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.
This commit is contained in:
Algent committed 2026-08-15 15:28:17 +02:00
1 parent ca9bd5dd89
commit 3e68e67599
2 files changed
+265

No files matched your search

@@ -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<String> 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;
}
}
}
@@ -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<String> 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;
}
}
}