p2 project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

735 lines
23 KiB

2 months ago
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise\Tests;
use GuzzleHttp\Promise\AggregateException;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\RejectionException;
use GuzzleHttp\Promise\TaskQueue;
use PHPUnit\Framework\TestCase;
class UtilsTest extends TestCase
{
public function testWaitsOnAllPromisesIntoArray(): void
{
$e = new \Exception();
$a = new Promise(function () use (&$a): void { $a->resolve('a'); });
$b = new Promise(function () use (&$b): void { $b->reject('b'); });
$c = new Promise(function () use (&$c, $e): void { $c->reject($e); });
$results = P\Utils::inspectAll([$a, $b, $c]);
$this->assertSame([
['state' => 'fulfilled', 'value' => 'a'],
['state' => 'rejected', 'reason' => 'b'],
['state' => 'rejected', 'reason' => $e],
], $results);
}
public function testUnwrapsPromisesWithNoDefaultAndFailure(): void
{
$this->expectException(RejectionException::class);
$promises = [new FulfilledPromise('a'), new Promise()];
P\Utils::unwrap($promises);
}
public function testUnwrapsPromisesWithNoDefault(): void
{
$promises = [new FulfilledPromise('a')];
$this->assertSame(['a'], P\Utils::unwrap($promises));
}
public function testUnwrapsPromisesWithKeys(): void
{
$promises = [
'foo' => new FulfilledPromise('a'),
'bar' => new FulfilledPromise('b'),
];
$this->assertSame([
'foo' => 'a',
'bar' => 'b',
], P\Utils::unwrap($promises));
}
public function testAllAggregatesSortedArray(): void
{
$a = new Promise();
$b = new Promise();
$c = new Promise();
$d = P\Utils::all([$a, $b, $c]);
$b->resolve('b');
$a->resolve('a');
$c->resolve('c');
$d->then(
function ($value) use (&$result): void { $result = $value; },
function ($reason) use (&$result): void { $result = $reason; }
);
P\Utils::queue()->run();
$this->assertSame(['a', 'b', 'c'], $result);
}
public function testPromisesDynamicallyAddedToStack(): void
{
$promises = new \ArrayIterator();
$counter = 0;
$promises['a'] = new FulfilledPromise('a');
$promises['b'] = $promise = new Promise(function () use (&$promise, &$promises, &$counter): void {
++$counter; // Make sure the wait function is called only once
$promise->resolve('b');
$promises['c'] = $subPromise = new Promise(function () use (&$subPromise): void {
$subPromise->resolve('c');
});
});
$result = P\Utils::all($promises, true)->wait();
$this->assertCount(3, $promises);
$this->assertCount(3, $result);
$this->assertSame($result['c'], 'c');
$this->assertSame(1, $counter);
}
public function testAllThrowsWhenAnyRejected(): void
{
$a = new Promise();
$b = new Promise();
$c = new Promise();
$d = P\Utils::all([$a, $b, $c]);
$b->resolve('b');
$a->reject('fail');
$c->resolve('c');
$d->then(
function ($value) use (&$result): void { $result = $value; },
function ($reason) use (&$result): void { $result = $reason; }
);
P\Utils::queue()->run();
$this->assertSame('fail', $result);
}
public function testSomeAggregatesSortedArrayWithMax(): void
{
$a = new Promise();
$b = new Promise();
$c = new Promise();
$d = P\Utils::some(2, [$a, $b, $c]);
$b->resolve('b');
$c->resolve('c');
$a->resolve('a');
$d->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertSame(['b', 'c'], $result);
}
public function testSomeRejectsWhenTooManyRejections(): void
{
$a = new Promise();
$b = new Promise();
$d = P\Utils::some(2, [$a, $b]);
$a->reject('bad');
$b->resolve('good');
P\Utils::queue()->run();
$this->assertTrue(P\Is::rejected($d));
$d->then(null, function ($reason) use (&$called): void {
$called = $reason;
});
P\Utils::queue()->run();
$this->assertInstanceOf(AggregateException::class, $called);
$this->assertContains('bad', $called->getReason());
}
public function testCanWaitUntilSomeCountIsSatisfied(): void
{
$a = new Promise(function () use (&$a): void { $a->resolve('a'); });
$b = new Promise(function () use (&$b): void { $b->resolve('b'); });
$c = new Promise(function () use (&$c): void { $c->resolve('c'); });
$d = P\Utils::some(2, [$a, $b, $c]);
$this->assertSame(['a', 'b'], $d->wait());
}
public function testThrowsIfImpossibleToWaitForSomeCount(): void
{
$this->expectException(AggregateException::class);
$this->expectExceptionMessage('Not enough promises to fulfill count');
$a = new Promise(function () use (&$a): void { $a->resolve('a'); });
$d = P\Utils::some(2, [$a]);
$d->wait();
}
public function testThrowsIfResolvedWithoutCountTotalResults(): void
{
$this->expectException(AggregateException::class);
$this->expectExceptionMessage('Not enough promises to fulfill count');
$a = new Promise();
$b = new Promise();
$d = P\Utils::some(3, [$a, $b]);
$a->resolve('a');
$b->resolve('b');
$d->wait();
}
public function testAnyReturnsFirstMatch(): void
{
$a = new Promise();
$b = new Promise();
$c = P\Utils::any([$a, $b]);
$b->resolve('b');
$a->resolve('a');
$c->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertSame('b', $result);
}
public function testSettleFulfillsWithFulfilledAndRejected(): void
{
$a = new Promise();
$b = new Promise();
$c = new Promise();
$d = P\Utils::settle([$a, $b, $c]);
$b->resolve('b');
$c->resolve('c');
$a->reject('a');
P\Utils::queue()->run();
$this->assertTrue(P\Is::fulfilled($d));
$d->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertSame([
['state' => 'rejected', 'reason' => 'a'],
['state' => 'fulfilled', 'value' => 'b'],
['state' => 'fulfilled', 'value' => 'c'],
], $result);
}
public function testCanInspectFulfilledPromise(): void
{
$p = new FulfilledPromise('foo');
$this->assertSame([
'state' => 'fulfilled',
'value' => 'foo',
], P\Utils::inspect($p));
}
public function testCanInspectRejectedPromise(): void
{
$p = new RejectedPromise('foo');
$this->assertSame([
'state' => 'rejected',
'reason' => 'foo',
], P\Utils::inspect($p));
}
public function testCanInspectRejectedPromiseWithNormalException(): void
{
$e = new \Exception('foo');
$p = new RejectedPromise($e);
$this->assertSame([
'state' => 'rejected',
'reason' => $e,
], P\Utils::inspect($p));
}
public function testReturnsTrampoline(): void
{
$this->assertInstanceOf(TaskQueue::class, P\Utils::queue());
$this->assertSame(P\Utils::queue(), P\Utils::queue());
}
public function testCanScheduleThunk(): void
{
$tramp = P\Utils::queue();
$promise = P\Utils::task(function () { return 'Hi!'; });
$c = null;
$promise->then(function ($v) use (&$c): void { $c = $v; });
$this->assertNull($c);
$tramp->run();
$this->assertSame('Hi!', $c);
}
public function testCanScheduleThunkWithRejection(): void
{
$tramp = P\Utils::queue();
$promise = P\Utils::task(function (): void { throw new \Exception('Hi!'); });
$c = null;
$promise->otherwise(function ($v) use (&$c): void { $c = $v; });
$this->assertNull($c);
$tramp->run();
$this->assertSame('Hi!', $c->getMessage());
}
public function testCanScheduleThunkWithWait(): void
{
$tramp = P\Utils::queue();
$promise = P\Utils::task(function () { return 'a'; });
$this->assertSame('a', $promise->wait());
$tramp->run();
}
public function testYieldsFromCoroutine(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = P\Coroutine::of(function () {
$value = (yield new FulfilledPromise('a'));
yield $value.'b';
});
$promise->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertSame('ab', $result);
}
public function testCanCatchExceptionsInCoroutine(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = P\Coroutine::of(function () {
try {
yield new RejectedPromise('a');
$this->fail('Should have thrown into the coroutine!');
} catch (RejectionException $e) {
$value = (yield new FulfilledPromise($e->getReason()));
yield $value.'b';
}
});
$promise->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertTrue(P\Is::fulfilled($promise));
$this->assertSame('ab', $result);
}
/**
* @dataProvider rejectsParentExceptionProvider
*/
public function testRejectsParentExceptionWhenException(PromiseInterface $promise): void
{
$promise->then(
function (): void { $this->fail(); },
function ($reason) use (&$result): void { $result = $reason; }
);
P\Utils::queue()->run();
$this->assertInstanceOf(\Exception::class, $result);
$this->assertSame('a', $result->getMessage());
}
public function rejectsParentExceptionProvider()
{
return [
[P\Coroutine::of(function () {
yield new FulfilledPromise(0);
throw new \Exception('a');
})],
[P\Coroutine::of(function () {
throw new \Exception('a');
yield new FulfilledPromise(0);
})],
];
}
public function testCanRejectFromRejectionCallback(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = P\Coroutine::of(function () {
yield new FulfilledPromise(0);
yield new RejectedPromise('no!');
});
$promise->then(
function (): void { $this->fail(); },
function ($reason) use (&$result): void { $result = $reason; }
);
P\Utils::queue()->run();
$this->assertInstanceOf(RejectionException::class, $result);
$this->assertSame('no!', $result->getReason());
}
public function testCanAsyncReject(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$rej = new Promise();
$promise = P\Coroutine::of(function () use ($rej) {
yield new FulfilledPromise(0);
yield $rej;
});
$promise->then(
function (): void { $this->fail(); },
function ($reason) use (&$result): void { $result = $reason; }
);
$rej->reject('no!');
P\Utils::queue()->run();
$this->assertInstanceOf(RejectionException::class, $result);
$this->assertSame('no!', $result->getReason());
}
public function testCanCatchAndThrowOtherException(): void
{
$promise = P\Coroutine::of(function () {
try {
yield new RejectedPromise('a');
$this->fail('Should have thrown into the coroutine!');
} catch (RejectionException $e) {
throw new \Exception('foo');
}
});
$promise->otherwise(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertTrue(P\Is::rejected($promise));
$this->assertStringContainsString('foo', $result->getMessage());
}
public function testCanCatchAndYieldOtherException(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = P\Coroutine::of(function () {
try {
yield new RejectedPromise('a');
$this->fail('Should have thrown into the coroutine!');
} catch (RejectionException $e) {
yield new RejectedPromise('foo');
}
});
$promise->otherwise(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertTrue(P\Is::rejected($promise));
$this->assertStringContainsString('foo', $result->getMessage());
}
public function createLotsOfSynchronousPromise()
{
return P\Coroutine::of(function () {
$value = 0;
for ($i = 0; $i < 1000; ++$i) {
$value = (yield new FulfilledPromise($i));
}
yield $value;
});
}
public function testLotsOfSynchronousDoesNotBlowStack(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = $this->createLotsOfSynchronousPromise();
$promise->then(function ($v) use (&$r): void { $r = $v; });
P\Utils::queue()->run();
$this->assertSame(999, $r);
}
public function testLotsOfSynchronousWaitDoesNotBlowStack(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = $this->createLotsOfSynchronousPromise();
$promise->then(function ($v) use (&$r): void { $r = $v; });
$this->assertSame(999, $promise->wait());
$this->assertSame(999, $r);
}
private function createLotsOfFlappingPromise()
{
return P\Coroutine::of(function () {
$value = 0;
for ($i = 0; $i < 1000; ++$i) {
try {
if ($i % 2) {
$value = (yield new FulfilledPromise($i));
} else {
$value = (yield new RejectedPromise($i));
}
} catch (\Exception $e) {
$value = (yield new FulfilledPromise($i));
}
}
yield $value;
});
}
public function testLotsOfTryCatchingDoesNotBlowStack(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = $this->createLotsOfFlappingPromise();
$promise->then(function ($v) use (&$r): void { $r = $v; });
P\Utils::queue()->run();
$this->assertSame(999, $r);
}
public function testLotsOfTryCatchingWaitingDoesNotBlowStack(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promise = $this->createLotsOfFlappingPromise();
$promise->then(function ($v) use (&$r): void { $r = $v; });
$this->assertSame(999, $promise->wait());
$this->assertSame(999, $r);
}
public function testAsyncPromisesWithCorrectlyYieldedValues(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promises = [
new Promise(),
new Promise(),
new Promise(),
];
eval('
$promise = \GuzzleHttp\Promise\Coroutine::of(function () use ($promises) {
$value = null;
$this->assertSame(\'skip\', (yield new \GuzzleHttp\Promise\FulfilledPromise(\'skip\')));
foreach ($promises as $idx => $p) {
$value = (yield $p);
$this->assertSame($idx, $value);
$this->assertSame(\'skip\', (yield new \GuzzleHttp\Promise\FulfilledPromise(\'skip\')));
}
$this->assertSame(\'skip\', (yield new \GuzzleHttp\Promise\FulfilledPromise(\'skip\')));
yield $value;
});
');
$promises[0]->resolve(0);
$promises[1]->resolve(1);
$promises[2]->resolve(2);
$promise->then(function ($v) use (&$r): void { $r = $v; });
P\Utils::queue()->run();
$this->assertSame(2, $r);
}
public function testYieldFinalWaitablePromise(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$p1 = new Promise(function () use (&$p1): void {
$p1->resolve('skip me');
});
$p2 = new Promise(function () use (&$p2): void {
$p2->resolve('hello!');
});
$co = P\Coroutine::of(function () use ($p1, $p2) {
yield $p1;
yield $p2;
});
P\Utils::queue()->run();
$this->assertSame('hello!', $co->wait());
}
public function testCanYieldFinalPendingPromise(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$p1 = new Promise();
$p2 = new Promise();
$co = P\Coroutine::of(function () use ($p1, $p2) {
yield $p1;
yield $p2;
});
$p1->resolve('a');
$p2->resolve('b');
$co->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertSame('b', $result);
}
public function testCanNestYieldsAndFailures(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$p1 = new Promise();
$p2 = new Promise();
$p3 = new Promise();
$p4 = new Promise();
$p5 = new Promise();
$co = P\Coroutine::of(function () use ($p1, $p2, $p3, $p4, $p5) {
try {
yield $p1;
} catch (\Exception $e) {
yield $p2;
try {
yield $p3;
yield $p4;
} catch (\Exception $e) {
yield $p5;
}
}
});
$p1->reject('a');
$p2->resolve('b');
$p3->resolve('c');
$p4->reject('d');
$p5->resolve('e');
$co->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertSame('e', $result);
}
public function testCanYieldErrorsAndSuccessesWithoutRecursion(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$promises = [];
for ($i = 0; $i < 20; ++$i) {
$promises[] = new Promise();
}
$co = P\Coroutine::of(function () use ($promises) {
for ($i = 0; $i < 20; $i += 4) {
try {
yield $promises[$i];
yield $promises[$i + 1];
} catch (\Exception $e) {
yield $promises[$i + 2];
yield $promises[$i + 3];
}
}
});
for ($i = 0; $i < 20; $i += 4) {
$promises[$i]->resolve($i);
$promises[$i + 1]->reject($i + 1);
$promises[$i + 2]->resolve($i + 2);
$promises[$i + 3]->resolve($i + 3);
}
$co->then(function ($value) use (&$result): void { $result = $value; });
P\Utils::queue()->run();
$this->assertSame(19, $result);
}
public function testCanWaitOnPromiseAfterFulfilled(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$f = function () {
static $i = 0;
++$i;
return $p = new Promise(function () use (&$p, $i): void {
$p->resolve($i.'-bar');
});
};
$promises = [];
for ($i = 0; $i < 20; ++$i) {
$promises[] = $f();
}
$p = P\Coroutine::of(function () use ($promises) {
yield new FulfilledPromise('foo!');
foreach ($promises as $promise) {
yield $promise;
}
});
$this->assertSame('20-bar', $p->wait());
}
public function testCanWaitOnErroredPromises(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$p1 = new Promise(function () use (&$p1): void { $p1->reject('a'); });
$p2 = new Promise(function () use (&$p2): void { $p2->resolve('b'); });
$p3 = new Promise(function () use (&$p3): void { $p3->resolve('c'); });
$p4 = new Promise(function () use (&$p4): void { $p4->reject('d'); });
$p5 = new Promise(function () use (&$p5): void { $p5->resolve('e'); });
$p6 = new Promise(function () use (&$p6): void { $p6->reject('f'); });
$co = P\Coroutine::of(function () use ($p1, $p2, $p3, $p4, $p5, $p6) {
try {
yield $p1;
} catch (\Exception $e) {
yield $p2;
try {
yield $p3;
yield $p4;
} catch (\Exception $e) {
yield $p5;
yield $p6;
}
}
});
$res = P\Utils::inspect($co);
$this->assertSame('f', $res['reason']);
}
public function testCoroutineOtherwiseIntegrationTest(): void
{
if (defined('HHVM_VERSION')) {
$this->markTestIncomplete('Broken on HHVM.');
}
$a = new Promise();
$b = new Promise();
$promise = P\Coroutine::of(function () use ($a, $b) {
// Execute the pool of commands concurrently, and process errors.
yield $a;
yield $b;
})->otherwise(function (\Exception $e): void {
// Throw errors from the operations as a specific Multipart error.
throw new \OutOfBoundsException('a', 0, $e);
});
$a->resolve('a');
$b->reject('b');
$reason = P\Utils::inspect($promise)['reason'];
$this->assertInstanceOf(\OutOfBoundsException::class, $reason);
$this->assertInstanceOf(RejectionException::class, $reason->getPrevious());
}
public function testCanManuallySettleTaskQueueGeneratedPromises(): void
{
$p1 = P\Utils::task(function () { return 'a'; });
$p2 = P\Utils::task(function () { return 'b'; });
$p3 = P\Utils::task(function () { return 'c'; });
$p1->cancel();
$p2->resolve('b2');
$results = P\Utils::inspectAll([$p1, $p2, $p3]);
$this->assertSame([
['state' => 'rejected', 'reason' => 'Promise has been cancelled'],
['state' => 'fulfilled', 'value' => 'b2'],
['state' => 'fulfilled', 'value' => 'c'],
], $results);
}
}