1<?php
2
3namespace React\Promise;
4
5use React\Promise\PromiseAdapter\CallbackPromiseAdapter;
6
7class PromiseTest extends TestCase
8{
9    use PromiseTest\FullTestTrait;
10
11    public function getPromiseTestAdapter(callable $canceller = null)
12    {
13        $resolveCallback = $rejectCallback = $progressCallback = null;
14
15        $promise = new Promise(function ($resolve, $reject, $progress) use (&$resolveCallback, &$rejectCallback, &$progressCallback) {
16            $resolveCallback  = $resolve;
17            $rejectCallback   = $reject;
18            $progressCallback = $progress;
19        }, $canceller);
20
21        return new CallbackPromiseAdapter([
22            'promise' => function () use ($promise) {
23                return $promise;
24            },
25            'resolve' => $resolveCallback,
26            'reject'  => $rejectCallback,
27            'notify'  => $progressCallback,
28            'settle'  => $resolveCallback,
29        ]);
30    }
31
32    /** @test */
33    public function shouldRejectIfResolverThrowsException()
34    {
35        $exception = new \Exception('foo');
36
37        $promise = new Promise(function () use ($exception) {
38            throw $exception;
39        });
40
41        $mock = $this->createCallableMock();
42        $mock
43            ->expects($this->once())
44            ->method('__invoke')
45            ->with($this->identicalTo($exception));
46
47        $promise
48            ->then($this->expectCallableNever(), $mock);
49    }
50
51    /** @test */
52    public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithException()
53    {
54        gc_collect_cycles();
55        $promise = new Promise(function ($resolve) {
56            $resolve(new \Exception('foo'));
57        });
58        unset($promise);
59
60        $this->assertSame(0, gc_collect_cycles());
61    }
62
63    /** @test */
64    public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver()
65    {
66        gc_collect_cycles();
67        $promise = new Promise(function () {
68            throw new \Exception('foo');
69        });
70        unset($promise);
71
72        $this->assertSame(0, gc_collect_cycles());
73    }
74
75    /** @test */
76    public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException()
77    {
78        gc_collect_cycles();
79        $promise = new Promise(function ($resolve, $reject) {
80            $reject(new \Exception('foo'));
81        });
82        unset($promise);
83
84        $this->assertSame(0, gc_collect_cycles());
85    }
86
87    /** @test */
88    public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
89    {
90        gc_collect_cycles();
91        $promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
92            $reject(new \Exception('foo'));
93        });
94        $promise->cancel();
95        unset($promise);
96
97        $this->assertSame(0, gc_collect_cycles());
98    }
99
100    /** @test */
101    public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
102    {
103        gc_collect_cycles();
104        $promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
105            $reject(new \Exception('foo'));
106        });
107        $promise->then()->then()->then()->cancel();
108        unset($promise);
109
110        $this->assertSame(0, gc_collect_cycles());
111    }
112
113    /** @test */
114    public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException()
115    {
116        gc_collect_cycles();
117        $promise = new Promise(function ($resolve, $reject) {
118            throw new \Exception('foo');
119        });
120        unset($promise);
121
122        $this->assertSame(0, gc_collect_cycles());
123    }
124
125    /**
126     * Test that checks number of garbage cycles after throwing from a canceller
127     * that explicitly uses a reference to the promise. This is rather synthetic,
128     * actual use cases often have implicit (hidden) references which ought not
129     * to be stored in the stack trace.
130     *
131     * Reassigned arguments only show up in the stack trace in PHP 7, so we can't
132     * avoid this on legacy PHP. As an alternative, consider explicitly unsetting
133     * any references before throwing.
134     *
135     * @test
136     * @requires PHP 7
137     */
138    public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException()
139    {
140        gc_collect_cycles();
141        $promise = new Promise(function () {}, function () use (&$promise) {
142            throw new \Exception('foo');
143        });
144        $promise->cancel();
145        unset($promise);
146
147        $this->assertSame(0, gc_collect_cycles());
148    }
149
150    /**
151     * @test
152     * @requires PHP 7
153     * @see self::shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException
154     */
155    public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException()
156    {
157        gc_collect_cycles();
158        $promise = new Promise(function () use (&$promise) {
159            throw new \Exception('foo');
160        });
161        unset($promise);
162
163        $this->assertSame(0, gc_collect_cycles());
164    }
165
166    /**
167     * @test
168     * @requires PHP 7
169     * @see self::shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException
170     */
171    public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException()
172    {
173        gc_collect_cycles();
174        $promise = new Promise(function () {
175            throw new \Exception('foo');
176        }, function () use (&$promise) { });
177        unset($promise);
178
179        $this->assertSame(0, gc_collect_cycles());
180    }
181
182    /** @test */
183    public function shouldIgnoreNotifyAfterReject()
184    {
185        $promise = new Promise(function () { }, function ($resolve, $reject, $notify) {
186            $reject(new \Exception('foo'));
187            $notify(42);
188        });
189
190        $promise->then(null, null, $this->expectCallableNever());
191        $promise->cancel();
192    }
193
194
195    /** @test */
196    public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromise()
197    {
198        gc_collect_cycles();
199        $promise = new Promise(function () { });
200        unset($promise);
201
202        $this->assertSame(0, gc_collect_cycles());
203    }
204
205    /** @test */
206    public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithThenFollowers()
207    {
208        gc_collect_cycles();
209        $promise = new Promise(function () { });
210        $promise->then()->then()->then();
211        unset($promise);
212
213        $this->assertSame(0, gc_collect_cycles());
214    }
215
216    /** @test */
217    public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithDoneFollowers()
218    {
219        gc_collect_cycles();
220        $promise = new Promise(function () { });
221        $promise->done();
222        unset($promise);
223
224        $this->assertSame(0, gc_collect_cycles());
225    }
226
227    /** @test */
228    public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithOtherwiseFollowers()
229    {
230        gc_collect_cycles();
231        $promise = new Promise(function () { });
232        $promise->otherwise(function () { });
233        unset($promise);
234
235        $this->assertSame(0, gc_collect_cycles());
236    }
237
238    /** @test */
239    public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithAlwaysFollowers()
240    {
241        gc_collect_cycles();
242        $promise = new Promise(function () { });
243        $promise->always(function () { });
244        unset($promise);
245
246        $this->assertSame(0, gc_collect_cycles());
247    }
248
249    /** @test */
250    public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithProgressFollowers()
251    {
252        gc_collect_cycles();
253        $promise = new Promise(function () { });
254        $promise->then(null, null, function () { });
255        unset($promise);
256
257        $this->assertSame(0, gc_collect_cycles());
258    }
259
260    /** @test */
261    public function shouldFulfillIfFullfilledWithSimplePromise()
262    {
263        $adapter = $this->getPromiseTestAdapter();
264
265        $mock = $this->createCallableMock();
266        $mock
267            ->expects($this->once())
268            ->method('__invoke')
269            ->with($this->identicalTo('foo'));
270
271        $adapter->promise()
272            ->then($mock);
273
274        $adapter->resolve(new SimpleFulfilledTestPromise());
275    }
276
277    /** @test */
278    public function shouldRejectIfRejectedWithSimplePromise()
279    {
280        $adapter = $this->getPromiseTestAdapter();
281
282        $mock = $this->createCallableMock();
283        $mock
284            ->expects($this->once())
285            ->method('__invoke')
286            ->with($this->identicalTo('foo'));
287
288        $adapter->promise()
289            ->then($this->expectCallableNever(), $mock);
290
291        $adapter->resolve(new SimpleRejectedTestPromise());
292    }
293}
294