From 836ca2d75c6d2eb7a4d97b63397f61623d213d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 28 Nov 2023 19:40:48 +0100 Subject: [PATCH] Refactor to move command queuing logic to `MysqlClient` --- src/Io/Connection.php | 18 +- src/Io/Factory.php | 2 +- src/Io/Parser.php | 11 + src/MysqlClient.php | 150 +++-- tests/Io/ConnectionTest.php | 144 ++++- tests/MysqlClientTest.php | 1043 ++++++++++++++++++++++++++++++++++- 6 files changed, 1269 insertions(+), 99 deletions(-) diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 749f456..74be321 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -40,6 +40,9 @@ class Connection extends EventEmitter */ private $stream; + /** @var Parser */ + private $parser; + /** @var LoopInterface */ private $loop; @@ -57,13 +60,15 @@ class Connection extends EventEmitter * * @param SocketConnectionInterface $stream * @param Executor $executor + * @param Parser $parser * @param LoopInterface $loop * @param ?float $idlePeriod */ - public function __construct(SocketConnectionInterface $stream, Executor $executor, LoopInterface $loop, $idlePeriod) + 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) { @@ -74,6 +79,17 @@ public function __construct(SocketConnectionInterface $stream, Executor $executo $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} */ diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 5233907..0300415 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -215,7 +215,7 @@ public function createConnection( $executor = new Executor(); $parser = new Parser($stream, $executor); - $connection = new Connection($stream, $executor, $this->loop, $idlePeriod); + $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 b5c0a9c..a7d8aa8 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -6,6 +6,7 @@ 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; @@ -58,6 +59,13 @@ class MysqlClient extends EventEmitter /** @var ?Connection */ private $connection; + /** + * array of outstanding connection requests to send next commands once a connection becomes ready + * + * @var array> + */ + private $pending = []; + /** * set to true only between calling `quit()` and the connection closing in response * @@ -77,44 +85,6 @@ public function __construct( $this->uri = $uri; } - /** - * @return PromiseInterface - */ - private function getConnection() - { - // happy path: reuse existing connection unless it is already closing after an idle timeout - if ($this->connection !== null && ($this->quitting || $this->connection->state !== Connection::STATE_CLOSING)) { - return \React\Promise\resolve($this->connection); - } - - if ($this->connecting !== null) { - return $this->connecting; - } - - // force-close connection if still waiting for previous disconnection - if ($this->connection !== null) { - assert($this->connection->state === Connection::STATE_CLOSING); - $this->connection->close(); - } - - // create new connection if not already connected or connecting - $this->connecting = $connecting = $this->factory->createConnection($this->uri); - $this->connecting->then(function (Connection $connection) { - $this->connection = $connection; - $this->connecting = null; - - // connection completed => remember only until closed - $connection->on('close', function () { - $this->connection = null; - }); - }, function () { - // connection failed => discard connection attempt - $this->connecting = null; - }); - - return $connecting; - } - /** * Performs an async query. * @@ -176,12 +146,18 @@ private function getConnection() */ public function query($sql, array $params = []) { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } return $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { - return $connection->query($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; + }); }); } @@ -246,13 +222,22 @@ public function query($sql, array $params = []) */ public function queryStream($sql, $params = []) { - if ($this->closed) { + if ($this->closed || $this->quitting) { throw new Exception('Connection closed'); } return \React\Promise\Stream\unwrapReadable( $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { - return $connection->queryStream($sql, $params); + $stream = $connection->queryStream($sql, $params); + + $stream->on('end', function () use ($connection) { + $this->handleConnectionReady($connection); + }); + $stream->on('error', function () use ($connection) { + $this->handleConnectionReady($connection); + }); + + return $stream; }) ); } @@ -279,12 +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->getConnection()->then(function (Connection $connection) { - return $connection->ping(); + return $connection->ping()->then(function () use ($connection) { + $this->handleConnectionReady($connection); + }, function (\Exception $e) use ($connection) { + $this->handleConnectionReady($connection); + throw $e; + }); }); } @@ -312,7 +302,7 @@ public function ping() */ public function quit() { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } @@ -379,7 +369,77 @@ public function close() $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 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]); + } + }); + } + + 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; + } + + 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 394545f..5a0a5ff 100644 --- a/tests/Io/ConnectionTest.php +++ b/tests/Io/ConnectionTest.php @@ -7,6 +7,42 @@ 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(); @@ -15,10 +51,12 @@ public function testQueryWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->query('SELECT 1'); } @@ -32,12 +70,14 @@ public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryComm 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -59,12 +99,14 @@ public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryComm 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -86,12 +128,14 @@ public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenIdlePerio 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, $loop, 1.0); + $connection = new Connection($stream, $executor, $parser, $loop, 1.0); $this->assertNull($currentCommand); @@ -113,10 +157,12 @@ public function testQueryWillReturnResolvedPromiseAndNotStartIdleTimerWhenIdlePe 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, $loop, -1); + $connection = new Connection($stream, $executor, $parser, $loop, -1); $this->assertNull($currentCommand); @@ -138,12 +184,14 @@ public function testQueryWillReturnRejectedPromiseAndStartIdleTimerWhenQueryComm 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -166,6 +214,8 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC 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(); @@ -174,7 +224,7 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC return true; }))->willReturn($timer); - $connection = new Connection($stream, $executor, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); @@ -203,6 +253,8 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC 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(); @@ -211,7 +263,7 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC return true; }))->willReturn($timer); - $connection = new Connection($stream, $executor, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); @@ -239,10 +291,12 @@ public function testQueryTwiceWillEnqueueSecondQueryWithoutStartingIdleTimerWhen 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -263,12 +317,14 @@ public function testQueryTwiceAfterIdleTimerWasStartedWillCancelIdleTimerAndEnqu 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -288,10 +344,12 @@ public function testQueryStreamWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->queryStream('SELECT 1'); } @@ -305,12 +363,14 @@ public function testQueryStreamWillReturnStreamThatWillEmitEndEventAndStartIdleT 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -333,12 +393,14 @@ public function testQueryStreamWillReturnStreamThatWillEmitErrorEventAndStartIdl 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -359,10 +421,12 @@ public function testPingWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->ping(); } @@ -376,12 +440,14 @@ public function testPingWillReturnResolvedPromiseAndStartIdleTimerWhenPingComman 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -403,12 +469,14 @@ public function testPingWillReturnRejectedPromiseAndStartIdleTimerWhenPingComman 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -426,10 +494,12 @@ public function testQuitWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); } @@ -443,10 +513,12 @@ public function testQuitWillResolveBeforeEmittingCloseEventWhenQuitCommandEmitsS return $pingCommand = $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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -479,10 +551,12 @@ public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsEr return $pingCommand = $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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -512,10 +586,12 @@ public function testCloseWillEmitCloseEvent() $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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); @@ -532,12 +608,14 @@ public function testCloseAfterIdleTimerWasStartedWillCancelIdleTimerAndEmitClose 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -557,9 +635,11 @@ public function testQueryAfterQuitRejectsImmediately() $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(); - $conn = new Connection($stream, $executor, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->query('SELECT 1'); @@ -582,9 +662,11 @@ public function testQueryAfterCloseRejectsImmediately() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $conn = new Connection($stream, $executor, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->close(); $promise = $conn->query('SELECT 1'); @@ -607,9 +689,11 @@ public function testQueryStreamAfterQuitThrows() $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(); - $conn = new Connection($stream, $executor, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); try { @@ -626,9 +710,11 @@ public function testPingAfterQuitRejectsImmediately() $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(); - $conn = new Connection($stream, $executor, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->ping(); @@ -651,9 +737,11 @@ public function testQuitAfterQuitRejectsImmediately() $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(); - $conn = new Connection($stream, $executor, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->quit(); @@ -684,9 +772,11 @@ public function testCloseStreamEmitsErrorEvent() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); + $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $conn = new Connection($stream, $executor, $loop, null); + $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 2576902..2cedb2b 100644 --- a/tests/MysqlClientTest.php +++ b/tests/MysqlClientTest.php @@ -158,42 +158,1036 @@ public function testPingAfterConnectionIsInClosingStateDueToIdleTimerWillCloseCo $connection->ping(); } - public function testQueryReturnsPendingPromiseWhenConnectionIsPending() + 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); + $ref->setValue($mysql, $factory); + + $stream1 = $mysql->queryStream('SELECT 1'); + $stream2 = $mysql->queryStream('SELECT 2'); + + $stream1->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); + $stream1->on('close', $this->expectCallableOnce()); + + $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 testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionIsPending() + { + $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->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionResolvesAndPingOnConnectionIsPending() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('ping')->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->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingWillReturnResolvedPromiseWhenPingOnConnectionResolves() + { + $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(\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->ping(); + + $promise->then($this->expectCallableOnce()); + } + + 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(); + + $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 testPingWillReturnRejectedPromiseWhenPingOnConnectionRejectsAfterCreateConnectionResolves() + { + $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($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->ping(); + + $promise->then(null, $this->expectCallableOnce()); + } + + public function testPingTwiceWillCreateSingleConnectionAndReturnPendingPromiseWhenCreateConnectionIsPending() + { + $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->ping(); + + $promise = $mysql->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingTwiceWillCallPingOnConnectionOnlyOnceWhenPingIsStillPending() + { + $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($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->ping(); + + $promise = $mysql->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingTwiceWillReuseConnectionForSecondPingWhenFirstPingIsAlreadyResolved() + { + $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($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->ping(); + + $promise = $mysql->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingResolvesWhenBothQueriesAreGivenBeforeCreateConnectionResolves() + { + $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($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->ping(); + + $promise = $mysql->ping(); + + $deferred->resolve($connection); + + $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($mysql, 'factory'); + $ref->setAccessible(true); + $ref->setValue($mysql, $factory); + + $mysql->ping(); + + assert($connection instanceof Connection); + $connection->emit('close'); + + $promise = $mysql->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondPingWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstPingIsResolved() + { + $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($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->ping(); + + // emulate triggering idle timer by setting connection state to closing + $connection->state = Connection::STATE_CLOSING; + + $promise = $mysql->ping(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingTwiceWillRejectFirstPingWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondPing() + { + $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->ping(); + + $promise2 = $mysql->ping(); + + $promise1->then(null, $this->expectCallableOnce()); + + $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testPingTwiceWillRejectBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects() { $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->ping(); + $promise2 = $mysql->ping(); + + $deferred->reject(new \RuntimeException()); + + $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->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( + $deferred->promise(), + new Promise(function () { }) + ); $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); - $ret = $connection->query('SELECT 1'); + $promise1 = $mysql->ping(); + $promise2 = $mysql->ping(); - $this->assertTrue($ret instanceof PromiseInterface); - $ret->then($this->expectCallableNever(), $this->expectCallableNever()); + $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 testQueryWillQueryUnderlyingConnectionWhenResolved() + public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); + $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(); - $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(); + + $deferred->resolve($connection); + + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryWillResolveWhenQueryFromUnderlyingConnectionResolves() @@ -571,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(); @@ -674,12 +1668,13 @@ public function testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyingConnectio $deferred->reject(new \RuntimeException()); } - public function testPingAfterQuitWillPassPingCommandToConnectionWhenItIsStillQuitting() + public function testPingAfterQuitWillNotPassPingCommandToConnection() { - $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $connection->expects($this->exactly(2))->method('ping')->willReturn(\React\Promise\resolve(null)); + $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)); @@ -698,7 +1693,7 @@ public function testPingAfterQuitWillPassPingCommandToConnectionWhenItIsStillQui $mysql->quit(); - $mysql->ping(); + $mysql->ping()->then(null, $this->expectCallableOnce()); } public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() @@ -732,7 +1727,7 @@ public function testCloseAfterPingCancelsPendingConnection() $ref->setAccessible(true); $ref->setValue($connection, $factory); - $connection->ping(); + $connection->ping()->then(null, $this->expectCallableOnce()); $connection->close(); } @@ -808,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)); @@ -891,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(); }