diff --git a/tests/plugins/TesterPlugin/src/AsyncEventConcurrencyTest.php b/tests/plugins/TesterPlugin/src/AsyncEventConcurrencyTest.php new file mode 100644 index 000000000..f372e8015 --- /dev/null +++ b/tests/plugins/TesterPlugin/src/AsyncEventConcurrencyTest.php @@ -0,0 +1,151 @@ +> + */ + private array $resolvers = []; + + private bool $activeExclusiveHandler = false; + private bool $activeConcurrentHandler = false; + + private int $done = 0; + + public function getName() : string{ + return "Async Event Concurrency Lock"; + } + + public function getDescription() : string{ + return "Test that exclusive lock on async event handlers works correctly"; + } + + public function run() : void{ + HandlerListManager::global()->unregisterAll(); + + $main = $this->getPlugin(); + $pluginManager = $main->getServer()->getPluginManager(); + + $pluginManager->registerAsyncEvent( + GrandchildAsyncEvent::class, + function(GrandchildAsyncEvent $event) use ($main) : ?Promise{ + if($this->activeExclusiveHandler){ + $main->getLogger()->error("Concurrent handler can't run while exclusive handlers are waiting to complete"); + $this->setResult(Test::RESULT_FAILED); + return null; + } + $this->activeConcurrentHandler = true; + $resolver = new PromiseResolver(); + $this->resolvers[] = $resolver; + $resolver->getPromise()->onCompletion( + fn() => $this->complete($this->activeConcurrentHandler, "concurrent"), + fn() => $main->getLogger()->error("Not expecting this to be rejected") + ); + return $resolver->getPromise(); + }, + EventPriority::NORMAL, + $main, + //non-exclusive - this must be completed before any exclusive handlers are run (or run after them) + ); + $pluginManager->registerAsyncEvent( + GrandchildAsyncEvent::class, + function(GrandchildAsyncEvent $event) use ($main) : ?Promise{ + $main->getLogger()->info("Entering exclusive handler 1"); + if($this->activeExclusiveHandler || $this->activeConcurrentHandler){ + $main->getLogger()->error("Can't run multiple exclusive handlers at once"); + $this->setResult(Test::RESULT_FAILED); + return null; + } + $this->activeExclusiveHandler = true; + $resolver = new PromiseResolver(); + $this->resolvers[] = $resolver; + $resolver->getPromise()->onCompletion( + fn() => $this->complete($this->activeExclusiveHandler, "exclusive 1"), + fn() => $main->getLogger()->error("Not expecting this to be rejected") + ); + return null; + }, + EventPriority::NORMAL, + $main, + exclusiveCall: true + ); + + $pluginManager->registerAsyncEvent( + GrandchildAsyncEvent::class, + function(GrandchildAsyncEvent $event) use ($main) : ?Promise{ + $this->getPlugin()->getLogger()->info("Entering exclusive handler 2"); + if($this->activeExclusiveHandler || $this->activeConcurrentHandler){ + $main->getLogger()->error("Exclusive lock handlers must not run at the same time as any other handlers"); + $this->setResult(Test::RESULT_FAILED); + return null; + } + $this->activeExclusiveHandler = true; + /** @phpstan-var PromiseResolver $resolver */ + $resolver = new PromiseResolver(); + $this->resolvers[] = $resolver; + $resolver->getPromise()->onCompletion( + function() use ($main) : void{ + $main->getLogger()->info("Exiting exclusive handler asynchronously"); + $this->complete($this->activeExclusiveHandler, "exclusive 2"); + }, + function() use ($main) : void{ + $main->getLogger()->error("Not expecting this promise to be rejected"); + $this->setResult(Test::RESULT_ERROR); + } + ); + return $resolver->getPromise(); + }, + EventPriority::NORMAL, + $main, + exclusiveCall: true + ); + + (new GrandchildAsyncEvent())->call(); + } + + private function complete(bool &$flag, string $what) : void{ + $this->getPlugin()->getLogger()->info("Completing $what"); + $flag = false; + if(++$this->done === 3){ + $this->setResult(Test::RESULT_OK); + } + } + + public function tick() : void{ + foreach($this->resolvers as $k => $resolver){ + $resolver->resolve(null); + //don't clear the array here - resolving this will trigger adding the next resolver + unset($this->resolvers[$k]); + } + } +} diff --git a/tests/plugins/TesterPlugin/src/AsyncEventInheritanceTest.php b/tests/plugins/TesterPlugin/src/AsyncEventInheritanceTest.php new file mode 100644 index 000000000..5fc889e70 --- /dev/null +++ b/tests/plugins/TesterPlugin/src/AsyncEventInheritanceTest.php @@ -0,0 +1,83 @@ +unregisterAll(); + + $plugin = $this->getPlugin(); + $classes = self::EXPECTED_ORDER; + shuffle($classes); + foreach($classes as $event){ + $plugin->getServer()->getPluginManager()->registerAsyncEvent( + $event, + function(AsyncEvent $event) : ?Promise{ + $this->callOrder[] = $event::class; + return null; + }, + EventPriority::NORMAL, + $plugin + ); + } + + $event = new GrandchildAsyncEvent(); + $promise = $event->call(); + $promise->onCompletion(onSuccess: $this->collectResults(...), onFailure: $this->collectResults(...)); + } + + private function collectResults() : void{ + if($this->callOrder === self::EXPECTED_ORDER){ + $this->setResult(Test::RESULT_OK); + }else{ + $this->getPlugin()->getLogger()->error("Expected order: " . implode(", ", self::EXPECTED_ORDER) . ", got: " . implode(", ", $this->callOrder)); + $this->setResult(Test::RESULT_FAILED); + } + } +} diff --git a/tests/plugins/TesterPlugin/src/AsyncEventPriorityTest.php b/tests/plugins/TesterPlugin/src/AsyncEventPriorityTest.php new file mode 100644 index 000000000..ac8b56407 --- /dev/null +++ b/tests/plugins/TesterPlugin/src/AsyncEventPriorityTest.php @@ -0,0 +1,96 @@ +> + */ + private array $resolvers = []; + + private bool $firstHandlerCompleted = false; + + public function getName() : string{ + return "Async Event Handler Priority Lock"; + } + + public function getDescription() : string{ + return "Tests that async events do not call handlers from the next priority until all promises from the current priority are resolved"; + } + + public function run() : void{ + HandlerListManager::global()->unregisterAll(); + + $main = $this->getPlugin(); + $pluginManager = $main->getServer()->getPluginManager(); + $pluginManager->registerAsyncEvent( + GrandchildAsyncEvent::class, + function(GrandchildAsyncEvent $event) use ($main) : Promise{ + $resolver = new PromiseResolver(); + $this->resolvers[] = $resolver; + + $resolver->getPromise()->onCompletion(function() : void{ + $this->firstHandlerCompleted = true; + }, function() use ($main) : void{ + $main->getLogger()->error("Not expecting this to be rejected"); + $this->setResult(Test::RESULT_ERROR); + }); + + return $resolver->getPromise(); + }, + EventPriority::LOW, //anything below NORMAL is fine + $main + ); + $pluginManager->registerAsyncEvent( + GrandchildAsyncEvent::class, + function(GrandchildAsyncEvent $event) use ($main) : ?Promise{ + if(!$this->firstHandlerCompleted){ + $main->getLogger()->error("This shouldn't run until the previous priority is done"); + $this->setResult(Test::RESULT_FAILED); + }else{ + $this->setResult(Test::RESULT_OK); + } + return null; + }, + EventPriority::NORMAL, + $main + ); + + (new GrandchildAsyncEvent())->call(); + } + + public function tick() : void{ + foreach($this->resolvers as $k => $resolver){ + $resolver->resolve(null); + unset($this->resolvers[$k]); + } + } +} diff --git a/tests/plugins/TesterPlugin/src/Main.php b/tests/plugins/TesterPlugin/src/Main.php index 26d3441f4..f330af6d9 100644 --- a/tests/plugins/TesterPlugin/src/Main.php +++ b/tests/plugins/TesterPlugin/src/Main.php @@ -58,6 +58,9 @@ class Main extends PluginBase implements Listener{ $this->waitingTests = [ new EventHandlerInheritanceTest($this), + new AsyncEventInheritanceTest($this), + new AsyncEventConcurrencyTest($this), + new AsyncEventPriorityTest($this) ]; } diff --git a/tests/plugins/TesterPlugin/src/event/ChildAsyncEvent.php b/tests/plugins/TesterPlugin/src/event/ChildAsyncEvent.php new file mode 100644 index 000000000..c54a2503c --- /dev/null +++ b/tests/plugins/TesterPlugin/src/event/ChildAsyncEvent.php @@ -0,0 +1,28 @@ +