From 97cbaaa80533e48705d73301668747cdf8d6f563 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Fri, 24 Jul 2026 15:58:07 +0200 Subject: [PATCH] test(amp): cover future adapter contracts --- .../Executor/Promise/AmpFutureAdapterTest.php | 208 +++++++++++++++++- 1 file changed, 205 insertions(+), 3 deletions(-) diff --git a/tests/Executor/Promise/AmpFutureAdapterTest.php b/tests/Executor/Promise/AmpFutureAdapterTest.php index 5f4b0539e..416635373 100644 --- a/tests/Executor/Promise/AmpFutureAdapterTest.php +++ b/tests/Executor/Promise/AmpFutureAdapterTest.php @@ -74,6 +74,78 @@ static function ($value) use (&$result): void { self::assertSame(1, $result); } + public function testThenUnwrapsFutureReturnedByFulfillmentCallback(): void + { + $ampAdapter = new AmpFutureAdapter(); + $deferred = new DeferredFuture(); + $promise = $ampAdapter->convertThenable(Future::complete(1)); + + $resultPromise = $ampAdapter->then($promise, static function ($value) use ($deferred): Future { + self::assertSame(1, $value); + + return $deferred->getFuture(); + }); + + $deferred->complete(2); + + self::assertSame(2, $resultPromise->adoptedPromise->await()); + } + + public function testThenDoesNotInvokeRejectionCallbackForFulfillmentCallbackException(): void + { + $ampAdapter = new AmpFutureAdapter(); + $promise = $ampAdapter->convertThenable(Future::complete()); + + $resultPromise = $ampAdapter->then( + $promise, + static function ($value): void { + self::assertNull($value); + + throw new \RuntimeException('fulfillment failed'); + }, + static function (\Throwable $reason): void { + self::fail('The rejection callback must not run for a fulfillment callback exception.'); + } + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('fulfillment failed'); + + $resultPromise->adoptedPromise->await(); + } + + public function testThenRejectsWhenNoRejectionCallbackIsProvided(): void + { + $ampAdapter = new AmpFutureAdapter(); + $promise = $ampAdapter->convertThenable(Future::error(new \RuntimeException('failed'))); + $resultPromise = $ampAdapter->then($promise); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('failed'); + + $resultPromise->adoptedPromise->await(); + } + + public function testThenRejectsWhenRejectionCallbackThrows(): void + { + $ampAdapter = new AmpFutureAdapter(); + $promise = $ampAdapter->convertThenable(Future::error(new \RuntimeException('failed'))); + $resultPromise = $ampAdapter->then( + $promise, + null, + static function (\Throwable $reason): void { + self::assertSame('failed', $reason->getMessage()); + + throw new \RuntimeException('rejection failed'); + } + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('rejection failed'); + + $resultPromise->adoptedPromise->await(); + } + public function testCreate(): void { $ampAdapter = new AmpFutureAdapter(); @@ -96,6 +168,31 @@ public function testCreate(): void self::assertSame(1, $result); } + public function testCreateUnwrapsPromise(): void + { + $ampAdapter = new AmpFutureAdapter(); + $nestedPromise = $ampAdapter->createFulfilled(1); + $promise = $ampAdapter->create(static function ($resolve) use ($nestedPromise): void { + $resolve($nestedPromise); + }); + + self::assertSame(1, $promise->adoptedPromise->await()); + } + + public function testCreateRejectsWhenNestedFutureFails(): void + { + $ampAdapter = new AmpFutureAdapter(); + $future = Future::error(new \RuntimeException('failed')); + $promise = $ampAdapter->create(static function ($resolve) use ($future): void { + $resolve($future); + }); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('failed'); + + $promise->adoptedPromise->await(); + } + public function testCreateFulfilled(): void { $ampAdapter = new AmpFutureAdapter(); @@ -140,6 +237,27 @@ static function ($error) use (&$exception): void { self::assertSame('I am a bad promise', $exception->getMessage()); } + public function testThenUnwrapsFutureReturnedByRejectionCallback(): void + { + $ampAdapter = new AmpFutureAdapter(); + $deferred = new DeferredFuture(); + $promise = $ampAdapter->convertThenable(Future::error(new \RuntimeException('failed'))); + + $resultPromise = $ampAdapter->then( + $promise, + null, + static function (\Throwable $reason) use ($deferred): Future { + self::assertSame('failed', $reason->getMessage()); + + return $deferred->getFuture(); + } + ); + + $deferred->complete('recovered'); + + self::assertSame('recovered', $resultPromise->adoptedPromise->await()); + } + public function testAll(): void { $ampAdapter = new AmpFutureAdapter(); @@ -154,11 +272,16 @@ public function testAll(): void self::assertSame([1, 2, 3], $result); } - public function testAllShouldPreserveTheOrderOfTheArrayWhenResolvingAsyncPromises(): void + public function testAllShouldPreserveKeysWhenResolvingAsyncPromises(): void { $ampAdapter = new AmpFutureAdapter(); $deferred = new DeferredFuture(); - $promises = [Future::complete(1), 2, $deferred->getFuture(), Future::complete(4)]; + $promises = [ + 'first' => Future::complete(1), + 'second' => 2, + 'third' => $deferred->getFuture(), + 'fourth' => Future::complete(4), + ]; $allPromise = $ampAdapter->all($promises); @@ -168,6 +291,85 @@ public function testAllShouldPreserveTheOrderOfTheArrayWhenResolvingAsyncPromise self::assertInstanceOf(Future::class, $allFuture); $result = $allFuture->await(); - self::assertSame([1, 2, 3, 4], $result); + self::assertSame([ + 'first' => 1, + 'second' => 2, + 'third' => 3, + 'fourth' => 4, + ], $result); + } + + public function testAllAcceptsPromisesInIterables(): void + { + $ampAdapter = new AmpFutureAdapter(); + $promise = $ampAdapter->createFulfilled(1); + $promises = (static function () use ($promise): \Generator { + yield 'promise' => $promise; + yield 'future' => Future::complete(2); + })(); + + $allPromise = $ampAdapter->all($promises); + + self::assertSame(['promise' => 1, 'future' => 2], $allPromise->adoptedPromise->await()); + } + + public function testAllRejectsWhenOneFutureFails(): void + { + $ampAdapter = new AmpFutureAdapter(); + $deferred = new DeferredFuture(); + $allPromise = $ampAdapter->all([ + 'resolved' => Future::complete(1), + 'rejected' => $deferred->getFuture(), + ]); + + $deferred->error(new \RuntimeException('failed')); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('failed'); + + $allPromise->adoptedPromise->await(); + } + + public function testAllRemainsRejectedWhenAnotherFutureResolves(): void + { + $ampAdapter = new AmpFutureAdapter(); + $rejected = new DeferredFuture(); + $resolved = new DeferredFuture(); + $allPromise = $ampAdapter->all([$rejected->getFuture(), $resolved->getFuture()]); + + $rejected->error(new \RuntimeException('failed')); + $resolved->complete('resolved'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('failed'); + + $allPromise->adoptedPromise->await(); + } + + public function testAllIgnoresOtherFuturesAfterRejection(): void + { + $ampAdapter = new AmpFutureAdapter(); + $rejected = new DeferredFuture(); + $resolved = new DeferredFuture(); + $alsoRejected = new DeferredFuture(); + $allPromise = $ampAdapter->all([ + $rejected->getFuture(), + $resolved->getFuture(), + $alsoRejected->getFuture(), + ]); + + $rejected->error(new \RuntimeException('failed')); + + try { + $allPromise->adoptedPromise->await(); + self::fail('Expected aggregate rejection.'); + } catch (\RuntimeException $exception) { + self::assertSame('failed', $exception->getMessage()); + } + + $resolved->complete('resolved'); + $alsoRejected->error(new \RuntimeException('also failed')); + + async(static function (): void {})->await(); } }