From 53e5901360a237232feb7ef5f65ceba33797224c Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 30 Aug 2026 15:29:59 +0200 Subject: [PATCH] test: characterize multipart compatibility Pin the Scala Function4 callback surface and MCPC fallback behavior before the Java port. --- .../MultipartCompatiblityFunctionalTest.java | 60 ++++++++++ ...ipartCompatiblityCharacterizationTest.java | 103 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/MultipartCompatiblityFunctionalTest.java create mode 100644 src/test/java/codechicken/multipart/handler/MultipartCompatiblityCharacterizationTest.java diff --git a/src/functionalTest/java/codechicken/multipart/test/MultipartCompatiblityFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MultipartCompatiblityFunctionalTest.java new file mode 100644 index 0000000..8b4a3d2 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MultipartCompatiblityFunctionalTest.java @@ -0,0 +1,60 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Locale; + +import net.minecraft.block.Block; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.multipart.handler.MCPCCompatModule; +import codechicken.multipart.handler.MultipartCompatiblity; +import cpw.mods.fml.common.FMLCommonHandler; +import scala.Function4; +import scala.runtime.AbstractFunction4; + +class MultipartCompatiblityFunctionalTest { + + @Test + void loadLeavesTheCallbackUntouchedOutsideMcpc() { + assertFalse(FMLCommonHandler.instance().getModName().toLowerCase(Locale.ROOT).contains("mcpc")); + Function4 original = MultipartCompatiblity.canAddPart(); + Function4 replacement = new DenyPlacement(); + try { + MultipartCompatiblity.canAddPart_$eq(replacement); + MultipartCompatiblity.load(); + assertSame(replacement, MultipartCompatiblity.canAddPart()); + } finally { + MultipartCompatiblity.canAddPart_$eq(original); + } + } + + @Test + void missingMcpcHookIsLoggedAndLeavesTheCallbackUntouched() { + assertThrows( + NoSuchMethodException.class, + () -> World.class.getDeclaredMethod("canPlaceMultipart", Block.class, int.class, int.class, int.class)); + Function4 original = MultipartCompatiblity.canAddPart(); + Function4 replacement = new DenyPlacement(); + try { + MultipartCompatiblity.canAddPart_$eq(replacement); + assertDoesNotThrow(MCPCCompatModule::load); + assertSame(replacement, MultipartCompatiblity.canAddPart()); + } finally { + MultipartCompatiblity.canAddPart_$eq(original); + } + } + + private static final class DenyPlacement extends AbstractFunction4 { + + @Override + public Object apply(World world, Object x, Object y, Object z) { + return Boolean.FALSE; + } + } +} diff --git a/src/test/java/codechicken/multipart/handler/MultipartCompatiblityCharacterizationTest.java b/src/test/java/codechicken/multipart/handler/MultipartCompatiblityCharacterizationTest.java new file mode 100644 index 0000000..a6262eb --- /dev/null +++ b/src/test/java/codechicken/multipart/handler/MultipartCompatiblityCharacterizationTest.java @@ -0,0 +1,103 @@ +package codechicken.multipart.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +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 org.objectweb.asm.Type; + +import scala.Function4; +import scala.runtime.AbstractFunction4; + +class MultipartCompatiblityCharacterizationTest { + + @Test + void keepsTheTwoStaticFacadesAndCompanionSingletons() throws Exception { + assertFacade( + MultipartCompatiblity.class, + "canAddPart()Lscala/Function4;", + "canAddPart_$eq(Lscala/Function4;)V", + "load()V"); + assertCompanion( + MultipartCompatiblity$.class, + "canAddPart()Lscala/Function4;", + "canAddPart_$eq(Lscala/Function4;)V", + "load()V"); + assertFacade(MCPCCompatModule.class, "load()V"); + assertCompanion(MCPCCompatModule$.class, "load()V"); + + Field callback = MultipartCompatiblity$.class.getDeclaredField("canAddPart"); + assertEquals(Function4.class, callback.getType()); + assertTrue(Modifier.isPrivate(callback.getModifiers())); + } + + @Test + void defaultPlacementCallbackAlwaysAllows() { + assertEquals(Boolean.TRUE, MultipartCompatiblity.canAddPart().apply(null, -1, 0, 1)); + } + + @Test + void facadeAndCompanionShareTheMutableCallback() { + Function4 original = MultipartCompatiblity.canAddPart(); + Function4 replacement = new PlacementCallback(); + try { + MultipartCompatiblity.canAddPart_$eq(replacement); + assertSame(replacement, MultipartCompatiblity.canAddPart()); + assertSame(replacement, MultipartCompatiblity$.MODULE$.canAddPart()); + assertEquals(Boolean.TRUE, replacement.apply(null, 4, 5, 6)); + assertEquals(Boolean.FALSE, replacement.apply(null, 4, 5, 7)); + + MultipartCompatiblity$.MODULE$.canAddPart_$eq(original); + assertSame(original, MultipartCompatiblity.canAddPart()); + } finally { + MultipartCompatiblity.canAddPart_$eq(original); + } + } + + private static void assertFacade(Class type, String... expectedMethods) { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertTrue(Modifier.isFinal(type.getModifiers())); + assertEquals(new TreeSet<>(Arrays.asList(expectedMethods)), publicMethodSignatures(type)); + assertEquals(0, type.getFields().length); + } + + private static void assertCompanion(Class type, String... expectedMethods) throws Exception { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertTrue(Modifier.isFinal(type.getModifiers())); + assertEquals(new TreeSet<>(Arrays.asList(expectedMethods)), publicMethodSignatures(type)); + + Field module = type.getField("MODULE$"); + assertSame(type, module.getType()); + assertTrue(Modifier.isStatic(module.getModifiers())); + assertTrue(Modifier.isFinal(module.getModifiers())); + assertSame(module.get(null), module.get(null)); + } + + private static Set publicMethodSignatures(Class type) { + Set signatures = new TreeSet<>(); + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + signatures.add(method.getName() + Type.getMethodDescriptor(method)); + } + } + return signatures; + } + + private static final class PlacementCallback extends AbstractFunction4 { + + @Override + public Object apply(World world, Object x, Object y, Object z) { + return ((Integer) x) + ((Integer) y) + ((Integer) z) == 15; + } + } +}