Skip to content

Commit

Permalink
Merge pull request #66 from clue-labs/coverage
Browse files Browse the repository at this point in the history
Update test suite to ensure 100% code coverage
  • Loading branch information
SimonFrings authored May 12, 2023
2 parents 23dd72c + c141918 commit edcac41
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 13 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
- 5.6
- 5.5
- 5.4
exclude: # ignore flaky results for legacy PHP on Windows
- os: windows-2022
php: 5.4
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
Expand All @@ -34,10 +37,17 @@ jobs:
coverage: xdebug
ini-file: development
- run: composer install
- run: vendor/bin/phpunit --coverage-text
- run: vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml
if: ${{ matrix.php >= 7.3 }}
- run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy
- run: vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml -c phpunit.xml.legacy
if: ${{ matrix.php < 7.3 }}
- name: Check 100% code coverage
if: ${{ matrix.os != 'windows-2022' }}
shell: php {0}
run: |
<?php
$metrics = simplexml_load_file('clover.xml')->project->metrics;
exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1);
- run: cd tests/install-as-dep && composer install && php query.php
- run: cd tests/install-as-dep && php -d phar.readonly=0 vendor/bin/phar-composer build . query.phar && php query.phar
- run: cd tests/install-as-dep && mv query.phar query.ext && php query.ext
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# clue/reactphp-sqlite

[![CI status](https://github.com/clue/reactphp-sqlite/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-sqlite/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-sqlite?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/reactphp-sqlite)

Async SQLite database, lightweight non-blocking process wrapper around file-based database extension (`ext-sqlite3`),
Expand Down Expand Up @@ -470,6 +471,15 @@ 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 (except the platform-specific code that does not execute
on Windows). 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
```

## License

This project is released under the permissive [MIT license](LICENSE).
Expand Down
20 changes: 15 additions & 5 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ public function open($filename, $flags = null)
return \React\Promise\resolve(new BlockingDatabase($filename, $flags));
} catch (\Exception $e) {
return \React\Promise\reject(new \RuntimeException($e->getMessage()) );
} catch (\Error $e) {
return \React\Promise\reject(new \RuntimeException($e->getMessage()));
} catch (\Error $e) { // @codeCoverageIgnore
assert(\PHP_VERSION_ID >= 70000); // @codeCoverageIgnore
return \React\Promise\reject(new \RuntimeException($e->getMessage())); // @codeCoverageIgnore
}
}

Expand Down Expand Up @@ -229,12 +230,15 @@ private function openProcessIo($filename, $flags = null)
$cwd = null;
$worker = \dirname(__DIR__) . '/res/sqlite-worker.php';

// launch worker process directly or inside Phar by mapping relative paths (covered by functional test suite)
// @codeCoverageIgnoreStart
if (\class_exists('Phar', false) && ($phar = \Phar::running(false)) !== '') {
$worker = '-r' . 'Phar::loadPhar(' . var_export($phar, true) . ');require(' . \var_export($worker, true) . ');'; // @codeCoverageIgnore
$worker = '-r' . 'Phar::loadPhar(' . var_export($phar, true) . ');require(' . \var_export($worker, true) . ');';
} else {
$cwd = __DIR__ . '/../res';
$worker = \basename($worker);
}
// @codeCoverageIgnoreEnd
$command = 'exec ' . \escapeshellarg($this->bin) . ' ' . escapeshellarg($worker);

// Try to get list of all open FDs (Linux/Mac and others)
Expand Down Expand Up @@ -297,12 +301,15 @@ private function openSocketIo($filename, $flags = null)
$cwd = null;
$worker = \dirname(__DIR__) . '/res/sqlite-worker.php';

// launch worker process directly or inside Phar by mapping relative paths (covered by functional test suite)
// @codeCoverageIgnoreStart
if (\class_exists('Phar', false) && ($phar = \Phar::running(false)) !== '') {
$worker = '-r' . 'Phar::loadPhar(' . var_export($phar, true) . ');require(' . \var_export($worker, true) . ');'; // @codeCoverageIgnore
$worker = '-r' . 'Phar::loadPhar(' . var_export($phar, true) . ');require(' . \var_export($worker, true) . ');';
} else {
$cwd = __DIR__ . '/../res';
$worker = \basename($worker);
}
// @codeCoverageIgnoreEnd
$command = \escapeshellarg($this->bin) . ' ' . escapeshellarg($worker);

// launch process without default STDIO pipes, but inherit STDERR
Expand All @@ -316,9 +323,12 @@ private function openSocketIo($filename, $flags = null)
// start temporary socket on random address
$server = @stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
if ($server === false) {
// report error if temporary socket server can not be started (unlikely)
// @codeCoverageIgnoreStart
return \React\Promise\reject(
new \RuntimeException('Unable to start temporary socket I/O server: ' . $errstr, $errno)
);
// @codeCoverageIgnoreEnd
}

// pass random server address to child process to connect back to parent process
Expand All @@ -342,7 +352,7 @@ private function openSocketIo($filename, $flags = null)
fclose($server);
$process->terminate();

$deferred->reject(new \RuntimeException('No connection detected'));
$deferred->reject(new \RuntimeException('Opening database socket timed out'));
});

$process->on('exit', function () use ($deferred, $server, $timeout) {
Expand Down
9 changes: 5 additions & 4 deletions src/Io/ProcessIoDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ public function query($sql, array $params = array())
foreach ($params as &$value) {
if (\is_string($value) && \preg_match('/[\x00-\x08\x11\x12\x14-\x1f\x7f]/u', $value) !== 0) {
$value = ['base64' => \base64_encode($value)];
} elseif (\is_float($value) && \PHP_VERSION_ID < 50606) {
} elseif (\is_float($value) && \PHP_VERSION_ID < 50606) { // @codeCoverageIgnoreStart
$value = ['float' => $value];
}
} // @codeCoverageIgnoreEnd
}

return $this->send('query', array($sql, $params))->then(function ($data) {
Expand All @@ -98,9 +98,10 @@ public function query($sql, array $params = array())
foreach ($row as &$value) {
if (isset($value['base64'])) {
$value = \base64_decode($value['base64']);
} elseif (isset($value['float'])) {
} elseif (isset($value['float'])) { // @codeCoverageIgnoreStart
assert(\PHP_VERSION_ID < 50606);
$value = (float)$value['float'];
}
} // @codeCoverageIgnoreEnd
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions tests/FunctionalDatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,56 @@ public function testQuerySelectEmptyResolvesWithEmptyResultSetWithColumnsAndNoRo
$this->assertSame([], $data->rows);
}

public function testCancelOpenWithSocketRejectsPromise()
{
$factory = new Factory();

$ref = new \ReflectionProperty($factory, 'useSocket');
$ref->setAccessible(true);
$ref->setValue($factory, true);

$promise = $factory->open(':memory:');
$promise->cancel();

$exception = null;
$promise->then(null, function ($reason) use (&$exception) {
$exception = $reason;
});

$this->assertInstanceOf('RuntimeException', $exception);
$this->assertEquals('Opening database cancelled', $exception->getMessage());
}

public function testOpenWithSocketWillRejectWhenSocketConnectionTimesOut()
{
$timer = null;
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$loop->expects($this->once())->method('addTimer')->with(5.0, $this->callback(function (callable $callback) use (&$timer) {
$timer = $callback;
return true;
}));
$loop->expects($this->never())->method('cancelTimer');

$factory = new Factory($loop);

$ref = new \ReflectionProperty($factory, 'useSocket');
$ref->setAccessible(true);
$ref->setValue($factory, true);

$promise = $factory->open(':memory:');

$this->assertNotNull($timer);
$timer();

$exception = null;
$promise->then(null, function ($reason) use (&$exception) {
$exception = $reason;
});

$this->assertInstanceOf('RuntimeException', $exception);
$this->assertEquals('Opening database socket timed out', $exception->getMessage());
}

protected function expectCallableNever()
{
$mock = $this->createCallableMock();
Expand Down
4 changes: 2 additions & 2 deletions tests/FunctionalExampleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function testQueryExampleExecutedWithCgiReturnsDefaultValueAfterContentTy
$this->markTestSkipped('Unable to execute "php-cgi"');
}

$output = $this->execExample('php-cgi query.php');
$output = $this->execExample('php-cgi -dopcache.jit=off query.php');

$this->assertStringEndsWith("\n\n" . 'value' . "\n" . '42' . "\n", $output);
}
Expand Down Expand Up @@ -55,7 +55,7 @@ public function testQueryExampleExecutedWithCgiAndOpenBasedirRestrictedRunsDefau
$this->markTestSkipped('Unable to execute "php-cgi" or "php"');
}

$output = $this->execExample('php-cgi -dopen_basedir=' . escapeshellarg(dirname(__DIR__)) . ' query.php');
$output = $this->execExample('php-cgi -dopcache.jit=off -dopen_basedir=' . escapeshellarg(dirname(__DIR__)) . ' query.php');

$this->assertStringEndsWith("\n\n" . 'value' . "\n" . '42' . "\n", $output);
}
Expand Down

0 comments on commit edcac41

Please sign in to comment.