diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 8fc2007..74be321 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -3,6 +3,7 @@ namespace React\Mysql\Io; use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; use React\Mysql\Commands\CommandInterface; use React\Mysql\Commands\PingCommand; use React\Mysql\Commands\QueryCommand; @@ -29,30 +30,66 @@ class Connection extends EventEmitter private $executor; /** - * @var integer + * @var int one of the state constants (may change, but should be used readonly from outside) + * @see self::STATE_* */ - private $state = self::STATE_AUTHENTICATED; + public $state = self::STATE_AUTHENTICATED; /** * @var SocketConnectionInterface */ private $stream; + /** @var Parser */ + private $parser; + + /** @var LoopInterface */ + private $loop; + + /** @var float */ + private $idlePeriod = 0.001; + + /** @var ?\React\EventLoop\TimerInterface */ + private $idleTimer; + + /** @var int */ + private $pending = 0; + /** * Connection constructor. * * @param SocketConnectionInterface $stream * @param Executor $executor + * @param Parser $parser + * @param LoopInterface $loop + * @param ?float $idlePeriod */ - public function __construct(SocketConnectionInterface $stream, Executor $executor) + public function __construct(SocketConnectionInterface $stream, Executor $executor, Parser $parser, LoopInterface $loop, $idlePeriod) { $this->stream = $stream; $this->executor = $executor; + $this->parser = $parser; + + $this->loop = $loop; + if ($idlePeriod !== null) { + $this->idlePeriod = $idlePeriod; + } $stream->on('error', [$this, 'handleConnectionError']); $stream->on('close', [$this, 'handleConnectionClosed']); } + /** + * busy executing some command such as query or ping + * + * @return bool + * @throws void + */ + public function isBusy() + { + return $this->parser->isBusy() || !$this->executor->isIdle(); + } + /** * {@inheritdoc} */ @@ -71,6 +108,7 @@ public function query($sql, array $params = []) return \React\Promise\reject($e); } + $this->awake(); $deferred = new Deferred(); // store all result set rows until result set end @@ -86,11 +124,13 @@ public function query($sql, array $params = []) $rows = []; + $this->idle(); $deferred->resolve($result); }); // resolve / reject status reply (response without result set) $command->on('error', function ($error) use ($deferred) { + $this->idle(); $deferred->reject($error); }); $command->on('success', function () use ($command, $deferred) { @@ -99,6 +139,7 @@ public function query($sql, array $params = []) $result->insertId = $command->insertId; $result->warningCount = $command->warningCount; + $this->idle(); $deferred->resolve($result); }); @@ -115,20 +156,30 @@ public function queryStream($sql, $params = []) $command = new QueryCommand(); $command->setQuery($query); $this->_doCommand($command); + $this->awake(); + + $stream = new QueryStream($command, $this->stream); + $stream->on('close', function () { + $this->idle(); + }); - return new QueryStream($command, $this->stream); + return $stream; } public function ping() { return new Promise(function ($resolve, $reject) { - $this->_doCommand(new PingCommand()) - ->on('error', function ($reason) use ($reject) { - $reject($reason); - }) - ->on('success', function () use ($resolve) { - $resolve(null); - }); + $command = $this->_doCommand(new PingCommand()); + $this->awake(); + + $command->on('success', function () use ($resolve) { + $this->idle(); + $resolve(null); + }); + $command->on('error', function ($reason) use ($reject) { + $this->idle(); + $reject($reason); + }); }); } @@ -137,6 +188,10 @@ public function quit() return new Promise(function ($resolve, $reject) { $command = $this->_doCommand(new QuitCommand()); $this->state = self::STATE_CLOSING; + + // mark connection as "awake" until it is closed, so never "idle" + $this->awake(); + $command->on('success', function () use ($resolve) { $resolve(null); $this->close(); @@ -158,6 +213,11 @@ public function close() $remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false; $this->stream->close(); + if ($this->idleTimer !== null) { + $this->loop->cancelTimer($this->idleTimer); + $this->idleTimer = null; + } + // reject all pending commands if connection is closed while (!$this->executor->isIdle()) { $command = $this->executor->dequeue(); @@ -223,4 +283,29 @@ protected function _doCommand(CommandInterface $command) return $this->executor->enqueue($command); } + + private function awake() + { + ++$this->pending; + + if ($this->idleTimer !== null) { + $this->loop->cancelTimer($this->idleTimer); + $this->idleTimer = null; + } + } + + private function idle() + { + --$this->pending; + + if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->state === self::STATE_AUTHENTICATED) { + $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { + // soft-close connection and emit close event afterwards both on success or on error + $this->idleTimer = null; + $this->quit()->then(null, function () { + // ignore to avoid reporting unhandled rejection + }); + }); + } + } } diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 60bf3b2..0300415 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -210,11 +210,12 @@ public function createConnection( $connecting->cancel(); }); - $connecting->then(function (SocketConnectionInterface $stream) use ($authCommand, $deferred, $uri) { + $idlePeriod = isset($args['idle']) ? (float) $args['idle'] : null; + $connecting->then(function (SocketConnectionInterface $stream) use ($authCommand, $deferred, $uri, $idlePeriod) { $executor = new Executor(); $parser = new Parser($stream, $executor); - $connection = new Connection($stream, $executor); + $connection = new Connection($stream, $executor, $parser, $this->loop, $idlePeriod); $command = $executor->enqueue($authCommand); $parser->start(); diff --git a/src/Io/Parser.php b/src/Io/Parser.php index f65ca5e..c3006e9 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -115,6 +115,17 @@ public function __construct(DuplexStreamInterface $stream, Executor $executor) }); } + /** + * busy executing some command such as query or ping + * + * @return bool + * @throws void + */ + public function isBusy() + { + return $this->currCommand !== null; + } + public function start() { $this->stream->on('data', [$this, 'handleData']); diff --git a/src/MysqlClient.php b/src/MysqlClient.php index 01ac492..a7d8aa8 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -3,10 +3,10 @@ namespace React\Mysql; use Evenement\EventEmitter; -use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Mysql\Io\Connection; use React\Mysql\Io\Factory; +use React\Promise\Deferred; use React\Promise\Promise; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -51,19 +51,29 @@ class MysqlClient extends EventEmitter { private $factory; private $uri; - private $connecting; private $closed = false; - private $busy = false; + + /** @var PromiseInterface|null */ + private $connecting; + + /** @var ?Connection */ + private $connection; /** - * @var Connection|null + * array of outstanding connection requests to send next commands once a connection becomes ready + * + * @var array> */ - private $disconnecting; + private $pending = []; - private $loop; - private $idlePeriod = 0.001; - private $idleTimer; - private $pending = 0; + /** + * set to true only between calling `quit()` and the connection closing in response + * + * @var bool + * @see self::quit() + * @see self::$closed + */ + private $quitting = false; public function __construct( #[\SensitiveParameter] @@ -71,81 +81,8 @@ public function __construct( ConnectorInterface $connector = null, LoopInterface $loop = null ) { - $args = []; - \parse_str((string) \parse_url($uri, \PHP_URL_QUERY), $args); - if (isset($args['idle'])) { - $this->idlePeriod = (float)$args['idle']; - } - $this->factory = new Factory($loop, $connector); $this->uri = $uri; - $this->loop = $loop ?: Loop::get(); - } - - private function connecting() - { - if ($this->connecting !== null) { - return $this->connecting; - } - - // force-close connection if still waiting for previous disconnection - if ($this->disconnecting !== null) { - $this->disconnecting->close(); - $this->disconnecting = null; - } - - $this->connecting = $connecting = $this->factory->createConnection($this->uri); - $this->connecting->then(function (Connection $connection) { - // connection completed => remember only until closed - $connection->on('close', function () { - $this->connecting = null; - - if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); - $this->idleTimer = null; - } - }); - }, function () { - // connection failed => discard connection attempt - $this->connecting = null; - }); - - return $connecting; - } - - private function awake() - { - ++$this->pending; - - if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); - $this->idleTimer = null; - } - } - - private function idle() - { - --$this->pending; - - if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->connecting !== null) { - $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { - $this->connecting->then(function (Connection $connection) { - $this->disconnecting = $connection; - $connection->quit()->then( - function () { - // successfully disconnected => remove reference - $this->disconnecting = null; - }, - function () { - // soft-close failed but will close anyway => remove reference - $this->disconnecting = null; - } - ); - }); - $this->connecting = null; - $this->idleTimer = null; - }); - } } /** @@ -209,22 +146,18 @@ function () { */ public function query($sql, array $params = []) { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } - return $this->connecting()->then(function (Connection $connection) use ($sql, $params) { - $this->awake(); - return $connection->query($sql, $params)->then( - function (MysqlResult $result) { - $this->idle(); - return $result; - }, - function (\Exception $e) { - $this->idle(); - throw $e; - } - ); + return $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { + return $connection->query($sql, $params)->then(function (MysqlResult $result) use ($connection) { + $this->handleConnectionReady($connection); + return $result; + }, function (\Exception $e) use ($connection) { + $this->handleConnectionReady($connection); + throw $e; + }); }); } @@ -289,17 +222,19 @@ function (\Exception $e) { */ public function queryStream($sql, $params = []) { - if ($this->closed) { + if ($this->closed || $this->quitting) { throw new Exception('Connection closed'); } return \React\Promise\Stream\unwrapReadable( - $this->connecting()->then(function (Connection $connection) use ($sql, $params) { + $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { $stream = $connection->queryStream($sql, $params); - $this->awake(); - $stream->on('close', function () { - $this->idle(); + $stream->on('end', function () use ($connection) { + $this->handleConnectionReady($connection); + }); + $stream->on('error', function () use ($connection) { + $this->handleConnectionReady($connection); }); return $stream; @@ -329,21 +264,17 @@ public function queryStream($sql, $params = []) */ public function ping() { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } - return $this->connecting()->then(function (Connection $connection) { - $this->awake(); - return $connection->ping()->then( - function () { - $this->idle(); - }, - function (\Exception $e) { - $this->idle(); - throw $e; - } - ); + return $this->getConnection()->then(function (Connection $connection) { + return $connection->ping()->then(function () use ($connection) { + $this->handleConnectionReady($connection); + }, function (\Exception $e) use ($connection) { + $this->handleConnectionReady($connection); + throw $e; + }); }); } @@ -371,19 +302,19 @@ function (\Exception $e) { */ public function quit() { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } // not already connecting => no need to connect, simply close virtual connection - if ($this->connecting === null) { + if ($this->connection === null && $this->connecting === null) { $this->close(); return \React\Promise\resolve(null); } + $this->quitting = true; return new Promise(function (callable $resolve, callable $reject) { - $this->connecting()->then(function (Connection $connection) use ($resolve, $reject) { - $this->awake(); + $this->getConnection()->then(function (Connection $connection) use ($resolve, $reject) { // soft-close connection and emit close event afterwards both on success or on error $connection->quit()->then( function () use ($resolve){ @@ -426,32 +357,89 @@ public function close() } $this->closed = true; + $this->quitting = false; - // force-close connection if still waiting for previous disconnection - if ($this->disconnecting !== null) { - $this->disconnecting->close(); - $this->disconnecting = null; + // either close active connection or cancel pending connection attempt + // below branches are exclusive, there can only be a single connection + if ($this->connection !== null) { + $this->connection->close(); + $this->connection = null; + } elseif ($this->connecting !== null) { + $this->connecting->cancel(); + $this->connecting = null; } - // either close active connection or cancel pending connection attempt - if ($this->connecting !== null) { + // clear all outstanding commands + foreach ($this->pending as $deferred) { + $deferred->reject(new \RuntimeException('Connection closed')); + } + $this->pending = []; + + $this->emit('close'); + $this->removeAllListeners(); + } + + + /** + * @return PromiseInterface + */ + private function getConnection() + { + $deferred = new Deferred(); + + // force-close connection if still waiting for previous disconnection due to idle timer + if ($this->connection !== null && $this->connection->state === Connection::STATE_CLOSING) { + $this->connection->close(); + $this->connection = null; + } + + // happy path: reuse existing connection unless it is currently busy executing another command + if ($this->connection !== null && !$this->connection->isBusy()) { + $deferred->resolve($this->connection); + return $deferred->promise(); + } + + // queue pending connection request until connection becomes ready + $this->pending[] = $deferred; + + // create new connection if not already connected or connecting + if ($this->connection === null && $this->connecting === null) { + $this->connecting = $this->factory->createConnection($this->uri); $this->connecting->then(function (Connection $connection) { - $connection->close(); - }, function () { - // ignore to avoid reporting unhandled rejection - }); - if ($this->connecting !== null) { - $this->connecting->cancel(); + // connection completed => remember only until closed $this->connecting = null; - } + $this->connection = $connection; + $connection->on('close', function () { + $this->connection = null; + }); + + // handle first command from queue when connection is ready + $this->handleConnectionReady($connection); + }, function (\Exception $e) { + // connection failed => discard connection attempt + $this->connecting = null; + + foreach ($this->pending as $key => $deferred) { + $deferred->reject($e); + unset($this->pending[$key]); + } + }); } - if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); - $this->idleTimer = null; + return $deferred->promise(); + } + + private function handleConnectionReady(Connection $connection) + { + $deferred = \reset($this->pending); + if ($deferred === false) { + // nothing to do if there are no outstanding connection requests + return; } - $this->emit('close'); - $this->removeAllListeners(); + assert($deferred instanceof Deferred); + unset($this->pending[\key($this->pending)]); + + $deferred->resolve($connection); } } diff --git a/tests/Io/ConnectionTest.php b/tests/Io/ConnectionTest.php index f0cb934..5a0a5ff 100644 --- a/tests/Io/ConnectionTest.php +++ b/tests/Io/ConnectionTest.php @@ -7,13 +7,499 @@ class ConnectionTest extends BaseTestCase { + public function testIsBusyReturnsTrueWhenParserIsBusy() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue', 'isIdle'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); + $executor->expects($this->never())->method('isIdle'); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + $parser->expects($this->once())->method('isBusy')->willReturn(true); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $connection->query('SELECT 1'); + + $this->assertTrue($connection->isBusy()); + } + + public function testIsBusyReturnsFalseWhenParserIsNotBusyAndExecutorIsIdle() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->getMock(); + $executor->expects($this->once())->method('isIdle')->willReturn(true); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertFalse($connection->isBusy()); + } + + public function testQueryWillEnqueueOneCommand() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->never())->method('close'); + + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $parser, $loop, null); + $conn->query('SELECT 1'); + } + + public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsSuccess() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->query('SELECT 1'); + + $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + } + + public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsEnd() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->query('SELECT 1'); + + $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('end'); + } + + public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenIdlePeriodIsGivenAndQueryCommandEmitsSuccess() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(1.0, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, 1.0); + + $this->assertNull($currentCommand); + + $promise = $connection->query('SELECT 1'); + + $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + } + + public function testQueryWillReturnResolvedPromiseAndNotStartIdleTimerWhenIdlePeriodIsNegativeAndQueryCommandEmitsSuccess() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, -1); + + $this->assertNull($currentCommand); + + $promise = $connection->query('SELECT 1'); + + $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + } + + public function testQueryWillReturnRejectedPromiseAndStartIdleTimerWhenQueryCommandEmitsError() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->query('SELECT 1'); + + $promise->then(null, $this->expectCallableOnce()); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('error', [new \RuntimeException()]); + } + + public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsSuccess() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->once())->method('close'); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timeout = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timeout) { + $timeout = $cb; + return true; + }))->willReturn($timer); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $connection->on('close', $this->expectCallableOnce()); + + $this->assertNull($currentCommand); + + $connection->query('SELECT 1'); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + + $this->assertNotNull($timeout); + $timeout(); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + } + + public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsError() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->once())->method('close'); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timeout = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timeout) { + $timeout = $cb; + return true; + }))->willReturn($timer); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $connection->on('close', $this->expectCallableOnce()); + + $this->assertNull($currentCommand); + + $connection->query('SELECT 1'); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + + $this->assertNotNull($timeout); + $timeout(); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('error', [new \RuntimeException()]); + } + + public function testQueryTwiceWillEnqueueSecondQueryWithoutStartingIdleTimerWhenFirstQueryCommandEmitsSuccess() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $connection->query('SELECT 1'); + $connection->query('SELECT 2'); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + } + + public function testQueryTwiceAfterIdleTimerWasStartedWillCancelIdleTimerAndEnqueueSecondCommand() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $connection->query('SELECT 1'); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + + $connection->query('SELECT 2'); + } + + public function testQueryStreamWillEnqueueOneCommand() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->never())->method('close'); + + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $parser, $loop, null); + $conn->queryStream('SELECT 1'); + } + + public function testQueryStreamWillReturnStreamThatWillEmitEndEventAndStartIdleTimerWhenQueryCommandEmitsSuccess() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $stream = $connection->queryStream('SELECT 1'); + + $stream->on('end', $this->expectCallableOnce()); + $stream->on('close', $this->expectCallableOnce()); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + } + + public function testQueryStreamWillReturnStreamThatWillEmitErrorEventAndStartIdleTimerWhenQueryCommandEmitsError() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $stream = $connection->queryStream('SELECT 1'); + + $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $stream->on('close', $this->expectCallableOnce()); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('error', [new \RuntimeException()]); + } + + public function testPingWillEnqueueOneCommand() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->never())->method('close'); + + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $parser, $loop, null); + $conn->ping(); + } + + public function testPingWillReturnResolvedPromiseAndStartIdleTimerWhenPingCommandEmitsSuccess() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->ping(); + + $promise->then($this->expectCallableOnce()); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + } + + public function testPingWillReturnRejectedPromiseAndStartIdleTimerWhenPingCommandEmitsError() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->ping(); + + $promise->then(null, $this->expectCallableOnce()); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('error', [new \RuntimeException()]); + } + public function testQuitWillEnqueueOneCommand() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); } @@ -22,12 +508,17 @@ public function testQuitWillResolveBeforeEmittingCloseEventWhenQuitCommandEmitsS $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $pingCommand = null; - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) { return $pingCommand = $command; }); - $connection = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -55,12 +546,17 @@ public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsEr $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $pingCommand = null; - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) { return $pingCommand = $command; }); - $connection = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -83,13 +579,67 @@ public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsEr $this->assertEquals('rejected.closed.', $events); } + public function testCloseWillEmitCloseEvent() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->getMock(); + $executor->expects($this->once())->method('isIdle')->willReturn(true); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $connection->on('close', $this->expectCallableOnce()); + + $connection->close(); + } + + public function testCloseAfterIdleTimerWasStartedWillCancelIdleTimerAndEmitCloseEvent() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $currentCommand = null; + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { + return $currentCommand = $command; + }); + + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connection = new Connection($stream, $executor, $parser, $loop, null); + + $this->assertNull($currentCommand); + + $connection->ping(); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + + $connection->on('close', $this->expectCallableOnce()); + + $connection->close(); + } + public function testQueryAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->query('SELECT 1'); @@ -112,7 +662,11 @@ public function testQueryAfterCloseRejectsImmediately() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); - $conn = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->close(); $promise = $conn->query('SELECT 1'); @@ -135,7 +689,11 @@ public function testQueryStreamAfterQuitThrows() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); try { @@ -152,7 +710,11 @@ public function testPingAfterQuitRejectsImmediately() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->ping(); @@ -175,7 +737,11 @@ public function testQuitAfterQuitRejectsImmediately() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->quit(); @@ -206,7 +772,11 @@ public function testCloseStreamEmitsErrorEvent() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); - $conn = new Connection($stream, $executor); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->on('error', $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), diff --git a/tests/MysqlClientTest.php b/tests/MysqlClientTest.php index 5adf222..2cedb2b 100644 --- a/tests/MysqlClientTest.php +++ b/tests/MysqlClientTest.php @@ -27,340 +27,1170 @@ public function testConstructWithoutConnectorAndLoopAssignsConnectorAndLoopAutom $this->assertInstanceOf('React\Socket\ConnectorInterface', $connector); - $ref = new \ReflectionProperty($mysql, 'loop'); + $ref = new \ReflectionProperty($factory, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($factory); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + + public function testConstructWithConnectorAndLoopAssignsGivenConnectorAndLoop() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('localhost', $connector, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $factory = $ref->getValue($mysql); + + $ref = new \ReflectionProperty($factory, 'connector'); + $ref->setAccessible(true); + + $this->assertSame($connector, $ref->getValue($factory)); + + $ref = new \ReflectionProperty($factory, 'loop'); + $ref->setAccessible(true); + + $this->assertSame($loop, $ref->getValue($factory)); + } + + public function testPingWillNotCloseConnectionWhenPendingConnectionFails() + { + $deferred = new Deferred(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); + + $connection->on('error', $this->expectCallableNever()); + $connection->on('close', $this->expectCallableNever()); + + $promise = $connection->ping(); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + + $deferred->reject(new \RuntimeException()); + } + + public function testConnectionCloseEventAfterPingWillNotEmitCloseEvent() + { + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); + + $connection->on('error', $this->expectCallableNever()); + $connection->on('close', $this->expectCallableNever()); + + $connection->ping(); + + assert($base instanceof Connection); + $base->emit('close'); + } + + public function testConnectionErrorEventAfterPingWillNotEmitErrorEvent() + { + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); + + $connection->on('error', $this->expectCallableNever()); + $connection->on('close', $this->expectCallableNever()); + + $connection->ping(); + + assert($base instanceof Connection); + $base->emit('error', [new \RuntimeException()]); + } + + public function testPingAfterConnectionIsInClosingStateDueToIdleTimerWillCloseConnectionBeforeCreatingSecondConnection() + { + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $base->expects($this->never())->method('quit'); + $base->expects($this->once())->method('close'); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\resolve($base), + new Promise(function () { }) + ); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); + + $connection->on('close', $this->expectCallableNever()); + + $connection->ping(); + + // emulate triggering idle timer by setting connection state to closing + $base->state = Connection::STATE_CLOSING; + + $connection->ping(); + } + + public function testQueryWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionIsPending() + { + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise = $mysql->query('SELECT 1'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionResolvesAndQueryOnConnectionIsPending() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise = $mysql->query('SELECT 1'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryWillReturnResolvedPromiseWhenQueryOnConnectionResolves() + { + $result = new MysqlResult(); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise = $mysql->query('SELECT 1'); + + $promise->then($this->expectCallableOnceWith($result)); + } + + public function testQueryWillReturnRejectedPromiseWhenCreateConnectionRejects() + { + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\reject(new \RuntimeException())); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise = $mysql->query('SELECT 1'); + + $promise->then(null, $this->expectCallableOnce()); + } + + public function testQueryWillReturnRejectedPromiseWhenQueryOnConnectionRejectsAfterCreateConnectionResolves() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\reject(new \RuntimeException())); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise = $mysql->query('SELECT 1'); + + $promise->then(null, $this->expectCallableOnce()); + } + + public function testQueryTwiceWillCreateSingleConnectionAndReturnPendingPromiseWhenCreateConnectionIsPending() + { + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->query('SELECT 1'); + + $promise = $mysql->query('SELECT 2'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillCallQueryOnConnectionOnlyOnceWhenQueryIsStillPending() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); + $connection->expects($this->once())->method('isBusy')->willReturn(true); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->query('SELECT 1'); + + $promise = $mysql->query('SELECT 2'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillReuseConnectionForSecondQueryWhenFirstQueryIsAlreadyResolved() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('query')->withConsecutive( + ['SELECT 1'], + ['SELECT 2'] + )->willReturnOnConsecutiveCalls( + \React\Promise\resolve(new MysqlResult()), + new Promise(function () { }) + ); + $connection->expects($this->once())->method('isBusy')->willReturn(false); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->query('SELECT 1'); + + $promise = $mysql->query('SELECT 2'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillCallSecondQueryOnConnectionAfterFirstQueryResolvesWhenBothQueriesAreGivenBeforeCreateConnectionResolves() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('query')->withConsecutive( + ['SELECT 1'], + ['SELECT 2'] + )->willReturnOnConsecutiveCalls( + \React\Promise\resolve(new MysqlResult()), + new Promise(function () { }) + ); + $connection->expects($this->never())->method('isBusy'); + + $deferred = new Deferred(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->query('SELECT 1'); + + $promise = $mysql->query('SELECT 2'); + + $deferred->resolve($connection); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillCreateNewConnectionForSecondQueryWhenFirstConnectionIsClosedAfterFirstQueryIsResolved() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['query', 'isBusy'])->getMock(); + $connection->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve(new MysqlResult())); + $connection->expects($this->never())->method('isBusy'); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\resolve($connection), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->query('SELECT 1'); + + assert($connection instanceof Connection); + $connection->emit('close'); + + $promise = $mysql->query('SELECT 2'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondQueryWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstQueryIsResolved() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['query', 'isBusy', 'close'])->getMock(); + $connection->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve(new MysqlResult())); + $connection->expects($this->once())->method('close'); + $connection->expects($this->never())->method('isBusy'); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\resolve($connection), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $mysql->on('close', $this->expectCallableNever()); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->query('SELECT 1'); + + // emulate triggering idle timer by setting connection state to closing + $connection->state = Connection::STATE_CLOSING; + + $promise = $mysql->query('SELECT 2'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillRejectFirstQueryWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondQuery() + { + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\reject(new \RuntimeException()), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise1 = $mysql->query('SELECT 1'); + + $promise2 = $mysql->query('SELECT 2'); + + $promise1->then(null, $this->expectCallableOnce()); + + $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillRejectBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects() + { + $deferred = new Deferred(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise1 = $mysql->query('SELECT 1'); + $promise2 = $mysql->query('SELECT 2'); + + $deferred->reject(new \RuntimeException()); + + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then(null, $this->expectCallableOnce()); + } + + public function testQueryTriceWillRejectFirstTwoQueriesAndKeepThirdPendingWhenTwoQueriesAreGivenBeforeCreateConnectionRejects() + { + $deferred = new Deferred(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + $deferred->promise(), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise1 = $mysql->query('SELECT 1'); + $promise2 = $mysql->query('SELECT 2'); + + $promise3 = $promise1->then(null, function () use ($mysql) { + return $mysql->query('SELECT 3'); + }); + + $deferred->reject(new \RuntimeException()); + + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then(null, $this->expectCallableOnce()); + $promise3->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryTwiceWillCallSecondQueryOnConnectionAfterFirstQueryRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('query')->withConsecutive( + ['SELECT 1'], + ['SELECT 2'] + )->willReturnOnConsecutiveCalls( + \React\Promise\reject(new \RuntimeException()), + new Promise(function () { }) + ); + $connection->expects($this->never())->method('isBusy'); + + $deferred = new Deferred(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise1 = $mysql->query('SELECT 1'); + + $promise2 = $mysql->query('SELECT 2'); + + $deferred->resolve($connection); + + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testQueryStreamWillCreateNewConnectionAndReturnReadableStreamWhenConnectionIsPending() + { + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $stream = $mysql->queryStream('SELECT 1'); + + $this->assertTrue($stream->isReadable()); + } + + public function testQueryStreamWillCreateNewConnectionAndReturnReadableStreamWhenConnectionResolvesAndQueryStreamOnConnectionReturnsReadableStream() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn(new ThroughStream()); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $stream = $mysql->queryStream('SELECT 1'); + + $this->assertTrue($stream->isReadable()); + } + + public function testQueryStreamTwiceWillCallQueryStreamOnConnectionOnlyOnceWhenQueryStreamIsStillReadable() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn(new ThroughStream()); + $connection->expects($this->once())->method('isBusy')->willReturn(true); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->queryStream('SELECT 1'); + + $stream = $mysql->queryStream('SELECT 2'); + + $this->assertTrue($stream->isReadable()); + } + + public function testQueryStreamTwiceWillReuseConnectionForSecondQueryStreamWhenFirstQueryStreamEnds() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('queryStream')->withConsecutive( + ['SELECT 1'], + ['SELECT 2'] + )->willReturnOnConsecutiveCalls( + $base = new ThroughStream(), + new ThroughStream() + ); + $connection->expects($this->once())->method('isBusy')->willReturn(false); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->queryStream('SELECT 1'); + + $base->end(); + + $stream = $mysql->queryStream('SELECT 2'); + + $this->assertTrue($stream->isReadable()); + } + + public function testQueryStreamTwiceWillReuseConnectionForSecondQueryStreamWhenFirstQueryStreamEmitsError() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('queryStream')->withConsecutive( + ['SELECT 1'], + ['SELECT 2'] + )->willReturnOnConsecutiveCalls( + $base = new ThroughStream(), + new ThroughStream() + ); + $connection->expects($this->once())->method('isBusy')->willReturn(true); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $stream1 = $mysql->queryStream('SELECT 1'); + $stream2 = $mysql->queryStream('SELECT 2'); + + $this->assertTrue($stream1->isReadable()); + $this->assertTrue($stream2->isReadable()); + + $base->emit('error', [new \RuntimeException()]); + + $this->assertFalse($stream1->isReadable()); + $this->assertTrue($stream2->isReadable()); + } + + public function testQueryStreamTwiceWillWaitForFirstQueryStreamToEndBeforeStartingSecondQueryStreamWhenFirstQueryStreamIsExplicitlyClosed() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn(new ThroughStream()); + $connection->expects($this->once())->method('isBusy')->willReturn(true); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $stream1 = $mysql->queryStream('SELECT 1'); + $stream2 = $mysql->queryStream('SELECT 2'); + + $this->assertTrue($stream1->isReadable()); + $this->assertTrue($stream2->isReadable()); + + $stream1->close(); + + $this->assertFalse($stream1->isReadable()); + $this->assertTrue($stream2->isReadable()); + } + + public function testQueryStreamTwiceWillCallSecondQueryStreamOnConnectionAfterFirstQueryStreamIsClosedWhenBothQueriesAreGivenBeforeCreateConnectionResolves() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('queryStream')->withConsecutive( + ['SELECT 1'], + ['SELECT 2'] + )->willReturnOnConsecutiveCalls( + $base = new ThroughStream(), + new ThroughStream() + ); + $connection->expects($this->never())->method('isBusy'); + + $deferred = new Deferred(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->queryStream('SELECT 1'); + + $stream = $mysql->queryStream('SELECT 2'); + + $deferred->resolve($connection); + $base->end(); + + $this->assertTrue($stream->isReadable()); + } + + public function testQueryStreamTwiceWillCreateNewConnectionForSecondQueryStreamWhenFirstConnectionIsClosedAfterFirstQueryStreamIsClosed() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['queryStream', 'isBusy'])->getMock(); + $connection->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($base = new ThroughStream()); + $connection->expects($this->never())->method('isBusy'); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\resolve($connection), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->queryStream('SELECT 1'); + + $base->end(); + assert($connection instanceof Connection); + $connection->emit('close'); + + $stream = $mysql->queryStream('SELECT 2'); + + $this->assertTrue($stream->isReadable()); + } + + public function testQueryStreamTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondQueryStreamWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstQueryStreamIsClosed() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['queryStream', 'isBusy', 'close'])->getMock(); + $connection->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($base = new ThroughStream()); + $connection->expects($this->once())->method('close'); + $connection->expects($this->never())->method('isBusy'); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\resolve($connection), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $mysql->on('close', $this->expectCallableNever()); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->queryStream('SELECT 1'); + + $base->end(); + // emulate triggering idle timer by setting connection state to closing + $connection->state = Connection::STATE_CLOSING; + + $stream = $mysql->queryStream('SELECT 2'); + + $this->assertTrue($stream->isReadable()); + } + + public function testQueryStreamTwiceWillEmitErrorOnFirstQueryStreamWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondQueryStream() + { + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\reject(new \RuntimeException()), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $stream1 = $mysql->queryStream('SELECT 1'); + + $this->assertFalse($stream1->isReadable()); + + $stream2 = $mysql->queryStream('SELECT 2'); + + $this->assertTrue($stream2->isReadable()); + } + + public function testQueryStreamTwiceWillEmitErrorOnBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects() + { + $deferred = new Deferred(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $loop = $ref->getValue($mysql); + $ref->setValue($mysql, $factory); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $stream1 = $mysql->queryStream('SELECT 1'); + $stream2 = $mysql->queryStream('SELECT 2'); - $ref = new \ReflectionProperty($factory, 'loop'); - $ref->setAccessible(true); - $loop = $ref->getValue($factory); + $stream1->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); + $stream1->on('close', $this->expectCallableOnce()); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $stream2->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); + $stream2->on('close', $this->expectCallableOnce()); + + $deferred->reject(new \RuntimeException()); + + $this->assertFalse($stream1->isReadable()); + $this->assertFalse($stream2->isReadable()); } - public function testConstructWithConnectorAndLoopAssignsGivenConnectorAndLoop() + public function testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionIsPending() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $mysql = new MysqlClient('localhost', $connector, $loop); + $mysql = new MysqlClient('', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $factory = $ref->getValue($mysql); + $ref->setValue($mysql, $factory); - $ref = new \ReflectionProperty($factory, 'connector'); - $ref->setAccessible(true); + $promise = $mysql->ping(); - $this->assertSame($connector, $ref->getValue($factory)); + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } - $ref = new \ReflectionProperty($mysql, 'loop'); - $ref->setAccessible(true); + public function testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionResolvesAndPingOnConnectionIsPending() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); - $this->assertSame($loop, $ref->getValue($mysql)); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $ref = new \ReflectionProperty($factory, 'loop'); + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); + $ref->setValue($mysql, $factory); - $this->assertSame($loop, $ref->getValue($factory)); + $promise = $mysql->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testPingWillNotCloseConnectionWhenPendingConnectionFails() + public function testPingWillReturnResolvedPromiseWhenPingOnConnectionResolves() { - $deferred = new Deferred(); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->on('error', $this->expectCallableNever()); - $connection->on('close', $this->expectCallableNever()); + $promise = $mysql->ping(); - $promise = $connection->ping(); + $promise->then($this->expectCallableOnce()); + } - $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + public function testPingWillReturnRejectedPromiseWhenCreateConnectionRejects() + { + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\reject(new \RuntimeException())); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $deferred->reject(new \RuntimeException()); + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $promise = $mysql->ping(); + + $promise->then(null, $this->expectCallableOnce()); } - public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() + public function testPingWillReturnRejectedPromiseWhenPingOnConnectionRejectsAfterCreateConnectionResolves() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\reject(new \RuntimeException())); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); - - $connection->on('error', $this->expectCallableNever()); - $connection->on('close', $this->expectCallableNever()); + $ref->setValue($mysql, $factory); - $connection->ping(); + $promise = $mysql->ping(); - assert($base instanceof Connection); - $base->emit('close'); + $promise->then(null, $this->expectCallableOnce()); } - public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingConnectionCloses() + public function testPingTwiceWillCreateSingleConnectionAndReturnPendingPromiseWhenCreateConnectionIsPending() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); + $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->willReturn($timer); - $loop->expects($this->once())->method('cancelTimer')->with($timer); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->on('close', $this->expectCallableNever()); + $mysql->ping(); - $connection->ping(); + $promise = $mysql->ping(); - assert($base instanceof Connection); - $base->emit('close'); + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testPingWillNotForwardErrorFromUnderlyingConnection() + public function testPingTwiceWillCallPingOnConnectionOnlyOnceWhenPingIsStillPending() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); + $connection->expects($this->once())->method('isBusy')->willReturn(true); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->on('error', $this->expectCallableNever()); - $connection->on('close', $this->expectCallableNever()); + $mysql->ping(); - $connection->ping(); + $promise = $mysql->ping(); - $base->emit('error', [new \RuntimeException()]); + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() + public function testPingTwiceWillReuseConnectionForSecondPingWhenFirstPingIsAlreadyResolved() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve(null)); - $base->expects($this->never())->method('close'); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('ping')->willReturnOnConsecutiveCalls( + \React\Promise\resolve(null), + new Promise(function () { }) + ); + $connection->expects($this->once())->method('isBusy')->willReturn(false); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); - - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->on('close', $this->expectCallableNever()); + $mysql->ping(); - $connection->ping(); + $promise = $mysql->ping(); - $this->assertNotNull($timeout); - $timeout(); + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testPingFollowedByIdleTimerWillNotHaveToCloseUnderlyingConnectionWhenQuitFailsBecauseUnderlyingConnectionEmitsCloseAutomatically() + public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingResolvesWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject(new \RuntimeException())); - $base->expects($this->never())->method('close'); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('ping')->willReturnOnConsecutiveCalls( + \React\Promise\resolve(new MysqlResult()), + new Promise(function () { }) + ); + $connection->expects($this->never())->method('isBusy'); + $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); - - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->on('close', $this->expectCallableNever()); + $mysql->ping(); - $connection->ping(); + $promise = $mysql->ping(); - $this->assertNotNull($timeout); - $timeout(); + $deferred->resolve($connection); - assert($base instanceof Connection); - $base->emit('close'); + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingTwiceWillCreateNewConnectionForSecondPingWhenFirstConnectionIsClosedAfterFirstPingIsResolved() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['ping', 'isBusy'])->getMock(); + $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $connection->expects($this->never())->method('isBusy'); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\resolve($connection), + new Promise(function () { }) + ); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'connecting'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $connecting = $ref->getValue($connection); + $ref->setValue($mysql, $factory); + + $mysql->ping(); - $this->assertNull($connecting); + assert($connection instanceof Connection); + $connection->emit('close'); + + $promise = $mysql->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection() + public function testPingTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondPingWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstPingIsResolved() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $base->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); - $base->expects($this->once())->method('close'); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['ping', 'isBusy', 'close'])->getMock(); + $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $connection->expects($this->once())->method('close'); + $connection->expects($this->never())->method('isBusy'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( - \React\Promise\resolve($base), + \React\Promise\resolve($connection), new Promise(function () { }) ); - - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $mysql->on('close', $this->expectCallableNever()); + + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->on('close', $this->expectCallableNever()); + $mysql->ping(); - $connection->ping(); + // emulate triggering idle timer by setting connection state to closing + $connection->state = Connection::STATE_CLOSING; - $this->assertNotNull($timeout); - $timeout(); + $promise = $mysql->ping(); - $connection->ping(); + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } - - public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnectionIsPending() + public function testPingTwiceWillRejectFirstPingWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondPing() { - $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); - + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + \React\Promise\reject(new \RuntimeException()), + new Promise(function () { }) + ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $ret = $connection->query('SELECT 1'); + $promise1 = $mysql->ping(); - $this->assertTrue($ret instanceof PromiseInterface); - $ret->then($this->expectCallableNever(), $this->expectCallableNever()); + $promise2 = $mysql->ping(); + + $promise1->then(null, $this->expectCallableOnce()); + + $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testQueryWillQueryUnderlyingConnectionWhenResolved() + public function testPingTwiceWillRejectBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); - + $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->query('SELECT 1'); - } + $promise1 = $mysql->ping(); + $promise2 = $mysql->ping(); - public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFromUnderlyingConnectionResolves() - { - $result = new MysqlResult(); + $deferred->reject(new \RuntimeException()); - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then(null, $this->expectCallableOnce()); + } + public function testPingTriceWillRejectFirstTwoQueriesAndKeepThirdPendingWhenTwoQueriesAreGivenBeforeCreateConnectionRejects() + { + $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); - + $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + $deferred->promise(), + new Promise(function () { }) + ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $ret = $connection->query('SELECT 1'); - $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); + $promise1 = $mysql->ping(); + $promise2 = $mysql->ping(); + + $promise3 = $promise1->then(null, function () use ($mysql) { + return $mysql->ping(); + }); + + $deferred->reject(new \RuntimeException()); + + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then(null, $this->expectCallableOnce()); + $promise3->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testQueryWillResolveAndStartTimerWithIntervalFromIdleParameterWhenQueryFromUnderlyingConnectionResolves() + public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { - $result = new MysqlResult(); - - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('ping')->willReturnOnConsecutiveCalls( + \React\Promise\reject(new \RuntimeException()), + new Promise(function () { }) + ); + $connection->expects($this->never())->method('isBusy'); + $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); - + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with(2.5, $this->anything()); - $connection = new MysqlClient('mysql://localhost?idle=2.5', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $ret = $connection->query('SELECT 1'); - $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); + $promise1 = $mysql->ping(); + + $promise2 = $mysql->ping(); + + $deferred->resolve($connection); + + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingConnectionResolvesAndIdleParameterIsNegative() + public function testQueryWillResolveWhenQueryFromUnderlyingConnectionResolves() { $result = new MysqlResult(); @@ -371,9 +1201,8 @@ public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingC $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); - $connection = new MysqlClient('mysql://localhost?idle=-1', null, $loop); + $connection = new MysqlClient('', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); @@ -383,7 +1212,7 @@ public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingC $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); } - public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromUnderlyingConnectionResolvesBecausePingIsStillPending() + public function testPingAfterQueryWillPassPingToConnectionWhenQueryResolves() { $result = new MysqlResult(); $deferred = new Deferred(); @@ -396,7 +1225,6 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -412,31 +1240,7 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); } - public function testQueryAfterPingWillCancelTimerAgainWhenPingFromUnderlyingConnectionResolved() - { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); - - $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); - $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); - - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->willReturn($timer); - $loop->expects($this->once())->method('cancelTimer')->with($timer); - - $connection = new MysqlClient('', null, $loop); - - $ref = new \ReflectionProperty($connection, 'factory'); - $ref->setAccessible(true); - $ref->setValue($connection, $factory); - - $connection->ping(); - $connection->query('SELECT 1'); - } - - public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectionRejects() + public function testQueryWillRejectWhenQueryFromUnderlyingConnectionRejects() { $error = new \RuntimeException(); @@ -447,7 +1251,6 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -459,14 +1262,13 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } - public function testQueryWillRejectWithoutStartingTimerWhenUnderlyingConnectionRejects() + public function testQueryWillRejectWhenUnderlyingConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -499,7 +1301,7 @@ public function testQueryStreamReturnsReadableStreamWhenConnectionIsPending() $this->assertTrue($ret->isReadable()); } - public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutStartingTimerWhenResolved() + public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolved() { $stream = new ThroughStream(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); @@ -509,7 +1311,6 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutSt $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -525,7 +1326,7 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutSt $this->assertTrue($ret->isReadable()); } - public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartTimerWhenResolvedAndClosed() + public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolvedAndClosed() { $stream = new ThroughStream(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); @@ -535,7 +1336,6 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartT $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -653,7 +1453,7 @@ public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingF $connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } - public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnectionResolves() + public function testPingWillResolveWhenPingFromUnderlyingConnectionResolves() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); @@ -662,7 +1462,6 @@ public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnection $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -674,7 +1473,7 @@ public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnection $ret->then($this->expectCallableOnce(), $this->expectCallableNever()); } - public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionRejects() + public function testPingWillRejectWhenPingFromUnderlyingConnectionRejects() { $error = new \RuntimeException(); @@ -685,7 +1484,6 @@ public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionR $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -697,7 +1495,7 @@ public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionR $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } - public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConnectionRejectsBecauseConnectionIsDead() + public function testPingWillRejectWhenPingFromUnderlyingConnectionEmitsCloseEventAndRejects() { $error = new \RuntimeException(); @@ -712,7 +1510,6 @@ public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConn $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -768,7 +1565,7 @@ public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() public function testQuitAfterPingRejectsAndThenEmitsCloseWhenFactoryFailsToCreateUnderlyingConnection() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -871,6 +1668,34 @@ public function testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyingConnectio $deferred->reject(new \RuntimeException()); } + public function testPingAfterQuitWillNotPassPingCommandToConnection() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close', 'isBusy'])->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $connection->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); + $connection->expects($this->never())->method('close'); + $connection->expects($this->once())->method('isBusy')->willReturn(false); + + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $mysql = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->on('close', $this->expectCallableNever()); + + $mysql->ping(); + + $mysql->quit(); + + $mysql->ping()->then(null, $this->expectCallableOnce()); + } + public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -902,7 +1727,7 @@ public function testCloseAfterPingCancelsPendingConnection() $ref->setAccessible(true); $ref->setValue($connection, $factory); - $connection->ping(); + $connection->ping()->then(null, $this->expectCallableOnce()); $connection->close(); } @@ -953,18 +1778,16 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio $connection->close(); } - public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectionResolves() + public function testCloseAfterPingWillCloseUnderlyingConnection() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->willReturn($timer); - $loop->expects($this->once())->method('cancelTimer')->with($timer); $connection = new MysqlClient('', null, $loop); @@ -980,9 +1803,7 @@ public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithou { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $base->expects($this->once())->method('close')->willReturnCallback(function () use ($base) { - $base->emit('close'); - }); + $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); @@ -1021,23 +1842,17 @@ public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuit $connection->close(); } - public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending() + public function testCloseAfterConnectionIsInClosingStateDueToIdleTimerWillCloseUnderlyingConnection() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $base->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); + $base->expects($this->never())->method('quit'); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); $connection = new MysqlClient('', null, $loop); @@ -1047,8 +1862,8 @@ public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionW $connection->ping(); - $this->assertNotNull($timeout); - $timeout(); + // emulate triggering idle timer by setting connection state to closing + $base->state = Connection::STATE_CLOSING; $connection->close(); } @@ -1069,7 +1884,7 @@ public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPendin $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); - $connection->ping(); + $connection->ping()->then(null, $this->expectCallableOnce()); $connection->close(); $connection->close(); }