From 09cc024a3f3745b117ed7a74659a886d993a05fb Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 7 Oct 2013 04:29:21 +0200 Subject: [PATCH] Added a simple integration test to ensure ProtocolLib actually works. We do this by running a CraftBukkit server in target/server, and copying ProtocolLib to its plugin folder. --- ProtocolLib/pom.xml | 19 ++- .../comphenix/protocol/ProtocolLibrary.java | 6 +- .../protocol/SimpleCraftBukkitITCase.java | 119 +++++++++++++++++ .../protocol/SimpleMinecraftClient.java | 120 ++++++++++++++++++ .../integration/protocol/TestPingPacket.java | 80 ++++++++++++ 5 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleCraftBukkitITCase.java create mode 100644 ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleMinecraftClient.java create mode 100644 ProtocolLib/src/test/java/com/comphenix/integration/protocol/TestPingPacket.java diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index 1e832648..e88b3005 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -91,6 +91,23 @@ + + org.apache.maven.plugins + maven-failsafe-plugin + 2.12.4 + + ${basedir}/target/server/ + -Xmx1024m -Xms1024M -Dnojline=true + + + + + integration-test + verify + + + + @@ -146,7 +163,7 @@ - + org.apache.maven.plugins maven-gpg-plugin diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index a61db9b7..a48c2302 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -126,7 +126,7 @@ public class ProtocolLibrary extends JavaPlugin { // Updater private Updater updater; - private boolean updateDisabled; + private static boolean UPDATES_DISABLED; // Logger private Logger logger; @@ -479,7 +479,7 @@ public class ProtocolLibrary extends JavaPlugin { manager.sendProcessedPackets(tickCounter++, true); // Check for updates too - if (!updateDisabled) { + if (!UPDATES_DISABLED) { checkUpdates(); } } @@ -511,7 +511,7 @@ public class ProtocolLibrary extends JavaPlugin { } } catch (Exception e) { reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(e)); - updateDisabled = true; + UPDATES_DISABLED = true; } } diff --git a/ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleCraftBukkitITCase.java b/ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleCraftBukkitITCase.java new file mode 100644 index 00000000..07e57a01 --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleCraftBukkitITCase.java @@ -0,0 +1,119 @@ +package com.comphenix.integration.protocol; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.Callable; +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoadOrder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.reflect.FieldUtils; +import com.google.common.collect.Lists; + +// Damn final classes ... +@RunWith(org.powermock.modules.junit4.PowerMockRunner.class) +@PrepareForTest(PluginDescriptionFile.class) +public class SimpleCraftBukkitITCase { + // The fake plugin + private static volatile Plugin FAKE_PLUGIN = null; + + /** + * Setup the CraftBukkit server for all the tests. + * @throws IOException Unable to setup server. + * @throws InterruptedException Thread interrupted. + */ + @BeforeClass + public static void setupCraftBukkit() throws Exception { + setupPlugins(); + org.bukkit.craftbukkit.Main.main(new String[0]); + + // We need to wait until the server object is ready + while (Bukkit.getServer() == null) + Thread.sleep(1); + + // Make it clear this plugin doesn't exist + FAKE_PLUGIN = createPlugin("FakeTestPluginIntegration"); + + // No need to look for updates + FieldUtils.writeStaticField(ProtocolLibrary.class, "UPDATES_DISABLED", Boolean.TRUE, true); + + // Wait until the server and all the plugins have loaded + Bukkit.getScheduler().callSyncMethod(FAKE_PLUGIN, new Callable() { + @Override + public Object call() throws Exception { + return null; + } + }).get(); + + // Plugins are now ready + } + + /** + * Close the CraftBukkit server when they're done. + */ + @AfterClass + public static void shutdownCraftBukkit() { + Bukkit.shutdown(); + } + + @Test + public void testPingPacket() throws Throwable { + TestPingPacket.newTest().startTest(FAKE_PLUGIN); + } + + /** + * Copy ProtocolLib into the plugins folder. + * @throws IOException If anything went wrong. + */ + private static void setupPlugins() throws IOException { + File pluginDirectory = new File("plugins/"); + File bestFile = null; + int bestLength = Integer.MAX_VALUE; + + // Copy the ProtocolLib plugin to the server + FileUtils.cleanDirectory(pluginDirectory); + + for (File file : new File("../").listFiles()) { + String name = file.getName(); + + if (name.startsWith("ProtocolLib") && name.length() < bestLength) { + bestLength = name.length(); + bestFile = file; + } + } + FileUtils.copyFile(bestFile, new File(pluginDirectory, bestFile.getName())); + } + + /** + * Create a mockable plugin for all the tests. + * @param fakePluginName - the fake plugin name. + * @return The plugin. + */ + private static Plugin createPlugin(String fakePluginName) { + Plugin plugin = mock(Plugin.class); + PluginDescriptionFile description = mock(PluginDescriptionFile.class); + + when(description.getDepend()).thenReturn(Lists.newArrayList("ProtocolLib")); + when(description.getSoftDepend()).thenReturn(Collections.emptyList()); + when(description.getLoadBefore()).thenReturn(Collections.emptyList()); + when(description.getLoad()).thenReturn(PluginLoadOrder.POSTWORLD); + + when(plugin.getName()).thenReturn(fakePluginName); + when(plugin.getServer()).thenReturn(Bukkit.getServer()); + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getDescription()).thenReturn(description); + return plugin; + } +} diff --git a/ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleMinecraftClient.java b/ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleMinecraftClient.java new file mode 100644 index 00000000..f51aff95 --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/integration/protocol/SimpleMinecraftClient.java @@ -0,0 +1,120 @@ +package com.comphenix.integration.protocol; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.Charset; + +import com.comphenix.protocol.utility.MinecraftVersion; +import com.google.common.base.Charsets; + +public class SimpleMinecraftClient { + private static final int CONNECT_TIMEOUT = 2500; + private static final int READ_TIMEOUT = 15000; + + // The version after which we must send a plugin message with the host name + private static final String PLUGIN_MESSAGE_VERSION = "1.6.0"; + + // Current Minecraft version + private final MinecraftVersion version; + private final int protocolVersion; + + public SimpleMinecraftClient(MinecraftVersion version, int protocolVersion) { + this.version = version; + this.protocolVersion = protocolVersion; + } + + /** + * Query the local server for ping information. + * @return The server information. + * @throws IOException + */ + public String queryLocalPing() throws IOException { + return queryServerPing(new InetSocketAddress("localhost", 25565)); + } + + /** + * Query the given server for its list ping information. + * @param address - the server hostname and port. + * @return The server information. + * @throws IOException + */ + public String queryServerPing(InetSocketAddress address) throws IOException { + Socket socket = null; + OutputStream output = null; + InputStream input = null; + InputStreamReader reader = null; + + // UTF-16! + Charset charset = Charsets.UTF_16BE; + + try { + socket = new Socket(); + socket.connect(address, CONNECT_TIMEOUT); + + // Shouldn't take that long + socket.setSoTimeout(READ_TIMEOUT); + + // Retrieve sockets + output = socket.getOutputStream(); + input = socket.getInputStream(); + reader = new InputStreamReader(input, charset); + + // Get the server to send a MOTD + output.write(new byte[] { (byte) 0xFE, (byte) 0x01 }); + + // For 1.6 + if (version.compareTo(new MinecraftVersion(PLUGIN_MESSAGE_VERSION)) >= 0) { + DataOutputStream data = new DataOutputStream(output); + String host = address.getHostString(); + + data.writeByte(0xFA); + writeString(data, "MC|PingHost"); + data.writeShort(3 + 2 * host.length() + 4); + + data.writeByte(protocolVersion); + writeString(data, host); + data.writeInt(address.getPort()); + data.flush(); + } + + int packetId = input.read(); + int length = reader.read(); + + if (packetId != 255) + throw new IOException("Invalid packet ID: " + packetId); + if (length <= 0) + throw new IOException("Invalid string length."); + + char[] chars = new char[length]; + + // Read all the characters + if (reader.read(chars, 0, length) != length) { + throw new IOException("Premature end of stream."); + } + + return new String(chars); + + } finally { + if (reader != null) + reader.close(); + if (input != null) + input.close(); + if (output != null) + output.close(); + if (socket != null) + socket.close(); + } + } + + private void writeString(DataOutputStream output, String text) throws IOException { + if (text.length() > 32767) + throw new IOException("String too big: " + text.length()); + output.writeShort(text.length()); + output.writeChars(text); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/test/java/com/comphenix/integration/protocol/TestPingPacket.java b/ProtocolLib/src/test/java/com/comphenix/integration/protocol/TestPingPacket.java new file mode 100644 index 00000000..c5f90d22 --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/integration/protocol/TestPingPacket.java @@ -0,0 +1,80 @@ +package com.comphenix.integration.protocol; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.utility.MinecraftVersion; + +public class TestPingPacket { + // Current versions + private static final String CRAFTBUKKIT_VERSION = "1.6.2"; + private static final int PROTOCOL_VERSION = 74; + + private volatile String source; + + private TestPingPacket() { + // Prevent external constructors + } + + /** + * Create a new test ping packet test. + * @return The new test. + */ + public static TestPingPacket newTest() { + return new TestPingPacket(); + } + + /** + * Invoked when the test should be started. + * @param plugin - the current plugin. + * @throws Throwable Anything went wrong. + */ + public void startTest(Plugin plugin) throws Throwable { + try { + String transmitted = testInterception(plugin).get(); + + // Make sure it's the same + System.out.println("Server string: " + transmitted); + assertEquals(transmitted, source); + } catch (ExecutionException e) { + throw e.getCause(); + } + } + + private Future testInterception(Plugin test) { + ProtocolLibrary.getProtocolManager().addPacketListener( + new PacketAdapter(test, ConnectionSide.SERVER_SIDE, GamePhase.LOGIN, Packets.Server.KICK_DISCONNECT) { + @Override + public void onPacketSending(PacketEvent event) { + source = event.getPacket().getStrings().read(0); + } + }); + + // Invoke the client on a separate thread + return Executors.newSingleThreadExecutor().submit(new Callable() { + @Override + public String call() throws Exception { + SimpleMinecraftClient client = new SimpleMinecraftClient(new MinecraftVersion(CRAFTBUKKIT_VERSION), PROTOCOL_VERSION); + String information = client.queryLocalPing(); + + // Wait for the listener to catch up + for (int i = 0; i < 1000 && (source == null); i++) + Thread.sleep(1); + + return information; + } + }); + } +}