Skip to content

Commit cdef98b

Browse files
authored
Merge pull request #221 from clue-labs/coverage
Update test suite to ensure 100% code coverage
2 parents 893e608 + d047054 commit cdef98b

File tree

12 files changed

+306
-42
lines changed

12 files changed

+306
-42
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@ jobs:
2929
coverage: xdebug
3030
ini-file: development
3131
- run: composer install
32-
- run: vendor/bin/phpunit --coverage-text --stderr
32+
- run: vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml
3333
if: ${{ matrix.php >= 7.3 }}
34-
- run: vendor/bin/phpunit --coverage-text --stderr -c phpunit.xml.legacy
34+
- run: vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml -c phpunit.xml.legacy
3535
if: ${{ matrix.php < 7.3 }}
36+
- name: Check 100% code coverage
37+
shell: php {0}
38+
run: |
39+
<?php
40+
$metrics = simplexml_load_file('clover.xml')->project->metrics;
41+
exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1);
3642
3743
PHPStan:
3844
name: PHPStan (PHP ${{ matrix.php }})

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Framework X
22

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

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

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

117118
```bash
118-
$ vendor/bin/phpunit --stderr
119+
$ vendor/bin/phpunit
120+
```
121+
122+
The test suite is set up to always ensure 100% code coverage across all
123+
supported environments. If you have the Xdebug extension installed, you can also
124+
generate a code coverage report locally like this:
125+
126+
```bash
127+
$ XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
119128
```
120129

121130
Additionally, you can run some simple acceptance tests to verify the framework

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
"autoload-dev": {
3131
"psr-4": {
3232
"FrameworkX\\Tests\\": "tests/"
33-
}
33+
},
34+
"files": [
35+
"tests/FiberStub.php"
36+
]
3437
}
3538
}

phpstan.neon.dist

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,3 @@ parameters:
1010
ignoreErrors:
1111
# ignore generic usage like `PromiseInterface<ResponseInterface>` until fixed upstream
1212
- '/^PHPDoc tag @return contains generic type React\\Promise\\PromiseInterface<Psr\\Http\\Message\\ResponseInterface> but interface React\\Promise\\PromiseInterface is not generic\.$/'
13-
# ignore unknown `Fiber` class (PHP 8.1+)
14-
- '/^Instantiated class Fiber not found\.$/'
15-
- '/^Call to method (start|isTerminated|getReturn)\(\) on an unknown class Fiber\.$/'

phpunit.xml.dist

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
bootstrap="vendor/autoload.php"
77
cacheResult="false"
88
colors="true"
9-
convertDeprecationsToExceptions="true">
9+
convertDeprecationsToExceptions="true"
10+
stderr="true">
1011
<testsuites>
1112
<testsuite name="Framework X test suite">
1213
<directory>./tests/</directory>

phpunit.xml.legacy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
55
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
66
bootstrap="vendor/autoload.php"
7-
colors="true">
7+
colors="true"
8+
stderr="true">
89
<testsuites>
910
<testsuite name="Framework X test suite">
1011
<directory>./tests/</directory>

src/App.php

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -242,18 +242,11 @@ private function runLoop(): void
242242

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

245-
$http->on('error', function (\Exception $e) {
246-
$orig = $e;
247-
$message = 'Error: ' . $e->getMessage();
248-
while (($e = $e->getPrevious()) !== null) {
249-
$message .= '. Previous: ' . $e->getMessage();
250-
}
251-
252-
$this->sapi->log($message);
253-
254-
\fwrite(STDERR, (string)$orig);
245+
$http->on('error', function (\Exception $e): void {
246+
$this->sapi->log('HTTP error: ' . $e->getMessage());
255247
});
256248

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

277271
do {
278272
Loop::run();

src/Io/FiberHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class FiberHandler
3535
* be turned into a valid error response before returning.
3636
* @throws void
3737
*/
38-
public function __invoke(ServerRequestInterface $request, callable $next): mixed
38+
public function __invoke(ServerRequestInterface $request, callable $next)
3939
{
4040
$deferred = null;
4141
$fiber = new \Fiber(function () use ($request, $next, &$deferred) {

tests/AppTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
use React\Promise\Deferred;
3131
use React\Promise\Promise;
3232
use React\Promise\PromiseInterface;
33+
use React\Socket\ConnectionInterface;
34+
use React\Socket\Connector;
3335
use ReflectionMethod;
3436
use ReflectionProperty;
3537
use function React\Async\await;
@@ -718,6 +720,88 @@ public function testRunWillRestartLoopUntilSocketIsClosed(): void
718720
$app->run();
719721
}
720722

723+
public function testRunWillListenForHttpRequestAndSendBackHttpResponseOverSocket(): void
724+
{
725+
$socket = stream_socket_server('127.0.0.1:0');
726+
assert(is_resource($socket));
727+
$addr = stream_socket_get_name($socket, false);
728+
assert(is_string($addr));
729+
fclose($socket);
730+
731+
$container = new Container([
732+
'X_LISTEN' => $addr
733+
]);
734+
735+
$app = new App($container);
736+
737+
Loop::futureTick(function () use ($addr): void {
738+
$connector = new Connector();
739+
$connector->connect($addr)->then(function (ConnectionInterface $connection): void {
740+
$connection->on('data', function (string $data): void {
741+
$this->assertStringStartsWith("HTTP/1.0 404 Not Found\r\n", $data);
742+
});
743+
744+
// lovely: remove socket server on client connection close to terminate loop
745+
$connection->on('close', function (): void {
746+
$resources = get_resources();
747+
end($resources);
748+
prev($resources);
749+
$socket = prev($resources);
750+
assert(is_resource($socket));
751+
752+
Loop::removeReadStream($socket);
753+
fclose($socket);
754+
755+
Loop::stop();
756+
});
757+
758+
$connection->write("GET /unknown HTTP/1.0\r\nHost: localhost\r\n\r\n");
759+
});
760+
});
761+
762+
$this->expectOutputRegex('/' . preg_quote('Listening on http://' . $addr . PHP_EOL, '/') . '.*/');
763+
$app->run();
764+
}
765+
766+
public function testRunWillReportHttpErrorForInvalidClientRequest(): void
767+
{
768+
$socket = stream_socket_server('127.0.0.1:0');
769+
assert(is_resource($socket));
770+
$addr = stream_socket_get_name($socket, false);
771+
assert(is_string($addr));
772+
fclose($socket);
773+
774+
$container = new Container([
775+
'X_LISTEN' => $addr
776+
]);
777+
778+
$app = new App($container);
779+
780+
Loop::futureTick(function () use ($addr): void {
781+
$connector = new Connector();
782+
$connector->connect($addr)->then(function (ConnectionInterface $connection): void {
783+
$connection->write("not a valid HTTP request\r\n\r\n");
784+
785+
// lovely: remove socket server on client connection close to terminate loop
786+
$connection->on('close', function (): void {
787+
$resources = get_resources();
788+
end($resources);
789+
prev($resources);
790+
$socket = prev($resources);
791+
assert(is_resource($socket));
792+
793+
Loop::removeReadStream($socket);
794+
fclose($socket);
795+
796+
Loop::stop();
797+
});
798+
});
799+
});
800+
801+
$this->expectOutputRegex('/HTTP error: .*' . PHP_EOL . '$/');
802+
$app->run();
803+
}
804+
721805
/**
722806
* @requires function pcntl_signal
723807
* @requires function posix_kill

tests/FiberStub.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
if (!class_exists(Fiber::class)) {
4+
class Fiber
5+
{
6+
/** @var callable */
7+
private $callback;
8+
9+
/** @var mixed */
10+
private $return;
11+
12+
/** @var bool */
13+
private $started = false;
14+
15+
/** @var bool */
16+
private $terminated = false;
17+
18+
/** @var bool */
19+
private static $halt = false;
20+
21+
/** @var ?Fiber */
22+
private static $suspended = null;
23+
24+
public function __construct(callable $callback)
25+
{
26+
$this->callback = $callback;
27+
}
28+
29+
/**
30+
* @param mixed ...$args
31+
* @return mixed
32+
* @throws \Throwable
33+
*/
34+
public function start(...$args)
35+
{
36+
if ($this->started) {
37+
throw new \FiberError();
38+
}
39+
$this->started = true;
40+
41+
if (self::$halt) {
42+
assert(self::$suspended === null);
43+
self::$suspended = $this;
44+
return null;
45+
}
46+
47+
try {
48+
return $this->return = ($this->callback)(...$args);
49+
} finally {
50+
$this->terminated = true;
51+
}
52+
}
53+
54+
/**
55+
* @param mixed $value
56+
* @return mixed
57+
* @throws \BadMethodCallException
58+
*/
59+
public function resume($value = null)
60+
{
61+
throw new \BadMethodCallException();
62+
}
63+
64+
/**
65+
* @param Throwable $exception
66+
* @return mixed
67+
* @throws \BadMethodCallException
68+
*/
69+
public function throw(Throwable $exception)
70+
{
71+
throw new \BadMethodCallException();
72+
}
73+
74+
/**
75+
* @return mixed
76+
* @throws FiberError
77+
*/
78+
public function getReturn()
79+
{
80+
if (!$this->terminated) {
81+
throw new \FiberError();
82+
}
83+
84+
return $this->return;
85+
}
86+
87+
public function isStarted(): bool
88+
{
89+
return $this->started;
90+
}
91+
92+
public function isSuspended(): bool
93+
{
94+
return false;
95+
}
96+
97+
public function isRunning(): bool
98+
{
99+
return $this->started && !$this->terminated;
100+
}
101+
102+
public function isTerminated(): bool
103+
{
104+
return $this->terminated;
105+
}
106+
107+
/**
108+
* @param mixed $value
109+
* @return mixed
110+
* @throws \Throwable
111+
*/
112+
public static function suspend($value = null)
113+
{
114+
throw new \BadMethodCallException();
115+
}
116+
117+
public static function getCurrent(): ?Fiber
118+
{
119+
return null;
120+
}
121+
122+
/**
123+
* @internal
124+
*/
125+
public static function mockSuspend(): void
126+
{
127+
assert(self::$halt === false);
128+
self::$halt = true;
129+
}
130+
131+
/**
132+
* @internal
133+
* @throws void
134+
*/
135+
public static function mockResume(): void
136+
{
137+
assert(self::$halt === true);
138+
assert(self::$suspended instanceof self);
139+
140+
$fiber = self::$suspended;
141+
assert($fiber->started);
142+
assert(!$fiber->terminated);
143+
144+
self::$halt = false;
145+
self::$suspended = null;
146+
147+
/** @throws void */
148+
$fiber->return = ($fiber->callback)();
149+
$fiber->terminated = true;
150+
}
151+
}
152+
153+
final class FiberError extends Error {
154+
155+
}
156+
}

0 commit comments

Comments
 (0)