diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 835e2ba..fefee30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,13 @@ jobs: - run: composer remove react/mysql --dev --no-interaction # do not install react/mysql example on legacy PHP if: ${{ matrix.php == 5.3 }} - run: composer install - - run: vendor/bin/phpunit --coverage-text ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }} + - run: vendor/bin/phpunit --coverage-text --coverage-clover clover.xml ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }} + - name: Check 100% code coverage + shell: php {0} + run: | + project->metrics; + exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1); PHPUnit-hhvm: name: PHPUnit (HHVM) diff --git a/README.md b/README.md index 3fd813b..c20f626 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # clue/reactphp-ssh-proxy [![CI status](https://github.com/clue/reactphp-ssh-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-ssh-proxy/actions) +[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests) [![installs on Packagist](https://img.shields.io/packagist/dt/clue/reactphp-ssh-proxy?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/reactphp-ssh-proxy) Async SSH proxy connector and forwarder, tunnel any TCP/IP-based protocol through an SSH server, @@ -603,6 +604,14 @@ To run the test suite, go to the project root and run: vendor/bin/phpunit ``` +The test suite is set up to always ensure 100% code coverage across all +supported environments. If you have the Xdebug extension installed, you can also +generate a code coverage report locally like this: + +```bash +XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text +``` + The test suite contains a number of tests that require an actual SSH proxy server. These tests will be skipped unless you configure your SSH login credentials to be able to create some actual test connections. You can assign the `SSH_PROXY` diff --git a/src/SshProcessConnector.php b/src/SshProcessConnector.php index 3b6e691..ac604c3 100644 --- a/src/SshProcessConnector.php +++ b/src/SshProcessConnector.php @@ -85,9 +85,11 @@ public function connect($uri) $process = Io\processWithoutFds($command); $process->start($this->loop); + // @codeCoverageIgnoreStart if ($this->debug) { - echo 'Launched "' . $command . '" with PID ' . $process->getPid() . PHP_EOL; // @codeCoverageIgnore + echo 'Launched "' . $command . '" with PID ' . $process->getPid() . PHP_EOL; } + // @codeCoverageIgnoreEnd $deferred = new Deferred(function () use ($process, $uri) { $process->stdin->close(); @@ -106,9 +108,11 @@ public function connect($uri) // remember last line for error output in case process exits $last = $line; + // @codeCoverageIgnoreStart if ($debug) { - echo \addcslashes($line, "\0..\032") . PHP_EOL; // @codeCoverageIgnore + echo \addcslashes($line, "\0..\032") . PHP_EOL; } + // @codeCoverageIgnoreEnd // match everything related to our forwarding channel // forwarding error will be printed right to stderr @@ -154,9 +158,12 @@ public function connect($uri) }); $process->on('exit', function ($code) use ($deferred, $uri, &$last, $debug) { + // @codeCoverageIgnoreStart if ($debug) { - echo 'Process exit with code ' . $code . PHP_EOL; // @codeCoverageIgnore + echo 'Process exit with code ' . $code . PHP_EOL; } + // @codeCoverageIgnoreEnd + $deferred->reject(new \RuntimeException( 'Connection to ' . $uri . ' failed because SSH client died (' . $last . ')', (int) $code diff --git a/src/SshSocksConnector.php b/src/SshSocksConnector.php index 17a14a5..15b1297 100644 --- a/src/SshSocksConnector.php +++ b/src/SshSocksConnector.php @@ -204,9 +204,11 @@ public function idle() $listen->cancel(); $listen = null; + // @codeCoverageIgnoreStart if ($debug) { - echo 'Stopped listening' . PHP_EOL; // @codeCoverageIgnore + echo 'Stopped listening' . PHP_EOL; } + // @codeCoverageIgnoreEnd }); } } @@ -232,9 +234,11 @@ private function listen() // Could not request local forwarding. $last = $line; + // @codeCoverageIgnoreStart if ($debug) { - echo \addcslashes($line, "\0..\032") . PHP_EOL; // @codeCoverageIgnore + echo \addcslashes($line, "\0..\032") . PHP_EOL; } + // @codeCoverageIgnoreEnd // match everything related to our forwarding channel // forwarding success will only be reported to stderr via debug1 / -v diff --git a/tests/IntegrationSshProcessConnectorTest.php b/tests/IntegrationSshProcessConnectorTest.php index 0ad2061..b415792 100644 --- a/tests/IntegrationSshProcessConnectorTest.php +++ b/tests/IntegrationSshProcessConnectorTest.php @@ -56,6 +56,22 @@ public function testConnectWillRejectWithExceptionWhenProcessOutputsEndsWithoutC Loop::run(); } + public function testConnectWillRejectWithExceptionWhenProcessOutputsEndsAfterChannelMessageWithoutOpenConfirm() + { + $connector = new SshProcessConnector('host'); + + $ref = new \ReflectionProperty($connector, 'cmd'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $ref->setValue($connector, 'echo "debug1: channel 0: new stdio-forward [stdio-forward] (inactive timeout: 0)" >&2; #'); + + $promise = $connector->connect('example.com:80'); + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + + Loop::run(); + } + public function testConnectCancellationWillRejectWithException() { $connector = new SshProcessConnector('host');