diff --git a/src/functions.php b/src/functions.php index 7f4100a..e418844 100644 --- a/src/functions.php +++ b/src/functions.php @@ -10,6 +10,29 @@ use function React\Promise\reject; use function React\Promise\resolve; +/** + * + * @template T + * @param callable(...$args):T $coroutine + * @param mixed ...$args + * @return PromiseInterface + */ +function async(callable $coroutine, ...$args): PromiseInterface +{ + return new Promise(function (callable $resolve, callable $reject) use ($coroutine, $args): void { + $fiber = new \Fiber(function () use ($resolve, $reject, $coroutine, $args): void { + try { + $resolve($coroutine(...$args)); + } catch (\Throwable $exception) { + $reject($exception); + } + }); + + Loop::futureTick(static fn() => $fiber->start()); + }); +} + + /** * Block waiting for the given `$promise` to be fulfilled. * diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 9b0a153..afe2660 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -8,7 +8,10 @@ class AwaitTest extends TestCase { - public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(callable $await) { $promise = new Promise(function () { throw new \Exception('test'); @@ -16,10 +19,13 @@ public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException() $this->expectException(\Exception::class); $this->expectExceptionMessage('test'); - React\Async\await($promise); + $await($promise); } - public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse(callable $await) { if (!interface_exists('React\Promise\CancellablePromiseInterface')) { $this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3'); @@ -31,10 +37,13 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('Promise rejected with unexpected value of type bool'); - React\Async\await($promise); + $await($promise); } - public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull(callable $await) { if (!interface_exists('React\Promise\CancellablePromiseInterface')) { $this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3'); @@ -46,10 +55,13 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('Promise rejected with unexpected value of type NULL'); - React\Async\await($promise); + $await($promise); } - public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError(callable $await) { $promise = new Promise(function ($_, $reject) { throw new \Error('Test', 42); @@ -58,19 +70,25 @@ public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError() $this->expectException(\Error::class); $this->expectExceptionMessage('Test'); $this->expectExceptionCode(42); - React\Async\await($promise); + $await($promise); } - public function testAwaitReturnsValueWhenPromiseIsFullfilled() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitReturnsValueWhenPromiseIsFullfilled(callable $await) { $promise = new Promise(function ($resolve) { $resolve(42); }); - $this->assertEquals(42, React\Async\await($promise)); + $this->assertEquals(42, $await($promise)); } - public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop(callable $await) { $this->markTestIncomplete(); @@ -83,10 +101,13 @@ public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerSto Loop::stop(); }); - $this->assertEquals(2, React\Async\await($promise)); + $this->assertEquals(2, $await($promise)); } - public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise(callable $await) { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); @@ -97,13 +118,16 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise() $promise = new Promise(function ($resolve) { $resolve(42); }); - React\Async\await($promise); + $await($promise); unset($promise); $this->assertEquals(0, gc_collect_cycles()); } - public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise(callable $await) { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); @@ -115,7 +139,7 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise() throw new \RuntimeException(); }); try { - React\Async\await($promise); + $await($promise); } catch (\Exception $e) { // no-op } @@ -124,7 +148,10 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise() $this->assertEquals(0, gc_collect_cycles()); } - public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue() + /** + * @dataProvider provideAwaiters + */ + public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue(callable $await) { if (!interface_exists('React\Promise\CancellablePromiseInterface')) { $this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3'); @@ -140,7 +167,7 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWi $reject(null); }); try { - React\Async\await($promise); + $await($promise); } catch (\Exception $e) { // no-op } @@ -148,4 +175,10 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWi $this->assertEquals(0, gc_collect_cycles()); } + + public function provideAwaiters(): iterable + { + yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)]; + yield 'async' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await(React\Async\async(static fn(): mixed => $promise))]; + } }