TcpConnectorTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. <?php
  2. namespace React\Tests\Socket;
  3. use React\EventLoop\Loop;
  4. use React\Socket\ConnectionInterface;
  5. use React\Socket\TcpConnector;
  6. use React\Socket\TcpServer;
  7. use React\Promise\Promise;
  8. class TcpConnectorTest extends TestCase
  9. {
  10. const TIMEOUT = 5.0;
  11. public function testCtorThrowsForInvalidLoop()
  12. {
  13. $this->setExpectedException('InvalidArgumentException', 'Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
  14. new TcpConnector('loop');
  15. }
  16. public function testConstructWithoutLoopAssignsLoopAutomatically()
  17. {
  18. $connector = new TcpConnector();
  19. $ref = new \ReflectionProperty($connector, 'loop');
  20. $ref->setAccessible(true);
  21. $loop = $ref->getValue($connector);
  22. $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
  23. }
  24. /** @test */
  25. public function connectionToEmptyPortShouldFailWithoutCallingCustomErrorHandler()
  26. {
  27. $connector = new TcpConnector();
  28. $promise = $connector->connect('127.0.0.1:9999');
  29. $error = null;
  30. set_error_handler(function ($_, $errstr) use (&$error) {
  31. $error = $errstr;
  32. });
  33. $this->setExpectedException(
  34. 'RuntimeException',
  35. 'Connection to tcp://127.0.0.1:9999 failed: Connection refused' . (function_exists('socket_import_stream') ? ' (ECONNREFUSED)' : ''),
  36. defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
  37. );
  38. try {
  39. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  40. restore_error_handler();
  41. } catch (\Exception $e) {
  42. restore_error_handler();
  43. $this->assertNull($error);
  44. throw $e;
  45. }
  46. }
  47. /** @test */
  48. public function connectionToTcpServerShouldAddResourceToLoop()
  49. {
  50. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  51. $connector = new TcpConnector($loop);
  52. $server = new TcpServer(0, $loop);
  53. $valid = false;
  54. $loop->expects($this->once())->method('addWriteStream')->with($this->callback(function ($arg) use (&$valid) {
  55. $valid = is_resource($arg);
  56. return true;
  57. }));
  58. $connector->connect($server->getAddress());
  59. $this->assertTrue($valid);
  60. }
  61. /** @test */
  62. public function connectionToTcpServerShouldSucceed()
  63. {
  64. $server = new TcpServer(9999);
  65. $connector = new TcpConnector();
  66. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  67. $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection);
  68. $connection->close();
  69. $server->close();
  70. }
  71. /** @test */
  72. public function connectionToTcpServerShouldFailIfFileDescriptorsAreExceeded()
  73. {
  74. $connector = new TcpConnector();
  75. /** @var string[] $_ */
  76. /** @var int $exit */
  77. $ulimit = exec('ulimit -n 2>&1', $_, $exit);
  78. if ($exit !== 0 || $ulimit < 1) {
  79. $this->markTestSkipped('Unable to determine limit of open files (ulimit not available?)');
  80. }
  81. $memory = ini_get('memory_limit');
  82. if ($memory === '-1') {
  83. $memory = PHP_INT_MAX;
  84. } elseif (preg_match('/^\d+G$/i', $memory)) {
  85. $memory = ((int) $memory) * 1024 * 1024 * 1024;
  86. } elseif (preg_match('/^\d+M$/i', $memory)) {
  87. $memory = ((int) $memory) * 1024 * 1024;
  88. } elseif (preg_match('/^\d+K$/i', $memory)) {
  89. $memory = ((int) $memory) * 1024;
  90. }
  91. // each file descriptor takes ~600 bytes of memory, so skip test if this would exceed memory_limit
  92. if ($ulimit * 600 > $memory || $ulimit > 100000) {
  93. $this->markTestSkipped('Test requires ~' . round($ulimit * 600 / 1024 / 1024) . '/' . round($memory / 1024 / 1024) . ' MiB memory with ' . $ulimit . ' file descriptors');
  94. }
  95. // dummy rejected promise to make sure autoloader has initialized all classes
  96. class_exists('React\Socket\SocketServer', true);
  97. class_exists('PHPUnit\Framework\Error\Warning', true);
  98. $promise = new Promise(function () { throw new \RuntimeException('dummy'); });
  99. $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
  100. unset($promise);
  101. // keep creating dummy file handles until all file descriptors are exhausted
  102. $fds = array();
  103. for ($i = 0; $i < $ulimit; ++$i) {
  104. $fd = @fopen('/dev/null', 'r');
  105. if ($fd === false) {
  106. break;
  107. }
  108. $fds[] = $fd;
  109. }
  110. $this->setExpectedException('RuntimeException');
  111. \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  112. }
  113. /** @test */
  114. public function connectionToInvalidNetworkShouldFailWithUnreachableError()
  115. {
  116. if (PHP_OS !== 'Linux' && !function_exists('socket_import_stream')) {
  117. $this->markTestSkipped('Test requires either Linux or ext-sockets on PHP 5.4+');
  118. }
  119. $enetunreach = defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101;
  120. // try to find an unreachable network by trying a couple of private network addresses
  121. $errno = 0;
  122. $errstr = '';
  123. for ($i = 0; $i < 20 && $errno !== $enetunreach; ++$i) {
  124. $address = 'tcp://192.168.' . mt_rand(0, 255) . '.' . mt_rand(1, 254) . ':8123';
  125. $client = @stream_socket_client($address, $errno, $errstr, 0.1);
  126. }
  127. if ($client || $errno !== $enetunreach) {
  128. $this->markTestSkipped('Expected error ' . $enetunreach . ' but got ' . $errno . ' (' . $errstr . ') for ' . $address);
  129. }
  130. $connector = new TcpConnector();
  131. $promise = $connector->connect($address);
  132. $this->setExpectedException(
  133. 'RuntimeException',
  134. 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) . ' (ENETUNREACH)' : 'Network is unreachable'),
  135. $enetunreach
  136. );
  137. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  138. }
  139. /** @test */
  140. public function connectionToTcpServerShouldSucceedWithRemoteAdressSameAsTarget()
  141. {
  142. $server = new TcpServer(9999);
  143. $connector = new TcpConnector();
  144. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  145. /* @var $connection ConnectionInterface */
  146. $this->assertEquals('tcp://127.0.0.1:9999', $connection->getRemoteAddress());
  147. $connection->close();
  148. $server->close();
  149. }
  150. /** @test */
  151. public function connectionToTcpServerShouldSucceedWithLocalAdressOnLocalhost()
  152. {
  153. $server = new TcpServer(9999);
  154. $connector = new TcpConnector();
  155. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  156. /* @var $connection ConnectionInterface */
  157. $this->assertContainsString('tcp://127.0.0.1:', $connection->getLocalAddress());
  158. $this->assertNotEquals('tcp://127.0.0.1:9999', $connection->getLocalAddress());
  159. $connection->close();
  160. $server->close();
  161. }
  162. /** @test */
  163. public function connectionToTcpServerShouldSucceedWithNullAddressesAfterConnectionClosed()
  164. {
  165. $server = new TcpServer(9999);
  166. $connector = new TcpConnector();
  167. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  168. /* @var $connection ConnectionInterface */
  169. $server->close();
  170. $connection->close();
  171. $this->assertNull($connection->getRemoteAddress());
  172. $this->assertNull($connection->getLocalAddress());
  173. }
  174. /** @test */
  175. public function connectionToTcpServerWillCloseWhenOtherSideCloses()
  176. {
  177. // immediately close connection and server once connection is in
  178. $server = new TcpServer(0);
  179. $server->on('connection', function (ConnectionInterface $conn) use ($server) {
  180. $conn->close();
  181. $server->close();
  182. });
  183. $once = $this->expectCallableOnce();
  184. $connector = new TcpConnector();
  185. $connector->connect($server->getAddress())->then(function (ConnectionInterface $conn) use ($once) {
  186. $conn->write('hello');
  187. $conn->on('close', $once);
  188. });
  189. Loop::run();
  190. }
  191. /** @test
  192. * @group test
  193. */
  194. public function connectionToEmptyIp6PortShouldFail()
  195. {
  196. $connector = new TcpConnector();
  197. $connector
  198. ->connect('[::1]:9999')
  199. ->then($this->expectCallableNever(), $this->expectCallableOnce());
  200. Loop::run();
  201. }
  202. /** @test */
  203. public function connectionToIp6TcpServerShouldSucceed()
  204. {
  205. try {
  206. $server = new TcpServer('[::1]:9999');
  207. } catch (\Exception $e) {
  208. $this->markTestSkipped('Unable to start IPv6 server socket (IPv6 not supported on this system?)');
  209. }
  210. $connector = new TcpConnector();
  211. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('[::1]:9999'), self::TIMEOUT));
  212. /* @var $connection ConnectionInterface */
  213. $this->assertEquals('tcp://[::1]:9999', $connection->getRemoteAddress());
  214. $this->assertContainsString('tcp://[::1]:', $connection->getLocalAddress());
  215. $this->assertNotEquals('tcp://[::1]:9999', $connection->getLocalAddress());
  216. $connection->close();
  217. $server->close();
  218. }
  219. /** @test */
  220. public function connectionToHostnameShouldFailImmediately()
  221. {
  222. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  223. $connector = new TcpConnector($loop);
  224. $promise = $connector->connect('www.google.com:80');
  225. $promise->then(null, $this->expectCallableOnceWithException(
  226. 'InvalidArgumentException',
  227. 'Given URI "tcp://www.google.com:80" does not contain a valid host IP (EINVAL)',
  228. defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
  229. ));
  230. }
  231. /** @test */
  232. public function connectionToInvalidPortShouldFailImmediately()
  233. {
  234. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  235. $connector = new TcpConnector($loop);
  236. $promise = $connector->connect('255.255.255.255:12345678');
  237. $promise->then(null, $this->expectCallableOnceWithException(
  238. 'InvalidArgumentException',
  239. 'Given URI "tcp://255.255.255.255:12345678" is invalid (EINVAL)',
  240. defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
  241. ));
  242. }
  243. /** @test */
  244. public function connectionToInvalidSchemeShouldFailImmediately()
  245. {
  246. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  247. $connector = new TcpConnector($loop);
  248. $connector->connect('tls://google.com:443')->then(
  249. $this->expectCallableNever(),
  250. $this->expectCallableOnce()
  251. );
  252. }
  253. /** @test */
  254. public function cancellingConnectionShouldRemoveResourceFromLoopAndCloseResource()
  255. {
  256. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  257. $connector = new TcpConnector($loop);
  258. $server = new TcpServer(0, $loop);
  259. $server->on('connection', $this->expectCallableNever());
  260. $loop->expects($this->once())->method('addWriteStream');
  261. $promise = $connector->connect($server->getAddress());
  262. $resource = null;
  263. $valid = false;
  264. $loop->expects($this->once())->method('removeWriteStream')->with($this->callback(function ($arg) use (&$resource, &$valid) {
  265. $resource = $arg;
  266. $valid = is_resource($arg);
  267. return true;
  268. }));
  269. $promise->cancel();
  270. // ensure that this was a valid resource during the removeWriteStream() call
  271. $this->assertTrue($valid);
  272. // ensure that this resource should now be closed after the cancel() call
  273. $this->assertFalse(is_resource($resource));
  274. }
  275. /** @test */
  276. public function cancellingConnectionShouldRejectPromise()
  277. {
  278. $connector = new TcpConnector();
  279. $server = new TcpServer(0);
  280. $promise = $connector->connect($server->getAddress());
  281. $promise->cancel();
  282. $this->setExpectedException(
  283. 'RuntimeException',
  284. 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake (ECONNABORTED)',
  285. defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
  286. );
  287. try {
  288. \React\Async\await($promise);
  289. } catch (\Exception $e) {
  290. $server->close();
  291. throw $e;
  292. }
  293. }
  294. public function testCancelDuringConnectionShouldNotCreateAnyGarbageReferences()
  295. {
  296. if (class_exists('React\Promise\When')) {
  297. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  298. }
  299. while (gc_collect_cycles()) {
  300. // collect all garbage cycles
  301. }
  302. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  303. $connector = new TcpConnector($loop);
  304. $promise = $connector->connect('127.0.0.1:9999');
  305. $promise->cancel();
  306. unset($promise);
  307. $this->assertEquals(0, gc_collect_cycles());
  308. }
  309. }