Skip to content

Commit 912fa99

Browse files
committed
Make HTTP stream stoppable; add phpunit; up PHP version to 8.1
1 parent 2520f41 commit 912fa99

File tree

6 files changed

+212
-13
lines changed

6 files changed

+212
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
# vendor/
33
.idea
44
vendor
5+
.phpunit.result.cache
56
composer.lock

composer.json

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,32 @@
1414
}
1515
],
1616
"require": {
17-
"php": ">=7.4",
17+
"php": ">=8.1",
1818
"ext-json": "*",
1919
"spiral/roadrunner-worker": "^2.2.0",
2020
"psr/http-factory": "^1.0.1",
2121
"psr/http-message": "^1.0.1"
2222
},
23-
"autoload": {
24-
"psr-4": {
25-
"Spiral\\RoadRunner\\Http\\": "src"
26-
}
27-
},
2823
"require-dev": {
2924
"nyholm/psr7": "^1.3",
30-
"phpstan/phpstan": "~0.12",
31-
"phpunit/phpunit": "~8.0",
25+
"phpunit/phpunit": "^9.5",
3226
"jetbrains/phpstorm-attributes": "^1.0",
3327
"vimeo/psalm": "^4.22",
3428
"symfony/var-dumper": "^5.1"
3529
},
36-
"scripts": {
37-
"analyze": "psalm"
30+
"autoload": {
31+
"psr-4": {
32+
"Spiral\\RoadRunner\\Http\\": "src"
33+
}
3834
},
39-
"extra": {
40-
"branch-alias": {
41-
"dev-master": "2.2.x-dev"
35+
"autoload-dev": {
36+
"psr-4": {
37+
"Spiral\\RoadRunner\\Tests\\Http\\": "tests"
4238
}
4339
},
40+
"scripts": {
41+
"analyze": "psalm"
42+
},
4443
"suggest": {
4544
"spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools"
4645
},

phpunit.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
4+
backupGlobals="false"
5+
backupStaticAttributes="false"
6+
colors="true"
7+
verbose="true"
8+
convertErrorsToExceptions="true"
9+
convertNoticesToExceptions="true"
10+
convertWarningsToExceptions="true"
11+
processIsolation="false"
12+
executionOrder="random"
13+
stopOnFailure="false"
14+
stopOnError="false"
15+
stderr="true"
16+
>
17+
<testsuites>
18+
<testsuite name="RR Worker Tests">
19+
<directory>tests</directory>
20+
</testsuite>
21+
</testsuites>
22+
23+
<coverage>
24+
<include>
25+
<directory>src</directory>
26+
</include>
27+
</coverage>
28+
</phpunit>

src/HttpWorker.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Spiral\RoadRunner\Http;
1313

1414
use Generator;
15+
use Spiral\RoadRunner\Message\Command\StreamStop;
1516
use Spiral\RoadRunner\Payload;
1617
use Spiral\RoadRunner\WorkerInterface;
1718
use Stringable;
@@ -114,6 +115,10 @@ public function respondStream(int $status, Generator $body, array $headers = [])
114115
break;
115116
}
116117
$content = (string)$body->current();
118+
if ($this->worker->getPayload(StreamStop::class) !== null) {
119+
$body->throw(new \RuntimeException('Stream has been stopped by the client.'));
120+
return;
121+
}
117122
$this->worker->respond(new Payload($content, $head, false));
118123
$body->next();
119124
$head = null;

tests/Unit/StreamResponseTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\RoadRunner\Tests\Http\Unit;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Spiral\RoadRunner\Http\HttpWorker;
9+
use Spiral\RoadRunner\Payload;
10+
use Spiral\RoadRunner\Tests\Http\Unit\Stub\TestRelay;
11+
use Spiral\RoadRunner\Worker;
12+
13+
class StreamResponseTest extends TestCase
14+
{
15+
private TestRelay $relay;
16+
private Worker $worker;
17+
18+
protected function tearDown(): void
19+
{
20+
unset($this->relay, $this->worker);
21+
parent::tearDown();
22+
}
23+
24+
/**
25+
* Regular case
26+
*/
27+
public function testRegularCase(): void
28+
{
29+
$worker = $this->getWorker();
30+
$this->getRelay()
31+
->addFrame(status: 200, body: 'Hello, World!', headers: ['Content-Type' => 'text/plain'], stream: true);
32+
33+
self::assertTrue($worker->hasPayload());
34+
self::assertInstanceOf(Payload::class, $payload = $worker->waitPayload());
35+
self::assertSame('Hello, World!', $payload->body);
36+
}
37+
38+
/**
39+
* Test stream response with multiple frames
40+
*/
41+
public function testStreamResponseWithMultipleFrames(): void
42+
{
43+
$httpWorker = $this->makeHttpWorker();
44+
45+
$httpWorker->respondStream(200, (function () {
46+
yield 'Hel';
47+
yield 'lo,';
48+
yield ' Wo';
49+
yield 'rld';
50+
yield '!';
51+
})());
52+
53+
self::assertFalse($this->worker->hasPayload());
54+
self::assertSame('Hello, World!', $this->getRelay()->getReceivedBody());
55+
}
56+
57+
public function testStopStreamResponse(): void
58+
{
59+
$httpWorker = $this->makeHttpWorker();
60+
61+
$httpWorker->respondStream(200, (function () {
62+
yield 'Hel';
63+
yield 'lo,';
64+
$this->getRelay()->addStopStreamFrame();
65+
try {
66+
yield ' Wo';
67+
} catch (\Throwable $e) {
68+
return;
69+
}
70+
yield 'rld';
71+
yield '!';
72+
})());
73+
74+
self::assertSame('Hello,', $this->getRelay()->getReceivedBody());
75+
}
76+
77+
private function getRelay(): TestRelay
78+
{
79+
return $this->relay ??= new TestRelay();
80+
}
81+
82+
private function getWorker(): Worker
83+
{
84+
return $this->worker ??= new Worker($this->getRelay(), false);
85+
}
86+
87+
private function makeHttpWorker(): HttpWorker
88+
{
89+
return new HttpWorker($this->getWorker());
90+
}
91+
}

tests/Unit/Stub/TestRelay.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\RoadRunner\Tests\Http\Unit\Stub;
6+
7+
use Spiral\Goridge\Frame;
8+
use Spiral\Goridge\Relay;
9+
10+
final class TestRelay extends Relay
11+
{
12+
/** @var Frame[] */
13+
private array $frames = [];
14+
15+
/** @var Frame[] */
16+
private array $received = [];
17+
18+
public function addFrames(Frame ...$frames): self
19+
{
20+
$this->frames = [...$this->frames, ...\array_values($frames)];
21+
return $this;
22+
}
23+
24+
public function addFrame(
25+
int $status = 200,
26+
string $body = '',
27+
array $headers = [],
28+
bool $stream = false,
29+
bool $stopStream = false,
30+
): self {
31+
$head = (string)\json_encode([
32+
'status' => $status,
33+
'headers' => $headers,
34+
], \JSON_THROW_ON_ERROR);
35+
$frame = new Frame($head .$body, [\strlen($head)]);
36+
$frame->byte10 |= $stream ? Frame::BYTE10_STREAM : 0;
37+
$frame->byte10 |= $stopStream ? Frame::BYTE10_STOP : 0;
38+
return $this->addFrames($frame);
39+
}
40+
41+
public function addStopStreamFrame(): self
42+
{
43+
return $this->addFrame(stopStream: true);
44+
}
45+
46+
public function getReceived(): array
47+
{
48+
return $this->received;
49+
}
50+
51+
public function getReceivedBody(): string
52+
{
53+
return \implode('', \array_map(static fn (Frame $frame)
54+
=> \substr($frame->payload, $frame->options[0]), $this->received));
55+
}
56+
57+
public function waitFrame(): Frame
58+
{
59+
if ($this->frames === []) {
60+
throw new \RuntimeException('There are no frames to return.');
61+
}
62+
63+
return \array_shift($this->frames);
64+
}
65+
66+
public function send(Frame $frame): void
67+
{
68+
$this->received[] = $frame;
69+
}
70+
71+
public function hasFrame(): bool
72+
{
73+
return $this->frames !== [];
74+
}
75+
}

0 commit comments

Comments
 (0)