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