123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- <?php
- namespace React\Tests\Socket;
- use React\EventLoop\Loop;
- use React\Socket\ConnectionInterface;
- use React\Socket\TcpConnector;
- use React\Socket\TcpServer;
- use React\Promise\Promise;
- class TcpConnectorTest extends TestCase
- {
- const TIMEOUT = 5.0;
- public function testConstructWithoutLoopAssignsLoopAutomatically()
- {
- $connector = new TcpConnector();
- $ref = new \ReflectionProperty($connector, 'loop');
- $ref->setAccessible(true);
- $loop = $ref->getValue($connector);
- $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
- }
- /** @test */
- public function connectionToEmptyPortShouldFailWithoutCallingCustomErrorHandler()
- {
- $connector = new TcpConnector();
- $promise = $connector->connect('127.0.0.1:9999');
- $error = null;
- set_error_handler(function ($_, $errstr) use (&$error) {
- $error = $errstr;
- });
- $this->setExpectedException(
- 'RuntimeException',
- 'Connection to tcp://127.0.0.1:9999 failed: Connection refused' . (function_exists('socket_import_stream') ? ' (ECONNREFUSED)' : ''),
- defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
- );
- try {
- \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
- restore_error_handler();
- } catch (\Exception $e) {
- restore_error_handler();
- $this->assertNull($error);
- throw $e;
- }
- }
- /** @test */
- public function connectionToTcpServerShouldAddResourceToLoop()
- {
- $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
- $connector = new TcpConnector($loop);
- $server = new TcpServer(0, $loop);
- $valid = false;
- $loop->expects($this->once())->method('addWriteStream')->with($this->callback(function ($arg) use (&$valid) {
- $valid = is_resource($arg);
- return true;
- }));
- $connector->connect($server->getAddress());
- $this->assertTrue($valid);
- }
- /** @test */
- public function connectionToTcpServerShouldSucceed()
- {
- $server = new TcpServer(9999);
- $connector = new TcpConnector();
- $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
- $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection);
- $connection->close();
- $server->close();
- }
- /** @test */
- public function connectionToTcpServerShouldFailIfFileDescriptorsAreExceeded()
- {
- $connector = new TcpConnector();
- /** @var string[] $_ */
- /** @var int $exit */
- $ulimit = exec('ulimit -n 2>&1', $_, $exit);
- if ($exit !== 0 || $ulimit < 1) {
- $this->markTestSkipped('Unable to determine limit of open files (ulimit not available?)');
- }
- $memory = ini_get('memory_limit');
- if ($memory === '-1') {
- $memory = PHP_INT_MAX;
- } elseif (preg_match('/^\d+G$/i', $memory)) {
- $memory = ((int) $memory) * 1024 * 1024 * 1024;
- } elseif (preg_match('/^\d+M$/i', $memory)) {
- $memory = ((int) $memory) * 1024 * 1024;
- } elseif (preg_match('/^\d+K$/i', $memory)) {
- $memory = ((int) $memory) * 1024;
- }
- // each file descriptor takes ~600 bytes of memory, so skip test if this would exceed memory_limit
- if ($ulimit * 600 > $memory || $ulimit > 100000) {
- $this->markTestSkipped('Test requires ~' . round($ulimit * 600 / 1024 / 1024) . '/' . round($memory / 1024 / 1024) . ' MiB memory with ' . $ulimit . ' file descriptors');
- }
- // dummy rejected promise to make sure autoloader has initialized all classes
- class_exists('React\Socket\SocketServer', true);
- class_exists('PHPUnit\Framework\Error\Warning', true);
- $promise = new Promise(function () { throw new \RuntimeException('dummy'); });
- $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
- unset($promise);
- // keep creating dummy file handles until all file descriptors are exhausted
- $fds = array();
- for ($i = 0; $i < $ulimit; ++$i) {
- $fd = @fopen('/dev/null', 'r');
- if ($fd === false) {
- break;
- }
- $fds[] = $fd;
- }
- $this->setExpectedException('RuntimeException');
- \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
- }
- /** @test */
- public function connectionToInvalidNetworkShouldFailWithUnreachableError()
- {
- if (PHP_OS !== 'Linux' && !function_exists('socket_import_stream')) {
- $this->markTestSkipped('Test requires either Linux or ext-sockets on PHP 5.4+');
- }
- $enetunreach = defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101;
- // try to find an unreachable network by trying a couple of private network addresses
- $errno = 0;
- $errstr = '';
- for ($i = 0; $i < 20 && $errno !== $enetunreach; ++$i) {
- $address = 'tcp://192.168.' . mt_rand(0, 255) . '.' . mt_rand(1, 254) . ':8123';
- $client = @stream_socket_client($address, $errno, $errstr, 0.1);
- }
- if ($client || $errno !== $enetunreach) {
- $this->markTestSkipped('Expected error ' . $enetunreach . ' but got ' . $errno . ' (' . $errstr . ') for ' . $address);
- }
- $connector = new TcpConnector();
- $promise = $connector->connect($address);
- $this->setExpectedException(
- 'RuntimeException',
- 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) . ' (ENETUNREACH)' : 'Network is unreachable'),
- $enetunreach
- );
- \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
- }
- /** @test */
- public function connectionToTcpServerShouldSucceedWithRemoteAdressSameAsTarget()
- {
- $server = new TcpServer(9999);
- $connector = new TcpConnector();
- $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
- /* @var $connection ConnectionInterface */
- $this->assertEquals('tcp://127.0.0.1:9999', $connection->getRemoteAddress());
- $connection->close();
- $server->close();
- }
- /** @test */
- public function connectionToTcpServerShouldSucceedWithLocalAdressOnLocalhost()
- {
- $server = new TcpServer(9999);
- $connector = new TcpConnector();
- $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
- /* @var $connection ConnectionInterface */
- $this->assertContainsString('tcp://127.0.0.1:', $connection->getLocalAddress());
- $this->assertNotEquals('tcp://127.0.0.1:9999', $connection->getLocalAddress());
- $connection->close();
- $server->close();
- }
- /** @test */
- public function connectionToTcpServerShouldSucceedWithNullAddressesAfterConnectionClosed()
- {
- $server = new TcpServer(9999);
- $connector = new TcpConnector();
- $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
- /* @var $connection ConnectionInterface */
- $server->close();
- $connection->close();
- $this->assertNull($connection->getRemoteAddress());
- $this->assertNull($connection->getLocalAddress());
- }
- /** @test */
- public function connectionToTcpServerWillCloseWhenOtherSideCloses()
- {
- // immediately close connection and server once connection is in
- $server = new TcpServer(0);
- $server->on('connection', function (ConnectionInterface $conn) use ($server) {
- $conn->close();
- $server->close();
- });
- $once = $this->expectCallableOnce();
- $connector = new TcpConnector();
- $connector->connect($server->getAddress())->then(function (ConnectionInterface $conn) use ($once) {
- $conn->write('hello');
- $conn->on('close', $once);
- });
- Loop::run();
- }
- /** @test
- * @group test
- */
- public function connectionToEmptyIp6PortShouldFail()
- {
- $connector = new TcpConnector();
- $connector
- ->connect('[::1]:9999')
- ->then($this->expectCallableNever(), $this->expectCallableOnce());
- Loop::run();
- }
- /** @test */
- public function connectionToIp6TcpServerShouldSucceed()
- {
- try {
- $server = new TcpServer('[::1]:9999');
- } catch (\Exception $e) {
- $this->markTestSkipped('Unable to start IPv6 server socket (IPv6 not supported on this system?)');
- }
- $connector = new TcpConnector();
- $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('[::1]:9999'), self::TIMEOUT));
- /* @var $connection ConnectionInterface */
- $this->assertEquals('tcp://[::1]:9999', $connection->getRemoteAddress());
- $this->assertContainsString('tcp://[::1]:', $connection->getLocalAddress());
- $this->assertNotEquals('tcp://[::1]:9999', $connection->getLocalAddress());
- $connection->close();
- $server->close();
- }
- /** @test */
- public function connectionToHostnameShouldFailImmediately()
- {
- $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
- $connector = new TcpConnector($loop);
- $promise = $connector->connect('www.google.com:80');
- $promise->then(null, $this->expectCallableOnceWithException(
- 'InvalidArgumentException',
- 'Given URI "tcp://www.google.com:80" does not contain a valid host IP (EINVAL)',
- defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
- ));
- }
- /** @test */
- public function connectionToInvalidPortShouldFailImmediately()
- {
- $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
- $connector = new TcpConnector($loop);
- $promise = $connector->connect('255.255.255.255:12345678');
- $promise->then(null, $this->expectCallableOnceWithException(
- 'InvalidArgumentException',
- 'Given URI "tcp://255.255.255.255:12345678" is invalid (EINVAL)',
- defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
- ));
- }
- /** @test */
- public function connectionToInvalidSchemeShouldFailImmediately()
- {
- $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
- $connector = new TcpConnector($loop);
- $connector->connect('tls://google.com:443')->then(
- $this->expectCallableNever(),
- $this->expectCallableOnce()
- );
- }
- /** @test */
- public function cancellingConnectionShouldRemoveResourceFromLoopAndCloseResource()
- {
- $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
- $connector = new TcpConnector($loop);
- $server = new TcpServer(0, $loop);
- $server->on('connection', $this->expectCallableNever());
- $loop->expects($this->once())->method('addWriteStream');
- $promise = $connector->connect($server->getAddress());
- $resource = null;
- $valid = false;
- $loop->expects($this->once())->method('removeWriteStream')->with($this->callback(function ($arg) use (&$resource, &$valid) {
- $resource = $arg;
- $valid = is_resource($arg);
- return true;
- }));
- $promise->cancel();
- // ensure that this was a valid resource during the removeWriteStream() call
- $this->assertTrue($valid);
- // ensure that this resource should now be closed after the cancel() call
- $this->assertFalse(is_resource($resource));
- }
- /** @test */
- public function cancellingConnectionShouldRejectPromise()
- {
- $connector = new TcpConnector();
- $server = new TcpServer(0);
- $promise = $connector->connect($server->getAddress());
- $promise->cancel();
- $this->setExpectedException(
- 'RuntimeException',
- 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake (ECONNABORTED)',
- defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
- );
- try {
- \React\Async\await($promise);
- } catch (\Exception $e) {
- $server->close();
- throw $e;
- }
- }
- public function testCancelDuringConnectionShouldNotCreateAnyGarbageReferences()
- {
- if (class_exists('React\Promise\When')) {
- $this->markTestSkipped('Not supported on legacy Promise v1 API');
- }
- while (gc_collect_cycles()) {
- // collect all garbage cycles
- }
- $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
- $connector = new TcpConnector($loop);
- $promise = $connector->connect('127.0.0.1:9999');
- $promise->cancel();
- unset($promise);
- $this->assertEquals(0, gc_collect_cycles());
- }
- }
|