From 406e2c6c576fa8ebd3f446d03abcc66e295a1b33 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Wed, 20 Nov 2024 16:35:20 +0000 Subject: [PATCH] Convert integration tests to unit tests this required mocking to get around #6524. Longer term we should make improvements to avoid the need for mocking here. --- .../event/AsyncEventConcurrencyTest.php | 125 +++++++++++++++ tests/phpunit/event/AsyncEventTest.php | 134 ++++++++++++++++ .../event/fixtures/TestChildAsyncEvent.php} | 4 +- .../fixtures/TestGrandchildAsyncEvent.php} | 4 +- .../event/fixtures/TestParentAsyncEvent.php} | 6 +- .../src/AsyncEventConcurrencyTest.php | 150 ------------------ .../src/AsyncEventInheritanceTest.php | 84 ---------- .../src/AsyncEventPriorityTest.php | 96 ----------- tests/plugins/TesterPlugin/src/Main.php | 4 +- 9 files changed, 268 insertions(+), 339 deletions(-) create mode 100644 tests/phpunit/event/AsyncEventConcurrencyTest.php create mode 100644 tests/phpunit/event/AsyncEventTest.php rename tests/{plugins/TesterPlugin/src/event/GrandchildAsyncEvent.php => phpunit/event/fixtures/TestChildAsyncEvent.php} (88%) rename tests/{plugins/TesterPlugin/src/event/ParentAsyncEvent.php => phpunit/event/fixtures/TestGrandchildAsyncEvent.php} (88%) rename tests/{plugins/TesterPlugin/src/event/ChildAsyncEvent.php => phpunit/event/fixtures/TestParentAsyncEvent.php} (86%) delete mode 100644 tests/plugins/TesterPlugin/src/AsyncEventConcurrencyTest.php delete mode 100644 tests/plugins/TesterPlugin/src/AsyncEventInheritanceTest.php delete mode 100644 tests/plugins/TesterPlugin/src/AsyncEventPriorityTest.php diff --git a/tests/phpunit/event/AsyncEventConcurrencyTest.php b/tests/phpunit/event/AsyncEventConcurrencyTest.php new file mode 100644 index 000000000..bfbe74384 --- /dev/null +++ b/tests/phpunit/event/AsyncEventConcurrencyTest.php @@ -0,0 +1,125 @@ +> + */ + private array $resolvers = []; + + private bool $activeExclusiveHandler = false; + private bool $activeConcurrentHandler = false; + + private int $done = 0; + + protected function setUp() : void{ + AsyncHandlerListManager::global()->unregisterAll(); + + //TODO: this is a really bad hack and could break any time if PluginManager decides to access its Server field + //we really need to make it possible to register events without a Plugin or Server context + $this->mockServer = $this->createMock(Server::class); + $this->mockPlugin = self::createStub(Plugin::class); + $this->mockPlugin->method('isEnabled')->willReturn(true); + $this->pluginManager = new PluginManager($this->mockServer, null); + } + + public static function tearDownAfterClass() : void{ + AsyncHandlerListManager::global()->unregisterAll(); + } + + /** + * @phpstan-return Promise + */ + private function handler(bool &$flag, string $label) : Promise{ + $flag = true; + $resolver = new PromiseResolver(); + $this->resolvers[] = $resolver; + $resolver->getPromise()->onCompletion( + function() use (&$flag) : void{ + $flag = false; + $this->done++; + }, + fn() => self::fail("Not expecting this to be rejected for $label") + ); + return $resolver->getPromise(); + } + + public function testConcurrency() : void{ + $this->pluginManager->registerAsyncEvent( + TestGrandchildAsyncEvent::class, + function(TestGrandchildAsyncEvent $event) : Promise{ + self::assertFalse($this->activeExclusiveHandler, "Concurrent handler can't run while exclusive handlers are waiting to complete"); + + return $this->handler($this->activeConcurrentHandler, "concurrent"); + }, + EventPriority::NORMAL, + $this->mockPlugin, + //non-exclusive - this must be completed before any exclusive handlers are run (or run after them) + ); + for($i = 0; $i < 2; $i++){ + $this->pluginManager->registerAsyncEvent( + TestGrandchildAsyncEvent::class, + function(TestGrandchildAsyncEvent $event) use ($i) : Promise{ + self::assertFalse($this->activeExclusiveHandler, "Exclusive handler $i can't run alongside other exclusive handlers"); + self::assertFalse($this->activeConcurrentHandler, "Exclusive handler $i can't run alongside concurrent handler"); + + return $this->handler($this->activeExclusiveHandler, "exclusive $i"); + }, + EventPriority::NORMAL, + $this->mockPlugin, + exclusiveCall: true + ); + } + + (new TestGrandchildAsyncEvent())->call(); + + while(count($this->resolvers) > 0 && $this->done < 3){ + foreach($this->resolvers as $k => $resolver){ + unset($this->resolvers[$k]); + //don't clear the array here - resolving this will trigger adding the next resolver + $resolver->resolve(null); + } + } + + self::assertSame(3, $this->done, "Expected feedback from exactly 3 handlers"); + } +} diff --git a/tests/phpunit/event/AsyncEventTest.php b/tests/phpunit/event/AsyncEventTest.php new file mode 100644 index 000000000..f9c20b412 --- /dev/null +++ b/tests/phpunit/event/AsyncEventTest.php @@ -0,0 +1,134 @@ +unregisterAll(); + + //TODO: this is a really bad hack and could break any time if PluginManager decides to access its Server field + //we really need to make it possible to register events without a Plugin or Server context + $this->mockServer = $this->createMock(Server::class); + $this->mockPlugin = self::createStub(Plugin::class); + $this->mockPlugin->method('isEnabled')->willReturn(true); + $this->pluginManager = new PluginManager($this->mockServer, null); + } + + public static function tearDownAfterClass() : void{ + AsyncHandlerListManager::global()->unregisterAll(); + } + + public function testHandlerInheritance() : void{ + $expectedOrder = [ + TestGrandchildAsyncEvent::class, + TestChildAsyncEvent::class, + TestParentAsyncEvent::class + ]; + $classes = $expectedOrder; + $actualOrder = []; + shuffle($classes); + foreach($classes as $class){ + $this->pluginManager->registerAsyncEvent( + $class, + function(AsyncEvent $event) use (&$actualOrder, $class) : ?Promise{ + $actualOrder[] = $class; + return null; + }, + EventPriority::NORMAL, + $this->mockPlugin + ); + } + + $event = new TestGrandchildAsyncEvent(); + $promise = $event->call(); + + $resolved = false; + $promise->onCompletion( + function() use ($expectedOrder, $actualOrder, &$resolved){ + self::assertSame($expectedOrder, $actualOrder, "Expected event handlers to be called from most specific to least specific"); + $resolved = true; + }, + fn() => self::fail("Not expecting this to be rejected") + ); + + self::assertTrue($resolved, "No promises were used, expected this promise to resolve immediately"); + } + + public function testPriorityLock() : void{ + $resolver = null; + $firstCompleted = false; + $run = 0; + + $this->pluginManager->registerAsyncEvent( + TestGrandchildAsyncEvent::class, + function(TestGrandchildAsyncEvent $event) use (&$resolver, &$firstCompleted, &$run) : Promise{ + $run++; + $resolver = new PromiseResolver(); + + $resolver->getPromise()->onCompletion( + function() use (&$firstCompleted) : void{ $firstCompleted = true; }, + fn() => self::fail("Not expecting this to be rejected") + ); + + return $resolver->getPromise(); + }, + EventPriority::LOW, //anything below NORMAL is fine + $this->mockPlugin + ); + $this->pluginManager->registerAsyncEvent( + TestGrandchildAsyncEvent::class, + function(TestGrandchildAsyncEvent $event) use (&$firstCompleted, &$run) : ?Promise{ + $run++; + self::assertTrue($firstCompleted, "This shouldn't run until the previous priority is done"); + return null; + }, + EventPriority::NORMAL, + $this->mockPlugin + ); + + (new TestGrandchildAsyncEvent())->call(); + self::assertNotNull($resolver, "First handler didn't provide a resolver"); + $resolver->resolve(null); + self::assertSame(2, $run, "Expected feedback from 2 handlers"); + } +} diff --git a/tests/plugins/TesterPlugin/src/event/GrandchildAsyncEvent.php b/tests/phpunit/event/fixtures/TestChildAsyncEvent.php similarity index 88% rename from tests/plugins/TesterPlugin/src/event/GrandchildAsyncEvent.php rename to tests/phpunit/event/fixtures/TestChildAsyncEvent.php index 3325268e0..4b19d31d1 100644 --- a/tests/plugins/TesterPlugin/src/event/GrandchildAsyncEvent.php +++ b/tests/phpunit/event/fixtures/TestChildAsyncEvent.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pmmp\TesterPlugin\event; +namespace pocketmine\event\fixtures; -class GrandchildAsyncEvent extends ChildAsyncEvent{ +class TestChildAsyncEvent extends TestParentAsyncEvent{ } diff --git a/tests/plugins/TesterPlugin/src/event/ParentAsyncEvent.php b/tests/phpunit/event/fixtures/TestGrandchildAsyncEvent.php similarity index 88% rename from tests/plugins/TesterPlugin/src/event/ParentAsyncEvent.php rename to tests/phpunit/event/fixtures/TestGrandchildAsyncEvent.php index 5c7796697..7be2373c6 100644 --- a/tests/plugins/TesterPlugin/src/event/ParentAsyncEvent.php +++ b/tests/phpunit/event/fixtures/TestGrandchildAsyncEvent.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pmmp\TesterPlugin\event; +namespace pocketmine\event\fixtures; -class ParentAsyncEvent extends \pocketmine\event\AsyncEvent{ +class TestGrandchildAsyncEvent extends TestChildAsyncEvent{ } diff --git a/tests/plugins/TesterPlugin/src/event/ChildAsyncEvent.php b/tests/phpunit/event/fixtures/TestParentAsyncEvent.php similarity index 86% rename from tests/plugins/TesterPlugin/src/event/ChildAsyncEvent.php rename to tests/phpunit/event/fixtures/TestParentAsyncEvent.php index 79d88b5d3..17b3d1189 100644 --- a/tests/plugins/TesterPlugin/src/event/ChildAsyncEvent.php +++ b/tests/phpunit/event/fixtures/TestParentAsyncEvent.php @@ -21,8 +21,10 @@ declare(strict_types=1); -namespace pmmp\TesterPlugin\event; +namespace pocketmine\event\fixtures; -class ChildAsyncEvent extends ParentAsyncEvent{ +use pocketmine\event\AsyncEvent; + +class TestParentAsyncEvent extends AsyncEvent{ } diff --git a/tests/plugins/TesterPlugin/src/AsyncEventConcurrencyTest.php b/tests/plugins/TesterPlugin/src/AsyncEventConcurrencyTest.php deleted file mode 100644 index 856ee45ad..000000000 --- a/tests/plugins/TesterPlugin/src/AsyncEventConcurrencyTest.php +++ /dev/null @@ -1,150 +0,0 @@ -> - */ - 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{ - AsyncHandlerListManager::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 $resolver->getPromise(); - }, - 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 deleted file mode 100644 index d952e36b1..000000000 --- a/tests/plugins/TesterPlugin/src/AsyncEventInheritanceTest.php +++ /dev/null @@ -1,84 +0,0 @@ -unregisterAll(); - - $plugin = $this->getPlugin(); - $classes = self::EXPECTED_ORDER; - shuffle($classes); - foreach($classes as $class){ - $plugin->getServer()->getPluginManager()->registerAsyncEvent( - $class, - function(AsyncEvent $event) use ($class) : ?Promise{ - $this->callOrder[] = $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 deleted file mode 100644 index 4ac5766d1..000000000 --- a/tests/plugins/TesterPlugin/src/AsyncEventPriorityTest.php +++ /dev/null @@ -1,96 +0,0 @@ -> - */ - 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{ - AsyncHandlerListManager::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 0a7c52479..08b59dbac 100644 --- a/tests/plugins/TesterPlugin/src/Main.php +++ b/tests/plugins/TesterPlugin/src/Main.php @@ -57,9 +57,7 @@ class Main extends PluginBase implements Listener{ }), 10); $this->waitingTests = [ - new AsyncEventInheritanceTest($this), - new AsyncEventConcurrencyTest($this), - new AsyncEventPriorityTest($this) + //Add test objects here ]; }