Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update test suite to ensure 100% code coverage #221

Merged
merged 3 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ jobs:
coverage: xdebug
ini-file: development
- run: composer install
- run: vendor/bin/phpunit --coverage-text --stderr
- run: vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml
if: ${{ matrix.php >= 7.3 }}
- run: vendor/bin/phpunit --coverage-text --stderr -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
shell: php {0}
run: |
<?php
$metrics = simplexml_load_file('clover.xml')->project->metrics;
exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1);

PHPStan:
name: PHPStan (PHP ${{ matrix.php }})
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Framework X

[![CI status](https://github.com/clue-access/framework-x/workflows/CI/badge.svg)](https://github.com/clue-access/framework-x/actions)
[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests)

Framework X – the simple and fast micro framework for building reactive web applications that run anywhere.

Expand Down Expand Up @@ -115,7 +116,15 @@ $ composer install
To run the test suite, go to the project root and run:

```bash
$ vendor/bin/phpunit --stderr
$ 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
```

Additionally, you can run some simple acceptance tests to verify the framework
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"autoload-dev": {
"psr-4": {
"FrameworkX\\Tests\\": "tests/"
}
},
"files": [
"tests/FiberStub.php"
]
}
}
3 changes: 0 additions & 3 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,3 @@ parameters:
ignoreErrors:
# ignore generic usage like `PromiseInterface<ResponseInterface>` until fixed upstream
- '/^PHPDoc tag @return contains generic type React\\Promise\\PromiseInterface<Psr\\Http\\Message\\ResponseInterface> but interface React\\Promise\\PromiseInterface is not generic\.$/'
# ignore unknown `Fiber` class (PHP 8.1+)
- '/^Instantiated class Fiber not found\.$/'
- '/^Call to method (start|isTerminated|getReturn)\(\) on an unknown class Fiber\.$/'
3 changes: 2 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
bootstrap="vendor/autoload.php"
cacheResult="false"
colors="true"
convertDeprecationsToExceptions="true">
convertDeprecationsToExceptions="true"
stderr="true">
<testsuites>
<testsuite name="Framework X test suite">
<directory>./tests/</directory>
Expand Down
3 changes: 2 additions & 1 deletion phpunit.xml.legacy
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
colors="true"
stderr="true">
<testsuites>
<testsuite name="Framework X test suite">
<directory>./tests/</directory>
Expand Down
18 changes: 6 additions & 12 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,18 +242,11 @@ private function runLoop(): void

$this->sapi->log('Listening on ' . \str_replace('tcp:', 'http:', (string) $socket->getAddress()));

$http->on('error', function (\Exception $e) {
$orig = $e;
$message = 'Error: ' . $e->getMessage();
while (($e = $e->getPrevious()) !== null) {
$message .= '. Previous: ' . $e->getMessage();
}

$this->sapi->log($message);

\fwrite(STDERR, (string)$orig);
$http->on('error', function (\Exception $e): void {
$this->sapi->log('HTTP error: ' . $e->getMessage());
});

// @codeCoverageIgnoreStart
try {
Loop::addSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 = function () use ($socket) {
if (\PHP_VERSION_ID >= 70200 && \stream_isatty(\STDIN)) {
Expand All @@ -270,9 +263,10 @@ private function runLoop(): void
$socket->close();
Loop::stop();
});
} catch (\BadMethodCallException $e) { // @codeCoverageIgnoreStart
} catch (\BadMethodCallException $e) {
$this->sapi->log('Notice: No signal handler support, installing ext-ev or ext-pcntl recommended for production use.');
} // @codeCoverageIgnoreEnd
}
// @codeCoverageIgnoreEnd

do {
Loop::run();
Expand Down
2 changes: 1 addition & 1 deletion src/Io/FiberHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class FiberHandler
* be turned into a valid error response before returning.
* @throws void
*/
public function __invoke(ServerRequestInterface $request, callable $next): mixed
public function __invoke(ServerRequestInterface $request, callable $next)
{
$deferred = null;
$fiber = new \Fiber(function () use ($request, $next, &$deferred) {
Expand Down
84 changes: 84 additions & 0 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
use React\Promise\Deferred;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use ReflectionMethod;
use ReflectionProperty;
use function React\Async\await;
Expand Down Expand Up @@ -718,6 +720,88 @@ public function testRunWillRestartLoopUntilSocketIsClosed(): void
$app->run();
}

public function testRunWillListenForHttpRequestAndSendBackHttpResponseOverSocket(): void
{
$socket = stream_socket_server('127.0.0.1:0');
assert(is_resource($socket));
$addr = stream_socket_get_name($socket, false);
assert(is_string($addr));
fclose($socket);

$container = new Container([
'X_LISTEN' => $addr
]);

$app = new App($container);

Loop::futureTick(function () use ($addr): void {
$connector = new Connector();
$connector->connect($addr)->then(function (ConnectionInterface $connection): void {
$connection->on('data', function (string $data): void {
$this->assertStringStartsWith("HTTP/1.0 404 Not Found\r\n", $data);
});

// lovely: remove socket server on client connection close to terminate loop
$connection->on('close', function (): void {
$resources = get_resources();
end($resources);
prev($resources);
$socket = prev($resources);
assert(is_resource($socket));

Loop::removeReadStream($socket);
fclose($socket);

Loop::stop();
});

$connection->write("GET /unknown HTTP/1.0\r\nHost: localhost\r\n\r\n");
});
});

$this->expectOutputRegex('/' . preg_quote('Listening on http://' . $addr . PHP_EOL, '/') . '.*/');
$app->run();
}

public function testRunWillReportHttpErrorForInvalidClientRequest(): void
{
$socket = stream_socket_server('127.0.0.1:0');
assert(is_resource($socket));
$addr = stream_socket_get_name($socket, false);
assert(is_string($addr));
fclose($socket);

$container = new Container([
'X_LISTEN' => $addr
]);

$app = new App($container);

Loop::futureTick(function () use ($addr): void {
$connector = new Connector();
$connector->connect($addr)->then(function (ConnectionInterface $connection): void {
$connection->write("not a valid HTTP request\r\n\r\n");

// lovely: remove socket server on client connection close to terminate loop
$connection->on('close', function (): void {
$resources = get_resources();
end($resources);
prev($resources);
$socket = prev($resources);
assert(is_resource($socket));

Loop::removeReadStream($socket);
fclose($socket);

Loop::stop();
});
});
});

$this->expectOutputRegex('/HTTP error: .*' . PHP_EOL . '$/');
$app->run();
}

/**
* @requires function pcntl_signal
* @requires function posix_kill
Expand Down
156 changes: 156 additions & 0 deletions tests/FiberStub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

if (!class_exists(Fiber::class)) {
class Fiber
{
/** @var callable */
private $callback;

/** @var mixed */
private $return;

/** @var bool */
private $started = false;

/** @var bool */
private $terminated = false;

/** @var bool */
private static $halt = false;

/** @var ?Fiber */
private static $suspended = null;

public function __construct(callable $callback)
{
$this->callback = $callback;
}

/**
* @param mixed ...$args
* @return mixed
* @throws \Throwable
*/
public function start(...$args)
{
if ($this->started) {
throw new \FiberError();
}
$this->started = true;

if (self::$halt) {
assert(self::$suspended === null);
self::$suspended = $this;
return null;
}

try {
return $this->return = ($this->callback)(...$args);
} finally {
$this->terminated = true;
}
}

/**
* @param mixed $value
* @return mixed
* @throws \BadMethodCallException
*/
public function resume($value = null)
{
throw new \BadMethodCallException();
}

/**
* @param Throwable $exception
* @return mixed
* @throws \BadMethodCallException
*/
public function throw(Throwable $exception)
{
throw new \BadMethodCallException();
}

/**
* @return mixed
* @throws FiberError
*/
public function getReturn()
{
if (!$this->terminated) {
throw new \FiberError();
}

return $this->return;
}

public function isStarted(): bool
{
return $this->started;
}

public function isSuspended(): bool
{
return false;
}

public function isRunning(): bool
{
return $this->started && !$this->terminated;
}

public function isTerminated(): bool
{
return $this->terminated;
}

/**
* @param mixed $value
* @return mixed
* @throws \Throwable
*/
public static function suspend($value = null)
{
throw new \BadMethodCallException();
}

public static function getCurrent(): ?Fiber
{
return null;
}

/**
* @internal
*/
public static function mockSuspend(): void
{
assert(self::$halt === false);
self::$halt = true;
}

/**
* @internal
* @throws void
*/
public static function mockResume(): void
{
assert(self::$halt === true);
assert(self::$suspended instanceof self);

$fiber = self::$suspended;
assert($fiber->started);
assert(!$fiber->terminated);

self::$halt = false;
self::$suspended = null;

/** @throws void */
$fiber->return = ($fiber->callback)();
$fiber->terminated = true;
}
}

final class FiberError extends Error {

}
}
Loading