mirror of
https://github.com/GTNewHorizons/ForgeMultipart.git
synced 2026-11-13 17:04:25 +00:00
test: characterize multipart packet handlers
This commit is contained in:
1 parent
f063cc94b4
commit
bed000cacf
2 files changed
+389
No files matched your search
@@ -0,0 +1,212 @@
|
||||
package codechicken.multipart.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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.WorldClient;
|
||||
import net.minecraft.client.network.NetHandlerPlayClient;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.network.play.INetHandlerPlayClient;
|
||||
import net.minecraft.network.play.server.S40PacketDisconnect;
|
||||
import net.minecraft.profiler.Profiler;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.ChatComponentText;
|
||||
import net.minecraft.util.ChatComponentTranslation;
|
||||
import net.minecraft.world.EnumDifficulty;
|
||||
import net.minecraft.world.WorldSettings;
|
||||
import net.minecraft.world.chunk.IChunkProvider;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import codechicken.lib.packet.PacketCustom;
|
||||
import codechicken.lib.vec.BlockCoord;
|
||||
import codechicken.multipart.ControlKeyModifer;
|
||||
import codechicken.multipart.MultiPartRegistry;
|
||||
import codechicken.multipart.TileCache;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import scala.MatchError;
|
||||
|
||||
class MultipartPacketHandlerBehaviorTest {
|
||||
|
||||
@Test
|
||||
void reportsEveryMissingServerPartInOrder() {
|
||||
AtomicReference<S40PacketDisconnect> disconnect = new AtomicReference<>();
|
||||
PacketCustom packet = new PacketCustom("test", 1).writeInt(2).writeString("server:missing-one")
|
||||
.writeString("server:missing-two");
|
||||
try {
|
||||
MultipartCPH.handlePartRegistration(incoming(packet), clientHandler(disconnect));
|
||||
} finally {
|
||||
MultiPartRegistry.beforeServerStart();
|
||||
}
|
||||
|
||||
S40PacketDisconnect disconnectPacket = disconnect.get();
|
||||
assertNotNull(disconnectPacket);
|
||||
ChatComponentTranslation reason = (ChatComponentTranslation) disconnectPacket.func_149165_c();
|
||||
assertEquals("multipart.missing", reason.getKey());
|
||||
assertArrayEquals(new Object[] { "server:missing-one, server:missing-two" }, reason.getFormatArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void turnsClientDesyncErrorsIntoAPlainDisconnectReason() throws Exception {
|
||||
TileCache.clear();
|
||||
EmptyWorldClient world = allocateInstance(EmptyWorldClient.class);
|
||||
world.isRemote = true;
|
||||
Minecraft minecraft = allocateInstance(Minecraft.class);
|
||||
minecraft.theWorld = world;
|
||||
AtomicReference<S40PacketDisconnect> disconnect = new AtomicReference<>();
|
||||
PacketCustom packet = new PacketCustom("test", 3).writeInt(4).writeInt(5).writeInt(6).writeByte(0);
|
||||
|
||||
MultipartCPH.handlePacket(incoming(packet), minecraft, clientHandler(disconnect));
|
||||
|
||||
S40PacketDisconnect disconnectPacket = disconnect.get();
|
||||
assertNotNull(disconnectPacket);
|
||||
ChatComponentText reason = (ChatComponentText) disconnectPacket.func_149165_c();
|
||||
String text = reason.getUnformattedText();
|
||||
assertTrue(text.startsWith("Client multipart @"));
|
||||
assertTrue(text.endsWith(" not found"));
|
||||
assertFalse(text.startsWith("DC: "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsBothCompressedPacketTerminators() {
|
||||
PacketCustom description = incoming(new PacketCustom("test", 2).writeInt(-3).writeInt(7).writeShort(0));
|
||||
PacketCustom updates = incoming(
|
||||
new PacketCustom("test", 3).writeInt(16).writeInt(64).writeInt(32).writeByte(255)
|
||||
.writeInt(Integer.MAX_VALUE));
|
||||
|
||||
assertDoesNotThrow(() -> MultipartCPH.handleCompressedTileDesc(description, null));
|
||||
assertDoesNotThrow(() -> MultipartCPH.handleCompressedTileData(updates, null));
|
||||
assertFullyRead(description);
|
||||
assertFullyRead(updates);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownPacketTypesLikeTheScalaMatches() {
|
||||
assertThrows(
|
||||
MatchError.class,
|
||||
() -> MultipartCPH.handlePacket(incoming(new PacketCustom("test", 4)), null, clientHandler(null)));
|
||||
assertThrows(
|
||||
MatchError.class,
|
||||
() -> MultipartSPH.handlePacket(incoming(new PacketCustom("test", 2)), null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverPacketOneUpdatesTheControlKeyState() {
|
||||
ControlKeyModifer.map().remove(null);
|
||||
try {
|
||||
MultipartSPH.handlePacket(incoming(new PacketCustom("test", 1).writeBoolean(true)), null, null);
|
||||
assertEquals(Boolean.TRUE, ControlKeyModifer.map().get(null));
|
||||
|
||||
MultipartSPH$.MODULE$.handlePacket(incoming(new PacketCustom("test", 1).writeBoolean(false)), null, null);
|
||||
assertEquals(Boolean.FALSE, ControlKeyModifer.map().get(null));
|
||||
} finally {
|
||||
ControlKeyModifer.map().remove(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void tileStreamsAreCachedPerPositionAndStartWithTheCoordinate() throws Exception {
|
||||
EmptyWorldClient world = allocateInstance(EmptyWorldClient.class);
|
||||
world.isRemote = false;
|
||||
BlockCoord position = new BlockCoord(0x01020304, -2, 0x0A0B0C0D);
|
||||
scala.collection.mutable.Map updates = MultipartSPH$.MODULE$
|
||||
.codechicken$multipart$handler$MultipartSPH$$updateMap();
|
||||
updates.remove(world);
|
||||
try {
|
||||
MultipartSPH.MCByteStream stream = MultipartSPH.getTileStream(world, position);
|
||||
assertSame(stream, MultipartSPH$.MODULE$.getTileStream(world, position));
|
||||
stream.writeByte(0x7F);
|
||||
assertArrayEquals(new byte[] { 1, 2, 3, 4, -1, -1, -1, -2, 10, 11, 12, 13, 127 }, stream.getBytes());
|
||||
} finally {
|
||||
updates.remove(world);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void tileStreamsRejectClientWorldsBeforeCreatingState() throws Exception {
|
||||
EmptyWorldClient world = allocateInstance(EmptyWorldClient.class);
|
||||
world.isRemote = true;
|
||||
scala.collection.mutable.Map updates = MultipartSPH$.MODULE$
|
||||
.codechicken$multipart$handler$MultipartSPH$$updateMap();
|
||||
updates.remove(world);
|
||||
|
||||
IllegalArgumentException error = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> MultipartSPH.getTileStream(world, new BlockCoord(1, 2, 3)));
|
||||
|
||||
assertEquals("Cannot use MultipartSPH on a client world", error.getMessage());
|
||||
assertFalse(updates.contains(world));
|
||||
}
|
||||
|
||||
private static PacketCustom incoming(PacketCustom packet) {
|
||||
return new PacketCustom(packet.getByteBuf().copy());
|
||||
}
|
||||
|
||||
private static void assertFullyRead(PacketCustom packet) {
|
||||
ByteBuf buffer = packet.getByteBuf();
|
||||
assertEquals(buffer.writerIndex(), buffer.readerIndex());
|
||||
}
|
||||
|
||||
private static INetHandlerPlayClient clientHandler(AtomicReference<S40PacketDisconnect> 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());
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T allocateInstance(Class<T> type) throws Exception {
|
||||
Class<?> unsafe = Class.forName("sun.misc.Unsafe");
|
||||
Field field = unsafe.getDeclaredField("theUnsafe");
|
||||
field.setAccessible(true);
|
||||
return (T) unsafe.getMethod("allocateInstance", Class.class).invoke(field.get(null), type);
|
||||
}
|
||||
|
||||
private static final class EmptyWorldClient extends WorldClient {
|
||||
|
||||
private EmptyWorldClient() {
|
||||
super((NetHandlerPlayClient) null, (WorldSettings) null, 0, (EnumDifficulty) null, (Profiler) null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IChunkProvider createChunkProvider() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int func_152379_p() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getEntityByID(int id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TileEntity getTileEntity(int x, int y, int z) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package codechicken.multipart.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.io.ByteArrayOutputStream;
|
||||
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.data.MCDataOutputWrapper;
|
||||
import codechicken.lib.packet.PacketCustom.IClientPacketHandler;
|
||||
import codechicken.lib.packet.PacketCustom.IHandshakeHandler;
|
||||
import codechicken.lib.packet.PacketCustom.IServerPacketHandler;
|
||||
import scala.collection.mutable.HashMap;
|
||||
import scala.collection.mutable.Map;
|
||||
|
||||
class MultipartPacketHandlerCharacterizationTest {
|
||||
|
||||
private static final Set<String> CLIENT_METHODS = new TreeSet<>(
|
||||
Arrays.asList(
|
||||
"handleCompressedTileData(Lcodechicken/lib/packet/PacketCustom;Lnet/minecraft/world/World;)V",
|
||||
"handleCompressedTileDesc(Lcodechicken/lib/packet/PacketCustom;Lnet/minecraft/world/World;)V",
|
||||
"handlePartRegistration(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<String> SERVER_METHODS = new TreeSet<>(
|
||||
Arrays.asList(
|
||||
"codechicken$multipart$handler$MultipartSPH$$chunkWatchers()Lscala/collection/mutable/HashMap;",
|
||||
"codechicken$multipart$handler$MultipartSPH$$newWatchers()Lscala/collection/mutable/Map;",
|
||||
"codechicken$multipart$handler$MultipartSPH$$updateMap()Lscala/collection/mutable/Map;",
|
||||
"getDescPacket(Lnet/minecraft/world/chunk/Chunk;Ljava/util/Iterator;)Lcodechicken/lib/packet/PacketCustom;",
|
||||
"getTileStream(Lnet/minecraft/world/World;Lcodechicken/lib/vec/BlockCoord;)Lcodechicken/multipart/handler/MultipartSPH$MCByteStream;",
|
||||
"handlePacket(Lcodechicken/lib/packet/PacketCustom;Lnet/minecraft/entity/player/EntityPlayerMP;Lnet/minecraft/network/play/INetHandlerPlayServer;)V",
|
||||
"handshakeRecieved(Lnet/minecraft/network/NetHandlerPlayServer;)V",
|
||||
"onChunkUnWatch(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/world/ChunkCoordIntPair;)V",
|
||||
"onChunkWatch(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/world/ChunkCoordIntPair;)V",
|
||||
"onTickEnd(Lscala/collection/Seq;)V",
|
||||
"onWorldUnload(Lnet/minecraft/world/World;)V"));
|
||||
|
||||
@Test
|
||||
void keepsTheSharedPacketHandlerBase() throws Exception {
|
||||
assertTrue(Modifier.isPublic(MultipartPH.class.getModifiers()));
|
||||
assertFalse(Modifier.isFinal(MultipartPH.class.getModifiers()));
|
||||
assertArrayEquals(new Class<?>[0], MultipartPH.class.getInterfaces());
|
||||
assertEquals(
|
||||
new TreeSet<>(
|
||||
Arrays.asList(
|
||||
"channel()Lcodechicken/multipart/handler/MultipartMod$;",
|
||||
"registryChannel()Ljava/lang/String;")),
|
||||
publicMethodSignatures(MultipartPH.class));
|
||||
|
||||
assertPrivateFinalField(MultipartPH.class, "channel", MultipartMod$.class);
|
||||
assertPrivateFinalField(MultipartPH.class, "registryChannel", String.class);
|
||||
MultipartPH handler = new MultipartPH();
|
||||
assertSame(MultipartMod$.MODULE$, handler.channel());
|
||||
assertEquals("ForgeMultipart", handler.registryChannel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsTheClientFacadeAndCompanion() throws Exception {
|
||||
assertFacade(MultipartCPH.class, withSharedChannels(CLIENT_METHODS));
|
||||
assertCompanion(MultipartCPH$.class, new Class<?>[] { IClientPacketHandler.class }, CLIENT_METHODS);
|
||||
assertSame(MultipartCPH$.class, MultipartCPH$.class.getField("MODULE$").getType());
|
||||
assertSame(MultipartCPH$.MODULE$, MultipartCPH$.class.getField("MODULE$").get(null));
|
||||
assertSame(MultipartMod$.MODULE$, MultipartCPH.channel());
|
||||
assertEquals("ForgeMultipart", MultipartCPH.registryChannel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsTheServerFacadeCompanionAndStateAccessors() throws Exception {
|
||||
Set<String> facadeMethods = withSharedChannels(SERVER_METHODS);
|
||||
facadeMethods.remove(
|
||||
"codechicken$multipart$handler$MultipartSPH$$chunkWatchers()Lscala/collection/mutable/HashMap;");
|
||||
facadeMethods.remove("codechicken$multipart$handler$MultipartSPH$$newWatchers()Lscala/collection/mutable/Map;");
|
||||
facadeMethods.remove("codechicken$multipart$handler$MultipartSPH$$updateMap()Lscala/collection/mutable/Map;");
|
||||
assertFacade(MultipartSPH.class, facadeMethods);
|
||||
assertCompanion(
|
||||
MultipartSPH$.class,
|
||||
new Class<?>[] { IServerPacketHandler.class, IHandshakeHandler.class },
|
||||
SERVER_METHODS);
|
||||
assertSame(MultipartSPH$.class, MultipartSPH$.class.getField("MODULE$").getType());
|
||||
assertSame(MultipartSPH$.MODULE$, MultipartSPH$.class.getField("MODULE$").get(null));
|
||||
|
||||
assertPrivateFinalField(
|
||||
MultipartSPH$.class,
|
||||
"codechicken$multipart$handler$MultipartSPH$$updateMap",
|
||||
Map.class);
|
||||
assertPrivateFinalField(
|
||||
MultipartSPH$.class,
|
||||
"codechicken$multipart$handler$MultipartSPH$$chunkWatchers",
|
||||
HashMap.class);
|
||||
assertPrivateFinalField(
|
||||
MultipartSPH$.class,
|
||||
"codechicken$multipart$handler$MultipartSPH$$newWatchers",
|
||||
Map.class);
|
||||
assertSame(MultipartMod$.MODULE$, MultipartSPH.channel());
|
||||
assertEquals("ForgeMultipart", MultipartSPH.registryChannel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsTheNestedByteStreamShape() throws Exception {
|
||||
Class<?> type = MultipartSPH.MCByteStream.class;
|
||||
assertSame(MultipartSPH.class, type.getDeclaringClass());
|
||||
assertTrue(Modifier.isPublic(type.getModifiers()));
|
||||
assertTrue(Modifier.isStatic(type.getModifiers()));
|
||||
assertFalse(Modifier.isFinal(type.getModifiers()));
|
||||
assertSame(MCDataOutputWrapper.class, type.getSuperclass());
|
||||
assertArrayEquals(new Class<?>[0], type.getInterfaces());
|
||||
assertEquals(new TreeSet<>(Arrays.asList("getBytes()[B")), publicMethodSignatures(type));
|
||||
assertPrivateFinalField(type, "bout", ByteArrayOutputStream.class);
|
||||
assertSame(type, type.getConstructor(ByteArrayOutputStream.class).getDeclaringClass());
|
||||
}
|
||||
|
||||
private static void assertFacade(Class<?> type, Set<String> 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<?>[] packetInterfaces, Set<String> methods)
|
||||
throws Exception {
|
||||
assertTrue(Modifier.isPublic(type.getModifiers()));
|
||||
assertTrue(Modifier.isFinal(type.getModifiers()));
|
||||
assertSame(MultipartPH.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 void assertPrivateFinalField(Class<?> owner, String name, Class<?> type) throws Exception {
|
||||
Field field = owner.getDeclaredField(name);
|
||||
assertSame(type, field.getType());
|
||||
assertTrue(Modifier.isPrivate(field.getModifiers()));
|
||||
assertTrue(Modifier.isFinal(field.getModifiers()));
|
||||
}
|
||||
|
||||
private static Set<String> withSharedChannels(Set<String> methods) {
|
||||
Set<String> result = new TreeSet<>(methods);
|
||||
result.add("channel()Lcodechicken/multipart/handler/MultipartMod$;");
|
||||
result.add("registryChannel()Ljava/lang/String;");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<String> publicMethodSignatures(Class<?> type) {
|
||||
Set<String> signatures = new TreeSet<>();
|
||||
for (Method method : type.getDeclaredMethods()) {
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
signatures.add(method.getName() + Type.getMethodDescriptor(method));
|
||||
}
|
||||
}
|
||||
return signatures;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user