diff --git a/README.md b/README.md index 3b6ec6d4..3b52482b 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,6 @@ $socket->on('error', function (Exception $e) { Note that this is not a fatal error event, i.e. the server keeps listening for new connections even after this event. - #### getAddress() The `getAddress(): ?string` method can be used to diff --git a/examples/01-echo-server.php b/examples/01-echo-server.php index a690f07a..e073c230 100644 --- a/examples/01-echo-server.php +++ b/examples/01-echo-server.php @@ -19,6 +19,7 @@ // You can also use systemd socket activation and listen on an inherited file descriptor: // // $ systemd-socket-activate -l 8000 php examples/01-echo-server.php php://fd/3 +// $ telnet localhost 8000 require __DIR__ . '/../vendor/autoload.php'; @@ -31,8 +32,14 @@ $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; $connection->pipe($connection); + + $connection->on('close', function () use ($connection) { + echo '[' . $connection->getRemoteAddress() . ' disconnected]' . PHP_EOL; + }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/examples/02-chat-server.php b/examples/02-chat-server.php index 3e2f354c..862a22de 100644 --- a/examples/02-chat-server.php +++ b/examples/02-chat-server.php @@ -19,6 +19,7 @@ // You can also use systemd socket activation and listen on an inherited file descriptor: // // $ systemd-socket-activate -l 8000 php examples/02-chat-server.php php://fd/3 +// $ telnet localhost 8000 require __DIR__ . '/../vendor/autoload.php'; @@ -30,9 +31,11 @@ $socket = new React\Socket\LimitingServer($socket, null); -$socket->on('connection', function (React\Socket\ConnectionInterface $client) use ($socket) { +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) use ($socket) { + echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; + // whenever a new message comes in - $client->on('data', function ($data) use ($client, $socket) { + $connection->on('data', function ($data) use ($connection, $socket) { // remove any non-word characters (just for the demo) $data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data)); @@ -42,13 +45,19 @@ } // prefix with client IP and broadcast to all connected clients - $data = trim(parse_url($client->getRemoteAddress(), PHP_URL_HOST), '[]') . ': ' . $data . PHP_EOL; + $data = trim(parse_url($connection->getRemoteAddress(), PHP_URL_HOST), '[]') . ': ' . $data . PHP_EOL; foreach ($socket->getConnections() as $connection) { $connection->write($data); } }); + + $connection->on('close', function () use ($connection) { + echo '[' . $connection->getRemoteAddress() . ' disconnected]' . PHP_EOL; + }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/examples/03-http-server.php b/examples/03-http-server.php index 09606ab7..dc861a1c 100644 --- a/examples/03-http-server.php +++ b/examples/03-http-server.php @@ -15,14 +15,14 @@ // $ php examples/03-http-server.php 127.0.0.1:8000 // $ curl -v http://localhost:8000/ // $ ab -n1000 -c10 http://localhost:8000/ -// $ docker run -it --rm --net=host jordi/ab ab -n1000 -c10 http://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab -n1000 -c10 http://localhost:8000/ // // You can also run a secure HTTPS echo server like this: // // $ php examples/03-http-server.php tls://127.0.0.1:8000 examples/localhost.pem // $ curl -v --insecure https://localhost:8000/ // $ ab -n1000 -c10 https://localhost:8000/ -// $ docker run -it --rm --net=host jordi/ab ab -n1000 -c10 https://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab -n1000 -c10 https://localhost:8000/ // // You can also run a Unix domain socket (UDS) server like this: // @@ -32,6 +32,9 @@ // You can also use systemd socket activation and listen on an inherited file descriptor: // // $ systemd-socket-activate -l 8000 php examples/03-http-server.php php://fd/3 +// $ curl -v --insecure https://localhost:8000/ +// $ ab -n1000 -c10 https://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab -n1000 -c10 https://localhost:8000/ require __DIR__ . '/../vendor/autoload.php'; @@ -42,12 +45,20 @@ )); $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { + echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; + $connection->once('data', function () use ($connection) { $body = "

Hello world!

\r\n"; $connection->end("HTTP/1.1 200 OK\r\nContent-Length: " . strlen($body) . "\r\nConnection: close\r\n\r\n" . $body); }); + + $connection->on('close', function () use ($connection) { + echo '[' . $connection->getRemoteAddress() . ' disconnected]' . PHP_EOL; + }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . strtr($socket->getAddress(), array('tcp:' => 'http:', 'tls:' => 'https:')) . PHP_EOL; diff --git a/examples/91-benchmark-server.php b/examples/91-benchmark-server.php index 0e3e2025..6a0e7828 100644 --- a/examples/91-benchmark-server.php +++ b/examples/91-benchmark-server.php @@ -21,6 +21,13 @@ // $ php examples/91-benchmark-server.php unix:///tmp/server.sock // $ nc -N -U /tmp/server.sock // $ dd if=/dev/zero bs=1M count=1000 | nc -N -U /tmp/server.sock +// +// You can also use systemd socket activation and listen on an inherited file descriptor: +// +// $ systemd-socket-activate -l 8000 php examples/91-benchmark-server.php php://fd/3 +// $ telnet localhost 8000 +// $ echo hello world | nc -N localhost 8000 +// $ dd if=/dev/zero bs=1M count=1000 | nc -N localhost 8000 require __DIR__ . '/../vendor/autoload.php'; @@ -31,7 +38,7 @@ )); $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { - echo '[connected]' . PHP_EOL; + echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; // count the number of bytes received from this connection $bytes = 0; @@ -43,10 +50,12 @@ $t = microtime(true); $connection->on('close', function () use ($connection, $t, &$bytes) { $t = microtime(true) - $t; - echo '[disconnected after receiving ' . $bytes . ' bytes in ' . round($t, 3) . 's => ' . round($bytes / $t / 1024 / 1024, 1) . ' MiB/s]' . PHP_EOL; + echo '[' . $connection->getRemoteAddress() . ' disconnected after receiving ' . $bytes . ' bytes in ' . round($t, 3) . 's => ' . round($bytes / $t / 1024 / 1024, 1) . ' MiB/s]' . PHP_EOL; }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/src/Connector.php b/src/Connector.php index 02c9561b..93477bd7 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -169,7 +169,8 @@ public function connect($uri) if (!isset($this->connectors[$scheme])) { return \React\Promise\reject(new \RuntimeException( - 'No connector available for URI scheme "' . $scheme . '"' + 'No connector available for URI scheme "' . $scheme . '" (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } diff --git a/src/DnsConnector.php b/src/DnsConnector.php index b68d7ea6..0b51a52c 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -29,7 +29,10 @@ public function connect($uri) } if (!$parts || !isset($parts['host'])) { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $original . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $original . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $host = \trim($parts['host'], '[]'); @@ -91,7 +94,10 @@ function ($_, $reject) use (&$promise, &$resolved, $uri) { // cancellation should reject connection attempt // reject DNS resolution with custom reason, otherwise rely on connection cancellation below if ($resolved === null) { - $reject(new \RuntimeException('Connection to ' . $uri . ' cancelled during DNS lookup')); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during DNS lookup (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); } // (try to) cancel pending DNS lookup / connection attempt diff --git a/src/FdServer.php b/src/FdServer.php index 4032d043..2c7a6c4d 100644 --- a/src/FdServer.php +++ b/src/FdServer.php @@ -81,7 +81,10 @@ public function __construct($fd, LoopInterface $loop = null) $fd = (int) $m[1]; } if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) { - throw new \InvalidArgumentException('Invalid FD number given'); + throw new \InvalidArgumentException( + 'Invalid FD number given (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $this->loop = $loop ?: Loop::get(); @@ -95,7 +98,10 @@ public function __construct($fd, LoopInterface $loop = null) $errno = isset($m[1]) ? (int) $m[1] : 0; $errstr = isset($m[2]) ? $m[2] : $error['message']; - throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr . SocketServer::errconst($errno), + $errno + ); } $meta = \stream_get_meta_data($this->master); @@ -105,7 +111,10 @@ public function __construct($fd, LoopInterface $loop = null) $errno = \defined('SOCKET_ENOTSOCK') ? \SOCKET_ENOTSOCK : 88; $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket'; - throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (ENOTSOCK)', + $errno + ); } // Socket should not have a peer address if this is a listening socket. @@ -116,7 +125,10 @@ public function __construct($fd, LoopInterface $loop = null) $errno = \defined('SOCKET_EISCONN') ? \SOCKET_EISCONN : 106; $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected'; - throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (EISCONN)', + $errno + ); } // Assume this is a Unix domain socket (UDS) when its listening address doesn't parse as a valid URL with a port. diff --git a/src/HappyEyeBallsConnectionBuilder.php b/src/HappyEyeBallsConnectionBuilder.php index 6183b177..6bd07168 100644 --- a/src/HappyEyeBallsConnectionBuilder.php +++ b/src/HappyEyeBallsConnectionBuilder.php @@ -103,7 +103,10 @@ public function connect() return $deferred->promise(); })->then($lookupResolve(Message::TYPE_A)); }, function ($_, $reject) use ($that, &$timer) { - $reject(new \RuntimeException('Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : ''))); + $reject(new \RuntimeException( + 'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : '') . ' (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); $_ = $reject = null; $that->cleanUp(); @@ -143,7 +146,11 @@ public function resolve($type, $reject) } if ($that->hasBeenResolved() && $that->ipsCount === 0) { - $reject(new \RuntimeException($that->error())); + $reject(new \RuntimeException( + $that->error(), + 0, + $e + )); } // Exception already handled above, so don't throw an unhandled rejection here @@ -201,7 +208,11 @@ public function check($resolve, $reject) if ($that->ipsCount === $that->failureCount) { $that->cleanUp(); - $reject(new \RuntimeException($that->error())); + $reject(new \RuntimeException( + $that->error(), + $e->getCode(), + $e + )); } }); diff --git a/src/HappyEyeBallsConnector.php b/src/HappyEyeBallsConnector.php index f7ea0ecf..0a7c6ecb 100644 --- a/src/HappyEyeBallsConnector.php +++ b/src/HappyEyeBallsConnector.php @@ -41,7 +41,10 @@ public function connect($uri) } if (!$parts || !isset($parts['host'])) { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $original . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $original . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $host = \trim($parts['host'], '[]'); diff --git a/src/SecureConnector.php b/src/SecureConnector.php index e6e85c4d..03c6e361 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -34,7 +34,10 @@ public function connect($uri) $parts = \parse_url($uri); if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $context = $this->context; @@ -105,7 +108,10 @@ function ($resolve, $reject) use ($promise) { }, function ($_, $reject) use (&$promise, $uri, &$connected) { if ($connected) { - $reject(new \RuntimeException('Connection to ' . $uri . ' cancelled during TLS handshake')); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during TLS handshake (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); } $promise->cancel(); diff --git a/src/SocketServer.php b/src/SocketServer.php index fa379732..2ea03bae 100644 --- a/src/SocketServer.php +++ b/src/SocketServer.php @@ -52,7 +52,10 @@ public function __construct($uri, array $context = array(), LoopInterface $loop $server = new FdServer($uri, $loop); } else { if (preg_match('#^(?:\w+://)?\d+$#', $uri)) { - throw new \InvalidArgumentException('Invalid URI given'); + throw new \InvalidArgumentException( + 'Invalid URI given (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']); @@ -110,26 +113,75 @@ public static function accept($socket) // stream_socket_accept(): accept failed: Connection timed out $error = \error_get_last(); $errstr = \preg_replace('#.*: #', '', $error['message']); - - // Go through list of possible error constants to find matching errno. - // @codeCoverageIgnoreStart - $errno = 0; - if (\function_exists('socket_strerror')) { - foreach (\get_defined_constants(false) as $name => $value) { - if (\strpos($name, 'SOCKET_E') === 0 && \socket_strerror($value) === $errstr) { - $errno = $value; - break; - } - } - } - // @codeCoverageIgnoreEnd + $errno = self::errno($errstr); throw new \RuntimeException( - 'Unable to accept new connection: ' . $errstr, + 'Unable to accept new connection: ' . $errstr . self::errconst($errno), $errno ); } return $newSocket; } + + /** + * [Internal] Returns errno value for given errstr + * + * The errno and errstr values describes the type of error that has been + * encountered. This method tries to look up the given errstr and find a + * matching errno value which can be useful to provide more context to error + * messages. It goes through the list of known errno constants when + * ext-sockets is available to find an errno matching the given errstr. + * + * @param string $errstr + * @return int errno value (e.g. value of `SOCKET_ECONNREFUSED`) or 0 if not found + * @internal + * @copyright Copyright (c) 2018 Christian Lück, taken from https://github.com/clue/errno with permission + * @codeCoverageIgnore + */ + public static function errno($errstr) + { + if (\function_exists('socket_strerror')) { + foreach (\get_defined_constants(false) as $name => $value) { + if (\strpos($name, 'SOCKET_E') === 0 && \socket_strerror($value) === $errstr) { + return $value; + } + } + } + + return 0; + } + + /** + * [Internal] Returns errno constant name for given errno value + * + * The errno value describes the type of error that has been encountered. + * This method tries to look up the given errno value and find a matching + * errno constant name which can be useful to provide more context and more + * descriptive error messages. It goes through the list of known errno + * constants when ext-sockets is available to find the matching errno + * constant name. + * + * Because this method is used to append more context to error messages, the + * constant name will be prefixed with a space and put between parenthesis + * when found. + * + * @param int $errno + * @return string e.g. ` (ECONNREFUSED)` or empty string if no matching const for the given errno could be found + * @internal + * @copyright Copyright (c) 2018 Christian Lück, taken from https://github.com/clue/errno with permission + * @codeCoverageIgnore + */ + public static function errconst($errno) + { + if (\function_exists('socket_strerror')) { + foreach (\get_defined_constants(false) as $name => $value) { + if ($value === $errno && \strpos($name, 'SOCKET_E') === 0) { + return ' (' . \substr($name, 7) . ')'; + } + } + } + + return ''; + } } diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 8321b699..4aa7fca0 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -125,13 +125,13 @@ public function toggleCrypto($socket, Deferred $deferred, $toggle, $method) if (\feof($socket) || $error === null) { // EOF or failed without error => connection closed during handshake $d->reject(new \UnexpectedValueException( - 'Connection lost during TLS handshake', - \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 0 + 'Connection lost during TLS handshake (ECONNRESET)', + \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 )); } else { // handshake failed with error message $d->reject(new \UnexpectedValueException( - 'Unable to complete TLS handshake: ' . $error + $error )); } } else { diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 9e0a8bc6..6195c6a7 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -27,12 +27,18 @@ public function connect($uri) $parts = \parse_url($uri); if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $ip = \trim($parts['host'], '[]'); if (false === \filter_var($ip, \FILTER_VALIDATE_IP)) { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $ip . '" does not contain a valid host IP')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } // use context given in constructor @@ -85,7 +91,7 @@ public function connect($uri) if (false === $stream) { return Promise\reject(new \RuntimeException( - \sprintf("Connection to %s failed: %s", $uri, $errstr), + 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno), $errno )); } @@ -125,7 +131,10 @@ public function connect($uri) // @codeCoverageIgnoreEnd \fclose($stream); - $reject(new \RuntimeException('Connection to ' . $uri . ' failed: ' . $errstr, $errno)); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno), + $errno + )); } else { $resolve(new Connection($stream, $loop)); } @@ -141,7 +150,10 @@ public function connect($uri) } // @codeCoverageIgnoreEnd - throw new \RuntimeException('Connection to ' . $uri . ' cancelled during TCP/IP handshake'); + throw new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during TCP/IP handshake (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + ); }); } } diff --git a/src/TcpServer.php b/src/TcpServer.php index 622d5575..53d5317b 100644 --- a/src/TcpServer.php +++ b/src/TcpServer.php @@ -154,11 +154,17 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a // ensure URI contains TCP scheme, host and port if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { - throw new \InvalidArgumentException('Invalid URI "' . $uri . '" given'); + throw new \InvalidArgumentException( + 'Invalid URI "' . $uri . '" given (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } if (false === \filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) { - throw new \InvalidArgumentException('Given URI "' . $uri . '" does not contain a valid host IP'); + throw new \InvalidArgumentException( + 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $this->master = @\stream_socket_server( @@ -169,7 +175,16 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a \stream_context_create(array('socket' => $context + array('backlog' => 511))) ); if (false === $this->master) { - throw new \RuntimeException('Failed to listen on "' . $uri . '": ' . $errstr, $errno); + if ($errno === 0) { + // PHP does not seem to report errno, so match errno from errstr + // @link https://3v4l.org/3qOBl + $errno = SocketServer::errno($errstr); + } + + throw new \RuntimeException( + 'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno), + $errno + ); } \stream_set_blocking($this->master, false); diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index 02ccceee..332369f8 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -40,8 +40,8 @@ private static function handler($uri) return function (\Exception $e) use ($uri) { if ($e instanceof TimeoutException) { throw new \RuntimeException( - 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds', - \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 0 + 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)', + \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 ); } diff --git a/src/UnixConnector.php b/src/UnixConnector.php index 4cfb5a37..513fb51b 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -28,13 +28,19 @@ public function connect($path) if (\strpos($path, '://') === false) { $path = 'unix://' . $path; } elseif (\substr($path, 0, 7) !== 'unix://') { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $path . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $path . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $resource = @\stream_socket_client($path, $errno, $errstr, 1.0); if (!$resource) { - return Promise\reject(new \RuntimeException('Unable to connect to unix domain socket "' . $path . '": ' . $errstr, $errno)); + return Promise\reject(new \RuntimeException( + 'Unable to connect to unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno), + $errno + )); } $connection = new Connection($resource, $this->loop); diff --git a/src/UnixServer.php b/src/UnixServer.php index 25accbe0..668e8cb3 100644 --- a/src/UnixServer.php +++ b/src/UnixServer.php @@ -57,7 +57,10 @@ public function __construct($path, LoopInterface $loop = null, array $context = if (\strpos($path, '://') === false) { $path = 'unix://' . $path; } elseif (\substr($path, 0, 7) !== 'unix://') { - throw new \InvalidArgumentException('Given URI "' . $path . '" is invalid'); + throw new \InvalidArgumentException( + 'Given URI "' . $path . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $this->master = @\stream_socket_server( @@ -79,7 +82,10 @@ public function __construct($path, LoopInterface $loop = null, array $context = } } - throw new \RuntimeException('Failed to listen on Unix domain socket "' . $path . '": ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on Unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno), + $errno + ); } \stream_set_blocking($this->master, 0); diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php index f85d8b1e..b8ac04c2 100644 --- a/tests/ConnectorTest.php +++ b/tests/ConnectorTest.php @@ -169,7 +169,12 @@ public function testConnectorWithUnknownSchemeAlwaysFails() $connector = new Connector(array(), $loop); $promise = $connector->connect('unknown://google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "unknown" (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledTcpDefaultSchemeAlwaysFails() @@ -180,7 +185,12 @@ public function testConnectorWithDisabledTcpDefaultSchemeAlwaysFails() ), $loop); $promise = $connector->connect('google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "tcp" (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledTcpSchemeAlwaysFails() @@ -191,7 +201,12 @@ public function testConnectorWithDisabledTcpSchemeAlwaysFails() ), $loop); $promise = $connector->connect('tcp://google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "tcp" (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledTlsSchemeAlwaysFails() @@ -202,7 +217,12 @@ public function testConnectorWithDisabledTlsSchemeAlwaysFails() ), $loop); $promise = $connector->connect('tls://google.com:443'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "tls" (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledUnixSchemeAlwaysFails() @@ -213,7 +233,12 @@ public function testConnectorWithDisabledUnixSchemeAlwaysFails() ), $loop); $promise = $connector->connect('unix://demo.sock'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "unix" (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorUsesGivenResolverInstance() diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index 16e73fd6..2dbc4020 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -78,7 +78,11 @@ public function testRejectsImmediatelyIfUriIsInvalid() $promise = $this->connector->connect('////'); - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "////" is invalid (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectRejectsIfGivenIpAndTcpConnectorRejectsWithRuntimeException() @@ -167,7 +171,11 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tcp://example.com:80 cancelled during DNS lookup'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tcp://example.com:80 cancelled during DNS lookup (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } @@ -196,7 +204,10 @@ public function testCancelDuringTcpConnectionCancelsTcpConnectionWithTcpRejectio $first = new Deferred(); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($first->promise()); $pending = new Promise\Promise(function () { }, function () { - throw new \RuntimeException('Connection cancelled'); + throw new \RuntimeException( + 'Connection cancelled', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); }); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($pending); @@ -205,7 +216,11 @@ public function testCancelDuringTcpConnectionCancelsTcpConnectionWithTcpRejectio $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection cancelled'); + $this->setExpectedException( + 'RuntimeException', + 'Connection cancelled', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } diff --git a/tests/FdServerTest.php b/tests/FdServerTest.php index b23231e4..3df1b296 100644 --- a/tests/FdServerTest.php +++ b/tests/FdServerTest.php @@ -30,7 +30,11 @@ public function testCtorThrowsForInvalidFd() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addReadStream'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid FD number given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new FdServer(-1, $loop); } @@ -39,7 +43,11 @@ public function testCtorThrowsForInvalidUrl() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addReadStream'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid FD number given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new FdServer('tcp://127.0.0.1:8080', $loop); } @@ -56,7 +64,7 @@ public function testCtorThrowsForUnknownFd() $this->setExpectedException( 'RuntimeException', - 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EBADF) : 'Bad file descriptor'), + 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EBADF) . ' (EBADF)' : 'Bad file descriptor'), defined('SOCKET_EBADF') ? SOCKET_EBADF : 9 ); new FdServer($fd, $loop); @@ -77,7 +85,7 @@ public function testCtorThrowsIfFdIsAFileAndNotASocket() $this->setExpectedException( 'RuntimeException', - 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_ENOTSOCK) : 'Not a socket'), + 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_ENOTSOCK) : 'Not a socket') . ' (ENOTSOCK)', defined('SOCKET_ENOTSOCK') ? SOCKET_ENOTSOCK : 88 ); new FdServer($fd, $loop); @@ -100,7 +108,7 @@ public function testCtorThrowsIfFdIsAConnectedSocketInsteadOfServerSocket() $this->setExpectedException( 'RuntimeException', - 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EISCONN) : 'Socket is connected'), + 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EISCONN) : 'Socket is connected') . ' (EISCONN)', defined('SOCKET_EISCONN') ? SOCKET_EISCONN : 106 ); new FdServer($fd, $loop); @@ -353,7 +361,7 @@ public function testEmitsErrorWhenAcceptListenerFails() */ public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception) { - $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT), $exception->getMessage()); + $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage()); $this->assertEquals(SOCKET_ETIMEDOUT, $exception->getCode()); } diff --git a/tests/FunctionalConnectorTest.php b/tests/FunctionalConnectorTest.php index f591295a..1536a2ea 100644 --- a/tests/FunctionalConnectorTest.php +++ b/tests/FunctionalConnectorTest.php @@ -168,7 +168,7 @@ public function testCancelPendingTlsConnectionDuringTlsHandshakeShouldCloseTcpCo $this->fail(); } catch (\Exception $e) { $this->assertInstanceOf('RuntimeException', $e); - $this->assertEquals('Connection to ' . $uri . ' cancelled during TLS handshake', $e->getMessage()); + $this->assertEquals('Connection to ' . $uri . ' cancelled during TLS handshake (ECONNABORTED)', $e->getMessage()); } } diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index 58b1cf4d..b81be4d5 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -664,11 +664,11 @@ public function testEmitsErrorIfConnectionIsClosedBeforeHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); - // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshak + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET) $this->assertInstanceOf('RuntimeException', $error); $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); - $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); - $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); + $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)', $error->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode()); $this->assertNull($error->getPrevious()); } @@ -692,11 +692,11 @@ public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); - // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshak + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET) $this->assertInstanceOf('RuntimeException', $error); $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); - $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); - $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); + $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)', $error->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode()); $this->assertNull($error->getPrevious()); } diff --git a/tests/FunctionalTcpServerTest.php b/tests/FunctionalTcpServerTest.php index 3f228a06..eae1ceaa 100644 --- a/tests/FunctionalTcpServerTest.php +++ b/tests/FunctionalTcpServerTest.php @@ -350,7 +350,11 @@ public function testFailsToListenOnInvalidUri() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "tcp://///" given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('///', $loop); } @@ -358,7 +362,11 @@ public function testFailsToListenOnUriWithoutPort() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "tcp://127.0.0.1" given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('127.0.0.1', $loop); } @@ -366,7 +374,11 @@ public function testFailsToListenOnUriWithWrongScheme() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "udp://127.0.0.1:0" given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('udp://127.0.0.1:0', $loop); } @@ -374,7 +386,11 @@ public function testFailsToListenOnUriWIthHostname() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Given URI "tcp://localhost:8080" does not contain a valid host IP (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('localhost:8080', $loop); } } diff --git a/tests/HappyEyeBallsConnectionBuilderTest.php b/tests/HappyEyeBallsConnectionBuilderTest.php index 1d7f815e..59b1c1fd 100644 --- a/tests/HappyEyeBallsConnectionBuilderTest.php +++ b/tests/HappyEyeBallsConnectionBuilderTest.php @@ -62,7 +62,11 @@ public function testConnectWillRejectWhenBothDnsLookupsReject() }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed during DNS lookup: DNS lookup error', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWhenBothDnsLookupsRejectWithDifferentMessages() @@ -98,7 +102,11 @@ public function testConnectWillRejectWhenBothDnsLookupsRejectWithDifferentMessag }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed during DNS lookup. Last error for IPv6: DNS6 error. Previous error for IPv4: DNS4 error', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillStartDelayTimerWhenIpv4ResolvesAndIpv6IsPending() @@ -468,7 +476,10 @@ public function testConnectWillRejectWhenOnlyTcp6ConnectionRejectsAndCancelNextA $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -476,7 +487,11 @@ public function testConnectWillRejectWhenOnlyTcp6ConnectionRejectsAndCancelNextA }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv6: Connection refused. Previous error for IPv4: DNS failed', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv6: Connection refused (ECONNREFUSED). Previous error for IPv4: DNS failed', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverStartNextAttemptTimer() @@ -504,7 +519,10 @@ public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverSt $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -512,7 +530,11 @@ public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverSt }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv4: Connection refused. Previous error for IPv6: DNS failed', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv4: Connection refused (ECONNREFUSED). Previous error for IPv6: DNS failed', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemptTimerImmediately() @@ -542,7 +564,10 @@ public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemp $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -550,7 +575,11 @@ public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemp }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Connection refused', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Connection refused (ECONNREFUSED)', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnectionsRejectAndCancelNextAttemptTimerImmediately() @@ -564,7 +593,10 @@ public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnection $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->exactly(2))->method('connect')->willReturnOnConsecutiveCalls( $deferred->promise(), - \React\Promise\reject(new \RuntimeException('Connection to tcp://127.0.0.1:80?hostname=localhost failed: Connection refused')) + \React\Promise\reject(new \RuntimeException( + 'Connection to tcp://127.0.0.1:80?hostname=localhost failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )) ); $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); @@ -583,7 +615,10 @@ public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnection $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection to tcp://[::1]:80?hostname=localhost failed: Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection to tcp://[::1]:80?hostname=localhost failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -591,7 +626,11 @@ public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnection }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://localhost:80 failed: Last error for IPv4: Connection to tcp://127.0.0.1:80 failed: Connection refused. Previous error for IPv6: Connection to tcp://[::1]:80 failed: Connection refused', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://localhost:80 failed: Last error for IPv4: Connection to tcp://127.0.0.1:80 failed: Connection refused (ECONNREFUSED). Previous error for IPv6: Connection to tcp://[::1]:80 failed: Connection refused (ECONNREFUSED)', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testCancelConnectWillRejectPromiseAndCancelBothDnsLookups() @@ -635,7 +674,10 @@ public function testCancelConnectWillRejectPromiseAndCancelBothDnsLookups() }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup (ECONNABORTED)', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6LookupAndCancelDelayTimer() @@ -672,7 +714,10 @@ public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6LookupAndC }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup (ECONNABORTED)', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6ConnectionAttemptAndPendingIpv4LookupAndCancelAttemptTimer() @@ -715,7 +760,10 @@ public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6Connection }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled (ECONNABORTED)', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } public function testResolveWillReturnResolvedPromiseWithEmptyListWhenDnsResolverFails() diff --git a/tests/HappyEyeBallsConnectorTest.php b/tests/HappyEyeBallsConnectorTest.php index d44b8625..6a26fd63 100644 --- a/tests/HappyEyeBallsConnectorTest.php +++ b/tests/HappyEyeBallsConnectorTest.php @@ -221,9 +221,11 @@ public function testRejectsImmediatelyIfUriIsInvalid() $promise = $this->connector->connect('////'); - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); - - $this->loop->run(); + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "////" is invalid (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testRejectsWithTcpConnectorRejectionIfGivenIp() @@ -275,7 +277,11 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $that->throwRejection($promise); }); - $this->setExpectedException('RuntimeException', 'Connection to tcp://example.com:80 cancelled during DNS lookup'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tcp://example.com:80 cancelled during DNS lookup (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + ); $this->loop->run(); } diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 1e0f4f87..af3a6f58 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -65,28 +65,46 @@ public function testConnectionToInvalidSchemeWillReject() $promise = $this->connector->connect('tcp://example.com:80'); - $promise->then(null, $this->expectCallableOnce()); + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "tcp://example.com:80" is invalid (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectWillRejectWithTlsUriWhenUnderlyingConnectorRejects() { - $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \RuntimeException('Connection to tcp://example.com:80 failed: Connection refused', 42))); + $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \RuntimeException( + 'Connection to tcp://example.com:80 failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ))); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tls://example.com:80 failed: Connection refused', 42); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tls://example.com:80 failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); $this->throwRejection($promise); } public function testConnectWillRejectWithOriginalMessageWhenUnderlyingConnectorRejectsWithInvalidArgumentException() { - $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \InvalidArgumentException('Invalid', 42))); + $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \InvalidArgumentException( + 'Invalid', + 42 + ))); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('InvalidArgumentException', 'Invalid', 42); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid', + 42 + ); $this->throwRejection($promise); } @@ -101,13 +119,20 @@ public function testCancelDuringTcpConnectionCancelsTcpConnection() public function testCancelDuringTcpConnectionCancelsTcpConnectionAndRejectsWithTcpRejection() { - $pending = new Promise\Promise(function () { }, function () { throw new \RuntimeException('Connection to tcp://example.com:80 cancelled', 42); }); + $pending = new Promise\Promise(function () { }, function () { throw new \RuntimeException( + 'Connection to tcp://example.com:80 cancelled (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); }); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tls://example.com:80 cancelled', 42); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tls://example.com:80 cancelled (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } @@ -187,7 +212,11 @@ public function testCancelDuringStreamEncryptionCancelsEncryptionAndClosesConnec $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tls://example.com:80 cancelled during TLS handshake'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tls://example.com:80 cancelled during TLS handshake (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 02e10301..b46949ba 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -102,7 +102,7 @@ public function testConstructorThrowsForExistingUnixPath() $this->assertStringEndsWith('Unknown error', $e->getMessage()); } else { $this->assertEquals(SOCKET_EADDRINUSE, $e->getCode()); - $this->assertStringEndsWith('Address already in use', $e->getMessage()); + $this->assertStringEndsWith('Address already in use (EADDRINUSE)', $e->getMessage()); } } } diff --git a/tests/SocketServerTest.php b/tests/SocketServerTest.php index 7092b8b4..8f453dd1 100644 --- a/tests/SocketServerTest.php +++ b/tests/SocketServerTest.php @@ -41,19 +41,31 @@ public function testCreateServerWithZeroPortAssignsRandomPort() public function testConstructorWithInvalidUriThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "tcp://invalid URI" given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new SocketServer('invalid URI'); } public function testConstructorWithInvalidUriWithPortOnlyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new SocketServer('0'); } public function testConstructorWithInvalidUriWithSchemaAndPortOnlyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new SocketServer('tcp://0'); } @@ -113,7 +125,7 @@ public function testConstructorThrowsForExistingUnixPath() $this->assertStringEndsWith('Unknown error', $e->getMessage()); } else { $this->assertEquals(SOCKET_EADDRINUSE, $e->getCode()); - $this->assertStringEndsWith('Address already in use', $e->getMessage()); + $this->assertStringEndsWith('Address already in use (EADDRINUSE)', $e->getMessage()); } } } diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index ee5b480e..68dc3d76 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -32,7 +32,11 @@ public function connectionToEmptyPortShouldFail() $connector = new TcpConnector($loop); $promise = $connector->connect('127.0.0.1:9999'); - $this->setExpectedException('RuntimeException', 'Connection to tcp://127.0.0.1:9999 failed: Connection refused'); + $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 + ); Block\await($promise, $loop, self::TIMEOUT); } @@ -127,7 +131,7 @@ public function connectionToInvalidNetworkShouldFailWithUnreachableError() $this->setExpectedException( 'RuntimeException', - 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) : 'Network is unreachable'), + 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) . ' (ENETUNREACH)' : 'Network is unreachable'), $enetunreach ); Block\await($promise, $loop, self::TIMEOUT); @@ -255,10 +259,13 @@ public function connectionToHostnameShouldFailImmediately() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = new TcpConnector($loop); - $connector->connect('www.google.com:80')->then( - $this->expectCallableNever(), - $this->expectCallableOnce() - ); + $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 : 22 + )); } /** @test */ @@ -267,10 +274,13 @@ public function connectionToInvalidPortShouldFailImmediately() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = new TcpConnector($loop); - $connector->connect('255.255.255.255:12345678')->then( - $this->expectCallableNever(), - $this->expectCallableOnce() - ); + $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 : 22 + )); } /** @test */ @@ -324,7 +334,11 @@ public function cancellingConnectionShouldRejectPromise() $promise = $connector->connect($server->getAddress()); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); Block\await($promise, $loop); } diff --git a/tests/TcpServerTest.php b/tests/TcpServerTest.php index 204d5680..b4749cf6 100644 --- a/tests/TcpServerTest.php +++ b/tests/TcpServerTest.php @@ -51,6 +51,7 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() public function testServerEmitsConnectionEventForNewConnection() { $client = stream_socket_client('tcp://localhost:'.$this->port); + assert($client !== false); $server = $this->server; $promise = new Promise(function ($resolve) use ($server) { @@ -70,6 +71,7 @@ public function testConnectionWithManyClients() $client1 = stream_socket_client('tcp://localhost:'.$this->port); $client2 = stream_socket_client('tcp://localhost:'.$this->port); $client3 = stream_socket_client('tcp://localhost:'.$this->port); + assert($client1 !== false && $client2 !== false && $client3 !== false); $this->server->on('connection', $this->expectCallableExactly(3)); $this->tick(); @@ -80,6 +82,7 @@ public function testConnectionWithManyClients() public function testDataEventWillNotBeEmittedWhenClientSendsNoData() { $client = stream_socket_client('tcp://localhost:'.$this->port); + assert($client !== false); $mock = $this->expectCallableNever(); @@ -150,6 +153,7 @@ public function testGetAddressAfterCloseReturnsNull() public function testLoopWillEndWhenServerIsClosedAfterSingleConnection() { $client = stream_socket_client('tcp://localhost:' . $this->port); + assert($client !== false); // explicitly unset server because we only accept a single connection // and then already call close() @@ -203,6 +207,7 @@ public function testDataWillBeEmittedInMultipleChunksWhenClientSendsExcessiveAmo public function testConnectionDoesNotEndWhenClientDoesNotClose() { $client = stream_socket_client('tcp://localhost:'.$this->port); + assert($client !== false); $mock = $this->expectCallableNever(); @@ -236,7 +241,7 @@ public function testCtorAddsResourceToLoop() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addReadStream'); - $server = new TcpServer(0, $loop); + new TcpServer(0, $loop); } public function testResumeWithoutPauseIsNoOp() @@ -316,10 +321,10 @@ public function testEmitsErrorWhenAcceptListenerFails() public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception) { if (defined('HHVM_VERSION')) { - $this->markTestSkipped('not supported on HHVM'); + $this->markTestSkipped('Not supported on HHVM'); } - $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT), $exception->getMessage()); + $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage()); $this->assertEquals(SOCKET_ETIMEDOUT, $exception->getCode()); } @@ -328,9 +333,16 @@ public function testListenOnBusyPortThrows() if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Windows supports listening on same port multiple times'); } + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } - $this->setExpectedException('RuntimeException'); - $another = new TcpServer($this->port, $this->loop); + $this->setExpectedException( + 'RuntimeException', + 'Failed to listen on "tcp://127.0.0.1:' . $this->port . '": ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EADDRINUSE) . ' (EADDRINUSE)' : 'Address already in use'), + defined('SOCKET_EADDRINUSE') ? SOCKET_EADDRINUSE : 0 + ); + new TcpServer($this->port, $this->loop); } /** diff --git a/tests/TestCase.php b/tests/TestCase.php index cdb8b1bc..6010b827 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -41,6 +41,21 @@ protected function expectCallableOnceWith($value) return $mock; } + protected function expectCallableOnceWithException($type, $message = null, $code = null) + { + return $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf($type), + $this->callback(function (\Exception $e) use ($message) { + return $message === null || $e->getMessage() === $message; + }), + $this->callback(function (\Exception $e) use ($code) { + return $code === null || $e->getCode() === $code; + }) + ) + ); + } + protected function expectCallableNever() { $mock = $this->createCallableMock(); diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index 98dedca7..81398279 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -34,13 +34,17 @@ public function testRejectsWithTimeoutReasonOnTimeout() $timeout = new TimeoutConnector($connector, 0.01, $loop); - $this->setExpectedException('RuntimeException', 'Connection to google.com:80 timed out after 0.01 seconds'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to google.com:80 timed out after 0.01 seconds (ETIMEDOUT)', + \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 + ); Block\await($timeout->connect('google.com:80'), $loop); } public function testRejectsWithOriginalReasonWhenConnectorRejects() { - $promise = Promise\reject(new \RuntimeException('Failed')); + $promise = Promise\reject(new \RuntimeException('Failed', 42)); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); @@ -49,7 +53,11 @@ public function testRejectsWithOriginalReasonWhenConnectorRejects() $timeout = new TimeoutConnector($connector, 5.0, $loop); - $this->setExpectedException('RuntimeException', 'Failed'); + $this->setExpectedException( + 'RuntimeException', + 'Failed', + 42 + ); Block\await($timeout->connect('google.com:80'), $loop); } diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index 9f68c2d9..183c0d3e 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -33,13 +33,21 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() public function testInvalid() { $promise = $this->connector->connect('google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException' + )); } public function testInvalidScheme() { $promise = $this->connector->connect('tcp://google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "tcp://google.com:80" is invalid (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testValid() diff --git a/tests/UnixServerTest.php b/tests/UnixServerTest.php index 463fab12..b2d4b59f 100644 --- a/tests/UnixServerTest.php +++ b/tests/UnixServerTest.php @@ -232,8 +232,12 @@ public function testCtorThrowsForInvalidAddressScheme() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $this->setExpectedException('InvalidArgumentException'); - $server = new UnixServer('tcp://localhost:0', $loop); + $this->setExpectedException( + 'InvalidArgumentException', + 'Given URI "tcp://localhost:0" is invalid (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); + new UnixServer('tcp://localhost:0', $loop); } public function testCtorThrowsWhenPathIsNotWritable() @@ -324,7 +328,7 @@ public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $ $this->markTestSkipped('not supported on HHVM'); } - $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT), $exception->getMessage()); + $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage()); $this->assertEquals(SOCKET_ETIMEDOUT, $exception->getCode()); }