TimeoutConnectorTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <?php
  2. namespace React\Tests\Socket;
  3. use React\EventLoop\Loop;
  4. use React\Promise\Deferred;
  5. use React\Promise\Promise;
  6. use React\Socket\TimeoutConnector;
  7. class TimeoutConnectorTest extends TestCase
  8. {
  9. public function testCtorThrowsForInvalidLoop()
  10. {
  11. $base = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  12. $this->setExpectedException('InvalidArgumentException', 'Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
  13. new TimeoutConnector($base, 0.001, 'loop');
  14. }
  15. public function testConstructWithoutLoopAssignsLoopAutomatically()
  16. {
  17. $base = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  18. $connector = new TimeoutConnector($base, 0.01);
  19. $ref = new \ReflectionProperty($connector, 'loop');
  20. $ref->setAccessible(true);
  21. $loop = $ref->getValue($connector);
  22. $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
  23. }
  24. public function testRejectsPromiseWithoutStartingTimerWhenWrappedConnectorReturnsRejectedPromise()
  25. {
  26. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  27. $loop->expects($this->never())->method('addTimer');
  28. $loop->expects($this->never())->method('cancelTimer');
  29. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  30. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \RuntimeException('Failed', 42)));
  31. $timeout = new TimeoutConnector($connector, 5.0, $loop);
  32. $promise = $timeout->connect('example.com:80');
  33. $exception = null;
  34. $promise->then(null, function ($reason) use (&$exception) {
  35. $exception = $reason;
  36. });
  37. assert($exception instanceof \RuntimeException);
  38. $this->assertEquals('Failed', $exception->getMessage());
  39. $this->assertEquals(42, $exception->getCode());
  40. }
  41. public function testRejectsPromiseAfterCancellingTimerWhenWrappedConnectorReturnsPendingPromiseThatRejects()
  42. {
  43. $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
  44. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  45. $loop->expects($this->once())->method('addTimer')->with(5.0, $this->anything())->willReturn($timer);
  46. $loop->expects($this->once())->method('cancelTimer')->with($timer);
  47. $deferred = new Deferred();
  48. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  49. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($deferred->promise());
  50. $timeout = new TimeoutConnector($connector, 5.0, $loop);
  51. $promise = $timeout->connect('example.com:80');
  52. $deferred->reject(new \RuntimeException('Failed', 42));
  53. $exception = null;
  54. $promise->then(null, function ($reason) use (&$exception) {
  55. $exception = $reason;
  56. });
  57. assert($exception instanceof \RuntimeException);
  58. $this->assertEquals('Failed', $exception->getMessage());
  59. $this->assertEquals(42, $exception->getCode());
  60. }
  61. public function testResolvesPromiseWithoutStartingTimerWhenWrappedConnectorReturnsResolvedPromise()
  62. {
  63. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  64. $loop->expects($this->never())->method('addTimer');
  65. $loop->expects($this->never())->method('cancelTimer');
  66. $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
  67. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  68. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\resolve($connection));
  69. $timeout = new TimeoutConnector($connector, 5.0, $loop);
  70. $promise = $timeout->connect('example.com:80');
  71. $resolved = null;
  72. $promise->then(function ($value) use (&$resolved) {
  73. $resolved = $value;
  74. });
  75. $this->assertSame($connection, $resolved);
  76. }
  77. public function testResolvesPromiseAfterCancellingTimerWhenWrappedConnectorReturnsPendingPromiseThatResolves()
  78. {
  79. $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
  80. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  81. $loop->expects($this->once())->method('addTimer')->with(5.0, $this->anything())->willReturn($timer);
  82. $loop->expects($this->once())->method('cancelTimer')->with($timer);
  83. $deferred = new Deferred();
  84. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  85. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($deferred->promise());
  86. $timeout = new TimeoutConnector($connector, 5.0, $loop);
  87. $promise = $timeout->connect('example.com:80');
  88. $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
  89. $deferred->resolve($connection);
  90. $resolved = null;
  91. $promise->then(function ($value) use (&$resolved) {
  92. $resolved = $value;
  93. });
  94. $this->assertSame($connection, $resolved);
  95. }
  96. public function testRejectsPromiseAndCancelsPendingConnectionWhenTimeoutTriggers()
  97. {
  98. $timerCallback = null;
  99. $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
  100. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  101. $loop->expects($this->once())->method('addTimer')->with(0.01, $this->callback(function ($callback) use (&$timerCallback) {
  102. $timerCallback = $callback;
  103. return true;
  104. }))->willReturn($timer);
  105. $loop->expects($this->once())->method('cancelTimer')->with($timer);
  106. $cancelled = 0;
  107. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  108. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn(new Promise(function () { }, function () use (&$cancelled) {
  109. ++$cancelled;
  110. throw new \RuntimeException();
  111. }));
  112. $timeout = new TimeoutConnector($connector, 0.01, $loop);
  113. $promise = $timeout->connect('example.com:80');
  114. $this->assertEquals(0, $cancelled);
  115. $this->assertNotNull($timerCallback);
  116. $timerCallback();
  117. $this->assertEquals(1, $cancelled);
  118. $exception = null;
  119. $promise->then(null, function ($reason) use (&$exception) {
  120. $exception = $reason;
  121. });
  122. assert($exception instanceof \RuntimeException);
  123. $this->assertEquals('Connection to example.com:80 timed out after 0.01 seconds (ETIMEDOUT)' , $exception->getMessage());
  124. $this->assertEquals(\defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110, $exception->getCode());
  125. }
  126. public function testCancellingPromiseWillCancelPendingConnectionAndRejectPromise()
  127. {
  128. $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
  129. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  130. $loop->expects($this->once())->method('addTimer')->with(0.01, $this->anything())->willReturn($timer);
  131. $loop->expects($this->once())->method('cancelTimer')->with($timer);
  132. $cancelled = 0;
  133. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  134. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn(new Promise(function () { }, function () use (&$cancelled) {
  135. ++$cancelled;
  136. throw new \RuntimeException('Cancelled');
  137. }));
  138. $timeout = new TimeoutConnector($connector, 0.01, $loop);
  139. $promise = $timeout->connect('example.com:80');
  140. $this->assertEquals(0, $cancelled);
  141. assert(method_exists($promise, 'cancel'));
  142. $promise->cancel();
  143. $this->assertEquals(1, $cancelled);
  144. $exception = null;
  145. $promise->then(null, function ($reason) use (&$exception) {
  146. $exception = $reason;
  147. });
  148. assert($exception instanceof \RuntimeException);
  149. $this->assertEquals('Cancelled', $exception->getMessage());
  150. }
  151. public function testRejectionDuringConnectionShouldNotCreateAnyGarbageReferences()
  152. {
  153. if (class_exists('React\Promise\When')) {
  154. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  155. }
  156. while (gc_collect_cycles()) {
  157. // collect all garbage cycles
  158. }
  159. $connection = new Deferred();
  160. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  161. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($connection->promise());
  162. $timeout = new TimeoutConnector($connector, 0.01);
  163. $promise = $timeout->connect('example.com:80');
  164. $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
  165. $connection->reject(new \RuntimeException('Connection failed'));
  166. unset($promise, $connection);
  167. $this->assertEquals(0, gc_collect_cycles());
  168. }
  169. public function testRejectionDueToTimeoutShouldNotCreateAnyGarbageReferences()
  170. {
  171. if (class_exists('React\Promise\When')) {
  172. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  173. }
  174. while (gc_collect_cycles()) {
  175. // collect all garbage cycles
  176. }
  177. $connection = new Deferred(function () {
  178. throw new \RuntimeException('Connection cancelled');
  179. });
  180. $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  181. $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($connection->promise());
  182. $timeout = new TimeoutConnector($connector, 0);
  183. $promise = $timeout->connect('example.com:80');
  184. $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
  185. Loop::run();
  186. unset($promise, $connection);
  187. $this->assertEquals(0, gc_collect_cycles());
  188. }
  189. }