diff --git a/dependencies.gradle b/dependencies.gradle index b27f057..6579077 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -3,6 +3,7 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.11.4")) testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") diff --git a/src/functionalTest/java/codechicken/multipart/test/MicroblockPacketHandlerFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MicroblockPacketHandlerFunctionalTest.java new file mode 100644 index 0000000..7aeb2e9 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MicroblockPacketHandlerFunctionalTest.java @@ -0,0 +1,66 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.Packet; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldServer; +import net.minecraftforge.common.util.FakePlayerFactory; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.packet.PacketCustom; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.handler.MicroblockSPH; +import cpw.mods.fml.common.network.internal.FMLProxyPacket; +import scala.Tuple2; + +class MicroblockPacketHandlerFunctionalTest { + + @Test + void sendsTheCompleteMaterialIdMapDuringTheServerHandshake() { + MinecraftServer server = MinecraftServer.getServer(); + WorldServer world = server.worldServers[0]; + EntityPlayerMP player = FakePlayerFactory.getMinecraft(world); + NetHandlerPlayServer previousHandler = player.playerNetServerHandler; + + RecordingNetHandler handler = new RecordingNetHandler(server, player); + try { + MicroblockSPH.handshakeRecieved(handler); + } finally { + player.playerNetServerHandler = previousHandler; + } + + FMLProxyPacket sent = assertInstanceOf(FMLProxyPacket.class, handler.sent); + assertEquals("ForgeMicroblock", sent.channel()); + + PacketCustom payload = new PacketCustom(sent.payload().copy()); + assertEquals(1, payload.getType()); + Tuple2[] materials = MicroMaterialRegistry.getIdMap(); + assertEquals(materials.length, payload.readInt()); + for (Tuple2 material : materials) { + assertEquals(material._1(), payload.readString()); + } + assertEquals(0, payload.getByteBuf().readableBytes()); + } + + private static final class RecordingNetHandler extends NetHandlerPlayServer { + + private Packet sent; + + private RecordingNetHandler(MinecraftServer server, EntityPlayerMP player) { + super(server, new NetworkManager(false), player); + } + + @Override + public void sendPacket(Packet packet) { + assertNotNull(packet); + sent = packet; + } + } +} diff --git a/src/test/java/codechicken/microblock/handler/MicroblockPacketHandlerBehaviorTest.java b/src/test/java/codechicken/microblock/handler/MicroblockPacketHandlerBehaviorTest.java new file mode 100644 index 0000000..af519f0 --- /dev/null +++ b/src/test/java/codechicken/microblock/handler/MicroblockPacketHandlerBehaviorTest.java @@ -0,0 +1,114 @@ +package codechicken.microblock.handler; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicReference; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.play.INetHandlerPlayClient; +import net.minecraft.network.play.server.S40PacketDisconnect; +import net.minecraft.util.ChatComponentTranslation; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.packet.PacketCustom; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.MissingMicroMaterial; +import codechicken.microblock.MissingMicroMaterial$; +import io.netty.buffer.Unpooled; +import scala.MatchError; + +class MicroblockPacketHandlerBehaviorTest { + + @Test + void skipsTheSharedRegistryPacketInSinglePlayer() throws Exception { + PacketCustom packetWithoutPayload = new PacketCustom(Unpooled.buffer().writeByte(1)); + + assertDoesNotThrow( + () -> MicroblockCPH.handlePacket(packetWithoutPayload, minecraft(true), clientHandler(null))); + } + + @Test + void reportsEveryMissingServerMaterialInOrder() throws Exception { + if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) { + MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key()); + } + + AtomicReference disconnect = new AtomicReference<>(); + PacketCustom packet = incomingPacket(1, "server:missing-one", "server:missing-two"); + try { + MicroblockCPH.handleMaterialRegistration(packet, clientHandler(disconnect)); + } finally { + MicroMaterialRegistry.setupIDMap(); + } + + S40PacketDisconnect disconnectPacket = disconnect.get(); + assertNotNull(disconnectPacket); + ChatComponentTranslation reason = (ChatComponentTranslation) disconnectPacket.func_149165_c(); + assertEquals("microblock.missing", reason.getKey()); + assertArrayEquals(new Object[] { "server:missing-one, server:missing-two" }, reason.getFormatArgs()); + } + + @Test + void rejectsUnknownClientPacketTypesLikeTheScalaMatch() throws Exception { + PacketCustom packet = new PacketCustom(Unpooled.buffer().writeByte(2)); + + assertThrows(MatchError.class, () -> MicroblockCPH.handlePacket(packet, null, clientHandler(null))); + } + + @Test + void leavesTheServerPacketCallbackAsANoOp() { + assertDoesNotThrow(() -> MicroblockSPH.handlePacket(null, null, null)); + assertDoesNotThrow(() -> MicroblockSPH$.MODULE$.handlePacket(null, null, null)); + } + + private static PacketCustom incomingPacket(int type, String... materialNames) { + PacketCustom packet = new PacketCustom("test", type).writeInt(materialNames.length); + for (String materialName : materialNames) { + packet.writeString(materialName); + } + return new PacketCustom(packet.getByteBuf().copy()); + } + + private static INetHandlerPlayClient clientHandler(AtomicReference disconnect) { + return (INetHandlerPlayClient) Proxy.newProxyInstance( + INetHandlerPlayClient.class.getClassLoader(), + new Class[] { INetHandlerPlayClient.class }, + (proxy, method, arguments) -> { + if ("handleDisconnect".equals(method.getName())) { + if (disconnect != null) { + disconnect.set((S40PacketDisconnect) arguments[0]); + } + return null; + } + throw new AssertionError("Unexpected client callback: " + method.getName()); + }); + } + + private static Minecraft minecraft(boolean singlePlayer) throws Exception { + Minecraft minecraft = (Minecraft) allocateInstance(Minecraft.class); + if (singlePlayer) { + Field running = Minecraft.class.getDeclaredField("integratedServerIsRunning"); + running.setAccessible(true); + running.setBoolean(minecraft, true); + + Field server = Minecraft.class.getDeclaredField("theIntegratedServer"); + server.setAccessible(true); + server.set(minecraft, allocateInstance(server.getType())); + } + return minecraft; + } + + private static Object allocateInstance(Class type) throws Exception { + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field field = unsafe.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return unsafe.getMethod("allocateInstance", Class.class).invoke(field.get(null), type); + } +} diff --git a/src/test/java/codechicken/microblock/handler/MicroblockPacketHandlerCharacterizationTest.java b/src/test/java/codechicken/microblock/handler/MicroblockPacketHandlerCharacterizationTest.java new file mode 100644 index 0000000..2b629ad --- /dev/null +++ b/src/test/java/codechicken/microblock/handler/MicroblockPacketHandlerCharacterizationTest.java @@ -0,0 +1,121 @@ +package codechicken.microblock.handler; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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 codechicken.lib.packet.PacketCustom.IClientPacketHandler; +import codechicken.lib.packet.PacketCustom.IHandshakeHandler; +import codechicken.lib.packet.PacketCustom.IServerPacketHandler; + +class MicroblockPacketHandlerCharacterizationTest { + + private static final Set CLIENT_METHODS = new TreeSet<>( + Arrays.asList( + "handleMaterialRegistration(Lcodechicken/lib/packet/PacketCustom;Lnet/minecraft/network/play/INetHandlerPlayClient;)V", + "handlePacket(Lcodechicken/lib/packet/PacketCustom;Lnet/minecraft/client/Minecraft;Lnet/minecraft/network/play/INetHandlerPlayClient;)V")); + private static final Set SERVER_METHODS = new TreeSet<>( + Arrays.asList( + "handlePacket(Lcodechicken/lib/packet/PacketCustom;Lnet/minecraft/entity/player/EntityPlayerMP;Lnet/minecraft/network/play/INetHandlerPlayServer;)V", + "handshakeRecieved(Lnet/minecraft/network/NetHandlerPlayServer;)V")); + + @Test + void keepsTheSharedPacketHandlerBase() throws Exception { + assertTrue(Modifier.isPublic(MicroblockPH.class.getModifiers())); + assertFalse(Modifier.isFinal(MicroblockPH.class.getModifiers())); + assertArrayEquals(new Class[0], MicroblockPH.class.getInterfaces()); + assertEquals( + new TreeSet<>(Arrays.asList("registryChannel()Ljava/lang/String;")), + publicMethodSignatures(MicroblockPH.class)); + + Field channel = MicroblockPH.class.getDeclaredField("registryChannel"); + assertSame(String.class, channel.getType()); + assertTrue(Modifier.isPrivate(channel.getModifiers())); + assertTrue(Modifier.isFinal(channel.getModifiers())); + assertEquals("ForgeMicroblock", new MicroblockPH().registryChannel()); + } + + @Test + void keepsTheClientFacadeAndCompanion() throws Exception { + assertFacade(MicroblockCPH.class, withRegistryChannel(CLIENT_METHODS)); + assertCompanion(MicroblockCPH$.class, IClientPacketHandler.class, CLIENT_METHODS); + assertSame(MicroblockCPH$.class, MicroblockCPH$.class.getField("MODULE$").getType()); + assertSame(MicroblockCPH$.MODULE$, MicroblockCPH$.class.getField("MODULE$").get(null)); + assertEquals("ForgeMicroblock", MicroblockCPH.registryChannel()); + } + + @Test + void keepsTheServerFacadeAndCompanion() throws Exception { + assertFacade(MicroblockSPH.class, withRegistryChannel(SERVER_METHODS)); + assertCompanion( + MicroblockSPH$.class, + new Class[] { IServerPacketHandler.class, IHandshakeHandler.class }, + SERVER_METHODS); + assertSame(MicroblockSPH$.class, MicroblockSPH$.class.getField("MODULE$").getType()); + assertSame(MicroblockSPH$.MODULE$, MicroblockSPH$.class.getField("MODULE$").get(null)); + assertEquals("ForgeMicroblock", MicroblockSPH.registryChannel()); + } + + private static void assertFacade(Class type, Set methods) { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertTrue(Modifier.isFinal(type.getModifiers())); + assertArrayEquals(new Class[0], type.getInterfaces()); + assertEquals(methods, publicMethodSignatures(type)); + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + assertTrue(Modifier.isStatic(method.getModifiers()), method.toString()); + } + } + } + + private static void assertCompanion(Class type, Class packetInterface, Set methods) throws Exception { + assertCompanion(type, new Class[] { packetInterface }, methods); + } + + private static void assertCompanion(Class type, Class[] packetInterfaces, Set methods) + throws Exception { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertTrue(Modifier.isFinal(type.getModifiers())); + assertSame(MicroblockPH.class, type.getSuperclass()); + assertArrayEquals(packetInterfaces, type.getInterfaces()); + assertEquals(methods, publicMethodSignatures(type)); + + Field module = type.getField("MODULE$"); + assertTrue(Modifier.isPublic(module.getModifiers())); + assertTrue(Modifier.isStatic(module.getModifiers())); + assertTrue(Modifier.isFinal(module.getModifiers())); + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + assertFalse(Modifier.isStatic(method.getModifiers()), method.toString()); + } + } + } + + private static Set withRegistryChannel(Set methods) { + Set result = new TreeSet<>(methods); + result.add("registryChannel()Ljava/lang/String;"); + return result; + } + + 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; + } +}