Repository: mockbukkit/mockbukkit
Issue: Wait for async event execution?

Is there a proper way to wait for async events to finish?

I saw that `BukkitSchedulerMock#waitAsyncTasksFinished()` exists, but it doesn't cover async events like `AsyncPlayerChatEvent`.

I am currently trying to test a plugin's reaction on `PlayerMock#chat(String)` and calling `Thread#sleep(long)` isn't optimal in any way.

I am currently using `MockBukkit-v1.16` version `1.5.2`.

Changed files:
src/main/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMock.java, src/test/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMockTest.java

diff --git a/src/main/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMock.java b/src/main/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMock.java
index e0ad468a..9e4d7df6 100644
--- a/src/main/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMock.java
+++ b/src/main/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMock.java
@@ -50,13 +50,16 @@ public class BukkitSchedulerMock implements BukkitScheduler
 		};
 	}
 
-
+	/**
+	 * Sets the maximum time to wait for async tasks to finish before terminating them.
+	 *
+	 * @param timeout The timeout in milliseconds.
+	 */
 	public void setShutdownTimeout(long timeout)
 	{
 		this.executorTimeout = timeout;
 	}
 
-
 	/**
 	 * Shuts the scheduler down. Note that this function will throw exception that where thrown by old asynchronous
 	 * tasks.
@@ -64,10 +67,36 @@ public class BukkitSchedulerMock implements BukkitScheduler
 	public void shutdown()
 	{
 		waitAsyncTasksFinished();
-		pool.shutdown();
+
+		shutdownPool(pool);
+
 		if (asyncException.get() != null)
 			throw new AsyncTaskException(asyncException.get());
-		asyncEventExecutor.shutdownNow();
+
+		shutdownPool(asyncEventExecutor);
+	}
+
+	/**
+	 * Shuts down the given executor service, waiting up to the shutdown timeout for all tasks to finish.
+	 *
+	 * @param pool The pool to shut down.
+	 * @see #setShutdownTimeout(long)
+	 */
+	private void shutdownPool(ExecutorService pool)
+	{
+		pool.shutdown();
+		try
+		{
+			if (!pool.awaitTermination(this.executorTimeout, TimeUnit.MILLISECONDS))
+			{
+				pool.shutdownNow();
+			}
+		}
+		catch (InterruptedException e)
+		{
+			pool.shutdownNow();
+			Thread.currentThread().interrupt();
+		}
 	}
 
 	public @NotNull Future<?> executeAsyncEvent(Event event)
@@ -150,7 +179,7 @@ public class BukkitSchedulerMock implements BukkitScheduler
 
 	/**
 	 * Waits until all asynchronous tasks have finished executing. If you have an asynchronous task that runs
-	 * indefinitely, this function will never return.
+	 * indefinitely, this function will never return. Note that this will not wait for async events to finish.
 	 */
 	public void waitAsyncTasksFinished()
 	{
@@ -184,8 +213,8 @@ public class BukkitSchedulerMock implements BukkitScheduler
 			}
 			if (System.currentTimeMillis() > (systemTime + executorTimeout))
 			{
-				// If a plugin has left a a runnable going and not cancelled it we could call this bad practice.
-				// we should now force interrupt all these runnables forcing them to throw Interrupted Exceptions.
+				// If a plugin has left a runnable going and not cancelled it we could call this bad practice.
+				// We should force interrupt all those runnables forcing them to throw Interrupted Exceptions-
 				// if they handle that
 				for (ScheduledTask task : scheduledTasks.getCurrentTaskList())
 				{
diff --git a/src/test/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMockTest.java b/src/test/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMockTest.java
index d42ea9a1..f014e89a 100644
--- a/src/test/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMockTest.java
+++ b/src/test/java/be/seeseemelk/mockbukkit/scheduler/BukkitSchedulerMockTest.java
@@ -15,7 +15,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import be.seeseemelk.mockbukkit.MockBukkit;
+import be.seeseemelk.mockbukkit.ServerMock;
 import be.seeseemelk.mockbukkit.TestPlugin;
+import io.papermc.paper.event.player.AsyncChatEvent;
+import org.bukkit.Bukkit;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
 import org.bukkit.plugin.Plugin;
 import org.bukkit.scheduler.BukkitTask;
 import org.junit.jupiter.api.BeforeEach;
@@ -197,10 +202,10 @@ class BukkitSchedulerMockTest
 	@Test
 	public void cancellingAllTaskByPlugin()
 	{
-		MockBukkit.mock();
+		ServerMock server = MockBukkit.mock();
 		MockBukkit.load(TestPlugin.class);
-		Plugin plugin = MockBukkit.getMock().getPluginManager().getPlugin("MockBukkitTestPlugin");
-		BukkitSchedulerMock scheduler1 = MockBukkit.getMock().getScheduler();
+		Plugin plugin = server.getPluginManager().getPlugin("MockBukkitTestPlugin");
+		BukkitSchedulerMock scheduler1 = server.getScheduler();
 		assertEquals(0, scheduler1.getNumberOfQueuedAsyncTasks());
 		scheduler1.runTaskLaterAsynchronously(plugin, () -> {}, 5);
 		scheduler1.runTaskLaterAsynchronously(plugin, () -> {}, 10);
@@ -295,4 +300,29 @@ class BukkitSchedulerMockTest
 			scheduler.shutdown();
 		});
 	}
+
+	@Test
+	void shutdown_waitsForAsyncEvents()
+	{
+		MockBukkit.mock();
+		AtomicBoolean done = new AtomicBoolean(false);
+		Bukkit.getPluginManager().registerEvents(new Listener()
+		{
+			@EventHandler
+			public void onChat(AsyncChatEvent event) throws Exception
+			{
+				Thread.sleep(50);
+				done.set(true);
+			}
+		}, MockBukkit.createMockPlugin());
+		AsyncChatEvent event = new AsyncChatEvent(true, null, null, null, null, null);
+		scheduler.executeAsyncEvent(event);
+		assertFalse(done.get());
+
+		scheduler.shutdown();
+
+		assertTrue(done.get());
+		MockBukkit.unmock();
+	}
+
 }