From e9f244fefa92349c19aea29ddec5dd6c8492a9fa Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 30 Aug 2026 21:15:20 +0200 Subject: [PATCH] test: characterize multipart event handler Pin event annotations, bus identity, and server dispatch before the Java port. --- .../MultipartEventHandlerFunctionalTest.java | 130 ++++++++++++++++++ ...ipartEventHandlerCharacterizationTest.java | 100 ++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/MultipartEventHandlerFunctionalTest.java create mode 100644 src/test/java/codechicken/multipart/handler/MultipartEventHandlerCharacterizationTest.java diff --git a/src/functionalTest/java/codechicken/multipart/test/MultipartEventHandlerFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MultipartEventHandlerFunctionalTest.java new file mode 100644 index 0000000..2e10a81 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MultipartEventHandlerFunctionalTest.java @@ -0,0 +1,130 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.Field; +import java.util.List; +import java.util.Map; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.common.util.FakePlayerFactory; +import net.minecraftforge.event.world.ChunkDataEvent; +import net.minecraftforge.event.world.ChunkWatchEvent; +import net.minecraftforge.event.world.WorldEvent; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.handler.MultipartEventHandler; +import codechicken.multipart.handler.MultipartEventHandler$; +import codechicken.multipart.handler.MultipartSPH$; +import codechicken.multipart.handler.MultipartSaveLoad; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.EventBus; +import cpw.mods.fml.common.gameevent.TickEvent; + +class MultipartEventHandlerFunctionalTest { + + @Test + void bothEventBusesUseTheCompanionIdentity() throws Exception { + assertSame(MultipartEventHandler$.MODULE$, registeredCompanion(FMLCommonHandler.instance().bus())); + assertSame(MultipartEventHandler$.MODULE$, registeredCompanion(MinecraftForge.EVENT_BUS)); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + void chunkLoadAndWorldUnloadMaintainTransientServerState() { + WorldServer world = world(); + World originalLoadingWorld = MultipartSaveLoad.loadingWorld(); + scala.collection.mutable.Map updates = MultipartSPH$.MODULE$ + .codechicken$multipart$handler$MultipartSPH$$updateMap(); + try { + MultipartSaveLoad.loadingWorld_$eq(null); + Chunk chunk = new Chunk(world, 75, 75); + + MultipartEventHandler.tileEntityLoad(new ChunkDataEvent.Load(chunk, new NBTTagCompound())); + + assertSame(world, MultipartSaveLoad.loadingWorld()); + MultipartSPH$.MODULE$.getTileStream(world, new BlockCoord(1200, 70, 1200)); + assertTrue(updates.contains(world)); + + MultipartEventHandler.worldUnLoad(new WorldEvent.Unload(world)); + + assertNull(MultipartSaveLoad.loadingWorld()); + assertFalse(updates.contains(world)); + } finally { + updates.remove(world); + MultipartSaveLoad.loadingWorld_$eq(originalLoadingWorld); + } + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + void watchChangesAreAppliedOnlyAtTheEndOfTheServerTick() { + WorldServer world = world(); + FakePlayer player = FakePlayerFactory.getMinecraft(world); + assertNotNull(player); + player.setEntityId(-123456789); + Integer playerId = player.getEntityId(); + ChunkCoordIntPair chunk = new ChunkCoordIntPair(0, 0); + scala.collection.mutable.Map newWatchers = MultipartSPH$.MODULE$ + .codechicken$multipart$handler$MultipartSPH$$newWatchers(); + scala.collection.mutable.HashMap chunkWatchers = MultipartSPH$.MODULE$ + .codechicken$multipart$handler$MultipartSPH$$chunkWatchers(); + List players = MinecraftServer.getServer().getConfigurationManager().playerEntityList; + + newWatchers.remove(playerId); + chunkWatchers.remove(playerId); + players.add(player); + try { + MultipartEventHandler.chunkWatch(new ChunkWatchEvent.Watch(chunk, player)); + assertTrue(newWatchers.contains(playerId)); + + MultipartEventHandler.serverTick(new TickEvent.ServerTickEvent(TickEvent.Phase.START)); + assertTrue(newWatchers.contains(playerId)); + assertFalse(chunkWatchers.contains(playerId)); + + MultipartEventHandler.serverTick(new TickEvent.ServerTickEvent(TickEvent.Phase.END)); + assertFalse(newWatchers.contains(playerId)); + assertTrue(((scala.collection.Set) chunkWatchers.apply(playerId)).contains(chunk)); + + MultipartEventHandler.chunkUnWatch(new ChunkWatchEvent.UnWatch(chunk, player)); + assertFalse(chunkWatchers.contains(playerId)); + } finally { + players.remove(player); + newWatchers.remove(playerId); + chunkWatchers.remove(playerId); + } + } + + private static Object registeredCompanion(EventBus bus) throws Exception { + Field listenersField = EventBus.class.getDeclaredField("listeners"); + listenersField.setAccessible(true); + Map listeners = (Map) listenersField.get(bus); + for (Object target : listeners.keySet()) { + if (target == MultipartEventHandler$.MODULE$) { + return target; + } + } + return null; + } + + private static WorldServer world() { + WorldServer world = MinecraftServer.getServer().worldServers[0]; + assertNotNull(world); + assertFalse(world.isRemote); + return world; + } +} diff --git a/src/test/java/codechicken/multipart/handler/MultipartEventHandlerCharacterizationTest.java b/src/test/java/codechicken/multipart/handler/MultipartEventHandlerCharacterizationTest.java new file mode 100644 index 0000000..fb419b1 --- /dev/null +++ b/src/test/java/codechicken/multipart/handler/MultipartEventHandlerCharacterizationTest.java @@ -0,0 +1,100 @@ +package codechicken.multipart.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; + +import cpw.mods.fml.common.eventhandler.EventPriority; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +class MultipartEventHandlerCharacterizationTest { + + private static final Set EVENT_METHODS = new TreeSet<>( + Arrays.asList( + "chunkUnWatch(Lnet/minecraftforge/event/world/ChunkWatchEvent$UnWatch;)V", + "chunkWatch(Lnet/minecraftforge/event/world/ChunkWatchEvent$Watch;)V", + "drawBlockHighlight(Lnet/minecraftforge/client/event/DrawBlockHighlightEvent;)V", + "serverTick(Lcpw/mods/fml/common/gameevent/TickEvent$ServerTickEvent;)V", + "tileEntityLoad(Lnet/minecraftforge/event/world/ChunkDataEvent$Load;)V", + "worldUnLoad(Lnet/minecraftforge/event/world/WorldEvent$Unload;)V")); + + @Test + void keepsBothSingletonTypesAndEventDescriptors() throws Exception { + assertHandlerType(MultipartEventHandler.class, true); + assertHandlerType(MultipartEventHandler$.class, false); + + assertEquals(0, MultipartEventHandler.class.getFields().length); + Field module = MultipartEventHandler$.class.getField("MODULE$"); + assertSame(MultipartEventHandler$.class, module.getType()); + assertTrue(Modifier.isStatic(module.getModifiers())); + assertTrue(Modifier.isFinal(module.getModifiers())); + assertSame(MultipartEventHandler$.MODULE$, module.get(null)); + } + + @Test + void keepsEventPrioritiesAndClientOnlyHighlight() { + assertEventAnnotations(MultipartEventHandler.class); + assertEventAnnotations(MultipartEventHandler$.class); + } + + private static void assertHandlerType(Class type, boolean staticMethods) { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertTrue(Modifier.isFinal(type.getModifiers())); + assertEquals(EVENT_METHODS, publicMethodSignatures(type)); + + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + assertEquals(staticMethods, Modifier.isStatic(method.getModifiers())); + } + } + } + + private static void assertEventAnnotations(Class type) { + for (Method method : type.getDeclaredMethods()) { + if (!Modifier.isPublic(method.getModifiers())) { + continue; + } + + SubscribeEvent subscribe = method.getAnnotation(SubscribeEvent.class); + assertNotNull(subscribe, method.toString()); + assertEquals( + method.getName().equals("tileEntityLoad") ? EventPriority.HIGHEST : EventPriority.NORMAL, + subscribe.priority(), + method.toString()); + assertFalse(subscribe.receiveCanceled(), method.toString()); + + SideOnly sideOnly = method.getAnnotation(SideOnly.class); + if (method.getName().equals("drawBlockHighlight")) { + assertNotNull(sideOnly, method.toString()); + assertEquals(Side.CLIENT, sideOnly.value()); + } else { + assertNull(sideOnly, method.toString()); + } + } + } + + 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; + } +}