EachPromiseTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <?php
  2. declare(strict_types=1);
  3. namespace GuzzleHttp\Promise\Tests;
  4. use GuzzleHttp\Promise as P;
  5. use GuzzleHttp\Promise\EachPromise;
  6. use GuzzleHttp\Promise\FulfilledPromise;
  7. use GuzzleHttp\Promise\Promise;
  8. use GuzzleHttp\Promise\RejectedPromise;
  9. use PHPUnit\Framework\TestCase;
  10. /**
  11. * @covers \GuzzleHttp\Promise\EachPromise
  12. */
  13. class EachPromiseTest extends TestCase
  14. {
  15. public function testReturnsSameInstance(): void
  16. {
  17. $each = new EachPromise([], ['concurrency' => 100]);
  18. $this->assertSame($each->promise(), $each->promise());
  19. }
  20. public function testResolvesInCaseOfAnEmptyList(): void
  21. {
  22. $promises = [];
  23. $each = new EachPromise($promises);
  24. $p = $each->promise();
  25. $this->assertNull($p->wait());
  26. $this->assertTrue(P\Is::fulfilled($p));
  27. }
  28. public function testResolvesInCaseOfAnEmptyListAndInvokesFulfilled(): void
  29. {
  30. $promises = [];
  31. $each = new EachPromise($promises);
  32. $p = $each->promise();
  33. $onFulfilledCalled = false;
  34. $onRejectedCalled = false;
  35. $p->then(
  36. function () use (&$onFulfilledCalled): void {
  37. $onFulfilledCalled = true;
  38. },
  39. function () use (&$onRejectedCalled): void {
  40. $onRejectedCalled = true;
  41. }
  42. );
  43. $this->assertNull($p->wait());
  44. $this->assertTrue(P\Is::fulfilled($p));
  45. $this->assertTrue($onFulfilledCalled);
  46. $this->assertFalse($onRejectedCalled);
  47. }
  48. public function testInvokesAllPromises(): void
  49. {
  50. $promises = [new Promise(), new Promise(), new Promise()];
  51. $called = [];
  52. $each = new EachPromise($promises, [
  53. 'fulfilled' => function ($value) use (&$called): void {
  54. $called[] = $value;
  55. },
  56. ]);
  57. $p = $each->promise();
  58. $promises[0]->resolve('a');
  59. $promises[1]->resolve('c');
  60. $promises[2]->resolve('b');
  61. P\Utils::queue()->run();
  62. $this->assertSame(['a', 'c', 'b'], $called);
  63. $this->assertTrue(P\Is::fulfilled($p));
  64. }
  65. public function testIsWaitable(): void
  66. {
  67. $a = $this->createSelfResolvingPromise('a');
  68. $b = $this->createSelfResolvingPromise('b');
  69. $called = [];
  70. $each = new EachPromise([$a, $b], [
  71. 'fulfilled' => function ($value) use (&$called): void { $called[] = $value; },
  72. ]);
  73. $p = $each->promise();
  74. $this->assertNull($p->wait());
  75. $this->assertTrue(P\Is::fulfilled($p));
  76. $this->assertSame(['a', 'b'], $called);
  77. }
  78. public function testCanResolveBeforeConsumingAll(): void
  79. {
  80. $called = 0;
  81. $a = $this->createSelfResolvingPromise('a');
  82. $b = new Promise(function (): void { $this->fail(); });
  83. $each = new EachPromise([$a, $b], [
  84. 'fulfilled' => function ($value, $idx, Promise $aggregate) use (&$called): void {
  85. $this->assertSame($idx, 0);
  86. $this->assertSame('a', $value);
  87. $aggregate->resolve(null);
  88. ++$called;
  89. },
  90. 'rejected' => function (\Exception $reason): void {
  91. $this->fail($reason->getMessage());
  92. },
  93. ]);
  94. $p = $each->promise();
  95. $p->wait();
  96. $this->assertNull($p->wait());
  97. $this->assertSame(1, $called);
  98. $this->assertTrue(P\Is::fulfilled($a));
  99. $this->assertTrue(P\Is::pending($b));
  100. // Resolving $b has no effect on the aggregate promise.
  101. $b->resolve('foo');
  102. $this->assertSame(1, $called);
  103. }
  104. public function testLimitsPendingPromises(): void
  105. {
  106. $pending = [new Promise(), new Promise(), new Promise(), new Promise()];
  107. $promises = new \ArrayIterator($pending);
  108. $each = new EachPromise($promises, ['concurrency' => 2]);
  109. $p = $each->promise();
  110. $this->assertCount(2, PropertyHelper::get($each, 'pending'));
  111. $pending[0]->resolve('a');
  112. $this->assertCount(2, PropertyHelper::get($each, 'pending'));
  113. $this->assertTrue($promises->valid());
  114. $pending[1]->resolve('b');
  115. P\Utils::queue()->run();
  116. $this->assertCount(2, PropertyHelper::get($each, 'pending'));
  117. $this->assertTrue($promises->valid());
  118. $promises[2]->resolve('c');
  119. P\Utils::queue()->run();
  120. $this->assertCount(1, PropertyHelper::get($each, 'pending'));
  121. $this->assertTrue(P\Is::pending($p));
  122. $promises[3]->resolve('d');
  123. P\Utils::queue()->run();
  124. $this->assertNull(PropertyHelper::get($each, 'pending'));
  125. $this->assertTrue(P\Is::fulfilled($p));
  126. $this->assertFalse($promises->valid());
  127. }
  128. public function testDynamicallyLimitsPendingPromises(): void
  129. {
  130. $calls = [];
  131. $pendingFn = function ($count) use (&$calls) {
  132. $calls[] = $count;
  133. return 2;
  134. };
  135. $pending = [new Promise(), new Promise(), new Promise(), new Promise()];
  136. $promises = new \ArrayIterator($pending);
  137. $each = new EachPromise($promises, ['concurrency' => $pendingFn]);
  138. $p = $each->promise();
  139. $this->assertCount(2, PropertyHelper::get($each, 'pending'));
  140. $pending[0]->resolve('a');
  141. $this->assertCount(2, PropertyHelper::get($each, 'pending'));
  142. $this->assertTrue($promises->valid());
  143. $pending[1]->resolve('b');
  144. $this->assertCount(2, PropertyHelper::get($each, 'pending'));
  145. P\Utils::queue()->run();
  146. $this->assertTrue($promises->valid());
  147. $promises[2]->resolve('c');
  148. P\Utils::queue()->run();
  149. $this->assertCount(1, PropertyHelper::get($each, 'pending'));
  150. $this->assertTrue(P\Is::pending($p));
  151. $promises[3]->resolve('d');
  152. P\Utils::queue()->run();
  153. $this->assertNull(PropertyHelper::get($each, 'pending'));
  154. $this->assertTrue(P\Is::fulfilled($p));
  155. $this->assertSame([0, 1, 1, 1], $calls);
  156. $this->assertFalse($promises->valid());
  157. }
  158. public function testClearsReferencesWhenResolved(): void
  159. {
  160. $called = false;
  161. $a = new Promise(function () use (&$a, &$called): void {
  162. $a->resolve('a');
  163. $called = true;
  164. });
  165. $each = new EachPromise([$a], [
  166. 'concurrency' => function () { return 1; },
  167. 'fulfilled' => function (): void {},
  168. 'rejected' => function (): void {},
  169. ]);
  170. $each->promise()->wait();
  171. $this->assertNull(PropertyHelper::get($each, 'onFulfilled'));
  172. $this->assertNull(PropertyHelper::get($each, 'onRejected'));
  173. $this->assertNull(PropertyHelper::get($each, 'iterable'));
  174. $this->assertNull(PropertyHelper::get($each, 'pending'));
  175. $this->assertNull(PropertyHelper::get($each, 'concurrency'));
  176. $this->assertTrue($called);
  177. }
  178. public function testCanBeCancelled(): void
  179. {
  180. $called = false;
  181. $a = new FulfilledPromise('a');
  182. $b = new Promise(function () use (&$called): void { $called = true; });
  183. $each = new EachPromise([$a, $b], [
  184. 'fulfilled' => function ($value, $idx, Promise $aggregate): void {
  185. $aggregate->cancel();
  186. },
  187. 'rejected' => function ($reason) use (&$called): void {
  188. $called = true;
  189. },
  190. ]);
  191. $p = $each->promise();
  192. $p->wait(false);
  193. $this->assertTrue(P\Is::fulfilled($a));
  194. $this->assertTrue(P\Is::pending($b));
  195. $this->assertTrue(P\Is::rejected($p));
  196. $this->assertFalse($called);
  197. }
  198. public function testDoesNotBlowStackWithFulfilledPromises(): void
  199. {
  200. $pending = [];
  201. for ($i = 0; $i < 100; ++$i) {
  202. $pending[] = new FulfilledPromise($i);
  203. }
  204. $values = [];
  205. $each = new EachPromise($pending, [
  206. 'fulfilled' => function ($value) use (&$values): void {
  207. $values[] = $value;
  208. },
  209. ]);
  210. $called = false;
  211. $each->promise()->then(function () use (&$called): void {
  212. $called = true;
  213. });
  214. $this->assertFalse($called);
  215. P\Utils::queue()->run();
  216. $this->assertTrue($called);
  217. $this->assertSame(range(0, 99), $values);
  218. }
  219. public function testDoesNotBlowStackWithRejectedPromises(): void
  220. {
  221. $pending = [];
  222. for ($i = 0; $i < 100; ++$i) {
  223. $pending[] = new RejectedPromise($i);
  224. }
  225. $values = [];
  226. $each = new EachPromise($pending, [
  227. 'rejected' => function ($value) use (&$values): void {
  228. $values[] = $value;
  229. },
  230. ]);
  231. $called = false;
  232. $each->promise()->then(
  233. function () use (&$called): void { $called = true; },
  234. function (): void { $this->fail('Should not have rejected.'); }
  235. );
  236. $this->assertFalse($called);
  237. P\Utils::queue()->run();
  238. $this->assertTrue($called);
  239. $this->assertSame(range(0, 99), $values);
  240. }
  241. public function testReturnsPromiseForWhatever(): void
  242. {
  243. $called = [];
  244. $arr = ['a', 'b'];
  245. $each = new EachPromise($arr, [
  246. 'fulfilled' => function ($v) use (&$called): void { $called[] = $v; },
  247. ]);
  248. $p = $each->promise();
  249. $this->assertNull($p->wait());
  250. $this->assertSame(['a', 'b'], $called);
  251. }
  252. public function testRejectsAggregateWhenNextThrows(): void
  253. {
  254. $iter = function () {
  255. yield 'a';
  256. throw new \Exception('Failure');
  257. };
  258. $each = new EachPromise($iter());
  259. $p = $each->promise();
  260. $e = null;
  261. $received = null;
  262. $p->then(null, function ($reason) use (&$e): void { $e = $reason; });
  263. P\Utils::queue()->run();
  264. $this->assertInstanceOf(\Exception::class, $e);
  265. $this->assertSame('Failure', $e->getMessage());
  266. }
  267. public function testDoesNotCallNextOnIteratorUntilNeededWhenWaiting(): void
  268. {
  269. $results = [];
  270. $values = [10];
  271. $remaining = 9;
  272. $iter = function () use (&$values) {
  273. while ($value = array_pop($values)) {
  274. yield $value;
  275. }
  276. };
  277. $each = new EachPromise($iter(), [
  278. 'concurrency' => 1,
  279. 'fulfilled' => function ($r) use (&$results, &$values, &$remaining): void {
  280. $results[] = $r;
  281. if ($remaining > 0) {
  282. $values[] = $remaining--;
  283. }
  284. },
  285. ]);
  286. $each->promise()->wait();
  287. $this->assertSame(range(10, 1), $results);
  288. }
  289. public function testDoesNotCallNextOnIteratorUntilNeededWhenAsync(): void
  290. {
  291. $firstPromise = new Promise();
  292. $pending = [$firstPromise];
  293. $values = [$firstPromise];
  294. $results = [];
  295. $remaining = 9;
  296. $iter = function () use (&$values) {
  297. while ($value = array_pop($values)) {
  298. yield $value;
  299. }
  300. };
  301. $each = new EachPromise($iter(), [
  302. 'concurrency' => 1,
  303. 'fulfilled' => function ($r) use (&$results, &$values, &$remaining, &$pending): void {
  304. $results[] = $r;
  305. if ($remaining-- > 0) {
  306. $pending[] = $values[] = new Promise();
  307. }
  308. },
  309. ]);
  310. $i = 0;
  311. $each->promise();
  312. while ($promise = array_pop($pending)) {
  313. $promise->resolve($i++);
  314. P\Utils::queue()->run();
  315. }
  316. $this->assertSame(range(0, 9), $results);
  317. }
  318. private function createSelfResolvingPromise($value)
  319. {
  320. $p = new Promise(function () use (&$p, $value): void {
  321. $p->resolve($value);
  322. });
  323. $trickCsFixer = true;
  324. return $p;
  325. }
  326. public function testMutexPreventsGeneratorRecursion(): void
  327. {
  328. if (defined('HHVM_VERSION')) {
  329. $this->markTestIncomplete('Broken on HHVM.');
  330. }
  331. $results = $promises = [];
  332. for ($i = 0; $i < 20; ++$i) {
  333. $p = $this->createSelfResolvingPromise($i);
  334. $pending[] = $p;
  335. $promises[] = $p;
  336. }
  337. $iter = function () use (&$promises, &$pending) {
  338. foreach ($promises as $promise) {
  339. // Resolve a promises, which will trigger the then() function,
  340. // which would cause the EachPromise to try to add more
  341. // promises to the queue. Without a lock, this would trigger
  342. // a "Cannot resume an already running generator" fatal error.
  343. if ($p = array_pop($pending)) {
  344. $p->wait();
  345. }
  346. yield $promise;
  347. }
  348. };
  349. $each = new EachPromise($iter(), [
  350. 'concurrency' => 5,
  351. 'fulfilled' => function ($r) use (&$results, &$pending): void {
  352. $results[] = $r;
  353. },
  354. ]);
  355. $each->promise()->wait();
  356. $this->assertCount(20, $results);
  357. }
  358. public function testIteratorWithSameKey(): void
  359. {
  360. if (defined('HHVM_VERSION')) {
  361. $this->markTestIncomplete('Broken on HHVM.');
  362. }
  363. $iter = function () {
  364. yield 'foo' => $this->createSelfResolvingPromise(1);
  365. yield 'foo' => $this->createSelfResolvingPromise(2);
  366. yield 1 => $this->createSelfResolvingPromise(3);
  367. yield 1 => $this->createSelfResolvingPromise(4);
  368. };
  369. $called = 0;
  370. $each = new EachPromise($iter(), [
  371. 'fulfilled' => function ($value, $idx, Promise $aggregate) use (&$called): void {
  372. ++$called;
  373. if ($value < 3) {
  374. $this->assertSame('foo', $idx);
  375. } else {
  376. $this->assertSame(1, $idx);
  377. }
  378. },
  379. ]);
  380. $each->promise()->wait();
  381. $this->assertSame(4, $called);
  382. }
  383. public function testIsWaitableWhenLimited(): void
  384. {
  385. $promises = [
  386. $this->createSelfResolvingPromise('a'),
  387. $this->createSelfResolvingPromise('c'),
  388. $this->createSelfResolvingPromise('b'),
  389. $this->createSelfResolvingPromise('d'),
  390. ];
  391. $called = [];
  392. $each = new EachPromise($promises, [
  393. 'concurrency' => 2,
  394. 'fulfilled' => function ($value) use (&$called): void {
  395. $called[] = $value;
  396. },
  397. ]);
  398. $p = $each->promise();
  399. $this->assertNull($p->wait());
  400. $this->assertSame(['a', 'c', 'b', 'd'], $called);
  401. $this->assertTrue(P\Is::fulfilled($p));
  402. }
  403. }