diff --git a/composer.json b/composer.json index bc85aabc..cad0aef0 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,8 @@ "react/dns": "^0.4.13", "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", "react/stream": "^1.0 || ^0.7.1", - "react/promise": "^2.1 || ^1.2", - "react/promise-timer": "~1.0" + "react/promise": "^2.6.0 || ^1.2.1", + "react/promise-timer": "^1.4.0" }, "require-dev": { "clue/block-react": "^1.2", diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 9d523792..53d55a34 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -112,7 +112,7 @@ private function waitForStreamOnce($stream) $resolve(new Connection($stream, $loop)); } }); - }, function ($resolve, $reject, $progress) use ($loop, $stream) { + }, function () use ($loop, $stream) { $loop->removeWriteStream($stream); fclose($stream); @@ -123,7 +123,6 @@ private function waitForStreamOnce($stream) } // @codeCoverageIgnoreEnd - $resolve = $reject = $progress = null; throw new RuntimeException('Cancelled while waiting for TCP/IP connection to be established'); }); } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 24dbe378..59dff4f1 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -114,6 +114,163 @@ public function testConnectingFailsIfDnsUsesInvalidResolver() Block\await($connector->connect('google.com:80'), $loop, self::TIMEOUT); } + public function testCancellingPendingConnectionWithoutTimeoutShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + $loop = Factory::create(); + $connector = new Connector($loop, array('timeout' => false)); + + gc_collect_cycles(); + $promise = $connector->connect('8.8.8.8:80'); + $promise->cancel(); + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + + public function testCancellingPendingConnectionShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + $loop = Factory::create(); + $connector = new Connector($loop); + + gc_collect_cycles(); + $promise = $connector->connect('8.8.8.8:80'); + $promise->cancel(); + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + + public function testWaitingForRejectedConnectionShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + $loop = Factory::create(); + $connector = new Connector($loop, array('timeout' => false)); + + gc_collect_cycles(); + + $wait = true; + $promise = $connector->connect('127.0.0.1:1')->then( + null, + function ($e) use (&$wait) { + $wait = false; + throw $e; + } + ); + + // run loop for short period to ensure we detect connection refused error + Block\sleep(0.01, $loop); + if ($wait) { + Block\sleep(0.2, $loop); + if ($wait) { + $this->fail('Connection attempt did not fail'); + } + } + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + + /** + * @requires PHP 7 + */ + public function testWaitingForConnectionTimeoutShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + $loop = Factory::create(); + $connector = new Connector($loop, array('timeout' => 0.001)); + + gc_collect_cycles(); + + $wait = true; + $promise = $connector->connect('google.com:80')->then( + null, + function ($e) use (&$wait) { + $wait = false; + throw $e; + } + ); + + // run loop for short period to ensure we detect connection timeout error + Block\sleep(0.01, $loop); + if ($wait) { + Block\sleep(0.2, $loop); + if ($wait) { + $this->fail('Connection attempt did not fail'); + } + } + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + + public function testWaitingForInvalidDnsConnectionShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + $loop = Factory::create(); + $connector = new Connector($loop, array('timeout' => false)); + + gc_collect_cycles(); + + $wait = true; + $promise = $connector->connect('example.invalid:80')->then( + null, + function ($e) use (&$wait) { + $wait = false; + throw $e; + } + ); + + // run loop for short period to ensure we detect DNS error + Block\sleep(0.01, $loop); + if ($wait) { + Block\sleep(0.2, $loop); + if ($wait) { + $this->fail('Connection attempt did not fail'); + } + } + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + + public function testWaitingForSuccessfullyClosedConnectionShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + $loop = Factory::create(); + $connector = new Connector($loop, array('timeout' => false)); + + gc_collect_cycles(); + $promise = $connector->connect('google.com:80')->then( + function ($conn) { + $conn->close(); + } + ); + Block\await($promise, $loop, self::TIMEOUT); + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + public function testConnectingFailsIfTimeoutIsTooSmall() { if (!function_exists('stream_socket_enable_crypto')) {