From 279ffd155b6fe8a24f75a7d97b324f00d3cc20d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 22 Dec 2017 15:32:26 +0100 Subject: [PATCH 01/51] Fix forward compatibility with upcoming EventLoop releases --- composer.json | 2 +- tests/DuplexResourceStreamIntegrationTest.php | 58 ++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index b5e2f8a..f6faa66 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=5.3.8", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index f7f2860..4c2243c 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -3,18 +3,52 @@ namespace React\Tests\Stream; use React\Stream\DuplexResourceStream; -use React\EventLoop as rel; use React\Stream\ReadableResourceStream; +use React\EventLoop\ExtEventLoop; +use React\EventLoop\ExtLibeventLoop; +use React\EventLoop\ExtLibevLoop; +use React\EventLoop\LoopInterface; +use React\EventLoop\LibEventLoop; +use React\EventLoop\LibEvLoop; +use React\EventLoop\StreamSelectLoop; class DuplexResourceStreamIntegrationTest extends TestCase { public function loopProvider() { return array( - array(function() { return true; }, function() { return new rel\StreamSelectLoop; }), - array(function() { return function_exists('event_base_new'); }, function() { return new rel\LibEventLoop; }), - array(function() { return class_exists('libev\EventLoop'); }, function() { return new rel\LibEvLoop; }), - array(function() { return class_exists('EventBase'); }, function() { return new rel\ExtEventLoop; }) + array( + function() { + return true; + }, + function () { + return new StreamSelectLoop(); + } + ), + array( + function () { + return function_exists('event_base_new'); + }, + function () { + return class_exists('React\EventLoop\ExtLibeventLoop') ? new ExtLibeventLoop() : LibEventLoop(); + } + ), + array( + function () { + return class_exists('libev\EventLoop'); + }, + function () { + return class_exists('React\EventLoop\ExtLibevLoop') ? new ExtLibevLoop() : new LibEvLoop(); + } + ), + array( + function () { + return class_exists('EventBase') && class_exists('React\EventLoop\ExtEventLoop'); + }, + function () { + return new ExtEventLoop(); + } + ) ); } @@ -44,9 +78,9 @@ public function testBufferReadsLargeChunks($condition, $loopFactory) $streamA->write($testString); - $loop->tick(); - $loop->tick(); - $loop->tick(); + $this->loopTick($loop); + $this->loopTick($loop); + $this->loopTick($loop); $streamA->close(); $streamB->close(); @@ -307,4 +341,12 @@ public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFac $loop->run(); } + + private function loopTick(LoopInterface $loop) + { + $loop->addTimer(0, function () use ($loop) { + $loop->stop(); + }); + $loop->run(); + } } From f85ff19f2c96a8a1789bdc23bd6d876f045e1eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Jan 2018 12:16:04 +0100 Subject: [PATCH 02/51] Reduce traffic for functional tests to max 50KB per test --- tests/FunctionalInternetTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/FunctionalInternetTest.php b/tests/FunctionalInternetTest.php index fdbec53..773265a 100644 --- a/tests/FunctionalInternetTest.php +++ b/tests/FunctionalInternetTest.php @@ -2,8 +2,8 @@ namespace React\Tests\Stream; -use React\Stream\DuplexResourceStream; use React\EventLoop\Factory; +use React\Stream\DuplexResourceStream; use React\Stream\WritableResourceStream; /** @@ -35,7 +35,7 @@ public function testUploadKilobytePlain() public function testUploadBiggerBlockPlain() { - $size = 1000 * 30; + $size = 50 * 1000; $stream = stream_socket_client('tcp://httpbin.org:80'); $loop = Factory::create(); @@ -79,7 +79,7 @@ public function testUploadKilobyteSecure() public function testUploadBiggerBlockSecureRequiresSmallerChunkSize() { - $size = 1000 * 30000; + $size = 50 * 1000; $stream = stream_socket_client('tls://httpbin.org:443'); $loop = Factory::create(); From ef87054fd0031929a3468b0d042985ec7205b1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Jan 2018 12:24:53 +0100 Subject: [PATCH 03/51] Apply timeout for integration tests relying on internet connection --- README.md | 8 ++++++++ tests/FunctionalInternetTest.php | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9ad33bf..a0ed336 100644 --- a/README.md +++ b/README.md @@ -1203,6 +1203,14 @@ To run the test suite, go to the project root and run: $ php vendor/bin/phpunit ``` +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +$ php vendor/bin/phpunit --exclude-group internet +``` + ## License MIT, see [LICENSE file](LICENSE). diff --git a/tests/FunctionalInternetTest.php b/tests/FunctionalInternetTest.php index 773265a..4d31e8e 100644 --- a/tests/FunctionalInternetTest.php +++ b/tests/FunctionalInternetTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Stream; use React\EventLoop\Factory; +use React\EventLoop\LoopInterface; use React\Stream\DuplexResourceStream; use React\Stream\WritableResourceStream; @@ -28,7 +29,7 @@ public function testUploadKilobytePlain() $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); - $loop->run(); + $this->awaitStreamClose($stream, $loop); $this->assertNotEquals('', $buffer); } @@ -50,7 +51,7 @@ public function testUploadBiggerBlockPlain() $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); - $loop->run(); + $this->awaitStreamClose($stream, $loop); $this->assertNotEquals('', $buffer); } @@ -72,7 +73,7 @@ public function testUploadKilobyteSecure() $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); - $loop->run(); + $this->awaitStreamClose($stream, $loop); $this->assertNotEquals('', $buffer); } @@ -99,8 +100,23 @@ public function testUploadBiggerBlockSecureRequiresSmallerChunkSize() $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); - $loop->run(); + $this->awaitStreamClose($stream, $loop); $this->assertNotEquals('', $buffer); } + + private function awaitStreamClose(DuplexResourceStream $stream, LoopInterface $loop, $timeout = 10.0) + { + $stream->on('close', function () use ($loop) { + $loop->stop(); + }); + + $that = $this; + $loop->addTimer($timeout, function () use ($loop, $that) { + $loop->stop(); + $that->fail('Timed out while waiting for stream to close'); + }); + + $loop->run(); + } } From d98ba35f34f6fedd8674db963f52a68bd5b1cbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Jan 2018 12:50:38 +0100 Subject: [PATCH 04/51] Fix testing against legacy LibEventLoop --- tests/DuplexResourceStreamIntegrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index 4c2243c..fb5f02a 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -30,7 +30,7 @@ function () { return function_exists('event_base_new'); }, function () { - return class_exists('React\EventLoop\ExtLibeventLoop') ? new ExtLibeventLoop() : LibEventLoop(); + return class_exists('React\EventLoop\ExtLibeventLoop') ? new ExtLibeventLoop() : new LibEventLoop(); } ), array( From 63112d6046d828b0332cf23d9c259aaf7c6033ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Jan 2018 12:41:26 +0100 Subject: [PATCH 05/51] Avoid warnings about risky tests --- tests/DuplexResourceStreamTest.php | 6 +++++- tests/ReadableResourceStreamTest.php | 3 +++ tests/WritableStreamResourceTest.php | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index 0aba089..3212ae8 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -10,17 +10,19 @@ class DuplexResourceStreamTest extends TestCase { /** * @covers React\Stream\DuplexResourceStream::__construct + * @doesNotPerformAssertions */ public function testConstructor() { $stream = fopen('php://temp', 'r+'); $loop = $this->createLoopMock(); - $conn = new DuplexResourceStream($stream, $loop); + new DuplexResourceStream($stream, $loop); } /** * @covers React\Stream\DuplexResourceStream::__construct + * @doesNotPerformAssertions */ public function testConstructorWithExcessiveMode() { @@ -93,6 +95,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( /** * @covers React\Stream\DuplexResourceStream::__construct + * @doesNotPerformAssertions */ public function testConstructorAcceptsBuffer() { @@ -405,6 +408,7 @@ public function testClosingStreamInDataEventShouldNotTriggerError() $loop = $this->createLoopMock(); $conn = new DuplexResourceStream($stream, $loop); + $conn->on('error', $this->expectCallableNever()); $conn->on('data', function ($data) use ($conn) { $conn->close(); }); diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 8a960e1..20da96f 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -9,6 +9,7 @@ class ReadableResourceStreamTest extends TestCase { /** * @covers React\Stream\ReadableResourceStream::__construct + * @doesNotPerformAssertions */ public function testConstructor() { @@ -20,6 +21,7 @@ public function testConstructor() /** * @covers React\Stream\ReadableResourceStream::__construct + * @doesNotPerformAssertions */ public function testConstructorWithExcessiveMode() { @@ -224,6 +226,7 @@ public function testClosingStreamInDataEventShouldNotTriggerError() $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); + $conn->on('error', $this->expectCallableNever()); $conn->on('data', function ($data) use ($conn) { $conn->close(); }); diff --git a/tests/WritableStreamResourceTest.php b/tests/WritableStreamResourceTest.php index fdf69c3..05bce9c 100644 --- a/tests/WritableStreamResourceTest.php +++ b/tests/WritableStreamResourceTest.php @@ -9,18 +9,19 @@ class WritableResourceStreamTest extends TestCase { /** * @covers React\Stream\WritableResourceStream::__construct + * @doesNotPerformAssertions */ public function testConstructor() { $stream = fopen('php://temp', 'r+'); $loop = $this->createLoopMock(); - $buffer = new WritableResourceStream($stream, $loop); - $buffer->on('error', $this->expectCallableNever()); + new WritableResourceStream($stream, $loop); } /** * @covers React\Stream\WritableResourceStream::__construct + * @doesNotPerformAssertions */ public function testConstructorWithExcessiveMode() { From 10100896018fd847a257cd81143b8e1b7be08e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Jan 2018 16:04:38 +0100 Subject: [PATCH 06/51] Prepare v0.7.7 release --- CHANGELOG.md | 7 +++++++ README.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2025b56..f64815d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.7.7 (2018-01-19) + +* Improve test suite by fixing forward compatibility with upcoming EventLoop + releases, avoid risky tests and add test group to skip integration tests + relying on internet connection and apply appropriate test timeouts. + (#128, #131 and #132 by @clue) + ## 0.7.6 (2017-12-21) * Fix: Work around reading from unbuffered pipe stream in legacy PHP < 5.4.28 and PHP < 5.5.12 diff --git a/README.md b/README.md index a0ed336..c362534 100644 --- a/README.md +++ b/README.md @@ -1178,7 +1178,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/stream:^0.7.6 +$ composer require react/stream:^0.7.7 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From fdd0140f42805d65bf9687636503db0b326d2244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 11 Jul 2018 16:38:16 +0200 Subject: [PATCH 07/51] Prepare v1.0.0 release --- CHANGELOG.md | 12 ++++++++++++ README.md | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f64815d..58aebca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.0.0 (2018-07-11) + +* First stable LTS release, now following [SemVer](https://semver.org/). + We'd like to emphasize that this component is production ready and battle-tested. + We plan to support all long-term support (LTS) releases for at least 24 months, + so you have a rock-solid foundation to build on top of. + +> Contains no other changes, so it's actually fully compatible with the v0.7.7 release. + ## 0.7.7 (2018-01-19) * Improve test suite by fixing forward compatibility with upcoming EventLoop @@ -129,6 +138,9 @@ * Feature: Explicitly allow custom events and exclude any semantics (#97 by @clue) +* Strict definition for event callback functions + (#101 by @clue) + * Support legacy PHP 5.3 through PHP 7.1 and HHVM and improve usage documentation (#100 and #102 by @clue) diff --git a/README.md b/README.md index c362534..b5bc907 100644 --- a/README.md +++ b/README.md @@ -1175,10 +1175,11 @@ $loop->run(); The recommended way to install this library is [through Composer](https://getcomposer.org). [New to Composer?](https://getcomposer.org/doc/00-intro.md) +This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/stream:^0.7.7 +$ composer require react/stream:^1.0 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From aea4ab49d506b3f6ff55dd4455e04a91923caf76 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 18 Oct 2018 22:55:48 +0200 Subject: [PATCH 08/51] Prefix all global functions calls with \ to skip the look up and resolve process and go straight to the global function --- src/DuplexResourceStream.php | 26 +++++++++++++------------- src/ReadableResourceStream.php | 26 +++++++++++++------------- src/ThroughStream.php | 4 ++-- src/Util.php | 2 +- src/WritableResourceStream.php | 22 +++++++++++----------- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index 982ebb0..cf9a894 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -37,19 +37,19 @@ final class DuplexResourceStream extends EventEmitter implements DuplexStreamInt public function __construct($stream, LoopInterface $loop, $readChunkSize = null, WritableStreamInterface $buffer = null) { - if (!is_resource($stream) || get_resource_type($stream) !== "stream") { + if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); } // ensure resource is opened for reading and wrting (fopen mode must contain "+") - $meta = stream_get_meta_data($stream); - if (isset($meta['mode']) && $meta['mode'] !== '' && strpos($meta['mode'], '+') === false) { + $meta = \stream_get_meta_data($stream); + if (isset($meta['mode']) && $meta['mode'] !== '' && \strpos($meta['mode'], '+') === false) { throw new InvalidArgumentException('Given stream resource is not opened in read and write mode'); } // this class relies on non-blocking I/O in order to not interrupt the event loop // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 - if (stream_set_blocking($stream, 0) !== true) { + if (\stream_set_blocking($stream, 0) !== true) { throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); } @@ -61,8 +61,8 @@ public function __construct($stream, LoopInterface $loop, $readChunkSize = null, // triggered), so we can ignore platforms not supporting this (HHVM). // Pipe streams (such as STDIN) do not seem to require this and legacy // PHP versions cause SEGFAULTs on unbuffered pipe streams, so skip this. - if (function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { - stream_set_read_buffer($stream, 0); + if (\function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { + \stream_set_read_buffer($stream, 0); } if ($buffer === null) { @@ -140,8 +140,8 @@ public function close() $this->buffer->close(); $this->removeAllListeners(); - if (is_resource($this->stream)) { - fclose($this->stream); + if (\is_resource($this->stream)) { + \fclose($this->stream); } } @@ -169,7 +169,7 @@ public function pipe(WritableStreamInterface $dest, array $options = array()) public function handleData($stream) { $error = null; - set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { $error = new \ErrorException( $errstr, 0, @@ -179,9 +179,9 @@ public function handleData($stream) ); }); - $data = stream_get_contents($stream, $this->bufferSize); + $data = \stream_get_contents($stream, $this->bufferSize); - restore_error_handler(); + \restore_error_handler(); if ($error !== null) { $this->emit('error', array(new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error))); @@ -212,8 +212,8 @@ public function handleData($stream) */ private function isLegacyPipe($resource) { - if (PHP_VERSION_ID < 50428 || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50512)) { - $meta = stream_get_meta_data($resource); + if (\PHP_VERSION_ID < 50428 || (\PHP_VERSION_ID >= 50500 && \PHP_VERSION_ID < 50512)) { + $meta = \stream_get_meta_data($resource); if (isset($meta['stream_type']) && $meta['stream_type'] === 'STDIO') { return true; diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 015a96b..231739b 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -40,19 +40,19 @@ final class ReadableResourceStream extends EventEmitter implements ReadableStrea public function __construct($stream, LoopInterface $loop, $readChunkSize = null) { - if (!is_resource($stream) || get_resource_type($stream) !== "stream") { + if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); } // ensure resource is opened for reading (fopen mode must contain "r" or "+") - $meta = stream_get_meta_data($stream); - if (isset($meta['mode']) && $meta['mode'] !== '' && strpos($meta['mode'], 'r') === strpos($meta['mode'], '+')) { + $meta = \stream_get_meta_data($stream); + if (isset($meta['mode']) && $meta['mode'] !== '' && \strpos($meta['mode'], 'r') === \strpos($meta['mode'], '+')) { throw new InvalidArgumentException('Given stream resource is not opened in read mode'); } // this class relies on non-blocking I/O in order to not interrupt the event loop // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 - if (stream_set_blocking($stream, 0) !== true) { + if (\stream_set_blocking($stream, 0) !== true) { throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); } @@ -64,8 +64,8 @@ public function __construct($stream, LoopInterface $loop, $readChunkSize = null) // triggered), so we can ignore platforms not supporting this (HHVM). // Pipe streams (such as STDIN) do not seem to require this and legacy // PHP versions cause SEGFAULTs on unbuffered pipe streams, so skip this. - if (function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { - stream_set_read_buffer($stream, 0); + if (\function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { + \stream_set_read_buffer($stream, 0); } $this->stream = $stream; @@ -113,8 +113,8 @@ public function close() $this->pause(); $this->removeAllListeners(); - if (is_resource($this->stream)) { - fclose($this->stream); + if (\is_resource($this->stream)) { + \fclose($this->stream); } } @@ -122,7 +122,7 @@ public function close() public function handleData() { $error = null; - set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { $error = new \ErrorException( $errstr, 0, @@ -132,9 +132,9 @@ public function handleData() ); }); - $data = stream_get_contents($this->stream, $this->bufferSize); + $data = \stream_get_contents($this->stream, $this->bufferSize); - restore_error_handler(); + \restore_error_handler(); if ($error !== null) { $this->emit('error', array(new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error))); @@ -165,8 +165,8 @@ public function handleData() */ private function isLegacyPipe($resource) { - if (PHP_VERSION_ID < 50428 || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50512)) { - $meta = stream_get_meta_data($resource); + if (\PHP_VERSION_ID < 50428 || (\PHP_VERSION_ID >= 50500 && \PHP_VERSION_ID < 50512)) { + $meta = \stream_get_meta_data($resource); if (isset($meta['stream_type']) && $meta['stream_type'] === 'STDIO') { return true; diff --git a/src/ThroughStream.php b/src/ThroughStream.php index da2fbb0..6f73fb8 100644 --- a/src/ThroughStream.php +++ b/src/ThroughStream.php @@ -84,7 +84,7 @@ final class ThroughStream extends EventEmitter implements DuplexStreamInterface public function __construct($callback = null) { - if ($callback !== null && !is_callable($callback)) { + if ($callback !== null && !\is_callable($callback)) { throw new InvalidArgumentException('Invalid transformation callback given'); } @@ -128,7 +128,7 @@ public function write($data) if ($this->callback !== null) { try { - $data = call_user_func($this->callback, $data); + $data = \call_user_func($this->callback, $data); } catch (\Exception $e) { $this->emit('error', array($e)); $this->close(); diff --git a/src/Util.php b/src/Util.php index 14ddcfc..056b037 100644 --- a/src/Util.php +++ b/src/Util.php @@ -68,7 +68,7 @@ public static function forwardEvents($source, $target, array $events) { foreach ($events as $event) { $source->on($event, function () use ($event, $target) { - $target->emit($event, func_get_args()); + $target->emit($event, \func_get_args()); }); } } diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 7e04205..57c09b2 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -19,19 +19,19 @@ final class WritableResourceStream extends EventEmitter implements WritableStrea public function __construct($stream, LoopInterface $loop, $writeBufferSoftLimit = null, $writeChunkSize = null) { - if (!is_resource($stream) || get_resource_type($stream) !== "stream") { + if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new \InvalidArgumentException('First parameter must be a valid stream resource'); } // ensure resource is opened for writing (fopen mode must contain either of "waxc+") - $meta = stream_get_meta_data($stream); - if (isset($meta['mode']) && $meta['mode'] !== '' && strtr($meta['mode'], 'waxc+', '.....') === $meta['mode']) { + $meta = \stream_get_meta_data($stream); + if (isset($meta['mode']) && $meta['mode'] !== '' && \strtr($meta['mode'], 'waxc+', '.....') === $meta['mode']) { throw new \InvalidArgumentException('Given stream resource is not opened in write mode'); } // this class relies on non-blocking I/O in order to not interrupt the event loop // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 - if (stream_set_blocking($stream, 0) !== true) { + if (\stream_set_blocking($stream, 0) !== true) { throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); } @@ -96,8 +96,8 @@ public function close() $this->emit('close'); $this->removeAllListeners(); - if (is_resource($this->stream)) { - fclose($this->stream); + if (\is_resource($this->stream)) { + \fclose($this->stream); } } @@ -105,7 +105,7 @@ public function close() public function handleWrite() { $error = null; - set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { $error = array( 'message' => $errstr, 'number' => $errno, @@ -115,12 +115,12 @@ public function handleWrite() }); if ($this->writeChunkSize === -1) { - $sent = fwrite($this->stream, $this->data); + $sent = \fwrite($this->stream, $this->data); } else { - $sent = fwrite($this->stream, $this->data, $this->writeChunkSize); + $sent = \fwrite($this->stream, $this->data, $this->writeChunkSize); } - restore_error_handler(); + \restore_error_handler(); // Only report errors if *nothing* could be sent. // Any hard (permanent) error will fail to send any data at all. @@ -147,7 +147,7 @@ public function handleWrite() } $exceeded = isset($this->data[$this->softLimit - 1]); - $this->data = (string) substr($this->data, $sent); + $this->data = (string) \substr($this->data, $sent); // buffer has been above limit and is now below limit if ($exceeded && !isset($this->data[$this->softLimit - 1])) { From 4302b6bc738a29206ed2a557f58cd399e4ce43f0 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 26 Oct 2018 17:28:52 +0200 Subject: [PATCH 09/51] Test against PHP 7.3 on travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f4e3376..9ff354b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ php: # - 7.0 # Mac OS X test setup, ignore errors, see below - 7.1 - 7.2 + - 7.3 - nightly # ignore errors, see below - hhvm # ignore errors, see below From 5b9d87dfdd217fe38dba12065d5d840b1aecc45e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 24 Nov 2018 18:19:31 +0100 Subject: [PATCH 10/51] Only close stream when the we reached end of the stream --- src/DuplexResourceStream.php | 2 +- src/ReadableResourceStream.php | 2 +- tests/DuplexResourceStreamIntegrationTest.php | 38 +++++++++++++++++++ tests/ReadableResourceStreamTest.php | 19 ++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index cf9a894..5f038c6 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -191,7 +191,7 @@ public function handleData($stream) if ($data !== '') { $this->emit('data', array($data)); - } else{ + } elseif (\feof($this->stream)) { // no data read => we reached the end and close the stream $this->emit('end'); $this->close(); diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 231739b..461f6e4 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -144,7 +144,7 @@ public function handleData() if ($data !== '') { $this->emit('data', array($data)); - } else{ + } elseif (\feof($this->stream)) { // no data read => we reached the end and close the stream $this->emit('end'); $this->close(); diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index fb5f02a..7135e15 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Stream; +use Clue\StreamFilter as Filter; use React\Stream\DuplexResourceStream; use React\Stream\ReadableResourceStream; use React\EventLoop\ExtEventLoop; @@ -342,6 +343,43 @@ public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFac $loop->run(); } + /** + * @covers React\Stream\ReadableResourceStream::handleData + * @dataProvider loopProvider + */ + public function testEmptyReadShouldntFcloseStream($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $server = stream_socket_server('tcp://127.0.0.1:0'); + + $client = stream_socket_client(stream_socket_get_name($server, false)); + $stream = stream_socket_accept($server); + + + // add a filter which returns an error when encountering an 'a' when reading + Filter\append($stream, function ($chunk) { + return ''; + }, STREAM_FILTER_READ); + + $loop = $loopFactory(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->on('error', $this->expectCallableNever()); + $conn->on('data', $this->expectCallableNever()); + $conn->on('end', $this->expectCallableNever()); + + fwrite($client, "foobar\n"); + + $conn->handleData($stream); + + fclose($stream); + fclose($client); + fclose($server); + } + private function loopTick(LoopInterface $loop) { $loop->addTimer(0, function () use ($loop) { diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 20da96f..7566f92 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -365,6 +365,25 @@ public function testDataErrorShouldEmitErrorAndClose() $conn->handleData($stream); } + /** + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testEmptyReadShouldntFcloseStream() + { + list($stream, $_) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + $loop = $this->createLoopMock(); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('error', $this->expectCallableNever()); + $conn->on('data', $this->expectCallableNever()); + $conn->on('end', $this->expectCallableNever()); + + $conn->handleData(); + + fclose($stream); + fclose($_); + } + private function createLoopMock() { return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); From 50426855f7a77ddf43b9266c22320df5bf6c6ce6 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 1 Jan 2019 17:15:09 +0100 Subject: [PATCH 11/51] Prepare v1.1.0 release --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58aebca..c5a7dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.1.0 (2018-01-01) + +* Improvement: Increase performance by optimizing global function and constant look ups + (#137 by @WyriHaximus) +* Travis: Test against PHP 7.3 + (#138 by @WyriHaximus) +* Fix: Ignore empty reads + (#139 by @WyriHaximus) + ## 1.0.0 (2018-07-11) * First stable LTS release, now following [SemVer](https://semver.org/). From 07140bafc6a5a0c29bb07cf2b6985f6f80024028 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 14 May 2019 13:51:34 +0200 Subject: [PATCH 12/51] fix phpstan error output --- src/CompositeStream.php | 3 ++- src/DuplexResourceStream.php | 2 +- src/ReadableResourceStream.php | 2 +- src/WritableResourceStream.php | 10 +++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/CompositeStream.php b/src/CompositeStream.php index 153f2a3..dde091d 100644 --- a/src/CompositeStream.php +++ b/src/CompositeStream.php @@ -16,7 +16,8 @@ public function __construct(ReadableStreamInterface $readable, WritableStreamInt $this->writable = $writable; if (!$readable->isReadable() || !$writable->isWritable()) { - return $this->close(); + $this->close(); + return; } Util::forwardEvents($this->readable, $this, array('data', 'end', 'error')); diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index 5f038c6..c8c1c50 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -49,7 +49,7 @@ public function __construct($stream, LoopInterface $loop, $readChunkSize = null, // this class relies on non-blocking I/O in order to not interrupt the event loop // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 - if (\stream_set_blocking($stream, 0) !== true) { + if (\stream_set_blocking($stream, false) !== true) { throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); } diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 461f6e4..9d9c006 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -52,7 +52,7 @@ public function __construct($stream, LoopInterface $loop, $readChunkSize = null) // this class relies on non-blocking I/O in order to not interrupt the event loop // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 - if (\stream_set_blocking($stream, 0) !== true) { + if (\stream_set_blocking($stream, false) !== true) { throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); } diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 57c09b2..ff726cc 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -9,7 +9,15 @@ final class WritableResourceStream extends EventEmitter implements WritableStrea { private $stream; private $loop; + + /** + * @var int + */ private $softLimit; + + /** + * @var int + */ private $writeChunkSize; private $listening = false; @@ -31,7 +39,7 @@ public function __construct($stream, LoopInterface $loop, $writeBufferSoftLimit // this class relies on non-blocking I/O in order to not interrupt the event loop // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 - if (\stream_set_blocking($stream, 0) !== true) { + if (\stream_set_blocking($stream, false) !== true) { throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); } From 575e8872be76a7545729eb4b9b3aad1b0699a4c2 Mon Sep 17 00:00:00 2001 From: Reedy Date: Fri, 20 Dec 2019 15:14:13 +0000 Subject: [PATCH 13/51] Add .gitattributes to exclude dev files from exports --- .gitattributes | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f2f51dd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/examples export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore From 860446f36333203aa0f97b59776cf558c6dba7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 9 Jan 2020 13:28:59 +0100 Subject: [PATCH 14/51] Run tests on PHP 7.4 and simplify test matrix --- .travis.yml | 28 ++++++++++++---------------- composer.json | 2 +- phpunit.xml.dist | 1 - 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9ff354b..e840ca7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,5 @@ language: php -php: -# - 5.3 # requires old distro, see below - - 5.4 - - 5.5 - - 5.6 - - 7.0 -# - 7.0 # Mac OS X test setup, ignore errors, see below - - 7.1 - - 7.2 - - 7.3 - - nightly # ignore errors, see below - - hhvm # ignore errors, see below - # lock distro so new future defaults will not break the build dist: trusty @@ -20,15 +7,24 @@ matrix: include: - php: 5.3 dist: precise - include: + - php: 5.4 + - php: 5.5 + - php: 5.6 + - php: 7.0 + - php: 7.1 + - php: 7.2 + - php: 7.3 + - php: 7.4 + - php: hhvm-3.18 + install: + - composer require phpunit/phpunit:^5 --dev --no-interaction # requires legacy phpunit - os: osx language: generic php: 7.0 # just to look right on travis env: - PACKAGE: php70 allow_failures: - - php: nightly - - php: hhvm + - php: hhvm-3.18 - os: osx install: diff --git a/composer.json b/composer.json index f6faa66..4bf0c4f 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", "clue/stream-filter": "~1.2" }, "autoload": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 13d3fab..04d426b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="vendor/autoload.php" > From 63853824e54712f240333e127d04c1b6f30c2013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Apr 2020 12:24:42 +0200 Subject: [PATCH 15/51] Reproduce faulty write buffer behavior on Mac OS X --- tests/FunctionalInternetTest.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/FunctionalInternetTest.php b/tests/FunctionalInternetTest.php index 4d31e8e..f27c5f9 100644 --- a/tests/FunctionalInternetTest.php +++ b/tests/FunctionalInternetTest.php @@ -78,11 +78,20 @@ public function testUploadKilobyteSecure() $this->assertNotEquals('', $buffer); } - public function testUploadBiggerBlockSecureRequiresSmallerChunkSize() + public function testUploadBiggerBlockSecure() { - $size = 50 * 1000; + // A few dozen kilobytes should be enough to verify this works. + // Underlying buffer sizes are platform-specific, so let's increase this + // a bit to trigger different behavior on Linux vs Mac OS X. + $size = 136 * 1000; + $stream = stream_socket_client('tls://httpbin.org:443'); + // PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big + // chunks of data over TLS streams at once. + // We work around this by limiting the write chunk size to 8192 bytes + // here to also support older PHP versions. + // See https://github.com/reactphp/socket/issues/105 $loop = Factory::create(); $stream = new DuplexResourceStream( $stream, From 3eb342d87ca89e0c4c7c428505f36051c172f677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Apr 2020 12:38:29 +0200 Subject: [PATCH 16/51] Only report write error when write fails with error and nothing written --- src/WritableResourceStream.php | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index ff726cc..952f39c 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -113,13 +113,8 @@ public function close() public function handleWrite() { $error = null; - \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { - $error = array( - 'message' => $errstr, - 'number' => $errno, - 'file' => $errfile, - 'line' => $errline - ); + \set_error_handler(function ($_, $errstr) use (&$error) { + $error = $errstr; }); if ($this->writeChunkSize === -1) { @@ -130,25 +125,16 @@ public function handleWrite() \restore_error_handler(); - // Only report errors if *nothing* could be sent. + // Only report errors if *nothing* could be sent and an error has been raised. + // Ignore non-fatal warnings if *some* data could be sent. // Any hard (permanent) error will fail to send any data at all. // Sending excessive amounts of data will only flush *some* data and then // report a temporary error (EAGAIN) which we do not raise here in order // to keep the stream open for further tries to write. // Should this turn out to be a permanent error later, it will eventually // send *nothing* and we can detect this. - if ($sent === 0 || $sent === false) { - if ($error !== null) { - $error = new \ErrorException( - $error['message'], - 0, - $error['number'], - $error['file'], - $error['line'] - ); - } - - $this->emit('error', array(new \RuntimeException('Unable to write to stream: ' . ($error !== null ? $error->getMessage() : 'Unknown error'), 0, $error))); + if (($sent === 0 || $sent === false) && $error !== null) { + $this->emit('error', array(new \RuntimeException('Unable to write to stream: ' . $error))); $this->close(); return; From 26d564035da98f314ca21bd3f3bb83a8e3a0705b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Apr 2020 12:49:26 +0200 Subject: [PATCH 17/51] Simplify Travis CI test matrix for Mac OS X setup --- .travis.yml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index e840ca7..61f500c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,28 +18,17 @@ matrix: - php: hhvm-3.18 install: - composer require phpunit/phpunit:^5 --dev --no-interaction # requires legacy phpunit - - os: osx + - name: Mac OS X + os: osx language: generic - php: 7.0 # just to look right on travis - env: - - PACKAGE: php70 + before_install: + - curl -s http://getcomposer.org/installer | php + - mv composer.phar /usr/local/bin/composer allow_failures: - php: hhvm-3.18 - os: osx install: - # OSX install inspired by https://github.com/kiler129/TravisCI-OSX-PHP - - | - if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then - brew tap homebrew/homebrew-php - echo "Installing PHP ..." - brew install "${PACKAGE}" - brew install "${PACKAGE}"-xdebug - brew link "${PACKAGE}" - echo "Installing composer ..." - curl -s http://getcomposer.org/installer | php - mv composer.phar /usr/local/bin/composer - fi - composer install --no-interaction script: From 7c02b510ee3f582c810aeccd3a197b9c2f52ff1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 4 May 2020 12:17:57 +0200 Subject: [PATCH 18/51] Prepare v1.1.1 release --- CHANGELOG.md | 22 ++++++++++++++++++---- README.md | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a7dd3..ac00244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,26 @@ # Changelog -## 1.1.0 (2018-01-01) +## 1.1.1 (2020-05-04) -* Improvement: Increase performance by optimizing global function and constant look ups +* Fix: Fix faulty write buffer behavior when sending large data chunks over TLS (Mac OS X only). + (#150 by @clue) + +* Minor code style improvements to fix phpstan analysis warnings and + add `.gitattributes` to exclude dev files from exports. + (#140 by @flow-control and #144 by @reedy) + +* Improve test suite to run tests on PHP 7.4 and simplify test matrix. + (#147 by @clue) + +## 1.1.0 (2019-01-01) + +* Improvement: Increase performance by optimizing global function and constant look ups. (#137 by @WyriHaximus) -* Travis: Test against PHP 7.3 + +* Travis: Test against PHP 7.3. (#138 by @WyriHaximus) -* Fix: Ignore empty reads + +* Fix: Ignore empty reads. (#139 by @WyriHaximus) ## 1.0.0 (2018-07-11) diff --git a/README.md b/README.md index b5bc907..c79bacf 100644 --- a/README.md +++ b/README.md @@ -1179,7 +1179,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/stream:^1.0 +$ composer require react/stream:^1.1.1 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From d0db280b111afc40e5878886157fbe72ed379a6d Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 17 Jul 2020 14:07:53 +0200 Subject: [PATCH 19/51] Run tests on PHPUnit 9 --- composer.json | 2 +- tests/CallableStub.php | 10 ------ tests/DuplexResourceStreamTest.php | 8 ++--- tests/ReadableResourceStreamTest.php | 8 ++--- tests/TestCase.php | 47 +++++++++++++++++++++++++++- tests/ThroughStreamTest.php | 2 +- tests/WritableStreamResourceTest.php | 12 +++---- 7 files changed, 62 insertions(+), 27 deletions(-) delete mode 100644 tests/CallableStub.php diff --git a/composer.json b/composer.json index 4bf0c4f..c2aa433 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35", "clue/stream-filter": "~1.2" }, "autoload": { diff --git a/tests/CallableStub.php b/tests/CallableStub.php deleted file mode 100644 index 31cc834..0000000 --- a/tests/CallableStub.php +++ /dev/null @@ -1,10 +0,0 @@ -createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new DuplexResourceStream('breakme', $loop); } /** * @covers React\Stream\DuplexResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsExceptionOnWriteOnlyStream() { @@ -59,12 +58,12 @@ public function testConstructorThrowsExceptionOnWriteOnlyStream() $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new DuplexResourceStream(STDOUT, $loop); } /** * @covers React\Stream\DuplexResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode() { @@ -74,12 +73,12 @@ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode unlink($name); $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new DuplexResourceStream($stream, $loop); } /** * @covers React\Stream\DuplexResourceStream::__construct - * @expectedException RunTimeException */ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() { @@ -90,6 +89,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( $stream = fopen('blocking://test', 'r+'); $loop = $this->createLoopMock(); + $this->setExpectedException('RunTimeException'); new DuplexResourceStream($stream, $loop); } diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 7566f92..30503a6 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -37,18 +37,17 @@ public function testConstructorWithExcessiveMode() /** * @covers React\Stream\ReadableResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsExceptionOnInvalidStream() { $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new ReadableResourceStream(false, $loop); } /** * @covers React\Stream\ReadableResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsExceptionOnWriteOnlyStream() { @@ -58,12 +57,12 @@ public function testConstructorThrowsExceptionOnWriteOnlyStream() $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new ReadableResourceStream(STDOUT, $loop); } /** * @covers React\Stream\ReadableResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode() { @@ -73,12 +72,12 @@ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode unlink($name); $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new ReadableResourceStream($stream, $loop); } /** * @covers React\Stream\ReadableResourceStream::__construct - * @expectedException RuntimeException */ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() { @@ -89,6 +88,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( $stream = fopen('blocking://test', 'r+'); $loop = $this->createLoopMock(); + $this->setExpectedException('RuntimeException'); new ReadableResourceStream($stream, $loop); } diff --git a/tests/TestCase.php b/tests/TestCase.php index c8fc1db..2b0928f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -49,6 +49,51 @@ protected function expectCallableNever() protected function createCallableMock() { - return $this->getMockBuilder('React\Tests\Stream\CallableStub')->getMock(); + if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + // PHPUnit 9+ + return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + } else { + // legacy PHPUnit 4 - PHPUnit 9 + return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + } + } + + public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) + { + if (method_exists($this, 'expectException')) { + // PHPUnit 5+ + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); + } + } else { + // legacy PHPUnit 4 + parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + } + } + + public function assertContainsString($needle, $haystack) + { + if (method_exists($this, 'assertStringContainsString')) { + // PHPUnit 7.5+ + $this->assertStringContainsString($needle, $haystack); + } else { + // legacy PHPUnit 4 - PHPUnit 7.5 + $this->assertContains($needle, $haystack); + } + } + + public function assertContainsStringIgnoringCase($needle, $haystack) + { + if (method_exists($this, 'assertStringContainsStringIgnoringCase')) { + // PHPUnit 7.5+ + $this->assertStringContainsStringIgnoringCase($needle, $haystack); + } else { + // legacy PHPUnit 4 - PHPUnit 7.5 + $this->assertContains($needle, $haystack, '', true); + } } } diff --git a/tests/ThroughStreamTest.php b/tests/ThroughStreamTest.php index a98badf..444f3b1 100644 --- a/tests/ThroughStreamTest.php +++ b/tests/ThroughStreamTest.php @@ -11,10 +11,10 @@ class ThroughStreamTest extends TestCase { /** * @test - * @expectedException InvalidArgumentException */ public function itShouldRejectInvalidCallback() { + $this->setExpectedException('InvalidArgumentException'); new ThroughStream(123); } diff --git a/tests/WritableStreamResourceTest.php b/tests/WritableStreamResourceTest.php index 05bce9c..196a576 100644 --- a/tests/WritableStreamResourceTest.php +++ b/tests/WritableStreamResourceTest.php @@ -37,31 +37,30 @@ public function testConstructorWithExcessiveMode() /** * @covers React\Stream\WritableResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsIfNotAValidStreamResource() { $stream = null; $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new WritableResourceStream($stream, $loop); } /** * @covers React\Stream\WritableResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsExceptionOnReadOnlyStream() { $stream = fopen('php://temp', 'r'); $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new WritableResourceStream($stream, $loop); } /** * @covers React\Stream\WritableResourceStream::__construct - * @expectedException InvalidArgumentException */ public function testConstructorThrowsExceptionOnReadOnlyStreamWithExcessiveMode() { @@ -71,12 +70,12 @@ public function testConstructorThrowsExceptionOnReadOnlyStreamWithExcessiveMode( unlink($name); $loop = $this->createLoopMock(); + $this->setExpectedException('InvalidArgumentException'); new WritableResourceStream($stream, $loop); } /** * @covers React\Stream\WritableResourceStream::__construct - * @expectedException RuntimeException */ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() { @@ -87,6 +86,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( $stream = fopen('blocking://test', 'r+'); $loop = $this->createLoopMock(); + $this->setExpectedException('RuntimeException'); new WritableResourceStream($stream, $loop); } @@ -480,8 +480,8 @@ public function testErrorWhenStreamResourceIsInvalid() $this->assertInstanceOf('Exception', $error); // the error messages differ between PHP versions, let's just check substrings - $this->assertContains('Unable to write to stream: ', $error->getMessage()); - $this->assertContains(' not a valid stream resource', $error->getMessage(), '', true); + $this->assertContainsString('Unable to write to stream: ', $error->getMessage()); + $this->assertContainsStringIgnoringCase(' Not a valid stream resource', $error->getMessage()); } public function testWritingToClosedStream() From 56eafea92fad3ced6fa64ab8aeba83d423bdf80f Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 17 Jul 2020 14:09:48 +0200 Subject: [PATCH 20/51] Clean up test suite --- .travis.yml | 2 +- phpunit.xml.dist | 11 +---------- tests/WritableStreamResourceTest.php | 4 ++++ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 61f500c..cc82aaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: php # lock distro so new future defaults will not break the build dist: trusty -matrix: +jobs: include: - php: 5.3 dist: precise diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 04d426b..0e947b8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,6 @@ - + ./tests/ diff --git a/tests/WritableStreamResourceTest.php b/tests/WritableStreamResourceTest.php index 196a576..3192abd 100644 --- a/tests/WritableStreamResourceTest.php +++ b/tests/WritableStreamResourceTest.php @@ -325,6 +325,10 @@ public function testEndWithoutDataDoesNotCloseIfWritableResourceStreamIsFull() */ public function testEndWithDataClosesImmediatelyIfWritableResourceStreamFlushes() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $stream = fopen('php://temp', 'r+'); $filterBuffer = ''; $loop = $this->createLoopMock(); From 6fb67145a66e3858a7a29945ea6841c8e2416961 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 24 Aug 2020 19:36:12 +0200 Subject: [PATCH 21/51] Add full core team to the license Added the full core team in order of joining the team --- LICENSE | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index a808108..d6f8901 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -Copyright (c) 2012 Igor Wiedler, Chris Boden +The MIT License (MIT) + +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 77f4adee43e17751c2fcb7a783422147ebe32a70 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 24 Aug 2020 19:46:26 +0200 Subject: [PATCH 22/51] Add full core team to composer authors list --- composer.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/composer.json b/composer.json index c2aa433..d376084 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,28 @@ "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", "keywords": ["event-driven", "readable", "writable", "stream", "non-blocking", "io", "pipe", "ReactPHP"], "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], "require": { "php": ">=5.3.8", "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", From 0e984d7f1c5326778b65aa697487570ea9b0ee28 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 23 Sep 2020 11:41:33 +0200 Subject: [PATCH 23/51] Update PHPUnit configuration schema for PHPUnit 9.3 and minor clean up --- .gitattributes | 1 + .travis.yml | 5 +++-- composer.json | 2 +- phpunit.xml.dist | 16 ++++++++++------ phpunit.xml.legacy | 18 ++++++++++++++++++ tests/TestCase.php | 14 ++------------ tests/UtilTest.php | 5 ----- ...Test.php => WritableResourceStreamTest.php} | 0 8 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 phpunit.xml.legacy rename tests/{WritableStreamResourceTest.php => WritableResourceStreamTest.php} (100%) diff --git a/.gitattributes b/.gitattributes index f2f51dd..64ab6e0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,5 @@ /.travis.yml export-ignore /examples export-ignore /phpunit.xml.dist export-ignore +/phpunit.xml.legacy export-ignore /tests export-ignore diff --git a/.travis.yml b/.travis.yml index cc82aaf..a2498b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,8 +29,9 @@ jobs: - os: osx install: - - composer install --no-interaction + - composer install script: - - vendor/bin/phpunit --coverage-text + - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi + - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi - time php examples/91-benchmark-throughput.php diff --git a/composer.json b/composer.json index d376084..aada0be 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "clue/stream-filter": "~1.2" }, "autoload": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0e947b8..fa88e7e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,19 @@ - + + ./tests/ - - - + + ./src/ - - + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy new file mode 100644 index 0000000..fbb43e8 --- /dev/null +++ b/phpunit.xml.legacy @@ -0,0 +1,18 @@ + + + + + + + ./tests/ + + + + + ./src/ + + + diff --git a/tests/TestCase.php b/tests/TestCase.php index 2b0928f..7bef2f5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,16 +6,6 @@ class TestCase extends BaseTestCase { - protected function expectCallableExactly($amount) - { - $mock = $this->createCallableMock(); - $mock - ->expects($this->exactly($amount)) - ->method('__invoke'); - - return $mock; - } - protected function expectCallableOnce() { $mock = $this->createCallableMock(); @@ -61,7 +51,7 @@ protected function createCallableMock() public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) { if (method_exists($this, 'expectException')) { - // PHPUnit 5+ + // PHPUnit 5.2+ $this->expectException($exception); if ($exceptionMessage !== '') { $this->expectExceptionMessage($exceptionMessage); @@ -70,7 +60,7 @@ public function setExpectedException($exception, $exceptionMessage = '', $except $this->expectExceptionCode($exceptionCode); } } else { - // legacy PHPUnit 4 + // legacy PHPUnit 4 - PHPUnit 5.1 parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); } } diff --git a/tests/UtilTest.php b/tests/UtilTest.php index 3d113ab..f12baad 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -265,9 +265,4 @@ private function createLoopMock() { return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); } - - private function notEqualTo($value) - { - return new \PHPUnit_Framework_Constraint_Not($value); - } } diff --git a/tests/WritableStreamResourceTest.php b/tests/WritableResourceStreamTest.php similarity index 100% rename from tests/WritableStreamResourceTest.php rename to tests/WritableResourceStreamTest.php From cf710f5e8775ff865fe2bcbc8000795bc33eb022 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 24 Nov 2020 15:16:58 +0100 Subject: [PATCH 24/51] Remove test because case is already covered --- tests/WritableResourceStreamTest.php | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index 3192abd..7b36418 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -461,33 +461,6 @@ public function testWritingToClosedWritableResourceStreamShouldNotWriteToStream( $this->assertSame('', $filterBuffer); } - /** - * @covers React\Stream\WritableResourceStream::handleWrite - */ - public function testErrorWhenStreamResourceIsInvalid() - { - $stream = fopen('php://temp', 'r+'); - $loop = $this->createWriteableLoopMock(); - - $error = null; - - $buffer = new WritableResourceStream($stream, $loop); - $buffer->on('error', function ($message) use (&$error) { - $error = $message; - }); - - // invalidate stream resource - fclose($stream); - - $buffer->write('Attempting to write to bad stream'); - - $this->assertInstanceOf('Exception', $error); - - // the error messages differ between PHP versions, let's just check substrings - $this->assertContainsString('Unable to write to stream: ', $error->getMessage()); - $this->assertContainsStringIgnoringCase(' Not a valid stream resource', $error->getMessage()); - } - public function testWritingToClosedStream() { if ('Darwin' === PHP_OS) { From 2f5e3c9be55584cc2e4a0b620b973f963d6a1767 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 9 Feb 2021 12:04:45 +0100 Subject: [PATCH 25/51] Use GitHub actions for continuous integration (CI) Bye bye Travis CI, you've served us well. --- .gitattributes | 6 ++-- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++++++ .gitignore | 4 +-- .travis.yml | 37 ------------------- README.md | 2 +- tests/FunctionalInternetTest.php | 4 +-- 6 files changed, 69 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.gitattributes b/.gitattributes index 64ab6e0..fc0be87 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,7 @@ /.gitattributes export-ignore +/.github/ export-ignore /.gitignore export-ignore -/.travis.yml export-ignore -/examples export-ignore +/examples/ export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore -/tests export-ignore +/tests/ export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..366d72a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + pull_request: + +jobs: + PHPUnit: + name: PHPUnit (PHP ${{ matrix.php }}) + runs-on: ubuntu-20.04 + strategy: + matrix: + php: + - 7.4 + - 7.3 + - 7.2 + - 7.1 + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + - run: composer install + - run: vendor/bin/phpunit --coverage-text + if: ${{ matrix.php >= 7.3 }} + - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy + if: ${{ matrix.php < 7.3 }} + - run: time php examples/91-benchmark-throughput.php + + PHPUnit-macOS: + name: PHPUnit (macOS) + runs-on: macos-10.15 + continue-on-error: true + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + coverage: xdebug + - run: composer install + - run: vendor/bin/phpunit --coverage-text + - run: time php examples/91-benchmark-throughput.php + + PHPUnit-hhvm: + name: PHPUnit (HHVM) + runs-on: ubuntu-18.04 + continue-on-error: true + steps: + - uses: actions/checkout@v2 + - uses: azjezz/setup-hhvm@v1 + with: + version: lts-3.30 + - run: hhvm $(which composer) install + - run: hhvm vendor/bin/phpunit + - run: time php examples/91-benchmark-throughput.php diff --git a/.gitignore b/.gitignore index 987e2a2..c8153b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -composer.lock -vendor +/composer.lock +/vendor/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a2498b1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: php - -# lock distro so new future defaults will not break the build -dist: trusty - -jobs: - include: - - php: 5.3 - dist: precise - - php: 5.4 - - php: 5.5 - - php: 5.6 - - php: 7.0 - - php: 7.1 - - php: 7.2 - - php: 7.3 - - php: 7.4 - - php: hhvm-3.18 - install: - - composer require phpunit/phpunit:^5 --dev --no-interaction # requires legacy phpunit - - name: Mac OS X - os: osx - language: generic - before_install: - - curl -s http://getcomposer.org/installer | php - - mv composer.phar /usr/local/bin/composer - allow_failures: - - php: hhvm-3.18 - - os: osx - -install: - - composer install - -script: - - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi - - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi - - time php examples/91-benchmark-throughput.php diff --git a/README.md b/README.md index c79bacf..2ec6219 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stream -[![Build Status](https://travis-ci.org/reactphp/stream.svg?branch=master)](https://travis-ci.org/reactphp/stream) +[![CI status](https://github.com/reactphp/stream/workflows/CI/badge.svg)](https://github.com/reactphp/stream/actions) Event-driven readable and writable streams for non-blocking I/O in [ReactPHP](https://reactphp.org/). diff --git a/tests/FunctionalInternetTest.php b/tests/FunctionalInternetTest.php index f27c5f9..4f07537 100644 --- a/tests/FunctionalInternetTest.php +++ b/tests/FunctionalInternetTest.php @@ -59,7 +59,7 @@ public function testUploadBiggerBlockPlain() public function testUploadKilobyteSecure() { $size = 1000; - $stream = stream_socket_client('tls://httpbin.org:443'); + $stream = stream_socket_client('ssl://httpbin.org:443'); $loop = Factory::create(); $stream = new DuplexResourceStream($stream, $loop); @@ -85,7 +85,7 @@ public function testUploadBiggerBlockSecure() // a bit to trigger different behavior on Linux vs Mac OS X. $size = 136 * 1000; - $stream = stream_socket_client('tls://httpbin.org:443'); + $stream = stream_socket_client('ssl://httpbin.org:443'); // PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big // chunks of data over TLS streams at once. From fb4cf1167f54f55541e41b94b12a46133f837d6f Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 9 Feb 2021 13:57:51 +0100 Subject: [PATCH 26/51] Support PHP 8 --- .github/workflows/ci.yml | 3 ++- README.md | 2 +- tests/TestCase.php | 11 +++++++++++ tests/WritableResourceStreamTest.php | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 366d72a..dc9cf4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.0 - 7.4 - 7.3 - 7.2 @@ -41,7 +42,7 @@ jobs: - uses: actions/checkout@v2 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.0 coverage: xdebug - run: composer install - run: vendor/bin/phpunit --coverage-text diff --git a/README.md b/README.md index 2ec6219..a0f3ebb 100644 --- a/README.md +++ b/README.md @@ -1185,7 +1185,7 @@ $ composer require react/stream:^1.1.1 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.3 through current PHP 7+ and HHVM. +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. It's *highly recommended to use PHP 7+* for this project due to its vast performance improvements. diff --git a/tests/TestCase.php b/tests/TestCase.php index 7bef2f5..af07e3e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -86,4 +86,15 @@ public function assertContainsStringIgnoringCase($needle, $haystack) $this->assertContains($needle, $haystack, '', true); } } + + public function assertSameIgnoringCase($expected, $actual) + { + if (method_exists($this, 'assertEqualsIgnoringCase')) { + // PHPUnit 7.5+ + $this->assertEqualsIgnoringCase($expected, $actual); + } else { + // legacy PHPUnit 4 - PHPUnit 7.5 + $this->assertSame($expected, $actual); + } + } } diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index 7b36418..f0a3b42 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -485,7 +485,7 @@ public function testWritingToClosedStream() $buffer->handleWrite(); $this->assertInstanceOf('Exception', $error); - $this->assertSame('Unable to write to stream: fwrite(): send of 3 bytes failed with errno=32 Broken pipe', $error->getMessage()); + $this->assertSameIgnoringCase('Unable to write to stream: fwrite(): send of 3 bytes failed with errno=32 Broken pipe', $error->getMessage()); } private function createWriteableLoopMock() From b7a85ad981e7c6d1e21bad8dc4b0f4fe4a682178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 5 Jul 2021 10:36:41 +0200 Subject: [PATCH 27/51] Simplify usage by supporting new default loop --- README.md | 73 ++++++++++++++++++---------- composer.json | 2 +- examples/01-http.php | 6 +-- examples/02-https.php | 6 +-- examples/11-cat.php | 9 +--- examples/91-benchmark-throughput.php | 18 +++---- src/DuplexResourceStream.php | 7 ++- src/ReadableResourceStream.php | 6 ++- src/ReadableStreamInterface.php | 2 +- src/WritableResourceStream.php | 7 ++- src/WritableStreamInterface.php | 2 +- tests/DuplexResourceStreamTest.php | 13 +++++ tests/ReadableResourceStreamTest.php | 13 +++++ tests/WritableResourceStreamTest.php | 13 +++++ 14 files changed, 116 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index a0f3ebb..5284462 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ Re-attach the data source after a previous `pause()`. ```php $stream->pause(); -$loop->addTimer(1.0, function () use ($stream) { +Loop::addTimer(1.0, function () use ($stream) { $stream->resume(); }); ``` @@ -737,7 +737,7 @@ stream in order to stop waiting for the stream to flush its final data. ```php $stream->end(); -$loop->addTimer(1.0, function () use ($stream) { +Loop::addTimer(1.0, function () use ($stream) { $stream->close(); }); ``` @@ -821,7 +821,7 @@ This can be used to represent a read-only resource like a file stream opened in readable mode or a stream such as `STDIN`: ```php -$stream = new ReadableResourceStream(STDIN, $loop); +$stream = new ReadableResourceStream(STDIN); $stream->on('data', function ($chunk) { echo $chunk; }); @@ -838,7 +838,7 @@ Otherwise, it will throw an `InvalidArgumentException`: ```php // throws InvalidArgumentException -$stream = new ReadableResourceStream(false, $loop); +$stream = new ReadableResourceStream(false); ``` See also the [`DuplexResourceStream`](#readableresourcestream) for read-and-write @@ -851,7 +851,7 @@ If this fails, it will throw a `RuntimeException`: ```php // throws RuntimeException on Windows -$stream = new ReadableResourceStream(STDIN, $loop); +$stream = new ReadableResourceStream(STDIN); ``` Once the constructor is called with a valid stream resource, this class will @@ -859,6 +859,12 @@ take care of the underlying stream resource. You SHOULD only use its public API and SHOULD NOT interfere with the underlying stream resource manually. +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + This class takes an optional `int|null $readChunkSize` parameter that controls the maximum buffer size in bytes to read at once from the stream. You can use a `null` value here in order to apply its default value. @@ -874,7 +880,7 @@ This should read until the stream resource is not readable anymore mean it reached EOF. ```php -$stream = new ReadableResourceStream(STDIN, $loop, 8192); +$stream = new ReadableResourceStream(STDIN, null, 8192); ``` > PHP bug warning: If the PHP process has explicitly been started without a @@ -883,6 +889,9 @@ $stream = new ReadableResourceStream(STDIN, $loop, 8192); stream like `php test.php < /dev/null` instead of `php test.php <&-`. See [#81](https://github.com/reactphp/stream/issues/81) for more details. +> Changelog: As of v1.2.0 the `$loop` parameter can be omitted (or skipped with a + `null` value) to use the [default loop](https://github.com/reactphp/event-loop#loop). + ### WritableResourceStream The `WritableResourceStream` is a concrete implementation of the @@ -892,7 +901,7 @@ This can be used to represent a write-only resource like a file stream opened in writable mode or a stream such as `STDOUT` or `STDERR`: ```php -$stream = new WritableResourceStream(STDOUT, $loop); +$stream = new WritableResourceStream(STDOUT); $stream->write('hello!'); $stream->end(); ``` @@ -905,7 +914,7 @@ Otherwise, it will throw an `InvalidArgumentException`: ```php // throws InvalidArgumentException -$stream = new WritableResourceStream(false, $loop); +$stream = new WritableResourceStream(false); ``` See also the [`DuplexResourceStream`](#readableresourcestream) for read-and-write @@ -918,7 +927,7 @@ If this fails, it will throw a `RuntimeException`: ```php // throws RuntimeException on Windows -$stream = new WritableResourceStream(STDOUT, $loop); +$stream = new WritableResourceStream(STDOUT); ``` Once the constructor is called with a valid stream resource, this class will @@ -933,13 +942,19 @@ For this, it uses an in-memory buffer string to collect all outstanding writes. This buffer has a soft-limit applied which defines how much data it is willing to accept before the caller SHOULD stop sending further data. +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + This class takes an optional `int|null $writeBufferSoftLimit` parameter that controls this maximum buffer size in bytes. You can use a `null` value here in order to apply its default value. This value SHOULD NOT be changed unless you know what you're doing. ```php -$stream = new WritableResourceStream(STDOUT, $loop, 8192); +$stream = new WritableResourceStream(STDOUT, null, 8192); ``` This class takes an optional `int|null $writeChunkSize` parameter that controls @@ -954,11 +969,14 @@ This can be `-1` which means "write everything available" to the underlying stream resource. ```php -$stream = new WritableResourceStream(STDOUT, $loop, null, 8192); +$stream = new WritableResourceStream(STDOUT, null, null, 8192); ``` See also [`write()`](#write) for more details. +> Changelog: As of v1.2.0 the `$loop` parameter can be omitted (or skipped with a + `null` value) to use the [default loop](https://github.com/reactphp/event-loop#loop). + ### DuplexResourceStream The `DuplexResourceStream` is a concrete implementation of the @@ -969,7 +987,7 @@ in read and write mode mode or a stream such as a TCP/IP connection: ```php $conn = stream_socket_client('tcp://google.com:80'); -$stream = new DuplexResourceStream($conn, $loop); +$stream = new DuplexResourceStream($conn); $stream->write('hello!'); $stream->end(); ``` @@ -982,7 +1000,7 @@ Otherwise, it will throw an `InvalidArgumentException`: ```php // throws InvalidArgumentException -$stream = new DuplexResourceStream(false, $loop); +$stream = new DuplexResourceStream(false); ``` See also the [`ReadableResourceStream`](#readableresourcestream) for read-only @@ -996,7 +1014,7 @@ If this fails, it will throw a `RuntimeException`: ```php // throws RuntimeException on Windows -$stream = new DuplexResourceStream(STDOUT, $loop); +$stream = new DuplexResourceStream(STDOUT); ``` Once the constructor is called with a valid stream resource, this class will @@ -1004,6 +1022,12 @@ take care of the underlying stream resource. You SHOULD only use its public API and SHOULD NOT interfere with the underlying stream resource manually. +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + This class takes an optional `int|null $readChunkSize` parameter that controls the maximum buffer size in bytes to read at once from the stream. You can use a `null` value here in order to apply its default value. @@ -1020,7 +1044,7 @@ mean it reached EOF. ```php $conn = stream_socket_client('tcp://google.com:80'); -$stream = new DuplexResourceStream($conn, $loop, 8192); +$stream = new DuplexResourceStream($conn, null, 8192); ``` Any `write()` calls to this class will not be performed instantly, but will @@ -1040,12 +1064,15 @@ If you want to change the write buffer soft limit, you can pass an instance of ```php $conn = stream_socket_client('tcp://google.com:80'); -$buffer = new WritableResourceStream($conn, $loop, 8192); -$stream = new DuplexResourceStream($conn, $loop, null, $buffer); +$buffer = new WritableResourceStream($conn, null, 8192); +$stream = new DuplexResourceStream($conn, null, null, $buffer); ``` See also [`WritableResourceStream`](#writableresourcestream) for more details. +> Changelog: As of v1.2.0 the `$loop` parameter can be omitted (or skipped with a + `null` value) to use the [default loop](https://github.com/reactphp/event-loop#loop). + ### ThroughStream The `ThroughStream` implements the @@ -1123,8 +1150,8 @@ This is useful for some APIs which may require a single more convenient to work with a single stream instance like this: ```php -$stdin = new ReadableResourceStream(STDIN, $loop); -$stdout = new WritableResourceStream(STDOUT, $loop); +$stdin = new ReadableResourceStream(STDIN); +$stdout = new WritableResourceStream(STDOUT); $stdio = new CompositeStream($stdin, $stdout); @@ -1154,14 +1181,10 @@ The following example can be used to pipe the contents of a source file into a destination file without having to ever read the whole file into memory: ```php -$loop = new React\EventLoop\StreamSelectLoop; - -$source = new React\Stream\ReadableResourceStream(fopen('source.txt', 'r'), $loop); -$dest = new React\Stream\WritableResourceStream(fopen('destination.txt', 'w'), $loop); +$source = new React\Stream\ReadableResourceStream(fopen('source.txt', 'r')); +$dest = new React\Stream\WritableResourceStream(fopen('destination.txt', 'w')); $source->pipe($dest); - -$loop->run(); ``` > Note that this example uses `fopen()` for illustration purposes only. diff --git a/composer.json b/composer.json index aada0be..fd90e9f 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ ], "require": { "php": ">=5.3.8", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/event-loop": "dev-master#78f7f43 as 1.2.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { diff --git a/examples/01-http.php b/examples/01-http.php index 3687f7c..e70691d 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -11,7 +11,6 @@ // $ php examples/01-http.php // $ php examples/01-http.php reactphp.org -use React\EventLoop\Factory; use React\Stream\DuplexResourceStream; require __DIR__ . '/../vendor/autoload.php'; @@ -25,8 +24,7 @@ exit(1); } -$loop = Factory::create(); -$stream = new DuplexResourceStream($resource, $loop); +$stream = new DuplexResourceStream($resource); $stream->on('data', function ($chunk) { echo $chunk; @@ -36,5 +34,3 @@ }); $stream->write("GET / HTTP/1.0\r\nHost: $host\r\n\r\n"); - -$loop->run(); diff --git a/examples/02-https.php b/examples/02-https.php index 163f7c8..1d212da 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -11,7 +11,6 @@ // $ php examples/02-https.php // $ php examples/02-https.php reactphp.org -use React\EventLoop\Factory; use React\Stream\DuplexResourceStream; require __DIR__ . '/../vendor/autoload.php'; @@ -25,8 +24,7 @@ exit(1); } -$loop = Factory::create(); -$stream = new DuplexResourceStream($resource, $loop); +$stream = new DuplexResourceStream($resource); $stream->on('data', function ($chunk) { echo $chunk; @@ -36,5 +34,3 @@ }); $stream->write("GET / HTTP/1.0\r\nHost: $host\r\n\r\n"); - -$loop->run(); diff --git a/examples/11-cat.php b/examples/11-cat.php index 90fadc0..818ec8e 100644 --- a/examples/11-cat.php +++ b/examples/11-cat.php @@ -8,7 +8,6 @@ // $ php examples/11-cat.php < README.md // $ echo hello | php examples/11-cat.php -use React\EventLoop\Factory; use React\Stream\ReadableResourceStream; use React\Stream\WritableResourceStream; @@ -19,10 +18,6 @@ exit(1); } -$loop = Factory::create(); - -$stdout = new WritableResourceStream(STDOUT, $loop); -$stdin = new ReadableResourceStream(STDIN, $loop); +$stdout = new WritableResourceStream(STDOUT); +$stdin = new ReadableResourceStream(STDIN); $stdin->pipe($stdout); - -$loop->run(); diff --git a/examples/91-benchmark-throughput.php b/examples/91-benchmark-throughput.php index ecf695c..4203950 100644 --- a/examples/91-benchmark-throughput.php +++ b/examples/91-benchmark-throughput.php @@ -11,6 +11,8 @@ // $ php examples/91-benchmark-throughput.php -t 10 -o zero.bin // $ php examples/91-benchmark-throughput.php -t 60 -i zero.bin +use React\EventLoop\Loop; + require __DIR__ . '/../vendor/autoload.php'; if (DIRECTORY_SEPARATOR === '\\') { @@ -27,10 +29,8 @@ $if = str_replace('/dev/fd/', 'php://fd/', $if); $of = str_replace('/dev/fd/', 'php://fd/', $of); -$loop = new React\EventLoop\StreamSelectLoop(); - // setup information stream -$info = new React\Stream\WritableResourceStream(STDERR, $loop); +$info = new React\Stream\WritableResourceStream(STDERR); if (extension_loaded('xdebug')) { $info->write('NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL); } @@ -38,25 +38,23 @@ // setup input and output streams and pipe inbetween $fh = fopen($if, 'r'); -$in = new React\Stream\ReadableResourceStream($fh, $loop); -$out = new React\Stream\WritableResourceStream(fopen($of, 'w'), $loop); +$in = new React\Stream\ReadableResourceStream($fh); +$out = new React\Stream\WritableResourceStream(fopen($of, 'w')); $in->pipe($out); // stop input stream in $t seconds $start = microtime(true); -$timeout = $loop->addTimer($t, function () use ($in, &$bytes) { +$timeout = Loop::addTimer($t, function () use ($in) { $in->close(); }); // print stream position once stream closes -$in->on('close', function () use ($fh, $start, $loop, $timeout, $info) { +$in->on('close', function () use ($fh, $start, $timeout, $info) { $t = microtime(true) - $start; - $loop->cancelTimer($timeout); + Loop::cancelTimer($timeout); $bytes = ftell($fh); $info->write('read ' . $bytes . ' byte(s) in ' . round($t, 3) . ' second(s) => ' . round($bytes / 1024 / 1024 / $t, 1) . ' MiB/s' . PHP_EOL); $info->write('peak memory usage of ' . round(memory_get_peak_usage(true) / 1024 / 1024, 1) . ' MiB' . PHP_EOL); }); - -$loop->run(); diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index c8c1c50..c3163c6 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -3,12 +3,15 @@ namespace React\Stream; use Evenement\EventEmitter; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use InvalidArgumentException; final class DuplexResourceStream extends EventEmitter implements DuplexStreamInterface { private $stream; + + /** @var LoopInterface */ private $loop; /** @@ -35,7 +38,7 @@ final class DuplexResourceStream extends EventEmitter implements DuplexStreamInt private $closing = false; private $listening = false; - public function __construct($stream, LoopInterface $loop, $readChunkSize = null, WritableStreamInterface $buffer = null) + public function __construct($stream, LoopInterface $loop = null, $readChunkSize = null, WritableStreamInterface $buffer = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); @@ -70,7 +73,7 @@ public function __construct($stream, LoopInterface $loop, $readChunkSize = null, } $this->stream = $stream; - $this->loop = $loop; + $this->loop = $loop ?: Loop::get(); $this->bufferSize = ($readChunkSize === null) ? 65536 : (int)$readChunkSize; $this->buffer = $buffer; diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 9d9c006..1b0b08c 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -3,6 +3,7 @@ namespace React\Stream; use Evenement\EventEmitter; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use InvalidArgumentException; @@ -13,6 +14,7 @@ final class ReadableResourceStream extends EventEmitter implements ReadableStrea */ private $stream; + /** @var LoopInterface */ private $loop; /** @@ -38,7 +40,7 @@ final class ReadableResourceStream extends EventEmitter implements ReadableStrea private $closed = false; private $listening = false; - public function __construct($stream, LoopInterface $loop, $readChunkSize = null) + public function __construct($stream, LoopInterface $loop = null, $readChunkSize = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); @@ -69,7 +71,7 @@ public function __construct($stream, LoopInterface $loop, $readChunkSize = null) } $this->stream = $stream; - $this->loop = $loop; + $this->loop = $loop ?: Loop::get(); $this->bufferSize = ($readChunkSize === null) ? 65536 : (int)$readChunkSize; $this->resume(); diff --git a/src/ReadableStreamInterface.php b/src/ReadableStreamInterface.php index 2b4c3d0..fa3d59c 100644 --- a/src/ReadableStreamInterface.php +++ b/src/ReadableStreamInterface.php @@ -236,7 +236,7 @@ public function pause(); * ```php * $stream->pause(); * - * $loop->addTimer(1.0, function () use ($stream) { + * Loop::addTimer(1.0, function () use ($stream) { * $stream->resume(); * }); * ``` diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 952f39c..1af16b1 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -3,11 +3,14 @@ namespace React\Stream; use Evenement\EventEmitter; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; final class WritableResourceStream extends EventEmitter implements WritableStreamInterface { private $stream; + + /** @var LoopInterface */ private $loop; /** @@ -25,7 +28,7 @@ final class WritableResourceStream extends EventEmitter implements WritableStrea private $closed = false; private $data = ''; - public function __construct($stream, LoopInterface $loop, $writeBufferSoftLimit = null, $writeChunkSize = null) + public function __construct($stream, LoopInterface $loop = null, $writeBufferSoftLimit = null, $writeChunkSize = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new \InvalidArgumentException('First parameter must be a valid stream resource'); @@ -44,7 +47,7 @@ public function __construct($stream, LoopInterface $loop, $writeBufferSoftLimit } $this->stream = $stream; - $this->loop = $loop; + $this->loop = $loop ?: Loop::get(); $this->softLimit = ($writeBufferSoftLimit === null) ? 65536 : (int)$writeBufferSoftLimit; $this->writeChunkSize = ($writeChunkSize === null) ? -1 : (int)$writeChunkSize; } diff --git a/src/WritableStreamInterface.php b/src/WritableStreamInterface.php index 3bc932e..9b54680 100644 --- a/src/WritableStreamInterface.php +++ b/src/WritableStreamInterface.php @@ -330,7 +330,7 @@ public function end($data = null); * * ```php * $stream->end(); - * $loop->addTimer(1.0, function () use ($stream) { + * Loop::addTimer(1.0, function () use ($stream) { * $stream->close(); * }); * ``` diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index 2432efe..2edc07e 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -20,6 +20,19 @@ public function testConstructor() new DuplexResourceStream($stream, $loop); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $resource = fopen('php://temp', 'r+'); + + $stream = new DuplexResourceStream($resource); + + $ref = new \ReflectionProperty($stream, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($stream); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + /** * @covers React\Stream\DuplexResourceStream::__construct * @doesNotPerformAssertions diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 30503a6..f534488 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -19,6 +19,19 @@ public function testConstructor() new ReadableResourceStream($stream, $loop); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $resource = fopen('php://temp', 'r+'); + + $stream = new ReadableResourceStream($resource); + + $ref = new \ReflectionProperty($stream, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($stream); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + /** * @covers React\Stream\ReadableResourceStream::__construct * @doesNotPerformAssertions diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index f0a3b42..38d4259 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -19,6 +19,19 @@ public function testConstructor() new WritableResourceStream($stream, $loop); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $resource = fopen('php://temp', 'r+'); + + $stream = new WritableResourceStream($resource); + + $ref = new \ReflectionProperty($stream, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($stream); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + /** * @covers React\Stream\WritableResourceStream::__construct * @doesNotPerformAssertions From e617d632943bc8022950bcd53b8ed23c4edf4b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 5 Jul 2021 10:42:17 +0200 Subject: [PATCH 28/51] Update to stable reactphp/event-loop v1.2.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fd90e9f..b235f5a 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ ], "require": { "php": ">=5.3.8", - "react/event-loop": "dev-master#78f7f43 as 1.2.0", + "react/event-loop": "^1.2", "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { From 7a423506ee1903e89f1e08ec5f0ed430ff784ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Jul 2021 14:37:55 +0200 Subject: [PATCH 29/51] Prepare v1.2.0 release --- CHANGELOG.md | 23 +++++++++++++++++++++++ README.md | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac00244..9bafba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 1.2.0 (2021-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). + +* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop). + (#159 by @clue) + + ```php + // old (still supported) + $stream = new ReadableResourceStream($resource, $loop); + $stream = new WritabeResourceStream($resource, $loop); + $stream = new DuplexResourceStream($resource, $loop); + + // new (using default loop) + $stream = new ReadableResourceStream($resource); + $stream = new WritabeResourceStream($resource); + $stream = new DuplexResourceStream($resource); + ``` + +* Improve test suite, use GitHub actions for continuous integration (CI), + update PHPUnit config, run tests on PHP 8 and add full core team to the license. + (#153, #156 and #157 by @SimonFrings and #154 by @WyriHaximus) + ## 1.1.1 (2020-05-04) * Fix: Fix faulty write buffer behavior when sending large data chunks over TLS (Mac OS X only). diff --git a/README.md b/README.md index 5284462..460f51a 100644 --- a/README.md +++ b/README.md @@ -1202,7 +1202,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/stream:^1.1.1 +$ composer require react/stream:^1.2 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 526d6ad096cd60524f06294fc2f59bb15609e2d8 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 15 Nov 2021 16:08:39 +0100 Subject: [PATCH 30/51] Support PHP 8.1 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc9cf4b..ca7e8f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.1 - 8.0 - 7.4 - 7.3 From 99508eec9b52c8cfdfcb2205a95ddcc0173531f4 Mon Sep 17 00:00:00 2001 From: Simon Bennett Date: Wed, 16 Mar 2022 06:38:15 +0000 Subject: [PATCH 31/51] Update typo's in docs --- README.md | 2 +- src/WritableStreamInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 460f51a..7aacfed 100644 --- a/README.md +++ b/README.md @@ -609,7 +609,7 @@ data until the buffer drains. The stream SHOULD send a `drain` event once the buffer is ready to accept more data. -Similarly, if the the stream is not writable (already in a closed state) +Similarly, if the stream is not writable (already in a closed state) it MUST NOT process the given `$data` and SHOULD return `false`, indicating that the caller should stop sending data. diff --git a/src/WritableStreamInterface.php b/src/WritableStreamInterface.php index 9b54680..e262592 100644 --- a/src/WritableStreamInterface.php +++ b/src/WritableStreamInterface.php @@ -196,7 +196,7 @@ public function isWritable(); * The stream SHOULD send a `drain` event once the buffer is ready to accept * more data. * - * Similarly, if the the stream is not writable (already in a closed state) + * Similarly, if the stream is not writable (already in a closed state) * it MUST NOT process the given `$data` and SHOULD return `false`, * indicating that the caller should stop sending data. * From b8fc28d71addc187ae19aa5cdc67fce95b9c8426 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 16 Mar 2022 13:38:26 +0100 Subject: [PATCH 32/51] Add badge to show number of project installations --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 460f51a..cc435fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Stream [![CI status](https://github.com/reactphp/stream/workflows/CI/badge.svg)](https://github.com/reactphp/stream/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/stream?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/stream) Event-driven readable and writable streams for non-blocking I/O in [ReactPHP](https://reactphp.org/). From 85cd3386df6fbb90880b2a337b7330f491d5fb98 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 12 Apr 2022 10:00:43 +0200 Subject: [PATCH 33/51] Fix legacy HHVM build by downgrading Composer --- .github/workflows/ci.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca7e8f0..3ee70fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,7 @@ jobs: - uses: azjezz/setup-hhvm@v1 with: version: lts-3.30 + - run: composer self-update --2.2 # downgrade Composer for HHVM - run: hhvm $(which composer) install - run: hhvm vendor/bin/phpunit - run: time php examples/91-benchmark-throughput.php diff --git a/README.md b/README.md index a0e5ce1..f55c9c6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stream -[![CI status](https://github.com/reactphp/stream/workflows/CI/badge.svg)](https://github.com/reactphp/stream/actions) +[![CI status](https://github.com/reactphp/stream/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/stream/actions) [![installs on Packagist](https://img.shields.io/packagist/dt/react/stream?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/stream) Event-driven readable and writable streams for non-blocking I/O in [ReactPHP](https://reactphp.org/). From 2c436bb477ceabfb705fe773b123da23f077241d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 20 May 2022 10:18:27 +0200 Subject: [PATCH 34/51] Avoid unneeded syscall when creating non-blocking `DuplexResourceStream` --- src/DuplexResourceStream.php | 2 +- tests/DuplexResourceStreamTest.php | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index c3163c6..4da2139 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -52,7 +52,7 @@ public function __construct($stream, LoopInterface $loop = null, $readChunkSize // this class relies on non-blocking I/O in order to not interrupt the event loop // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 - if (\stream_set_blocking($stream, false) !== true) { + if ($buffer !== null && !$buffer instanceof WritableResourceStream && \stream_set_blocking($stream, false) !== true) { throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); } diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index 2edc07e..e61f14b 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -117,7 +117,25 @@ public function testConstructorAcceptsBuffer() $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $conn = new DuplexResourceStream($stream, $loop, null, $buffer); + new DuplexResourceStream($stream, $loop, null, $buffer); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + */ + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlockingWithBufferGiven() + { + if (!in_array('blocking', stream_get_wrappers())) { + stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); + } + + $stream = fopen('blocking://test', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $this->setExpectedException('RunTimeException'); + new DuplexResourceStream($stream, $loop, null, $buffer); } public function testCloseShouldEmitCloseEvent() From f96bfb2a211df35283ded6b5d2f09961d2bccecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Jun 2022 11:43:25 +0200 Subject: [PATCH 35/51] Make tests compatible with PHP 8.2 by avoiding dynamic properties --- tests/EnforceBlockingWrapper.php | 4 +++- tests/WritableResourceStreamTest.php | 22 ++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/EnforceBlockingWrapper.php b/tests/EnforceBlockingWrapper.php index 39c0487..a171b41 100644 --- a/tests/EnforceBlockingWrapper.php +++ b/tests/EnforceBlockingWrapper.php @@ -5,10 +5,12 @@ /** * Used to test dummy stream resources that do not support setting non-blocking mode * - * @link http://php.net/manual/de/class.streamwrapper.php + * @link https://www.php.net/manual/en/class.streamwrapper.php */ class EnforceBlockingWrapper { + public $context; + public function stream_open($path, $mode, $options, &$opened_path) { return true; diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index 38d4259..678db98 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -157,14 +157,23 @@ public function testEmptyWriteDoesNotAddToLoop() public function testWriteReturnsFalseWhenWritableResourceStreamIsFull() { $stream = fopen('php://temp', 'r+'); - $loop = $this->createWriteableLoopMock(); - $loop->preventWrites = true; + + $preventWrites = true; + $loop = $this->createLoopMock(); + $loop + ->expects($this->any()) + ->method('addWriteStream') + ->will($this->returnCallback(function ($stream, $listener) use (&$preventWrites) { + if (!$preventWrites) { + call_user_func($listener, $stream); + } + })); $buffer = new WritableResourceStream($stream, $loop, 4); $buffer->on('error', $this->expectCallableNever()); $this->assertTrue($buffer->write("foo")); - $loop->preventWrites = false; + $preventWrites = false; $this->assertFalse($buffer->write("bar\n")); } @@ -504,14 +513,11 @@ public function testWritingToClosedStream() private function createWriteableLoopMock() { $loop = $this->createLoopMock(); - $loop->preventWrites = false; $loop ->expects($this->any()) ->method('addWriteStream') - ->will($this->returnCallback(function ($stream, $listener) use ($loop) { - if (!$loop->preventWrites) { - call_user_func($listener, $stream); - } + ->will($this->returnCallback(function ($stream, $listener) { + call_user_func($listener, $stream); })); return $loop; From 2128f2c29de2bfe8cd368c7b6da59b217881e614 Mon Sep 17 00:00:00 2001 From: Nicolas Hedger Date: Mon, 20 Jun 2022 17:02:04 +0200 Subject: [PATCH 36/51] chore(docs): remove leading dollar sign --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f55c9c6..eb6962d 100644 --- a/README.md +++ b/README.md @@ -1203,7 +1203,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/stream:^1.2 +composer require react/stream:^1.2 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -1219,13 +1219,13 @@ To run the test suite, you first need to clone this repo and then install all dependencies [through Composer](https://getcomposer.org): ```bash -$ composer install +composer install ``` To run the test suite, go to the project root and run: ```bash -$ php vendor/bin/phpunit +vendor/bin/phpunit ``` The test suite also contains a number of functional integration tests that rely @@ -1233,7 +1233,7 @@ on a stable internet connection. If you do not want to run these, they can simply be skipped like this: ```bash -$ php vendor/bin/phpunit --exclude-group internet +vendor/bin/phpunit --exclude-group internet ``` ## License From d14450bc1f8926feb7c19501f6c510befccb7bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 6 Aug 2022 09:24:32 +0200 Subject: [PATCH 37/51] Fix failing test suite by updating macOS --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ee70fd..6d143b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: PHPUnit-macOS: name: PHPUnit (macOS) - runs-on: macos-10.15 + runs-on: macos-12 continue-on-error: true steps: - uses: actions/checkout@v2 From a0764a6d023f54cba8d9a467284bec6f500a8e4d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 14 Aug 2022 00:12:43 +0200 Subject: [PATCH 38/51] Test on PHP 8.2 With PHP 8.2 coming out later this year, we should be reading for it's release to ensure all out code works on it. Refs: https://github.com/reactphp/event-loop/pull/258 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d143b7..1250e9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.2 - 8.1 - 8.0 - 7.4 From 9b365777a99cf86803dcdfdae81e9a092a078394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 13 Nov 2022 22:47:10 +0100 Subject: [PATCH 39/51] Update test suite and report failed assertions --- .github/workflows/ci.yml | 32 ++++++++++++++++++++------------ composer.json | 6 +++--- phpunit.xml.dist | 17 +++++++++++++---- phpunit.xml.legacy | 10 +++++++++- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1250e9b..bc41ae6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php: @@ -24,11 +24,12 @@ jobs: - 5.4 - 5.3 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: xdebug + ini-file: development - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -41,25 +42,32 @@ jobs: runs-on: macos-12 continue-on-error: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: xdebug + ini-file: development - run: composer install - run: vendor/bin/phpunit --coverage-text - run: time php examples/91-benchmark-throughput.php PHPUnit-hhvm: name: PHPUnit (HHVM) - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 continue-on-error: true steps: - - uses: actions/checkout@v2 - - uses: azjezz/setup-hhvm@v1 + - uses: actions/checkout@v3 + - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM + - name: Run hhvm composer.phar install + uses: docker://hhvm/hhvm:3.30-lts-latest with: - version: lts-3.30 - - run: composer self-update --2.2 # downgrade Composer for HHVM - - run: hhvm $(which composer) install - - run: hhvm vendor/bin/phpunit - - run: time php examples/91-benchmark-throughput.php + args: hhvm composer.phar install + - name: Run hhvm vendor/bin/phpunit + uses: docker://hhvm/hhvm:3.30-lts-latest + with: + args: hhvm vendor/bin/phpunit + - name: Run time hhvm examples/91-benchmark-throughput.php + uses: docker://hhvm/hhvm:3.30-lts-latest + with: + args: bash -c "time hhvm examples/91-benchmark-throughput.php" diff --git a/composer.json b/composer.json index b235f5a..e139434 100644 --- a/composer.json +++ b/composer.json @@ -31,17 +31,17 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", "clue/stream-filter": "~1.2" }, "autoload": { "psr-4": { - "React\\Stream\\": "src" + "React\\Stream\\": "src/" } }, "autoload-dev": { "psr-4": { - "React\\Tests\\Stream\\": "tests" + "React\\Tests\\Stream\\": "tests/" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fa88e7e..7a9577e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,11 +1,12 @@ - - + + convertDeprecationsToExceptions="true"> ./tests/ @@ -16,4 +17,12 @@ ./src/ + + + + + + + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index fbb43e8..ac5600a 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -1,6 +1,6 @@ - + ./src/ + + + + + + + + From 6fbc9672905c7d5a885f2da2fc696f65840f4a66 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 16 Jun 2023 12:51:30 +0200 Subject: [PATCH 40/51] Prepare v1.3.0 release --- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bafba7..b4e2307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 1.3.0 (2023-06-16) + +* Feature: Full PHP 8.1 and PHP 8.2 compatibility. + (#160 by @SimonFrings, #165 by @clue and #169 by @WyriHaximus) + +* Feature: Avoid unneeded syscall when creating non-blocking `DuplexResourceStream`. + (#164 by @clue) + +* Minor documentation improvements. + (#161 by @mrsimonbennett, #162 by @SimonFrings and #166 by @nhedger) + +* Improve test suite and project setup and report failed assertions. + (#168 and #170 by @clue and #163 by @SimonFrings) + ## 1.2.0 (2021-07-11) A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). diff --git a/README.md b/README.md index eb6962d..2bfcbbe 100644 --- a/README.md +++ b/README.md @@ -1203,7 +1203,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/stream:^1.2 +composer require react/stream:^1.3 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 4e4e6388f9406e5902dbc066060a4066610ad38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 17 Sep 2023 13:19:15 +0200 Subject: [PATCH 41/51] Test on PHP 8.3 and update test environment --- .github/workflows/ci.yml | 9 +++++---- composer.json | 2 +- phpunit.xml.dist | 6 +++--- phpunit.xml.legacy | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc41ae6..bc36a4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.3 - 8.2 - 8.1 - 8.0 @@ -24,7 +25,7 @@ jobs: - 5.4 - 5.3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -42,10 +43,10 @@ jobs: runs-on: macos-12 continue-on-error: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: xdebug ini-file: development - run: composer install @@ -57,7 +58,7 @@ jobs: runs-on: ubuntu-22.04 continue-on-error: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM - name: Run hhvm composer.phar install uses: docker://hhvm/hhvm:3.30-lts-latest diff --git a/composer.json b/composer.json index e139434..09d8b71 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "clue/stream-filter": "~1.2" }, "autoload": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7a9577e..ac542e7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - + - + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index ac5600a..8916116 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -18,7 +18,7 @@ - + From 6273e35967202d32389b118e9ffba274163e3cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 22 Sep 2023 17:43:17 +0200 Subject: [PATCH 42/51] Fix `drain` event of `ThroughStream` to handle potential race condition --- src/ThroughStream.php | 19 +++++++----- tests/ThroughStreamTest.php | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/ThroughStream.php b/src/ThroughStream.php index 6f73fb8..3b4fbb7 100644 --- a/src/ThroughStream.php +++ b/src/ThroughStream.php @@ -93,16 +93,19 @@ public function __construct($callback = null) public function pause() { - $this->paused = true; + // only allow pause if still readable, false otherwise + $this->paused = $this->readable; } public function resume() { + $this->paused = false; + + // emit drain event if previous write was paused (throttled) if ($this->drain) { $this->drain = false; $this->emit('drain'); } - $this->paused = false; } public function pipe(WritableStreamInterface $dest, array $options = array()) @@ -139,12 +142,13 @@ public function write($data) $this->emit('data', array($data)); + // emit drain event on next resume if currently paused (throttled) if ($this->paused) { $this->drain = true; - return false; } - return true; + // continue writing if still writable and not paused (throttled), false otherwise + return $this->writable && !$this->paused; } public function end($data = null) @@ -164,7 +168,7 @@ public function end($data = null) $this->readable = false; $this->writable = false; - $this->paused = true; + $this->paused = false; $this->drain = false; $this->emit('end'); @@ -179,9 +183,10 @@ public function close() $this->readable = false; $this->writable = false; - $this->closed = true; - $this->paused = true; + $this->paused = false; $this->drain = false; + + $this->closed = true; $this->callback = null; $this->emit('close'); diff --git a/tests/ThroughStreamTest.php b/tests/ThroughStreamTest.php index 444f3b1..42c251a 100644 --- a/tests/ThroughStreamTest.php +++ b/tests/ThroughStreamTest.php @@ -95,6 +95,30 @@ public function itShouldReturnFalseForAnyDataWrittenToItWhenPaused() $this->assertFalse($ret); } + /** @test */ + public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventEndsStream() + { + $through = new ThroughStream(); + $through->on('data', function () use ($through) { + $through->end(); + }); + $ret = $through->write('foo'); + + $this->assertFalse($ret); + } + + /** @test */ + public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventClosesStream() + { + $through = new ThroughStream(); + $through->on('data', function () use ($through) { + $through->close(); + }); + $ret = $through->write('foo'); + + $this->assertFalse($ret); + } + /** @test */ public function itShouldEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenToItWhenPaused() { @@ -106,6 +130,40 @@ public function itShouldEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenToItWh $through->resume(); } + /** @test */ + public function itShouldNotEmitDrainOnResumeAfterClose() + { + $through = new ThroughStream(); + $through->close(); + + $through->on('drain', $this->expectCallableNever()); + $through->resume(); + } + + /** @test */ + public function itShouldNotEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenThatCausesStreamToClose() + { + $through = new ThroughStream(); + $through->on('data', function () use ($through) { $through->close(); }); + $through->write('foo'); + + $through->on('drain', $this->expectCallableNever()); + $through->resume(); + } + + /** @test */ + public function itShouldReturnFalseForAnyDataWrittenToItAfterPausingFromDrainEvent() + { + $through = new ThroughStream(); + $through->pause(); + $through->write('foo'); + + $through->on('drain', function () use ($through) { $through->pause(); }); + $through->resume(); + + $this->assertFalse($through->write('bar')); + } + /** @test */ public function itShouldReturnTrueForAnyDataWrittenToItWhenResumedAfterPause() { From abdd922d3f5a9eafb255a9bc8b8d2f205c4770cf Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 27 Jan 2024 10:17:21 +0100 Subject: [PATCH 43/51] Hello `3.x` development branch Once this PR is merged, we can start working on the new [v3.0.0 milestone](https://github.com/reactphp/stream/milestone/22). The default branch will be `3.x` and the old `1.x` branch still stay in place at least until `3.0.0` is released. Refs: Road map ticket for stream: #173 Plans for ReactPHP v3: https://github.com/orgs/reactphp/discussions/481 PR templated from: https://github.com/friends-of-reactphp/mysql/pull/185 --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2bfcbbe..3920a75 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,14 @@ Event-driven readable and writable streams for non-blocking I/O in [ReactPHP](https://reactphp.org/). +> **Development version:** This branch contains the code for the upcoming v3 +> release. For the code of the current stable v1 release, check out the +> [`1.x` branch](https://github.com/reactphp/stream/tree/1.x). +> +> The upcoming v3 release will be the way forward for this package. However, +> we will still actively support v1 for those not yet on the latest version. +> See also [installation instructions](#install) for more details. + In order to make the [EventLoop](https://github.com/reactphp/event-loop) easier to use, this component introduces the powerful concept of "streams". Streams allow you to efficiently process huge amounts of data (such as a multi @@ -1199,11 +1207,11 @@ $source->pipe($dest); The recommended way to install this library is [through Composer](https://getcomposer.org). [New to Composer?](https://getcomposer.org/doc/00-intro.md) -This project follows [SemVer](https://semver.org/). -This will install the latest supported version: +Once released, this project will follow [SemVer](https://semver.org/). +At the moment, this will install the latest development version: ```bash -composer require react/stream:^1.3 +composer require react/stream:^3@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 9bde4fdbed057edacfa063a922c2b7f6f9e29ab6 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 23 Feb 2024 22:06:08 +0100 Subject: [PATCH 44/51] Update to require PHP 7.1+ --- .github/workflows/ci.yml | 25 ------------- README.md | 5 +-- composer.json | 4 +- phpunit.xml.legacy | 2 +- src/DuplexResourceStream.php | 37 ++----------------- src/ReadableResourceStream.php | 37 ++----------------- src/WritableResourceStream.php | 2 +- tests/DuplexResourceStreamIntegrationTest.php | 20 ---------- tests/DuplexResourceStreamTest.php | 4 -- tests/FunctionalInternetTest.php | 4 +- tests/ReadableResourceStreamTest.php | 4 -- tests/WritableResourceStreamTest.php | 4 -- 12 files changed, 16 insertions(+), 132 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc36a4d..f654fb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,6 @@ jobs: - 7.3 - 7.2 - 7.1 - - 7.0 - - 5.6 - - 5.5 - - 5.4 - - 5.3 steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 @@ -52,23 +47,3 @@ jobs: - run: composer install - run: vendor/bin/phpunit --coverage-text - run: time php examples/91-benchmark-throughput.php - - PHPUnit-hhvm: - name: PHPUnit (HHVM) - runs-on: ubuntu-22.04 - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM - - name: Run hhvm composer.phar install - uses: docker://hhvm/hhvm:3.30-lts-latest - with: - args: hhvm composer.phar install - - name: Run hhvm vendor/bin/phpunit - uses: docker://hhvm/hhvm:3.30-lts-latest - with: - args: hhvm vendor/bin/phpunit - - name: Run time hhvm examples/91-benchmark-throughput.php - uses: docker://hhvm/hhvm:3.30-lts-latest - with: - args: bash -c "time hhvm examples/91-benchmark-throughput.php" diff --git a/README.md b/README.md index 3920a75..2ab41a1 100644 --- a/README.md +++ b/README.md @@ -1217,9 +1217,8 @@ composer require react/stream:^3@dev See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. -It's *highly recommended to use PHP 7+* for this project due to its vast -performance improvements. +extensions and supports running on PHP 7.1 through current PHP 8+. +It's *highly recommended to use the latest supported PHP version* for this project. ## Tests diff --git a/composer.json b/composer.json index 09d8b71..44e750c 100644 --- a/composer.json +++ b/composer.json @@ -26,12 +26,12 @@ } ], "require": { - "php": ">=5.3.8", + "php": ">=7.1", "react/event-loop": "^1.2", "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "phpunit/phpunit": "^9.6 || ^5.7", "clue/stream-filter": "~1.2" }, "autoload": { diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index 8916116..a018d7a 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,7 +2,7 @@ diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index 4da2139..1bbd698 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -46,7 +46,7 @@ public function __construct($stream, LoopInterface $loop = null, $readChunkSize // ensure resource is opened for reading and wrting (fopen mode must contain "+") $meta = \stream_get_meta_data($stream); - if (isset($meta['mode']) && $meta['mode'] !== '' && \strpos($meta['mode'], '+') === false) { + if (\strpos($meta['mode'], '+') === false) { throw new InvalidArgumentException('Given stream resource is not opened in read and write mode'); } @@ -59,14 +59,9 @@ public function __construct($stream, LoopInterface $loop = null, $readChunkSize // Use unbuffered read operations on the underlying stream resource. // Reading chunks from the stream may otherwise leave unread bytes in // PHP's stream buffers which some event loop implementations do not - // trigger events on (edge triggered). - // This does not affect the default event loop implementation (level - // triggered), so we can ignore platforms not supporting this (HHVM). - // Pipe streams (such as STDIN) do not seem to require this and legacy - // PHP versions cause SEGFAULTs on unbuffered pipe streams, so skip this. - if (\function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { - \stream_set_read_buffer($stream, 0); - } + // trigger events on (edge triggered). This does not affect the default + // event loop implementation (level triggered). + \stream_set_read_buffer($stream, 0); if ($buffer === null) { $buffer = new WritableResourceStream($stream, $loop); @@ -200,28 +195,4 @@ public function handleData($stream) $this->close(); } } - - /** - * Returns whether this is a pipe resource in a legacy environment - * - * This works around a legacy PHP bug (#61019) that was fixed in PHP 5.4.28+ - * and PHP 5.5.12+ and newer. - * - * @param resource $resource - * @return bool - * @link https://github.com/reactphp/child-process/issues/40 - * - * @codeCoverageIgnore - */ - private function isLegacyPipe($resource) - { - if (\PHP_VERSION_ID < 50428 || (\PHP_VERSION_ID >= 50500 && \PHP_VERSION_ID < 50512)) { - $meta = \stream_get_meta_data($resource); - - if (isset($meta['stream_type']) && $meta['stream_type'] === 'STDIO') { - return true; - } - } - return false; - } } diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 1b0b08c..3ef5f72 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -48,7 +48,7 @@ public function __construct($stream, LoopInterface $loop = null, $readChunkSize // ensure resource is opened for reading (fopen mode must contain "r" or "+") $meta = \stream_get_meta_data($stream); - if (isset($meta['mode']) && $meta['mode'] !== '' && \strpos($meta['mode'], 'r') === \strpos($meta['mode'], '+')) { + if (\strpos($meta['mode'], 'r') === \strpos($meta['mode'], '+')) { throw new InvalidArgumentException('Given stream resource is not opened in read mode'); } @@ -61,14 +61,9 @@ public function __construct($stream, LoopInterface $loop = null, $readChunkSize // Use unbuffered read operations on the underlying stream resource. // Reading chunks from the stream may otherwise leave unread bytes in // PHP's stream buffers which some event loop implementations do not - // trigger events on (edge triggered). - // This does not affect the default event loop implementation (level - // triggered), so we can ignore platforms not supporting this (HHVM). - // Pipe streams (such as STDIN) do not seem to require this and legacy - // PHP versions cause SEGFAULTs on unbuffered pipe streams, so skip this. - if (\function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { - \stream_set_read_buffer($stream, 0); - } + // trigger events on (edge triggered). This does not affect the default + // event loop implementation (level triggered). + \stream_set_read_buffer($stream, 0); $this->stream = $stream; $this->loop = $loop ?: Loop::get(); @@ -152,28 +147,4 @@ public function handleData() $this->close(); } } - - /** - * Returns whether this is a pipe resource in a legacy environment - * - * This works around a legacy PHP bug (#61019) that was fixed in PHP 5.4.28+ - * and PHP 5.5.12+ and newer. - * - * @param resource $resource - * @return bool - * @link https://github.com/reactphp/child-process/issues/40 - * - * @codeCoverageIgnore - */ - private function isLegacyPipe($resource) - { - if (\PHP_VERSION_ID < 50428 || (\PHP_VERSION_ID >= 50500 && \PHP_VERSION_ID < 50512)) { - $meta = \stream_get_meta_data($resource); - - if (isset($meta['stream_type']) && $meta['stream_type'] === 'STDIO') { - return true; - } - } - return false; - } } diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 1af16b1..b78ec1d 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -36,7 +36,7 @@ public function __construct($stream, LoopInterface $loop = null, $writeBufferSof // ensure resource is opened for writing (fopen mode must contain either of "waxc+") $meta = \stream_get_meta_data($stream); - if (isset($meta['mode']) && $meta['mode'] !== '' && \strtr($meta['mode'], 'waxc+', '.....') === $meta['mode']) { + if (\strtr($meta['mode'], 'waxc+', '.....') === $meta['mode']) { throw new \InvalidArgumentException('Given stream resource is not opened in write mode'); } diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index 7135e15..6bdf0eb 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -6,11 +6,7 @@ use React\Stream\DuplexResourceStream; use React\Stream\ReadableResourceStream; use React\EventLoop\ExtEventLoop; -use React\EventLoop\ExtLibeventLoop; -use React\EventLoop\ExtLibevLoop; use React\EventLoop\LoopInterface; -use React\EventLoop\LibEventLoop; -use React\EventLoop\LibEvLoop; use React\EventLoop\StreamSelectLoop; class DuplexResourceStreamIntegrationTest extends TestCase @@ -26,22 +22,6 @@ function () { return new StreamSelectLoop(); } ), - array( - function () { - return function_exists('event_base_new'); - }, - function () { - return class_exists('React\EventLoop\ExtLibeventLoop') ? new ExtLibeventLoop() : new LibEventLoop(); - } - ), - array( - function () { - return class_exists('libev\EventLoop'); - }, - function () { - return class_exists('React\EventLoop\ExtLibevLoop') ? new ExtLibevLoop() : new LibEvLoop(); - } - ), array( function () { return class_exists('EventBase') && class_exists('React\EventLoop\ExtEventLoop'); diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index e61f14b..f2bbb34 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -65,10 +65,6 @@ public function testConstructorThrowsExceptionOnInvalidStream() */ public function testConstructorThrowsExceptionOnWriteOnlyStream() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('HHVM does not report fopen mode for STDOUT'); - } - $loop = $this->createLoopMock(); $this->setExpectedException('InvalidArgumentException'); diff --git a/tests/FunctionalInternetTest.php b/tests/FunctionalInternetTest.php index 4f07537..fd062fe 100644 --- a/tests/FunctionalInternetTest.php +++ b/tests/FunctionalInternetTest.php @@ -87,8 +87,8 @@ public function testUploadBiggerBlockSecure() $stream = stream_socket_client('ssl://httpbin.org:443'); - // PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big - // chunks of data over TLS streams at once. + // PHP < 7.1.4 suffers from a bug when writing big chunks of data over + // TLS streams at once. // We work around this by limiting the write chunk size to 8192 bytes // here to also support older PHP versions. // See https://github.com/reactphp/socket/issues/105 diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index f534488..7390267 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -64,10 +64,6 @@ public function testConstructorThrowsExceptionOnInvalidStream() */ public function testConstructorThrowsExceptionOnWriteOnlyStream() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('HHVM does not report fopen mode for STDOUT'); - } - $loop = $this->createLoopMock(); $this->setExpectedException('InvalidArgumentException'); diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index 678db98..e3710ac 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -347,10 +347,6 @@ public function testEndWithoutDataDoesNotCloseIfWritableResourceStreamIsFull() */ public function testEndWithDataClosesImmediatelyIfWritableResourceStreamFlushes() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $stream = fopen('php://temp', 'r+'); $filterBuffer = ''; $loop = $this->createLoopMock(); From a06b91490fd1fdaf87ac2670e1b59579fcc8f754 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 28 Feb 2024 06:52:59 +0100 Subject: [PATCH 45/51] Update PHP language syntax and remove legacy workarounds --- README.md | 4 ++-- examples/01-http.php | 2 +- examples/02-https.php | 2 +- examples/91-benchmark-throughput.php | 6 +++--- src/CompositeStream.php | 10 +++++----- src/DuplexResourceStream.php | 20 +++++++++---------- src/ReadableResourceStream.php | 8 ++++---- src/ReadableStreamInterface.php | 4 ++-- src/ThroughStream.php | 8 ++++---- src/Util.php | 4 ++-- src/WritableResourceStream.php | 4 ++-- tests/CompositeStreamTest.php | 6 +++--- tests/DuplexResourceStreamIntegrationTest.php | 4 ++-- tests/DuplexResourceStreamTest.php | 8 ++++---- tests/FunctionalInternetTest.php | 5 ++--- tests/ReadableResourceStreamTest.php | 6 +++--- tests/Stub/ReadableStreamStub.php | 8 ++++---- tests/TestCase.php | 4 ++-- tests/ThroughStreamTest.php | 4 ++-- tests/UtilTest.php | 8 ++++---- tests/WritableResourceStreamTest.php | 8 ++++---- 21 files changed, 65 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 2ab41a1..cce1408 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ By default, this will call `end()` on the destination stream once the source stream emits an `end` event. This can be disabled like this: ```php -$source->pipe($dest, array('end' => false)); +$source->pipe($dest, ['end' => false]); ``` Note that this only applies to the `end` event. @@ -1126,7 +1126,7 @@ $through = new ThroughStream(function ($data) { }); $through->on('data', $this->expectCallableOnceWith("[2, true]\n")); -$through->write(array(2, true)); +$through->write([2, true]); ``` The callback function is allowed to throw an `Exception`. In this case, diff --git a/examples/01-http.php b/examples/01-http.php index e70691d..786aefc 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -15,7 +15,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$host = isset($argv[1]) ? $argv[1] : 'www.google.com'; +$host = $argv[1] ?? 'www.google.com'; // connect to tcp://www.google.com:80 (blocking call!) // for illustration purposes only, should use react/http-client or react/socket instead! diff --git a/examples/02-https.php b/examples/02-https.php index 1d212da..e629791 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -15,7 +15,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$host = isset($argv[1]) ? $argv[1] : 'www.google.com'; +$host = $argv[1] ?? 'www.google.com'; // connect to tls://www.google.com:443 (blocking call!) // for illustration purposes only, should use react/http-client or react/socket instead! diff --git a/examples/91-benchmark-throughput.php b/examples/91-benchmark-throughput.php index 4203950..c396606 100644 --- a/examples/91-benchmark-throughput.php +++ b/examples/91-benchmark-throughput.php @@ -21,9 +21,9 @@ } $args = getopt('i:o:t:'); -$if = isset($args['i']) ? $args['i'] : '/dev/zero'; -$of = isset($args['o']) ? $args['o'] : '/dev/null'; -$t = isset($args['t']) ? $args['t'] : 1; +$if = $args['i'] ?? '/dev/zero'; +$of = $args['o'] ?? '/dev/null'; +$t = $args['t'] ?? 1; // passing file descriptors requires mapping paths (https://bugs.php.net/bug.php?id=53465) $if = str_replace('/dev/fd/', 'php://fd/', $if); diff --git a/src/CompositeStream.php b/src/CompositeStream.php index dde091d..d0b934b 100644 --- a/src/CompositeStream.php +++ b/src/CompositeStream.php @@ -20,11 +20,11 @@ public function __construct(ReadableStreamInterface $readable, WritableStreamInt return; } - Util::forwardEvents($this->readable, $this, array('data', 'end', 'error')); - Util::forwardEvents($this->writable, $this, array('drain', 'error', 'pipe')); + Util::forwardEvents($this->readable, $this, ['data', 'end', 'error']); + Util::forwardEvents($this->writable, $this, ['drain', 'error', 'pipe']); - $this->readable->on('close', array($this, 'close')); - $this->writable->on('close', array($this, 'close')); + $this->readable->on('close', [$this, 'close']); + $this->writable->on('close', [$this, 'close']); } public function isReadable() @@ -46,7 +46,7 @@ public function resume() $this->readable->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index 1bbd698..0b52c9b 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -72,16 +72,14 @@ public function __construct($stream, LoopInterface $loop = null, $readChunkSize $this->bufferSize = ($readChunkSize === null) ? 65536 : (int)$readChunkSize; $this->buffer = $buffer; - $that = $this; - - $this->buffer->on('error', function ($error) use ($that) { - $that->emit('error', array($error)); + $this->buffer->on('error', function ($error) { + $this->emit('error', [$error]); }); - $this->buffer->on('close', array($this, 'close')); + $this->buffer->on('close', [$this, 'close']); - $this->buffer->on('drain', function () use ($that) { - $that->emit('drain'); + $this->buffer->on('drain', function () { + $this->emit('drain'); }); $this->resume(); @@ -108,7 +106,7 @@ public function pause() public function resume() { if (!$this->listening && $this->readable) { - $this->loop->addReadStream($this->stream, array($this, 'handleData')); + $this->loop->addReadStream($this->stream, [$this, 'handleData']); $this->listening = true; } } @@ -158,7 +156,7 @@ public function end($data = null) $this->buffer->end($data); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } @@ -182,13 +180,13 @@ public function handleData($stream) \restore_error_handler(); if ($error !== null) { - $this->emit('error', array(new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error))); + $this->emit('error', [new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error)]); $this->close(); return; } if ($data !== '') { - $this->emit('data', array($data)); + $this->emit('data', [$data]); } elseif (\feof($this->stream)) { // no data read => we reached the end and close the stream $this->emit('end'); diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 3ef5f72..41314fe 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -88,12 +88,12 @@ public function pause() public function resume() { if (!$this->listening && !$this->closed) { - $this->loop->addReadStream($this->stream, array($this, 'handleData')); + $this->loop->addReadStream($this->stream, [$this, 'handleData']); $this->listening = true; } } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } @@ -134,13 +134,13 @@ public function handleData() \restore_error_handler(); if ($error !== null) { - $this->emit('error', array(new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error))); + $this->emit('error', [new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error)]); $this->close(); return; } if ($data !== '') { - $this->emit('data', array($data)); + $this->emit('data', [$data]); } elseif (\feof($this->stream)) { // no data read => we reached the end and close the stream $this->emit('end'); diff --git a/src/ReadableStreamInterface.php b/src/ReadableStreamInterface.php index fa3d59c..ecc5267 100644 --- a/src/ReadableStreamInterface.php +++ b/src/ReadableStreamInterface.php @@ -278,7 +278,7 @@ public function resume(); * source stream emits an `end` event. This can be disabled like this: * * ```php - * $source->pipe($dest, array('end' => false)); + * $source->pipe($dest, ['end' => false]); * ``` * * Note that this only applies to the `end` event. @@ -322,7 +322,7 @@ public function resume(); * @param array $options * @return WritableStreamInterface $dest stream as-is */ - public function pipe(WritableStreamInterface $dest, array $options = array()); + public function pipe(WritableStreamInterface $dest, array $options = []); /** * Closes the stream (forcefully). diff --git a/src/ThroughStream.php b/src/ThroughStream.php index 3b4fbb7..c49ebfc 100644 --- a/src/ThroughStream.php +++ b/src/ThroughStream.php @@ -48,7 +48,7 @@ * }); * $through->on('data', $this->expectCallableOnceWith("[2, true]\n")); * - * $through->write(array(2, true)); + * $through->write([2, true]); * ``` * * The callback function is allowed to throw an `Exception`. In this case, @@ -108,7 +108,7 @@ public function resume() } } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } @@ -133,14 +133,14 @@ public function write($data) try { $data = \call_user_func($this->callback, $data); } catch (\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); return false; } } - $this->emit('data', array($data)); + $this->emit('data', [$data]); // emit drain event on next resume if currently paused (throttled) if ($this->paused) { diff --git a/src/Util.php b/src/Util.php index 056b037..114ccb8 100644 --- a/src/Util.php +++ b/src/Util.php @@ -13,7 +13,7 @@ final class Util * @return WritableStreamInterface $dest stream as-is * @see ReadableStreamInterface::pipe() for more details */ - public static function pipe(ReadableStreamInterface $source, WritableStreamInterface $dest, array $options = array()) + public static function pipe(ReadableStreamInterface $source, WritableStreamInterface $dest, array $options = []) { // source not readable => NO-OP if (!$source->isReadable()) { @@ -27,7 +27,7 @@ public static function pipe(ReadableStreamInterface $source, WritableStreamInter return $dest; } - $dest->emit('pipe', array($source)); + $dest->emit('pipe', [$source]); // forward all source data events as $dest->write() $source->on('data', $dataer = function ($data) use ($source, $dest) { diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index b78ec1d..9665721 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -68,7 +68,7 @@ public function write($data) if (!$this->listening && $this->data !== '') { $this->listening = true; - $this->loop->addWriteStream($this->stream, array($this, 'handleWrite')); + $this->loop->addWriteStream($this->stream, [$this, 'handleWrite']); } return !isset($this->data[$this->softLimit - 1]); @@ -137,7 +137,7 @@ public function handleWrite() // Should this turn out to be a permanent error later, it will eventually // send *nothing* and we can detect this. if (($sent === 0 || $sent === false) && $error !== null) { - $this->emit('error', array(new \RuntimeException('Unable to write to stream: ' . $error))); + $this->emit('error', [new \RuntimeException('Unable to write to stream: ' . $error)]); $this->close(); return; diff --git a/tests/CompositeStreamTest.php b/tests/CompositeStreamTest.php index df89c3e..c858e6b 100644 --- a/tests/CompositeStreamTest.php +++ b/tests/CompositeStreamTest.php @@ -217,7 +217,7 @@ public function itShouldReceiveForwardedEvents() $composite->on('data', $this->expectCallableOnce()); $composite->on('drain', $this->expectCallableOnce()); - $readable->emit('data', array('foo')); + $readable->emit('data', ['foo']); $writable->emit('drain'); } @@ -241,7 +241,7 @@ public function itShouldHandlePipingCorrectly() $input = new ThroughStream(); $input->pipe($composite); - $input->emit('data', array('foo')); + $input->emit('data', ['foo']); } /** @test */ @@ -262,6 +262,6 @@ public function itShouldForwardPipeCallsToReadableStream() ->with('foo'); $composite->pipe($output); - $readable->emit('data', array('foo')); + $readable->emit('data', ['foo']); } } diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index 6bdf0eb..196a319 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -2,12 +2,12 @@ namespace React\Tests\Stream; -use Clue\StreamFilter as Filter; use React\Stream\DuplexResourceStream; use React\Stream\ReadableResourceStream; use React\EventLoop\ExtEventLoop; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; +use function Clue\StreamFilter\append as filter_append; class DuplexResourceStreamIntegrationTest extends TestCase { @@ -340,7 +340,7 @@ public function testEmptyReadShouldntFcloseStream($condition, $loopFactory) // add a filter which returns an error when encountering an 'a' when reading - Filter\append($stream, function ($chunk) { + filter_append($stream, function ($chunk) { return ''; }, STREAM_FILTER_READ); diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index f2bbb34..f81fa90 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -3,8 +3,8 @@ namespace React\Tests\Stream; use React\Stream\DuplexResourceStream; -use Clue\StreamFilter as Filter; use React\Stream\WritableResourceStream; +use function Clue\StreamFilter\append as filter_append; class DuplexResourceStreamTest extends TestCase { @@ -423,7 +423,7 @@ public function testBufferEventsShouldBubbleUp() $conn->on('error', $this->expectCallableOnce()); $buffer->emit('drain'); - $buffer->emit('error', array(new \RuntimeException('Whoops'))); + $buffer->emit('error', [new \RuntimeException('Whoops')]); } /** @@ -454,7 +454,7 @@ public function testDataFiltered() $stream = fopen('php://temp', 'r+'); // add a filter which removes every 'a' when reading - Filter\append($stream, function ($chunk) { + filter_append($stream, function ($chunk) { return str_replace('a', '', $chunk); }, STREAM_FILTER_READ); @@ -482,7 +482,7 @@ public function testDataErrorShouldEmitErrorAndClose() $stream = fopen('php://temp', 'r+'); // add a filter which returns an error when encountering an 'a' when reading - Filter\append($stream, function ($chunk) { + filter_append($stream, function ($chunk) { if (strpos($chunk, 'a') !== false) { throw new \Exception('Invalid'); } diff --git a/tests/FunctionalInternetTest.php b/tests/FunctionalInternetTest.php index fd062fe..5113e7b 100644 --- a/tests/FunctionalInternetTest.php +++ b/tests/FunctionalInternetTest.php @@ -120,10 +120,9 @@ private function awaitStreamClose(DuplexResourceStream $stream, LoopInterface $l $loop->stop(); }); - $that = $this; - $loop->addTimer($timeout, function () use ($loop, $that) { + $loop->addTimer($timeout, function () use ($loop) { $loop->stop(); - $that->fail('Timed out while waiting for stream to close'); + $this->fail('Timed out while waiting for stream to close'); }); $loop->run(); diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 7390267..5d1f802 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Stream; use React\Stream\ReadableResourceStream; -use Clue\StreamFilter as Filter; +use function Clue\StreamFilter\append as filter_append; class ReadableResourceStreamTest extends TestCase { @@ -326,7 +326,7 @@ public function testDataFiltered() $stream = fopen('php://temp', 'r+'); // add a filter which removes every 'a' when reading - Filter\append($stream, function ($chunk) { + filter_append($stream, function ($chunk) { return str_replace('a', '', $chunk); }, STREAM_FILTER_READ); @@ -354,7 +354,7 @@ public function testDataErrorShouldEmitErrorAndClose() $stream = fopen('php://temp', 'r+'); // add a filter which returns an error when encountering an 'a' when reading - Filter\append($stream, function ($chunk) { + filter_append($stream, function ($chunk) { if (strpos($chunk, 'a') !== false) { throw new \Exception('Invalid'); } diff --git a/tests/Stub/ReadableStreamStub.php b/tests/Stub/ReadableStreamStub.php index 6984f24..3fa56a8 100644 --- a/tests/Stub/ReadableStreamStub.php +++ b/tests/Stub/ReadableStreamStub.php @@ -20,19 +20,19 @@ public function isReadable() // trigger data event public function write($data) { - $this->emit('data', array($data)); + $this->emit('data', [$data]); } // trigger error event public function error($error) { - $this->emit('error', array($error)); + $this->emit('error', [$error]); } // trigger end event public function end() { - $this->emit('end', array()); + $this->emit('end', []); } public function pause() @@ -52,7 +52,7 @@ public function close() $this->emit('close'); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); diff --git a/tests/TestCase.php b/tests/TestCase.php index af07e3e..4bca070 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -41,10 +41,10 @@ protected function createCallableMock() { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + return $this->getMockBuilder('stdClass')->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit 4 - PHPUnit 9 - return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + return $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); } } diff --git a/tests/ThroughStreamTest.php b/tests/ThroughStreamTest.php index 42c251a..3093136 100644 --- a/tests/ThroughStreamTest.php +++ b/tests/ThroughStreamTest.php @@ -185,7 +185,7 @@ public function pipingStuffIntoItShouldWork() $through->on('data', $this->expectCallableOnceWith('foo')); $readable->pipe($through); - $readable->emit('data', array('foo')); + $readable->emit('data', ['foo']); } /** @test */ @@ -243,7 +243,7 @@ public function writeAfterEndShouldReturnFalse() public function writeDataWillCloseStreamShouldReturnFalse() { $through = new ThroughStream(); - $through->on('data', array($through, 'close')); + $through->on('data', [$through, 'close']); $this->assertFalse($through->write('foo')); } diff --git a/tests/UtilTest.php b/tests/UtilTest.php index f12baad..47d3646 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -114,7 +114,7 @@ public function testPipeWithoutEnd() ->expects($this->never()) ->method('end'); - Util::pipe($readable, $writable, array('end' => false)); + Util::pipe($readable, $writable, ['end' => false]); $readable->end(); } @@ -253,12 +253,12 @@ public function forwardEventsShouldSetupForwards() $source = new ThroughStream(); $target = new ThroughStream(); - Util::forwardEvents($source, $target, array('data')); + Util::forwardEvents($source, $target, ['data']); $target->on('data', $this->expectCallableOnce()); $target->on('foo', $this->expectCallableNever()); - $source->emit('data', array('hello')); - $source->emit('foo', array('bar')); + $source->emit('data', ['hello']); + $source->emit('foo', ['bar']); } private function createLoopMock() diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index e3710ac..a05adc8 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -2,8 +2,8 @@ namespace React\Tests\Stream; -use Clue\StreamFilter as Filter; use React\Stream\WritableResourceStream; +use function Clue\StreamFilter\append as filter_append; class WritableResourceStreamTest extends TestCase { @@ -355,7 +355,7 @@ public function testEndWithDataClosesImmediatelyIfWritableResourceStreamFlushes( $buffer->on('error', $this->expectCallableNever()); $buffer->on('close', $this->expectCallableOnce()); - Filter\append($stream, function ($chunk) use (&$filterBuffer) { + filter_append($stream, function ($chunk) use (&$filterBuffer) { $filterBuffer .= $chunk; return $chunk; }); @@ -407,7 +407,7 @@ public function testClose() $buffer->close(); $this->assertFalse($buffer->isWritable()); - $this->assertEquals(array(), $buffer->listeners('close')); + $this->assertEquals([], $buffer->listeners('close')); } /** @@ -466,7 +466,7 @@ public function testWritingToClosedWritableResourceStreamShouldNotWriteToStream( $buffer = new WritableResourceStream($stream, $loop); - Filter\append($stream, function ($chunk) use (&$filterBuffer) { + filter_append($stream, function ($chunk) use (&$filterBuffer) { $filterBuffer .= $chunk; return $chunk; }); From 1bb959597975c7f6c879d3a3a66b7b95ba25f65f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 28 Feb 2024 07:09:20 +0100 Subject: [PATCH 46/51] Update test suite and remove legacy PHPUnit workarounds --- composer.json | 2 +- phpunit.xml.legacy | 2 +- tests/CompositeStreamTest.php | 38 ++++++------ tests/DuplexResourceStreamIntegrationTest.php | 34 +++++------ tests/DuplexResourceStreamTest.php | 22 +++---- tests/ReadableResourceStreamTest.php | 14 +++-- tests/TestCase.php | 59 ++----------------- tests/ThroughStreamTest.php | 5 +- tests/UtilTest.php | 40 ++++++------- tests/WritableResourceStreamTest.php | 15 ++--- 10 files changed, 93 insertions(+), 138 deletions(-) diff --git a/composer.json b/composer.json index 44e750c..b8d34d4 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7", + "phpunit/phpunit": "^9.6 || ^7.5", "clue/stream-filter": "~1.2" }, "autoload": { diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index a018d7a..0086860 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,7 +2,7 @@ diff --git a/tests/CompositeStreamTest.php b/tests/CompositeStreamTest.php index c858e6b..75a0426 100644 --- a/tests/CompositeStreamTest.php +++ b/tests/CompositeStreamTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Stream; use React\Stream\CompositeStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; /** * @covers React\Stream\CompositeStream @@ -13,7 +15,7 @@ class CompositeStreamTest extends TestCase /** @test */ public function itShouldCloseReadableIfNotWritable() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->once()) ->method('isReadable') @@ -22,7 +24,7 @@ public function itShouldCloseReadableIfNotWritable() ->expects($this->once()) ->method('close'); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->once()) ->method('isWritable') @@ -37,13 +39,13 @@ public function itShouldCloseReadableIfNotWritable() /** @test */ public function itShouldCloseWritableIfNotReadable() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->once()) ->method('isReadable') ->willReturn(false); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->once()) ->method('close'); @@ -57,13 +59,13 @@ public function itShouldCloseWritableIfNotReadable() /** @test */ public function itShouldForwardWritableCallsToWritableStream() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->once()) ->method('isReadable') ->willReturn(true); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->once()) ->method('write') @@ -81,7 +83,7 @@ public function itShouldForwardWritableCallsToWritableStream() /** @test */ public function itShouldForwardReadableCallsToReadableStream() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->exactly(2)) ->method('isReadable') @@ -93,7 +95,7 @@ public function itShouldForwardReadableCallsToReadableStream() ->expects($this->once()) ->method('resume'); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->any()) ->method('isWritable') @@ -108,7 +110,7 @@ public function itShouldForwardReadableCallsToReadableStream() /** @test */ public function itShouldNotForwardResumeIfStreamIsNotWritable() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->once()) ->method('isReadable') @@ -117,7 +119,7 @@ public function itShouldNotForwardResumeIfStreamIsNotWritable() ->expects($this->never()) ->method('resume'); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->exactly(2)) ->method('isWritable') @@ -130,13 +132,13 @@ public function itShouldNotForwardResumeIfStreamIsNotWritable() /** @test */ public function endShouldDelegateToWritableWithData() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->once()) ->method('isReadable') ->willReturn(true); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->once()) ->method('isWritable') @@ -153,7 +155,7 @@ public function endShouldDelegateToWritableWithData() /** @test */ public function closeShouldCloseBothStreams() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->once()) ->method('isReadable') @@ -162,7 +164,7 @@ public function closeShouldCloseBothStreams() ->expects($this->once()) ->method('close'); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->once()) ->method('isWritable') @@ -224,13 +226,13 @@ public function itShouldReceiveForwardedEvents() /** @test */ public function itShouldHandlePipingCorrectly() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->once()) ->method('isReadable') ->willReturn(true); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable->expects($this->any())->method('isWritable')->willReturn(True); $writable ->expects($this->once()) @@ -249,12 +251,12 @@ public function itShouldForwardPipeCallsToReadableStream() { $readable = new ThroughStream(); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable->expects($this->any())->method('isWritable')->willReturn(True); $composite = new CompositeStream($readable, $writable); - $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $output = $this->createMock(WritableStreamInterface::class); $output->expects($this->any())->method('isWritable')->willReturn(True); $output ->expects($this->once()) diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index 196a319..02da5d2 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -13,24 +13,22 @@ class DuplexResourceStreamIntegrationTest extends TestCase { public function loopProvider() { - return array( - array( - function() { - return true; - }, - function () { - return new StreamSelectLoop(); - } - ), - array( - function () { - return class_exists('EventBase') && class_exists('React\EventLoop\ExtEventLoop'); - }, - function () { - return new ExtEventLoop(); - } - ) - ); + yield [ + function() { + return true; + }, + function () { + return new StreamSelectLoop(); + } + ]; + yield [ + function () { + return class_exists('EventBase'); + }, + function () { + return new ExtEventLoop(); + } + ]; } /** diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index f81fa90..52477fe 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -2,8 +2,10 @@ namespace React\Tests\Stream; +use React\EventLoop\LoopInterface; use React\Stream\DuplexResourceStream; use React\Stream\WritableResourceStream; +use React\Stream\WritableStreamInterface; use function Clue\StreamFilter\append as filter_append; class DuplexResourceStreamTest extends TestCase @@ -56,7 +58,7 @@ public function testConstructorThrowsExceptionOnInvalidStream() { $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new DuplexResourceStream('breakme', $loop); } @@ -67,7 +69,7 @@ public function testConstructorThrowsExceptionOnWriteOnlyStream() { $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new DuplexResourceStream(STDOUT, $loop); } @@ -82,7 +84,7 @@ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode unlink($name); $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new DuplexResourceStream($stream, $loop); } @@ -98,7 +100,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( $stream = fopen('blocking://test', 'r+'); $loop = $this->createLoopMock(); - $this->setExpectedException('RunTimeException'); + $this->expectException(\RuntimeException::class); new DuplexResourceStream($stream, $loop); } @@ -111,7 +113,7 @@ public function testConstructorAcceptsBuffer() $stream = fopen('php://temp', 'r+'); $loop = $this->createLoopMock(); - $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $buffer = $this->createMock(WritableStreamInterface::class); new DuplexResourceStream($stream, $loop, null, $buffer); } @@ -130,7 +132,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlockingW $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $this->setExpectedException('RunTimeException'); + $this->expectException(\RuntimeException::class); new DuplexResourceStream($stream, $loop, null, $buffer); } @@ -153,7 +155,7 @@ public function testEndShouldEndBuffer() $stream = fopen('php://temp', 'r+'); $loop = $this->createLoopMock(); - $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $buffer = $this->createMock(WritableStreamInterface::class); $buffer->expects($this->once())->method('end')->with('foo'); $conn = new DuplexResourceStream($stream, $loop, null, $buffer); @@ -166,7 +168,7 @@ public function testEndAfterCloseIsNoOp() $stream = fopen('php://temp', 'r+'); $loop = $this->createLoopMock(); - $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $buffer = $this->createMock(WritableStreamInterface::class); $buffer->expects($this->never())->method('end'); $conn = new DuplexResourceStream($stream, $loop); @@ -406,7 +408,7 @@ public function testPipeShouldReturnDestination() $loop = $this->createLoopMock(); $conn = new DuplexResourceStream($stream, $loop); - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $this->assertSame($dest, $conn->pipe($dest)); } @@ -517,6 +519,6 @@ private function createWriteableLoopMock() private function createLoopMock() { - return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + return $this->createMock(LoopInterface::class); } } diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 5d1f802..4cebb5a 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -2,7 +2,9 @@ namespace React\Tests\Stream; +use React\EventLoop\LoopInterface; use React\Stream\ReadableResourceStream; +use React\Stream\WritableStreamInterface; use function Clue\StreamFilter\append as filter_append; class ReadableResourceStreamTest extends TestCase @@ -55,7 +57,7 @@ public function testConstructorThrowsExceptionOnInvalidStream() { $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ReadableResourceStream(false, $loop); } @@ -66,7 +68,7 @@ public function testConstructorThrowsExceptionOnWriteOnlyStream() { $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ReadableResourceStream(STDOUT, $loop); } @@ -81,7 +83,7 @@ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode unlink($name); $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ReadableResourceStream($stream, $loop); } @@ -97,7 +99,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( $stream = fopen('blocking://test', 'r+'); $loop = $this->createLoopMock(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); new ReadableResourceStream($stream, $loop); } @@ -221,7 +223,7 @@ public function testPipeShouldReturnDestination() $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $this->assertSame($dest, $conn->pipe($dest)); } @@ -395,6 +397,6 @@ public function testEmptyReadShouldntFcloseStream() private function createLoopMock() { - return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + return $this->createMock(LoopInterface::class); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 4bca070..b161c46 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -39,62 +39,13 @@ protected function expectCallableNever() protected function createCallableMock() { - if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + $builder = $this->getMockBuilder(\stdClass::class); + if (method_exists($builder, 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(['__invoke'])->getMock(); + return $builder->addMethods(['__invoke'])->getMock(); } else { - // legacy PHPUnit 4 - PHPUnit 9 - return $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); - } - } - - public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) - { - if (method_exists($this, 'expectException')) { - // PHPUnit 5.2+ - $this->expectException($exception); - if ($exceptionMessage !== '') { - $this->expectExceptionMessage($exceptionMessage); - } - if ($exceptionCode !== null) { - $this->expectExceptionCode($exceptionCode); - } - } else { - // legacy PHPUnit 4 - PHPUnit 5.1 - parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); - } - } - - public function assertContainsString($needle, $haystack) - { - if (method_exists($this, 'assertStringContainsString')) { - // PHPUnit 7.5+ - $this->assertStringContainsString($needle, $haystack); - } else { - // legacy PHPUnit 4 - PHPUnit 7.5 - $this->assertContains($needle, $haystack); - } - } - - public function assertContainsStringIgnoringCase($needle, $haystack) - { - if (method_exists($this, 'assertStringContainsStringIgnoringCase')) { - // PHPUnit 7.5+ - $this->assertStringContainsStringIgnoringCase($needle, $haystack); - } else { - // legacy PHPUnit 4 - PHPUnit 7.5 - $this->assertContains($needle, $haystack, '', true); - } - } - - public function assertSameIgnoringCase($expected, $actual) - { - if (method_exists($this, 'assertEqualsIgnoringCase')) { - // PHPUnit 7.5+ - $this->assertEqualsIgnoringCase($expected, $actual); - } else { - // legacy PHPUnit 4 - PHPUnit 7.5 - $this->assertSame($expected, $actual); + // legacy PHPUnit + return $builder->setMethods(['__invoke'])->getMock(); } } } diff --git a/tests/ThroughStreamTest.php b/tests/ThroughStreamTest.php index 3093136..04a49d0 100644 --- a/tests/ThroughStreamTest.php +++ b/tests/ThroughStreamTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Stream; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; /** * @covers React\Stream\ThroughStream @@ -14,7 +15,7 @@ class ThroughStreamTest extends TestCase */ public function itShouldRejectInvalidCallback() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ThroughStream(123); } @@ -311,7 +312,7 @@ public function doubleCloseShouldCloseOnce() /** @test */ public function pipeShouldPipeCorrectly() { - $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $output = $this->createMock(WritableStreamInterface::class); $output->expects($this->any())->method('isWritable')->willReturn(True); $output ->expects($this->once()) diff --git a/tests/UtilTest.php b/tests/UtilTest.php index 47d3646..50b2e75 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -2,10 +2,13 @@ namespace React\Tests\Stream; -use React\Stream\WritableResourceStream; -use React\Stream\Util; +use React\EventLoop\LoopInterface; use React\Stream\CompositeStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\Util; +use React\Stream\WritableResourceStream; +use React\Stream\WritableStreamInterface; /** * @covers React\Stream\Util @@ -14,9 +17,9 @@ class UtilTest extends TestCase { public function testPipeReturnsDestinationStream() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $ret = Util::pipe($readable, $writable); @@ -25,13 +28,13 @@ public function testPipeReturnsDestinationStream() public function testPipeNonReadableSourceShouldDoNothing() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->any()) ->method('isReadable') ->willReturn(false); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->never()) ->method('isWritable'); @@ -44,7 +47,7 @@ public function testPipeNonReadableSourceShouldDoNothing() public function testPipeIntoNonWritableDestinationShouldPauseSource() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->any()) ->method('isReadable') @@ -53,7 +56,7 @@ public function testPipeIntoNonWritableDestinationShouldPauseSource() ->expects($this->once()) ->method('pause'); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->any()) ->method('isWritable') @@ -67,7 +70,7 @@ public function testPipeIntoNonWritableDestinationShouldPauseSource() public function testPipeClosingDestPausesSource() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable ->expects($this->any()) ->method('isReadable') @@ -87,7 +90,7 @@ public function testPipeWithEnd() { $readable = new Stub\ReadableStreamStub(); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->any()) ->method('isWritable') @@ -105,7 +108,7 @@ public function testPipeWithoutEnd() { $readable = new Stub\ReadableStreamStub(); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->any()) ->method('isWritable') @@ -123,7 +126,7 @@ public function testPipeWithTooSlowWritableShouldPauseReadable() { $readable = new Stub\ReadableStreamStub(); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->any()) ->method('isWritable') @@ -147,7 +150,7 @@ public function testPipeWithTooSlowWritableShouldResumeOnDrain() $onDrain = null; - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->any()) ->method('isWritable') @@ -175,7 +178,7 @@ public function testPipeWithWritableResourceStream() $readable = new Stub\ReadableStreamStub(); $stream = fopen('php://temp', 'r+'); - $loop = $this->createLoopMock(); + $loop = $this->createMock(LoopInterface::class); $buffer = new WritableResourceStream($stream, $loop); $readable->pipe($buffer); @@ -234,9 +237,9 @@ public function testPipeClosingDestRemovesListeners() public function testPipeDuplexIntoSelfEndsOnEnd() { - $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable = $this->createMock(ReadableStreamInterface::class); $readable->expects($this->any())->method('isReadable')->willReturn(true); - $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable = $this->createMock(WritableStreamInterface::class); $writable->expects($this->any())->method('isWritable')->willReturn(true); $duplex = new CompositeStream($readable, $writable); @@ -260,9 +263,4 @@ public function forwardEventsShouldSetupForwards() $source->emit('data', ['hello']); $source->emit('foo', ['bar']); } - - private function createLoopMock() - { - return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - } } diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index a05adc8..02f2367 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Stream; +use React\EventLoop\LoopInterface; use React\Stream\WritableResourceStream; use function Clue\StreamFilter\append as filter_append; @@ -56,7 +57,7 @@ public function testConstructorThrowsIfNotAValidStreamResource() $stream = null; $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new WritableResourceStream($stream, $loop); } @@ -68,7 +69,7 @@ public function testConstructorThrowsExceptionOnReadOnlyStream() $stream = fopen('php://temp', 'r'); $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new WritableResourceStream($stream, $loop); } @@ -83,7 +84,7 @@ public function testConstructorThrowsExceptionOnReadOnlyStreamWithExcessiveMode( unlink($name); $loop = $this->createLoopMock(); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new WritableResourceStream($stream, $loop); } @@ -99,7 +100,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( $stream = fopen('blocking://test', 'r+'); $loop = $this->createLoopMock(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); new WritableResourceStream($stream, $loop); } @@ -502,8 +503,8 @@ public function testWritingToClosedStream() $buffer->write('bar'); $buffer->handleWrite(); - $this->assertInstanceOf('Exception', $error); - $this->assertSameIgnoringCase('Unable to write to stream: fwrite(): send of 3 bytes failed with errno=32 Broken pipe', $error->getMessage()); + $this->assertInstanceOf(\Exception::class, $error); + $this->assertEqualsIgnoringCase('Unable to write to stream: fwrite(): send of 3 bytes failed with errno=32 Broken pipe', $error->getMessage()); } private function createWriteableLoopMock() @@ -521,6 +522,6 @@ private function createWriteableLoopMock() private function createLoopMock() { - return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + return $this->createMock(LoopInterface::class); } } From 74d748d2d98f5d46eeb30a53ea827f857f2f41d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 May 2024 12:23:03 +0200 Subject: [PATCH 47/51] Improve PHP 8.4+ support by avoiding implicitly nullable types --- src/DuplexResourceStream.php | 8 +++++++- src/ReadableResourceStream.php | 7 ++++++- src/WritableResourceStream.php | 8 +++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index 0b52c9b..32a668d 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -38,7 +38,13 @@ final class DuplexResourceStream extends EventEmitter implements DuplexStreamInt private $closing = false; private $listening = false; - public function __construct($stream, LoopInterface $loop = null, $readChunkSize = null, WritableStreamInterface $buffer = null) + /** + * @param resource $stream + * @param ?LoopInterface $loop + * @param ?int $readChunkSize + * @param ?WritableStreamInterface $buffer + */ + public function __construct($stream, ?LoopInterface $loop = null, $readChunkSize = null, ?WritableStreamInterface $buffer = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 41314fe..627fbf9 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -40,7 +40,12 @@ final class ReadableResourceStream extends EventEmitter implements ReadableStrea private $closed = false; private $listening = false; - public function __construct($stream, LoopInterface $loop = null, $readChunkSize = null) + /** + * @param resource $stream + * @param ?LoopInterface $loop + * @param ?int $readChunkSize + */ + public function __construct($stream, ?LoopInterface $loop = null, $readChunkSize = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 9665721..9792a5f 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -28,7 +28,13 @@ final class WritableResourceStream extends EventEmitter implements WritableStrea private $closed = false; private $data = ''; - public function __construct($stream, LoopInterface $loop = null, $writeBufferSoftLimit = null, $writeChunkSize = null) + /** + * @param resource $stream + * @param ?LoopInterface $loop + * @param ?int $writeBufferSoftLimit + * @param ?int $writeChunkSize + */ + public function __construct($stream, ?LoopInterface $loop = null, $writeBufferSoftLimit = null, $writeChunkSize = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new \InvalidArgumentException('First parameter must be a valid stream resource'); From 74dd76353dde1eec7bba9a844273938cd099ecf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 15 May 2024 20:18:59 +0200 Subject: [PATCH 48/51] Add native types to public API --- README.md | 2 +- src/CompositeStream.php | 16 ++++++++-------- src/DuplexResourceStream.php | 20 ++++++++++---------- src/ReadableResourceStream.php | 14 +++++++------- src/ReadableStreamInterface.php | 10 +++++----- src/ThroughStream.php | 22 +++++++++------------- src/Util.php | 10 ++++++++-- src/WritableResourceStream.php | 14 +++++++------- src/WritableStreamInterface.php | 8 ++++---- tests/Stub/ReadableStreamStub.php | 10 +++++----- tests/ThroughStreamTest.php | 9 --------- 11 files changed, 64 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index cce1408..fee7f67 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ See also `pause()`. #### pipe() -The `pipe(WritableStreamInterface $dest, array $options = [])` method can be used to +The `pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface` method can be used to pipe all the data from this readable source into the given writable destination. Automatically sends all incoming data to the destination. diff --git a/src/CompositeStream.php b/src/CompositeStream.php index d0b934b..10cef07 100644 --- a/src/CompositeStream.php +++ b/src/CompositeStream.php @@ -27,17 +27,17 @@ public function __construct(ReadableStreamInterface $readable, WritableStreamInt $this->writable->on('close', [$this, 'close']); } - public function isReadable() + public function isReadable(): bool { return $this->readable->isReadable(); } - public function pause() + public function pause(): void { $this->readable->pause(); } - public function resume() + public function resume(): void { if (!$this->writable->isWritable()) { return; @@ -46,28 +46,28 @@ public function resume() $this->readable->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = []) + public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface { return Util::pipe($this, $dest, $options); } - public function isWritable() + public function isWritable(): bool { return $this->writable->isWritable(); } - public function write($data) + public function write($data): bool { return $this->writable->write($data); } - public function end($data = null) + public function end($data = null): void { $this->readable->pause(); $this->writable->end($data); } - public function close() + public function close(): void { if ($this->closed) { return; diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index 32a668d..bd05013 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -44,7 +44,7 @@ final class DuplexResourceStream extends EventEmitter implements DuplexStreamInt * @param ?int $readChunkSize * @param ?WritableStreamInterface $buffer */ - public function __construct($stream, ?LoopInterface $loop = null, $readChunkSize = null, ?WritableStreamInterface $buffer = null) + public function __construct($stream, ?LoopInterface $loop = null, ?int $readChunkSize = null, ?WritableStreamInterface $buffer = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); @@ -75,7 +75,7 @@ public function __construct($stream, ?LoopInterface $loop = null, $readChunkSize $this->stream = $stream; $this->loop = $loop ?: Loop::get(); - $this->bufferSize = ($readChunkSize === null) ? 65536 : (int)$readChunkSize; + $this->bufferSize = $readChunkSize ?? 65536; $this->buffer = $buffer; $this->buffer->on('error', function ($error) { @@ -91,17 +91,17 @@ public function __construct($stream, ?LoopInterface $loop = null, $readChunkSize $this->resume(); } - public function isReadable() + public function isReadable(): bool { return $this->readable; } - public function isWritable() + public function isWritable(): bool { return $this->writable; } - public function pause() + public function pause(): void { if ($this->listening) { $this->loop->removeReadStream($this->stream); @@ -109,7 +109,7 @@ public function pause() } } - public function resume() + public function resume(): void { if (!$this->listening && $this->readable) { $this->loop->addReadStream($this->stream, [$this, 'handleData']); @@ -117,7 +117,7 @@ public function resume() } } - public function write($data) + public function write($data): bool { if (!$this->writable) { return false; @@ -126,7 +126,7 @@ public function write($data) return $this->buffer->write($data); } - public function close() + public function close(): void { if (!$this->writable && !$this->closing) { return; @@ -147,7 +147,7 @@ public function close() } } - public function end($data = null) + public function end($data = null): void { if (!$this->writable) { return; @@ -162,7 +162,7 @@ public function end($data = null) $this->buffer->end($data); } - public function pipe(WritableStreamInterface $dest, array $options = []) + public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface { return Util::pipe($this, $dest, $options); } diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 627fbf9..592e35c 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -45,7 +45,7 @@ final class ReadableResourceStream extends EventEmitter implements ReadableStrea * @param ?LoopInterface $loop * @param ?int $readChunkSize */ - public function __construct($stream, ?LoopInterface $loop = null, $readChunkSize = null) + public function __construct($stream, ?LoopInterface $loop = null, ?int $readChunkSize = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new InvalidArgumentException('First parameter must be a valid stream resource'); @@ -72,17 +72,17 @@ public function __construct($stream, ?LoopInterface $loop = null, $readChunkSize $this->stream = $stream; $this->loop = $loop ?: Loop::get(); - $this->bufferSize = ($readChunkSize === null) ? 65536 : (int)$readChunkSize; + $this->bufferSize = $readChunkSize ?? 65536; $this->resume(); } - public function isReadable() + public function isReadable(): bool { return !$this->closed; } - public function pause() + public function pause(): void { if ($this->listening) { $this->loop->removeReadStream($this->stream); @@ -90,7 +90,7 @@ public function pause() } } - public function resume() + public function resume(): void { if (!$this->listening && !$this->closed) { $this->loop->addReadStream($this->stream, [$this, 'handleData']); @@ -98,12 +98,12 @@ public function resume() } } - public function pipe(WritableStreamInterface $dest, array $options = []) + public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface { return Util::pipe($this, $dest, $options); } - public function close() + public function close(): void { if ($this->closed) { return; diff --git a/src/ReadableStreamInterface.php b/src/ReadableStreamInterface.php index ecc5267..e4ddf32 100644 --- a/src/ReadableStreamInterface.php +++ b/src/ReadableStreamInterface.php @@ -194,7 +194,7 @@ interface ReadableStreamInterface extends EventEmitterInterface * * @return bool */ - public function isReadable(); + public function isReadable(): bool; /** * Pauses reading incoming data events. @@ -226,7 +226,7 @@ public function isReadable(); * @see self::resume() * @return void */ - public function pause(); + public function pause(): void; /** * Resumes reading incoming data events. @@ -247,7 +247,7 @@ public function pause(); * @see self::pause() * @return void */ - public function resume(); + public function resume(): void; /** * Pipes all the data from this readable source into the given writable destination. @@ -322,7 +322,7 @@ public function resume(); * @param array $options * @return WritableStreamInterface $dest stream as-is */ - public function pipe(WritableStreamInterface $dest, array $options = []); + public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface; /** * Closes the stream (forcefully). @@ -358,5 +358,5 @@ public function pipe(WritableStreamInterface $dest, array $options = []); * @return void * @see WritableStreamInterface::close() */ - public function close(); + public function close(): void; } diff --git a/src/ThroughStream.php b/src/ThroughStream.php index c49ebfc..4c1f4a2 100644 --- a/src/ThroughStream.php +++ b/src/ThroughStream.php @@ -82,22 +82,18 @@ final class ThroughStream extends EventEmitter implements DuplexStreamInterface private $drain = false; private $callback; - public function __construct($callback = null) + public function __construct(?callable $callback = null) { - if ($callback !== null && !\is_callable($callback)) { - throw new InvalidArgumentException('Invalid transformation callback given'); - } - $this->callback = $callback; } - public function pause() + public function pause(): void { // only allow pause if still readable, false otherwise $this->paused = $this->readable; } - public function resume() + public function resume(): void { $this->paused = false; @@ -108,22 +104,22 @@ public function resume() } } - public function pipe(WritableStreamInterface $dest, array $options = []) + public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface { return Util::pipe($this, $dest, $options); } - public function isReadable() + public function isReadable(): bool { return $this->readable; } - public function isWritable() + public function isWritable(): bool { return $this->writable; } - public function write($data) + public function write($data): bool { if (!$this->writable) { return false; @@ -151,7 +147,7 @@ public function write($data) return $this->writable && !$this->paused; } - public function end($data = null) + public function end($data = null): void { if (!$this->writable) { return; @@ -175,7 +171,7 @@ public function end($data = null) $this->close(); } - public function close() + public function close(): void { if ($this->closed) { return; diff --git a/src/Util.php b/src/Util.php index 114ccb8..23dd252 100644 --- a/src/Util.php +++ b/src/Util.php @@ -13,7 +13,7 @@ final class Util * @return WritableStreamInterface $dest stream as-is * @see ReadableStreamInterface::pipe() for more details */ - public static function pipe(ReadableStreamInterface $source, WritableStreamInterface $dest, array $options = []) + public static function pipe(ReadableStreamInterface $source, WritableStreamInterface $dest, array $options = []): WritableStreamInterface { // source not readable => NO-OP if (!$source->isReadable()) { @@ -64,7 +64,13 @@ public static function pipe(ReadableStreamInterface $source, WritableStreamInter return $dest; } - public static function forwardEvents($source, $target, array $events) + /** + * @param ReadableStreamInterface|WritableStreamInterface $source + * @param ReadableStreamInterface|WritableStreamInterface $target + * @param string[] $events + * @return void + */ + public static function forwardEvents($source, $target, array $events): void { foreach ($events as $event) { $source->on($event, function () use ($event, $target) { diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 9792a5f..84ec4f6 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -34,7 +34,7 @@ final class WritableResourceStream extends EventEmitter implements WritableStrea * @param ?int $writeBufferSoftLimit * @param ?int $writeChunkSize */ - public function __construct($stream, ?LoopInterface $loop = null, $writeBufferSoftLimit = null, $writeChunkSize = null) + public function __construct($stream, ?LoopInterface $loop = null, ?int $writeBufferSoftLimit = null, ?int $writeChunkSize = null) { if (!\is_resource($stream) || \get_resource_type($stream) !== "stream") { throw new \InvalidArgumentException('First parameter must be a valid stream resource'); @@ -54,16 +54,16 @@ public function __construct($stream, ?LoopInterface $loop = null, $writeBufferSo $this->stream = $stream; $this->loop = $loop ?: Loop::get(); - $this->softLimit = ($writeBufferSoftLimit === null) ? 65536 : (int)$writeBufferSoftLimit; - $this->writeChunkSize = ($writeChunkSize === null) ? -1 : (int)$writeChunkSize; + $this->softLimit = $writeBufferSoftLimit ?? 65536; + $this->writeChunkSize = $writeChunkSize ?? -1; } - public function isWritable() + public function isWritable(): bool { return $this->writable; } - public function write($data) + public function write($data): bool { if (!$this->writable) { return false; @@ -80,7 +80,7 @@ public function write($data) return !isset($this->data[$this->softLimit - 1]); } - public function end($data = null) + public function end($data = null): void { if (null !== $data) { $this->write($data); @@ -95,7 +95,7 @@ public function end($data = null) } } - public function close() + public function close(): void { if ($this->closed) { return; diff --git a/src/WritableStreamInterface.php b/src/WritableStreamInterface.php index e262592..77baaad 100644 --- a/src/WritableStreamInterface.php +++ b/src/WritableStreamInterface.php @@ -170,7 +170,7 @@ interface WritableStreamInterface extends EventEmitterInterface * * @return bool */ - public function isWritable(); + public function isWritable(): bool; /** * Write some data into the stream. @@ -219,7 +219,7 @@ public function isWritable(); * @param mixed|string $data * @return bool */ - public function write($data); + public function write($data): bool; /** * Successfully ends the stream (after optionally sending some final data). @@ -292,7 +292,7 @@ public function write($data); * @param mixed|string|null $data * @return void */ - public function end($data = null); + public function end($data = null): void; /** * Closes the stream (forcefully). @@ -343,5 +343,5 @@ public function end($data = null); * @return void * @see ReadableStreamInterface::close() */ - public function close(); + public function close(): void; } diff --git a/tests/Stub/ReadableStreamStub.php b/tests/Stub/ReadableStreamStub.php index 3fa56a8..669059c 100644 --- a/tests/Stub/ReadableStreamStub.php +++ b/tests/Stub/ReadableStreamStub.php @@ -12,7 +12,7 @@ class ReadableStreamStub extends EventEmitter implements ReadableStreamInterface public $readable = true; public $paused = false; - public function isReadable() + public function isReadable(): bool { return true; } @@ -35,24 +35,24 @@ public function end() $this->emit('end', []); } - public function pause() + public function pause(): void { $this->paused = true; } - public function resume() + public function resume(): void { $this->paused = false; } - public function close() + public function close(): void { $this->readable = false; $this->emit('close'); } - public function pipe(WritableStreamInterface $dest, array $options = []) + public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface { Util::pipe($this, $dest, $options); diff --git a/tests/ThroughStreamTest.php b/tests/ThroughStreamTest.php index 04a49d0..85c22de 100644 --- a/tests/ThroughStreamTest.php +++ b/tests/ThroughStreamTest.php @@ -10,15 +10,6 @@ */ class ThroughStreamTest extends TestCase { - /** - * @test - */ - public function itShouldRejectInvalidCallback() - { - $this->expectException(\InvalidArgumentException::class); - new ThroughStream(123); - } - /** @test */ public function itShouldReturnTrueForAnyDataWrittenToIt() { From 9234da07cb99728b5530c0534176d0dc4c847d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 15 May 2024 20:35:55 +0200 Subject: [PATCH 49/51] Add PHPStan to test environment --- .gitattributes | 1 + .github/workflows/ci.yml | 23 +++++++++++++++++++ README.md | 6 +++++ composer.json | 5 ++-- phpstan.neon.dist | 7 ++++++ src/DuplexResourceStream.php | 3 ++- src/ReadableResourceStream.php | 3 ++- src/ThroughStream.php | 2 ++ src/WritableResourceStream.php | 3 ++- tests/CompositeStreamTest.php | 18 +++++++++++++++ tests/DuplexResourceStreamIntegrationTest.php | 22 +++++++++--------- tests/DuplexResourceStreamTest.php | 7 +++++- tests/ReadableResourceStreamTest.php | 17 +++++++------- tests/ThroughStreamTest.php | 3 ++- tests/UtilTest.php | 16 +++++++++++++ tests/WritableResourceStreamTest.php | 3 ++- 16 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/.gitattributes b/.gitattributes index fc0be87..e3b263e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ /.github/ export-ignore /.gitignore export-ignore /examples/ export-ignore +/phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore /tests/ export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f654fb0..1bb8701 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,3 +47,26 @@ jobs: - run: composer install - run: vendor/bin/phpunit --coverage-text - run: time php examples/91-benchmark-throughput.php + + PHPStan: + name: PHPStan (PHP ${{ matrix.php }}) + runs-on: ubuntu-22.04 + strategy: + matrix: + php: + - 8.3 + - 8.2 + - 8.1 + - 8.0 + - 7.4 + - 7.3 + - 7.2 + - 7.1 + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + - run: composer install + - run: vendor/bin/phpstan diff --git a/README.md b/README.md index fee7f67..e27cbec 100644 --- a/README.md +++ b/README.md @@ -1243,6 +1243,12 @@ If you do not want to run these, they can simply be skipped like this: vendor/bin/phpunit --exclude-group internet ``` +On top of this, we use PHPStan on level 5 to ensure type safety across the project: + +```bash +vendor/bin/phpstan +``` + ## License MIT, see [LICENSE file](LICENSE). diff --git a/composer.json b/composer.json index b8d34d4..beb0a56 100644 --- a/composer.json +++ b/composer.json @@ -31,8 +31,9 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^7.5", - "clue/stream-filter": "~1.2" + "clue/stream-filter": "^1.2", + "phpstan/phpstan": "1.11.1 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "autoload": { "psr-4": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..d631a6d --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +parameters: + level: 5 + + paths: + - examples/ + - src/ + - tests/ diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index bd05013..94d013c 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -171,7 +171,7 @@ public function pipe(WritableStreamInterface $dest, array $options = []): Writab public function handleData($stream) { $error = null; - \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error): bool { $error = new \ErrorException( $errstr, 0, @@ -179,6 +179,7 @@ public function handleData($stream) $errfile, $errline ); + return true; }); $data = \stream_get_contents($stream, $this->bufferSize); diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index 592e35c..a0993bb 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -124,7 +124,7 @@ public function close(): void public function handleData() { $error = null; - \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error): bool { $error = new \ErrorException( $errstr, 0, @@ -132,6 +132,7 @@ public function handleData() $errfile, $errline ); + return true; }); $data = \stream_get_contents($this->stream, $this->bufferSize); diff --git a/src/ThroughStream.php b/src/ThroughStream.php index 4c1f4a2..a209982 100644 --- a/src/ThroughStream.php +++ b/src/ThroughStream.php @@ -144,6 +144,7 @@ public function write($data): bool } // continue writing if still writable and not paused (throttled), false otherwise + // @phpstan-ignore-next-line (may be false when write() causes stream to close) return $this->writable && !$this->paused; } @@ -157,6 +158,7 @@ public function end($data = null): void $this->write($data); // return if write() already caused the stream to close + // @phpstan-ignore-next-line (may be false when write() causes stream to close) if (!$this->writable) { return; } diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 84ec4f6..6140246 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -122,8 +122,9 @@ public function close(): void public function handleWrite() { $error = null; - \set_error_handler(function ($_, $errstr) use (&$error) { + \set_error_handler(function ($_, $errstr) use (&$error): bool { $error = $errstr; + return true; }); if ($this->writeChunkSize === -1) { diff --git a/tests/CompositeStreamTest.php b/tests/CompositeStreamTest.php index 75a0426..cc9ef28 100644 --- a/tests/CompositeStreamTest.php +++ b/tests/CompositeStreamTest.php @@ -23,12 +23,14 @@ public function itShouldCloseReadableIfNotWritable() $readable ->expects($this->once()) ->method('close'); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->once()) ->method('isWritable') ->willReturn(false); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); @@ -44,11 +46,13 @@ public function itShouldCloseWritableIfNotReadable() ->expects($this->once()) ->method('isReadable') ->willReturn(false); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->once()) ->method('close'); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); @@ -64,6 +68,7 @@ public function itShouldForwardWritableCallsToWritableStream() ->expects($this->once()) ->method('isReadable') ->willReturn(true); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable @@ -74,6 +79,7 @@ public function itShouldForwardWritableCallsToWritableStream() ->expects($this->exactly(2)) ->method('isWritable') ->willReturn(true); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); $composite->write('foo'); @@ -94,12 +100,14 @@ public function itShouldForwardReadableCallsToReadableStream() $readable ->expects($this->once()) ->method('resume'); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->any()) ->method('isWritable') ->willReturn(true); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); $composite->isReadable(); @@ -118,12 +126,14 @@ public function itShouldNotForwardResumeIfStreamIsNotWritable() $readable ->expects($this->never()) ->method('resume'); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable ->expects($this->exactly(2)) ->method('isWritable') ->willReturnOnConsecutiveCalls(true, false); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); $composite->resume(); @@ -137,6 +147,7 @@ public function endShouldDelegateToWritableWithData() ->expects($this->once()) ->method('isReadable') ->willReturn(true); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable @@ -147,6 +158,7 @@ public function endShouldDelegateToWritableWithData() ->expects($this->once()) ->method('end') ->with('foo'); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); $composite->end('foo'); @@ -163,6 +175,7 @@ public function closeShouldCloseBothStreams() $readable ->expects($this->once()) ->method('close'); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable @@ -172,6 +185,7 @@ public function closeShouldCloseBothStreams() $writable ->expects($this->once()) ->method('close'); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); $composite->close(); @@ -231,6 +245,7 @@ public function itShouldHandlePipingCorrectly() ->expects($this->once()) ->method('isReadable') ->willReturn(true); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable->expects($this->any())->method('isWritable')->willReturn(True); @@ -238,6 +253,7 @@ public function itShouldHandlePipingCorrectly() ->expects($this->once()) ->method('write') ->with('foo'); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); @@ -253,6 +269,7 @@ public function itShouldForwardPipeCallsToReadableStream() $writable = $this->createMock(WritableStreamInterface::class); $writable->expects($this->any())->method('isWritable')->willReturn(True); + assert($writable instanceof WritableStreamInterface); $composite = new CompositeStream($readable, $writable); @@ -262,6 +279,7 @@ public function itShouldForwardPipeCallsToReadableStream() ->expects($this->once()) ->method('write') ->with('foo'); + assert($output instanceof WritableStreamInterface); $composite->pipe($output); $readable->emit('data', ['foo']); diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index 02da5d2..a468aef 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -37,7 +37,7 @@ function () { public function testBufferReadsLargeChunks($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -73,7 +73,7 @@ public function testBufferReadsLargeChunks($condition, $loopFactory) public function testWriteLargeChunk($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -113,7 +113,7 @@ public function testWriteLargeChunk($condition, $loopFactory) public function testDoesNotEmitDataIfNothingHasBeenWritten($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -141,7 +141,7 @@ public function testDoesNotEmitDataIfNothingHasBeenWritten($condition, $loopFact public function testDoesNotWriteDataIfRemoteSideFromPairHasBeenClosed($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -171,7 +171,7 @@ public function testDoesNotWriteDataIfRemoteSideFromPairHasBeenClosed($condition public function testDoesNotWriteDataIfServerSideHasBeenClosed($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -204,7 +204,7 @@ public function testDoesNotWriteDataIfServerSideHasBeenClosed($condition, $loopF public function testDoesNotWriteDataIfClientSideHasBeenClosed($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -237,7 +237,7 @@ public function testDoesNotWriteDataIfClientSideHasBeenClosed($condition, $loopF public function testReadsSingleChunkFromProcessPipe($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -256,7 +256,7 @@ public function testReadsSingleChunkFromProcessPipe($condition, $loopFactory) public function testReadsMultipleChunksFromProcessPipe($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -282,7 +282,7 @@ public function testReadsMultipleChunksFromProcessPipe($condition, $loopFactory) public function testReadsLongChunksFromProcessPipe($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -308,7 +308,7 @@ public function testReadsLongChunksFromProcessPipe($condition, $loopFactory) public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $loop = $loopFactory(); @@ -328,7 +328,7 @@ public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFac public function testEmptyReadShouldntFcloseStream($condition, $loopFactory) { if (true !== $condition()) { - return $this->markTestSkipped('Loop implementation not available'); + $this->markTestSkipped('Loop implementation not available'); } $server = stream_socket_server('tcp://127.0.0.1:0'); diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index 52477fe..5b18553 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -59,7 +59,7 @@ public function testConstructorThrowsExceptionOnInvalidStream() $loop = $this->createLoopMock(); $this->expectException(\InvalidArgumentException::class); - new DuplexResourceStream('breakme', $loop); + new DuplexResourceStream('breakme', $loop); // @phpstan-ignore-line } /** @@ -114,6 +114,7 @@ public function testConstructorAcceptsBuffer() $loop = $this->createLoopMock(); $buffer = $this->createMock(WritableStreamInterface::class); + assert($buffer instanceof WritableStreamInterface); new DuplexResourceStream($stream, $loop, null, $buffer); } @@ -131,6 +132,7 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlockingW $loop = $this->createLoopMock(); $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + assert($buffer instanceof WritableStreamInterface); $this->expectException(\RuntimeException::class); new DuplexResourceStream($stream, $loop, null, $buffer); @@ -157,6 +159,7 @@ public function testEndShouldEndBuffer() $buffer = $this->createMock(WritableStreamInterface::class); $buffer->expects($this->once())->method('end')->with('foo'); + assert($buffer instanceof WritableStreamInterface); $conn = new DuplexResourceStream($stream, $loop, null, $buffer); $conn->end('foo'); @@ -170,6 +173,7 @@ public function testEndAfterCloseIsNoOp() $buffer = $this->createMock(WritableStreamInterface::class); $buffer->expects($this->never())->method('end'); + assert($buffer instanceof WritableStreamInterface); $conn = new DuplexResourceStream($stream, $loop); $conn->close(); @@ -409,6 +413,7 @@ public function testPipeShouldReturnDestination() $conn = new DuplexResourceStream($stream, $loop); $dest = $this->createMock(WritableStreamInterface::class); + assert($dest instanceof WritableStreamInterface); $this->assertSame($dest, $conn->pipe($dest)); } diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 4cebb5a..365ac0d 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -58,7 +58,7 @@ public function testConstructorThrowsExceptionOnInvalidStream() $loop = $this->createLoopMock(); $this->expectException(\InvalidArgumentException::class); - new ReadableResourceStream(false, $loop); + new ReadableResourceStream(false, $loop); // @phpstan-ignore-line } /** @@ -148,7 +148,7 @@ public function testDataEvent() fwrite($stream, "foobar\n"); rewind($stream); - $conn->handleData($stream); + $conn->handleData(); $this->assertSame("foobar\n", $capturedData); } @@ -171,7 +171,7 @@ public function testDataEventDoesEmitOneChunkMatchingBufferSize() fwrite($stream, str_repeat("a", 100000)); rewind($stream); - $conn->handleData($stream); + $conn->handleData(); $this->assertTrue($conn->isReadable()); $this->assertEquals(4321, strlen($capturedData)); @@ -197,7 +197,7 @@ public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfi fwrite($stream, str_repeat("a", 100000)); rewind($stream); - $conn->handleData($stream); + $conn->handleData(); $this->assertTrue($conn->isReadable()); $this->assertEquals(100000, strlen($capturedData)); @@ -214,7 +214,7 @@ public function testEmptyStreamShouldNotEmitData() $conn = new ReadableResourceStream($stream, $loop); $conn->on('data', $this->expectCallableNever()); - $conn->handleData($stream); + $conn->handleData(); } public function testPipeShouldReturnDestination() @@ -224,6 +224,7 @@ public function testPipeShouldReturnDestination() $conn = new ReadableResourceStream($stream, $loop); $dest = $this->createMock(WritableStreamInterface::class); + assert($dest instanceof WritableStreamInterface); $this->assertSame($dest, $conn->pipe($dest)); } @@ -245,7 +246,7 @@ public function testClosingStreamInDataEventShouldNotTriggerError() fwrite($stream, "foobar\n"); rewind($stream); - $conn->handleData($stream); + $conn->handleData(); } /** @@ -344,7 +345,7 @@ public function testDataFiltered() fwrite($stream, "foobar\n"); rewind($stream); - $conn->handleData($stream); + $conn->handleData(); $this->assertSame("foobr\n", $capturedData); } @@ -373,7 +374,7 @@ public function testDataErrorShouldEmitErrorAndClose() fwrite($stream, "foobar\n"); rewind($stream); - $conn->handleData($stream); + $conn->handleData(); } /** diff --git a/tests/ThroughStreamTest.php b/tests/ThroughStreamTest.php index 85c22de..ddd570f 100644 --- a/tests/ThroughStreamTest.php +++ b/tests/ThroughStreamTest.php @@ -216,7 +216,7 @@ public function endShouldWriteDataBeforeClosing() public function endTwiceShouldOnlyEmitOnce() { $through = new ThroughStream(); - $through->on('data', $this->expectCallableOnce('first')); + $through->on('data', $this->expectCallableOnceWith('first')); $through->end('first'); $through->end('ignored'); } @@ -309,6 +309,7 @@ public function pipeShouldPipeCorrectly() ->expects($this->once()) ->method('write') ->with('foo'); + assert($output instanceof WritableStreamInterface); $through = new ThroughStream(); $through->pipe($output); diff --git a/tests/UtilTest.php b/tests/UtilTest.php index 50b2e75..cd00c8b 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -18,8 +18,10 @@ class UtilTest extends TestCase public function testPipeReturnsDestinationStream() { $readable = $this->createMock(ReadableStreamInterface::class); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); + assert($writable instanceof WritableStreamInterface); $ret = Util::pipe($readable, $writable); @@ -33,6 +35,7 @@ public function testPipeNonReadableSourceShouldDoNothing() ->expects($this->any()) ->method('isReadable') ->willReturn(false); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable @@ -41,6 +44,7 @@ public function testPipeNonReadableSourceShouldDoNothing() $writable ->expects($this->never()) ->method('end'); + assert($writable instanceof WritableStreamInterface); Util::pipe($readable, $writable); } @@ -55,6 +59,7 @@ public function testPipeIntoNonWritableDestinationShouldPauseSource() $readable ->expects($this->once()) ->method('pause'); + assert($readable instanceof ReadableStreamInterface); $writable = $this->createMock(WritableStreamInterface::class); $writable @@ -64,6 +69,7 @@ public function testPipeIntoNonWritableDestinationShouldPauseSource() $writable ->expects($this->never()) ->method('end'); + assert($writable instanceof WritableStreamInterface); Util::pipe($readable, $writable); } @@ -78,6 +84,7 @@ public function testPipeClosingDestPausesSource() $readable ->expects($this->once()) ->method('pause'); + assert($readable instanceof ReadableStreamInterface); $writable = new ThroughStream(); @@ -98,6 +105,7 @@ public function testPipeWithEnd() $writable ->expects($this->once()) ->method('end'); + assert($writable instanceof WritableStreamInterface); Util::pipe($readable, $writable); @@ -116,6 +124,7 @@ public function testPipeWithoutEnd() $writable ->expects($this->never()) ->method('end'); + assert($writable instanceof WritableStreamInterface); Util::pipe($readable, $writable, ['end' => false]); @@ -136,6 +145,7 @@ public function testPipeWithTooSlowWritableShouldPauseReadable() ->method('write') ->with('some data') ->will($this->returnValue(false)); + assert($writable instanceof WritableStreamInterface); $readable->pipe($writable); @@ -163,6 +173,7 @@ public function testPipeWithTooSlowWritableShouldResumeOnDrain() $onDrain = $callback; } })); + assert($writable instanceof WritableStreamInterface); $readable->pipe($writable); $readable->pause(); @@ -179,6 +190,7 @@ public function testPipeWithWritableResourceStream() $stream = fopen('php://temp', 'r+'); $loop = $this->createMock(LoopInterface::class); + assert($loop instanceof LoopInterface); $buffer = new WritableResourceStream($stream, $loop); $readable->pipe($buffer); @@ -239,8 +251,12 @@ public function testPipeDuplexIntoSelfEndsOnEnd() { $readable = $this->createMock(ReadableStreamInterface::class); $readable->expects($this->any())->method('isReadable')->willReturn(true); + assert($readable instanceof ReadableStreamInterface); + $writable = $this->createMock(WritableStreamInterface::class); $writable->expects($this->any())->method('isWritable')->willReturn(true); + assert($writable instanceof WritableStreamInterface); + $duplex = new CompositeStream($readable, $writable); Util::pipe($duplex, $duplex); diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index 02f2367..df3e815 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -58,7 +58,7 @@ public function testConstructorThrowsIfNotAValidStreamResource() $loop = $this->createLoopMock(); $this->expectException(\InvalidArgumentException::class); - new WritableResourceStream($stream, $loop); + new WritableResourceStream($stream, $loop); // @phpstan-ignore-line } /** @@ -165,6 +165,7 @@ public function testWriteReturnsFalseWhenWritableResourceStreamIsFull() ->expects($this->any()) ->method('addWriteStream') ->will($this->returnCallback(function ($stream, $listener) use (&$preventWrites) { + /** @var bool $preventWrites */ if (!$preventWrites) { call_user_func($listener, $stream); } From 5047943c375e568f52ae67bb3a5abfb60af65025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 15 May 2024 21:15:56 +0200 Subject: [PATCH 50/51] Improve type definitions and update to PHPStan level `max` --- README.md | 36 ++--- examples/01-http.php | 4 +- examples/02-https.php | 4 +- examples/91-benchmark-throughput.php | 12 +- phpstan.neon.dist | 2 +- src/CompositeStream.php | 5 + src/DuplexResourceStream.php | 23 +++- src/ReadableResourceStream.php | 7 +- src/ReadableStreamInterface.php | 14 +- src/ThroughStream.php | 19 ++- src/Util.php | 16 +-- src/WritableResourceStream.php | 23 ++-- src/WritableStreamInterface.php | 12 +- tests/CompositeStreamTest.php | 24 ++-- tests/DuplexResourceStreamIntegrationTest.php | 80 +++++++---- tests/DuplexResourceStreamTest.php | 128 +++++++++++++----- tests/EnforceBlockingWrapper.php | 9 +- tests/FunctionalInternetTest.php | 14 +- tests/ReadableResourceStreamTest.php | 98 ++++++++++---- tests/Stub/ReadableStreamStub.php | 16 ++- tests/TestCase.php | 18 ++- tests/ThroughStreamTest.php | 56 ++++---- tests/UtilTest.php | 30 ++-- tests/WritableResourceStreamTest.php | 124 ++++++++++++----- 24 files changed, 511 insertions(+), 263 deletions(-) diff --git a/README.md b/README.md index e27cbec..870a907 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ from this source stream. The event receives a single mixed argument for incoming data. ```php -$stream->on('data', function ($data) { +$stream->on('data', function (mixed $data): void { echo $data; }); ``` @@ -142,7 +142,7 @@ The `end` event will be emitted once the source stream has successfully reached the end of the stream (EOF). ```php -$stream->on('end', function () { +$stream->on('end', function (): void { echo 'END'; }); ``` @@ -180,7 +180,7 @@ trying to read from this stream. The event receives a single `Exception` argument for the error instance. ```php -$server->on('error', function (Exception $e) { +$server->on('error', function (Exception $e): void { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -213,7 +213,7 @@ stream which should result in the same error processing. The `close` event will be emitted once the stream closes (terminates). ```php -$stream->on('close', function () { +$stream->on('close', function (): void { echo 'CLOSED'; }); ``` @@ -312,7 +312,7 @@ Re-attach the data source after a previous `pause()`. ```php $stream->pause(); -Loop::addTimer(1.0, function () use ($stream) { +Loop::addTimer(1.0, function () use ($stream): void { $stream->resume(); }); ``` @@ -362,7 +362,7 @@ you'll have to manually close the destination stream: ```php $source->pipe($dest); -$source->on('close', function () use ($dest) { +$source->on('close', function () use ($dest): void { $dest->end('BYE!'); }); ``` @@ -456,7 +456,7 @@ The `drain` event will be emitted whenever the write buffer became full previously and is now ready to accept more data. ```php -$stream->on('drain', function () use ($stream) { +$stream->on('drain', function () use ($stream): void { echo 'Stream is now ready to accept more data'; }); ``` @@ -478,11 +478,11 @@ The event receives a single `ReadableStreamInterface` argument for the source stream. ```php -$stream->on('pipe', function (ReadableStreamInterface $source) use ($stream) { +$stream->on('pipe', function (ReadableStreamInterface $source) use ($stream): void { echo 'Now receiving piped data'; // explicitly close target if source emits an error - $source->on('error', function () use ($stream) { + $source->on('error', function () use ($stream): void { $stream->close(); }); }); @@ -506,7 +506,7 @@ trying to write to this stream. The event receives a single `Exception` argument for the error instance. ```php -$stream->on('error', function (Exception $e) { +$stream->on('error', function (Exception $e): void { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -536,7 +536,7 @@ stream which should result in the same error processing. The `close` event will be emitted once the stream closes (terminates). ```php -$stream->on('close', function () { +$stream->on('close', function (): void { echo 'CLOSED'; }); ``` @@ -746,7 +746,7 @@ stream in order to stop waiting for the stream to flush its final data. ```php $stream->end(); -Loop::addTimer(1.0, function () use ($stream) { +Loop::addTimer(1.0, function () use ($stream): void { $stream->close(); }); ``` @@ -831,10 +831,10 @@ readable mode or a stream such as `STDIN`: ```php $stream = new ReadableResourceStream(STDIN); -$stream->on('data', function ($chunk) { +$stream->on('data', function (string $chunk): void { echo $chunk; }); -$stream->on('end', function () { +$stream->on('end', function (): void { echo 'END'; }); ``` @@ -1121,7 +1121,7 @@ used to convert data, for example for transforming any structured data into a newline-delimited JSON (NDJSON) stream like this: ```php -$through = new ThroughStream(function ($data) { +$through = new ThroughStream(function (mixed $data): string { return json_encode($data) . PHP_EOL; }); $through->on('data', $this->expectCallableOnceWith("[2, true]\n")); @@ -1133,7 +1133,7 @@ The callback function is allowed to throw an `Exception`. In this case, the stream will emit an `error` event and then [`close()`](#close-1) the stream. ```php -$through = new ThroughStream(function ($data) { +$through = new ThroughStream(function (mixed $data): string { if (!is_string($data)) { throw new \UnexpectedValueException('Only strings allowed'); } @@ -1164,7 +1164,7 @@ $stdout = new WritableResourceStream(STDOUT); $stdio = new CompositeStream($stdin, $stdout); -$stdio->on('data', function ($chunk) use ($stdio) { +$stdio->on('data', function (string $chunk) use ($stdio): void { $stdio->write('You said: ' . $chunk); }); ``` @@ -1243,7 +1243,7 @@ If you do not want to run these, they can simply be skipped like this: vendor/bin/phpunit --exclude-group internet ``` -On top of this, we use PHPStan on level 5 to ensure type safety across the project: +On top of this, we use PHPStan on max level to ensure type safety across the project: ```bash vendor/bin/phpstan diff --git a/examples/01-http.php b/examples/01-http.php index 786aefc..316e7e3 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -26,10 +26,10 @@ $stream = new DuplexResourceStream($resource); -$stream->on('data', function ($chunk) { +$stream->on('data', function (string $chunk): void { echo $chunk; }); -$stream->on('close', function () { +$stream->on('close', function (): void { echo '[CLOSED]' . PHP_EOL; }); diff --git a/examples/02-https.php b/examples/02-https.php index e629791..8b3ad79 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -26,10 +26,10 @@ $stream = new DuplexResourceStream($resource); -$stream->on('data', function ($chunk) { +$stream->on('data', function (string $chunk): void { echo $chunk; }); -$stream->on('close', function () { +$stream->on('close', function (): void { echo '[CLOSED]' . PHP_EOL; }); diff --git a/examples/91-benchmark-throughput.php b/examples/91-benchmark-throughput.php index c396606..3d0213c 100644 --- a/examples/91-benchmark-throughput.php +++ b/examples/91-benchmark-throughput.php @@ -22,8 +22,11 @@ $args = getopt('i:o:t:'); $if = $args['i'] ?? '/dev/zero'; +assert(is_string($if)); $of = $args['o'] ?? '/dev/null'; +assert(is_string($of)); $t = $args['t'] ?? 1; +assert(is_numeric($t)); // passing file descriptors requires mapping paths (https://bugs.php.net/bug.php?id=53465) $if = str_replace('/dev/fd/', 'php://fd/', $if); @@ -38,18 +41,21 @@ // setup input and output streams and pipe inbetween $fh = fopen($if, 'r'); +assert(is_resource($fh)); +$fo = fopen($of, 'w'); +assert(is_resource($fo)); $in = new React\Stream\ReadableResourceStream($fh); -$out = new React\Stream\WritableResourceStream(fopen($of, 'w')); +$out = new React\Stream\WritableResourceStream($fo); $in->pipe($out); // stop input stream in $t seconds $start = microtime(true); -$timeout = Loop::addTimer($t, function () use ($in) { +$timeout = Loop::addTimer((float) $t, function () use ($in): void { $in->close(); }); // print stream position once stream closes -$in->on('close', function () use ($fh, $start, $timeout, $info) { +$in->on('close', function () use ($fh, $start, $timeout, $info): void { $t = microtime(true) - $start; Loop::cancelTimer($timeout); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d631a6d..0fe275f 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 5 + level: max paths: - examples/ diff --git a/src/CompositeStream.php b/src/CompositeStream.php index 10cef07..db6db78 100644 --- a/src/CompositeStream.php +++ b/src/CompositeStream.php @@ -6,8 +6,13 @@ final class CompositeStream extends EventEmitter implements DuplexStreamInterface { + /** @var ReadableStreamInterface */ private $readable; + + /** @var WritableStreamInterface */ private $writable; + + /** @var bool */ private $closed = false; public function __construct(ReadableStreamInterface $readable, WritableStreamInterface $writable) diff --git a/src/DuplexResourceStream.php b/src/DuplexResourceStream.php index 94d013c..5990906 100644 --- a/src/DuplexResourceStream.php +++ b/src/DuplexResourceStream.php @@ -9,6 +9,7 @@ final class DuplexResourceStream extends EventEmitter implements DuplexStreamInterface { + /** @var resource */ private $stream; /** @var LoopInterface */ @@ -31,11 +32,20 @@ final class DuplexResourceStream extends EventEmitter implements DuplexStreamInt * @var int */ private $bufferSize; + + /** @var WritableStreamInterface */ private $buffer; + /** @var bool */ private $readable = true; + + /** @var bool */ private $writable = true; + + /** @var bool */ private $closing = false; + + /** @var bool */ private $listening = false; /** @@ -78,13 +88,13 @@ public function __construct($stream, ?LoopInterface $loop = null, ?int $readChun $this->bufferSize = $readChunkSize ?? 65536; $this->buffer = $buffer; - $this->buffer->on('error', function ($error) { + $this->buffer->on('error', function (\Exception $error): void { $this->emit('error', [$error]); }); $this->buffer->on('close', [$this, 'close']); - $this->buffer->on('drain', function () { + $this->buffer->on('drain', function (): void { $this->emit('drain'); }); @@ -167,11 +177,14 @@ public function pipe(WritableStreamInterface $dest, array $options = []): Writab return Util::pipe($this, $dest, $options); } - /** @internal */ - public function handleData($stream) + /** + * @internal + * @param resource $stream + */ + public function handleData($stream): void { $error = null; - \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error): bool { + \set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$error): bool { $error = new \ErrorException( $errstr, 0, diff --git a/src/ReadableResourceStream.php b/src/ReadableResourceStream.php index a0993bb..cf1161a 100644 --- a/src/ReadableResourceStream.php +++ b/src/ReadableResourceStream.php @@ -37,7 +37,10 @@ final class ReadableResourceStream extends EventEmitter implements ReadableStrea */ private $bufferSize; + /** @var bool */ private $closed = false; + + /** @var bool */ private $listening = false; /** @@ -121,10 +124,10 @@ public function close(): void } /** @internal */ - public function handleData() + public function handleData(): void { $error = null; - \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error): bool { + \set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$error): bool { $error = new \ErrorException( $errstr, 0, diff --git a/src/ReadableStreamInterface.php b/src/ReadableStreamInterface.php index e4ddf32..94e5915 100644 --- a/src/ReadableStreamInterface.php +++ b/src/ReadableStreamInterface.php @@ -17,7 +17,7 @@ * The event receives a single mixed argument for incoming data. * * ```php - * $stream->on('data', function ($data) { + * $stream->on('data', function (mixed $data): void { * echo $data; * }); * ``` @@ -47,7 +47,7 @@ * reached the end of the stream (EOF). * * ```php - * $stream->on('end', function () { + * $stream->on('end', function (): void { * echo 'END'; * }); * ``` @@ -84,7 +84,7 @@ * The event receives a single `Exception` argument for the error instance. * * ```php - * $stream->on('error', function (Exception $e) { + * $stream->on('error', function (Exception $e): void { * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` @@ -116,7 +116,7 @@ * The `close` event will be emitted once the stream closes (terminates). * * ```php - * $stream->on('close', function () { + * $stream->on('close', function (): void { * echo 'CLOSED'; * }); * ``` @@ -236,7 +236,7 @@ public function pause(): void; * ```php * $stream->pause(); * - * Loop::addTimer(1.0, function () use ($stream) { + * Loop::addTimer(1.0, function () use ($stream): void { * $stream->resume(); * }); * ``` @@ -287,7 +287,7 @@ public function resume(): void; * * ```php * $source->pipe($dest); - * $source->on('close', function () use ($dest) { + * $source->on('close', function () use ($dest): void { * $dest->end('BYE!'); * }); * ``` @@ -319,7 +319,7 @@ public function resume(): void; * a `pipe` event with this source stream an event argument. * * @param WritableStreamInterface $dest - * @param array $options + * @param array{end?:bool} $options * @return WritableStreamInterface $dest stream as-is */ public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface; diff --git a/src/ThroughStream.php b/src/ThroughStream.php index a209982..a8e4837 100644 --- a/src/ThroughStream.php +++ b/src/ThroughStream.php @@ -3,7 +3,6 @@ namespace React\Stream; use Evenement\EventEmitter; -use InvalidArgumentException; /** * The `ThroughStream` implements the @@ -43,7 +42,7 @@ * a newline-delimited JSON (NDJSON) stream like this: * * ```php - * $through = new ThroughStream(function ($data) { + * $through = new ThroughStream(function (mixed $data): string { * return json_encode($data) . PHP_EOL; * }); * $through->on('data', $this->expectCallableOnceWith("[2, true]\n")); @@ -55,7 +54,7 @@ * the stream will emit an `error` event and then [`close()`](#close-1) the stream. * * ```php - * $through = new ThroughStream(function ($data) { + * $through = new ThroughStream(function (mixed $data): string { * if (!is_string($data)) { * throw new \UnexpectedValueException('Only strings allowed'); * } @@ -75,13 +74,27 @@ */ final class ThroughStream extends EventEmitter implements DuplexStreamInterface { + /** @var bool */ private $readable = true; + + /** @var bool */ private $writable = true; + + /** @var bool */ private $closed = false; + + /** @var bool */ private $paused = false; + + /** @var bool */ private $drain = false; + + /** @var ?callable */ private $callback; + /** + * @param ?callable $callback + */ public function __construct(?callable $callback = null) { $this->callback = $callback; diff --git a/src/Util.php b/src/Util.php index 23dd252..c6c038d 100644 --- a/src/Util.php +++ b/src/Util.php @@ -9,7 +9,7 @@ final class Util * * @param ReadableStreamInterface $source * @param WritableStreamInterface $dest - * @param array $options + * @param array{end?:bool} $options * @return WritableStreamInterface $dest stream as-is * @see ReadableStreamInterface::pipe() for more details */ @@ -30,33 +30,33 @@ public static function pipe(ReadableStreamInterface $source, WritableStreamInter $dest->emit('pipe', [$source]); // forward all source data events as $dest->write() - $source->on('data', $dataer = function ($data) use ($source, $dest) { + $source->on('data', $dataer = function ($data) use ($source, $dest): void { $feedMore = $dest->write($data); if (false === $feedMore) { $source->pause(); } }); - $dest->on('close', function () use ($source, $dataer) { + $dest->on('close', function () use ($source, $dataer): void { $source->removeListener('data', $dataer); $source->pause(); }); // forward destination drain as $source->resume() - $dest->on('drain', $drainer = function () use ($source) { + $dest->on('drain', $drainer = function () use ($source): void { $source->resume(); }); - $source->on('close', function () use ($dest, $drainer) { + $source->on('close', function () use ($dest, $drainer): void { $dest->removeListener('drain', $drainer); }); // forward end event from source as $dest->end() $end = isset($options['end']) ? $options['end'] : true; if ($end) { - $source->on('end', $ender = function () use ($dest) { + $source->on('end', $ender = function () use ($dest): void { $dest->end(); }); - $dest->on('close', function () use ($source, $ender) { + $dest->on('close', function () use ($source, $ender): void { $source->removeListener('end', $ender); }); } @@ -73,7 +73,7 @@ public static function pipe(ReadableStreamInterface $source, WritableStreamInter public static function forwardEvents($source, $target, array $events): void { foreach ($events as $event) { - $source->on($event, function () use ($event, $target) { + $source->on($event, function () use ($event, $target): void { $target->emit($event, \func_get_args()); }); } diff --git a/src/WritableResourceStream.php b/src/WritableResourceStream.php index 6140246..68b2721 100644 --- a/src/WritableResourceStream.php +++ b/src/WritableResourceStream.php @@ -8,24 +8,28 @@ final class WritableResourceStream extends EventEmitter implements WritableStreamInterface { + /** @var resource */ private $stream; /** @var LoopInterface */ private $loop; - /** - * @var int - */ + /** @var int */ private $softLimit; - /** - * @var int - */ + /** @var int */ private $writeChunkSize; + /** @var bool */ private $listening = false; + + /** @var bool */ private $writable = true; + + /** @var bool */ private $closed = false; + + /** @var string */ private $data = ''; /** @@ -119,10 +123,10 @@ public function close(): void } /** @internal */ - public function handleWrite() + public function handleWrite(): void { $error = null; - \set_error_handler(function ($_, $errstr) use (&$error): bool { + \set_error_handler(function (int $_, string $errstr) use (&$error): bool { $error = $errstr; return true; }); @@ -130,6 +134,7 @@ public function handleWrite() if ($this->writeChunkSize === -1) { $sent = \fwrite($this->stream, $this->data); } else { + \assert($this->writeChunkSize >= -1); $sent = \fwrite($this->stream, $this->data, $this->writeChunkSize); } @@ -151,7 +156,7 @@ public function handleWrite() } $exceeded = isset($this->data[$this->softLimit - 1]); - $this->data = (string) \substr($this->data, $sent); + $this->data = (string) \substr($this->data, (int) $sent); // buffer has been above limit and is now below limit if ($exceeded && !isset($this->data[$this->softLimit - 1])) { diff --git a/src/WritableStreamInterface.php b/src/WritableStreamInterface.php index 77baaad..d48536d 100644 --- a/src/WritableStreamInterface.php +++ b/src/WritableStreamInterface.php @@ -16,7 +16,7 @@ * previously and is now ready to accept more data. * * ```php - * $stream->on('drain', function () use ($stream) { + * $stream->on('drain', function () use ($stream): void { * echo 'Stream is now ready to accept more data'; * }); * ``` @@ -37,11 +37,11 @@ * source stream. * * ```php - * $stream->on('pipe', function (ReadableStreamInterface $source) use ($stream) { + * $stream->on('pipe', function (ReadableStreamInterface $source) use ($stream): void { * echo 'Now receiving piped data'; * * // explicitly close target if source emits an error - * $source->on('error', function () use ($stream) { + * $source->on('error', function () use ($stream): void { * $stream->close(); * }); * }); @@ -64,7 +64,7 @@ * The event receives a single `Exception` argument for the error instance. * * ```php - * $stream->on('error', function (Exception $e) { + * $stream->on('error', function (Exception $e): void { * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` @@ -93,7 +93,7 @@ * The `close` event will be emitted once the stream closes (terminates). * * ```php - * $stream->on('close', function () { + * $stream->on('close', function (): void { * echo 'CLOSED'; * }); * ``` @@ -330,7 +330,7 @@ public function end($data = null): void; * * ```php * $stream->end(); - * Loop::addTimer(1.0, function () use ($stream) { + * Loop::addTimer(1.0, function () use ($stream): void { * $stream->close(); * }); * ``` diff --git a/tests/CompositeStreamTest.php b/tests/CompositeStreamTest.php index cc9ef28..6e43ad7 100644 --- a/tests/CompositeStreamTest.php +++ b/tests/CompositeStreamTest.php @@ -13,7 +13,7 @@ class CompositeStreamTest extends TestCase { /** @test */ - public function itShouldCloseReadableIfNotWritable() + public function itShouldCloseReadableIfNotWritable(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -39,7 +39,7 @@ public function itShouldCloseReadableIfNotWritable() } /** @test */ - public function itShouldCloseWritableIfNotReadable() + public function itShouldCloseWritableIfNotReadable(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -61,7 +61,7 @@ public function itShouldCloseWritableIfNotReadable() } /** @test */ - public function itShouldForwardWritableCallsToWritableStream() + public function itShouldForwardWritableCallsToWritableStream(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -87,7 +87,7 @@ public function itShouldForwardWritableCallsToWritableStream() } /** @test */ - public function itShouldForwardReadableCallsToReadableStream() + public function itShouldForwardReadableCallsToReadableStream(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -116,7 +116,7 @@ public function itShouldForwardReadableCallsToReadableStream() } /** @test */ - public function itShouldNotForwardResumeIfStreamIsNotWritable() + public function itShouldNotForwardResumeIfStreamIsNotWritable(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -140,7 +140,7 @@ public function itShouldNotForwardResumeIfStreamIsNotWritable() } /** @test */ - public function endShouldDelegateToWritableWithData() + public function endShouldDelegateToWritableWithData(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -165,7 +165,7 @@ public function endShouldDelegateToWritableWithData() } /** @test */ - public function closeShouldCloseBothStreams() + public function closeShouldCloseBothStreams(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -192,7 +192,7 @@ public function closeShouldCloseBothStreams() } /** @test */ - public function itShouldForwardCloseOnlyOnce() + public function itShouldForwardCloseOnlyOnce(): void { $readable = new ThroughStream(); $writable = new ThroughStream(); @@ -205,7 +205,7 @@ public function itShouldForwardCloseOnlyOnce() } /** @test */ - public function itShouldForwardCloseAndRemoveAllListeners() + public function itShouldForwardCloseAndRemoveAllListeners(): void { $in = new ThroughStream(); @@ -224,7 +224,7 @@ public function itShouldForwardCloseAndRemoveAllListeners() } /** @test */ - public function itShouldReceiveForwardedEvents() + public function itShouldReceiveForwardedEvents(): void { $readable = new ThroughStream(); $writable = new ThroughStream(); @@ -238,7 +238,7 @@ public function itShouldReceiveForwardedEvents() } /** @test */ - public function itShouldHandlePipingCorrectly() + public function itShouldHandlePipingCorrectly(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -263,7 +263,7 @@ public function itShouldHandlePipingCorrectly() } /** @test */ - public function itShouldForwardPipeCallsToReadableStream() + public function itShouldForwardPipeCallsToReadableStream(): void { $readable = new ThroughStream(); diff --git a/tests/DuplexResourceStreamIntegrationTest.php b/tests/DuplexResourceStreamIntegrationTest.php index a468aef..2c3d2ed 100644 --- a/tests/DuplexResourceStreamIntegrationTest.php +++ b/tests/DuplexResourceStreamIntegrationTest.php @@ -11,7 +11,7 @@ class DuplexResourceStreamIntegrationTest extends TestCase { - public function loopProvider() + public function loopProvider(): \Generator { yield [ function() { @@ -34,7 +34,7 @@ function () { /** * @dataProvider loopProvider */ - public function testBufferReadsLargeChunks($condition, $loopFactory) + public function testBufferReadsLargeChunks(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -42,7 +42,9 @@ public function testBufferReadsLargeChunks($condition, $loopFactory) $loop = $loopFactory(); - list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + assert(is_array($pair)); + [$sockA, $sockB] = $pair; $bufferSize = 4096; $streamA = new DuplexResourceStream($sockA, $loop, $bufferSize); @@ -70,7 +72,7 @@ public function testBufferReadsLargeChunks($condition, $loopFactory) /** * @dataProvider loopProvider */ - public function testWriteLargeChunk($condition, $loopFactory) + public function testWriteLargeChunk(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -78,7 +80,9 @@ public function testWriteLargeChunk($condition, $loopFactory) $loop = $loopFactory(); - list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + assert(is_array($pair)); + [$sockA, $sockB] = $pair; $streamA = new DuplexResourceStream($sockA, $loop); $streamB = new DuplexResourceStream($sockB, $loop); @@ -110,7 +114,7 @@ public function testWriteLargeChunk($condition, $loopFactory) /** * @dataProvider loopProvider */ - public function testDoesNotEmitDataIfNothingHasBeenWritten($condition, $loopFactory) + public function testDoesNotEmitDataIfNothingHasBeenWritten(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -118,7 +122,9 @@ public function testDoesNotEmitDataIfNothingHasBeenWritten($condition, $loopFact $loop = $loopFactory(); - list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + assert(is_array($pair)); + [$sockA, $sockB] = $pair; $streamA = new DuplexResourceStream($sockA, $loop); $streamB = new DuplexResourceStream($sockB, $loop); @@ -138,7 +144,7 @@ public function testDoesNotEmitDataIfNothingHasBeenWritten($condition, $loopFact /** * @dataProvider loopProvider */ - public function testDoesNotWriteDataIfRemoteSideFromPairHasBeenClosed($condition, $loopFactory) + public function testDoesNotWriteDataIfRemoteSideFromPairHasBeenClosed(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -146,7 +152,9 @@ public function testDoesNotWriteDataIfRemoteSideFromPairHasBeenClosed($condition $loop = $loopFactory(); - list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + assert(is_array($pair)); + [$sockA, $sockB] = $pair; $streamA = new DuplexResourceStream($sockA, $loop); $streamB = new DuplexResourceStream($sockB, $loop); @@ -168,7 +176,7 @@ public function testDoesNotWriteDataIfRemoteSideFromPairHasBeenClosed($condition /** * @dataProvider loopProvider */ - public function testDoesNotWriteDataIfServerSideHasBeenClosed($condition, $loopFactory) + public function testDoesNotWriteDataIfServerSideHasBeenClosed(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -177,9 +185,13 @@ public function testDoesNotWriteDataIfServerSideHasBeenClosed($condition, $loopF $loop = $loopFactory(); $server = stream_socket_server('tcp://127.0.0.1:0'); + assert(is_resource($server)); + + $client = stream_socket_client((string) stream_socket_get_name($server, false)); + assert(is_resource($client)); - $client = stream_socket_client(stream_socket_get_name($server, false)); $peer = stream_socket_accept($server); + assert(is_resource($peer)); $streamA = new DuplexResourceStream($client, $loop); $streamB = new DuplexResourceStream($peer, $loop); @@ -201,7 +213,7 @@ public function testDoesNotWriteDataIfServerSideHasBeenClosed($condition, $loopF /** * @dataProvider loopProvider */ - public function testDoesNotWriteDataIfClientSideHasBeenClosed($condition, $loopFactory) + public function testDoesNotWriteDataIfClientSideHasBeenClosed(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -210,9 +222,13 @@ public function testDoesNotWriteDataIfClientSideHasBeenClosed($condition, $loopF $loop = $loopFactory(); $server = stream_socket_server('tcp://127.0.0.1:0'); + assert(is_resource($server)); + + $client = stream_socket_client((string) stream_socket_get_name($server, false)); + assert(is_resource($client)); - $client = stream_socket_client(stream_socket_get_name($server, false)); $peer = stream_socket_accept($server); + assert(is_resource($peer)); $streamA = new DuplexResourceStream($peer, $loop); $streamB = new DuplexResourceStream($client, $loop); @@ -234,7 +250,7 @@ public function testDoesNotWriteDataIfClientSideHasBeenClosed($condition, $loopF /** * @dataProvider loopProvider */ - public function testReadsSingleChunkFromProcessPipe($condition, $loopFactory) + public function testReadsSingleChunkFromProcessPipe(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -242,7 +258,10 @@ public function testReadsSingleChunkFromProcessPipe($condition, $loopFactory) $loop = $loopFactory(); - $stream = new ReadableResourceStream(popen('echo test', 'r'), $loop); + $fh = popen('echo test', 'r'); + assert(is_resource($fh)); + + $stream = new ReadableResourceStream($fh, $loop); $stream->on('data', $this->expectCallableOnceWith("test\n")); $stream->on('end', $this->expectCallableOnce()); $stream->on('error', $this->expectCallableNever()); @@ -253,7 +272,7 @@ public function testReadsSingleChunkFromProcessPipe($condition, $loopFactory) /** * @dataProvider loopProvider */ - public function testReadsMultipleChunksFromProcessPipe($condition, $loopFactory) + public function testReadsMultipleChunksFromProcessPipe(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -261,7 +280,10 @@ public function testReadsMultipleChunksFromProcessPipe($condition, $loopFactory) $loop = $loopFactory(); - $stream = new ReadableResourceStream(popen('echo a;sleep 0.1;echo b;sleep 0.1;echo c', 'r'), $loop); + $fh = popen('echo a;sleep 0.1;echo b;sleep 0.1;echo c', 'r'); + assert(is_resource($fh)); + + $stream = new ReadableResourceStream($fh, $loop); $buffer = ''; $stream->on('data', function ($chunk) use (&$buffer) { @@ -279,7 +301,7 @@ public function testReadsMultipleChunksFromProcessPipe($condition, $loopFactory) /** * @dataProvider loopProvider */ - public function testReadsLongChunksFromProcessPipe($condition, $loopFactory) + public function testReadsLongChunksFromProcessPipe(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -287,7 +309,10 @@ public function testReadsLongChunksFromProcessPipe($condition, $loopFactory) $loop = $loopFactory(); - $stream = new ReadableResourceStream(popen('dd if=/dev/zero bs=12345 count=1234 2>&-', 'r'), $loop); + $fh = popen('dd if=/dev/zero bs=12345 count=1234 2>&-', 'r'); + assert(is_resource($fh)); + + $stream = new ReadableResourceStream($fh, $loop); $bytes = 0; $stream->on('data', function ($chunk) use (&$bytes) { @@ -305,7 +330,7 @@ public function testReadsLongChunksFromProcessPipe($condition, $loopFactory) /** * @dataProvider loopProvider */ - public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFactory) + public function testReadsNothingFromProcessPipeWithNoOutput(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); @@ -313,7 +338,10 @@ public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFac $loop = $loopFactory(); - $stream = new ReadableResourceStream(popen('true', 'r'), $loop); + $fh = popen('true', 'r'); + assert(is_resource($fh)); + + $stream = new ReadableResourceStream($fh, $loop); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableOnce()); $stream->on('error', $this->expectCallableNever()); @@ -325,16 +353,20 @@ public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFac * @covers React\Stream\ReadableResourceStream::handleData * @dataProvider loopProvider */ - public function testEmptyReadShouldntFcloseStream($condition, $loopFactory) + public function testEmptyReadShouldntFcloseStream(callable $condition, callable $loopFactory): void { if (true !== $condition()) { $this->markTestSkipped('Loop implementation not available'); } $server = stream_socket_server('tcp://127.0.0.1:0'); + assert(is_resource($server)); + + $client = stream_socket_client((string) stream_socket_get_name($server, false)); + assert(is_resource($client)); - $client = stream_socket_client(stream_socket_get_name($server, false)); $stream = stream_socket_accept($server); + assert(is_resource($stream)); // add a filter which returns an error when encountering an 'a' when reading @@ -358,7 +390,7 @@ public function testEmptyReadShouldntFcloseStream($condition, $loopFactory) fclose($server); } - private function loopTick(LoopInterface $loop) + private function loopTick(LoopInterface $loop): void { $loop->addTimer(0, function () use ($loop) { $loop->stop(); diff --git a/tests/DuplexResourceStreamTest.php b/tests/DuplexResourceStreamTest.php index 5b18553..c1076a6 100644 --- a/tests/DuplexResourceStreamTest.php +++ b/tests/DuplexResourceStreamTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Stream; +use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; use React\Stream\DuplexResourceStream; use React\Stream\WritableResourceStream; @@ -14,17 +15,20 @@ class DuplexResourceStreamTest extends TestCase * @covers React\Stream\DuplexResourceStream::__construct * @doesNotPerformAssertions */ - public function testConstructor() + public function testConstructor(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); new DuplexResourceStream($stream, $loop); } - public function testConstructWithoutLoopAssignsLoopAutomatically() + public function testConstructWithoutLoopAssignsLoopAutomatically(): void { $resource = fopen('php://temp', 'r+'); + assert(is_resource($resource)); $stream = new DuplexResourceStream($resource); @@ -39,11 +43,12 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() * @covers React\Stream\DuplexResourceStream::__construct * @doesNotPerformAssertions */ - public function testConstructorWithExcessiveMode() + public function testConstructorWithExcessiveMode(): void { // excessive flags are ignored for temp streams, so we have to use a file stream - $name = tempnam(sys_get_temp_dir(), 'test'); - $stream = @fopen($name, 'r+eANYTHING'); + $name = (string) tempnam(sys_get_temp_dir(), 'test'); + $stream = fopen($name, 'r+eANYTHING'); + assert(is_resource($stream)); unlink($name); $loop = $this->createLoopMock(); @@ -54,7 +59,7 @@ public function testConstructorWithExcessiveMode() /** * @covers React\Stream\DuplexResourceStream::__construct */ - public function testConstructorThrowsExceptionOnInvalidStream() + public function testConstructorThrowsExceptionOnInvalidStream(): void { $loop = $this->createLoopMock(); @@ -65,7 +70,7 @@ public function testConstructorThrowsExceptionOnInvalidStream() /** * @covers React\Stream\DuplexResourceStream::__construct */ - public function testConstructorThrowsExceptionOnWriteOnlyStream() + public function testConstructorThrowsExceptionOnWriteOnlyStream(): void { $loop = $this->createLoopMock(); @@ -76,11 +81,12 @@ public function testConstructorThrowsExceptionOnWriteOnlyStream() /** * @covers React\Stream\DuplexResourceStream::__construct */ - public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode() + public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode(): void { // excessive flags are ignored for temp streams, so we have to use a file stream - $name = tempnam(sys_get_temp_dir(), 'test'); + $name = (string) tempnam(sys_get_temp_dir(), 'test'); $stream = fopen($name, 'weANYTHING'); + assert(is_resource($stream)); unlink($name); $loop = $this->createLoopMock(); @@ -91,13 +97,15 @@ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode /** * @covers React\Stream\DuplexResourceStream::__construct */ - public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking(): void { if (!in_array('blocking', stream_get_wrappers())) { stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); } $stream = fopen('blocking://test', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $this->expectException(\RuntimeException::class); @@ -108,9 +116,11 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( * @covers React\Stream\DuplexResourceStream::__construct * @doesNotPerformAssertions */ - public function testConstructorAcceptsBuffer() + public function testConstructorAcceptsBuffer(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = $this->createMock(WritableStreamInterface::class); @@ -122,13 +132,15 @@ public function testConstructorAcceptsBuffer() /** * @covers React\Stream\DuplexResourceStream::__construct */ - public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlockingWithBufferGiven() + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlockingWithBufferGiven(): void { if (!in_array('blocking', stream_get_wrappers())) { stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); } $stream = fopen('blocking://test', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); @@ -138,9 +150,11 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlockingW new DuplexResourceStream($stream, $loop, null, $buffer); } - public function testCloseShouldEmitCloseEvent() + public function testCloseShouldEmitCloseEvent(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new DuplexResourceStream($stream, $loop); @@ -152,9 +166,11 @@ public function testCloseShouldEmitCloseEvent() $this->assertFalse($conn->isReadable()); } - public function testEndShouldEndBuffer() + public function testEndShouldEndBuffer(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = $this->createMock(WritableStreamInterface::class); @@ -166,9 +182,11 @@ public function testEndShouldEndBuffer() } - public function testEndAfterCloseIsNoOp() + public function testEndAfterCloseIsNoOp(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = $this->createMock(WritableStreamInterface::class); @@ -184,9 +202,11 @@ public function testEndAfterCloseIsNoOp() * @covers React\Stream\DuplexResourceStream::__construct * @covers React\Stream\DuplexResourceStream::handleData */ - public function testDataEvent() + public function testDataEvent(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $capturedData = null; @@ -207,9 +227,11 @@ public function testDataEvent() * @covers React\Stream\DuplexResourceStream::__construct * @covers React\Stream\DuplexResourceStream::handleData */ - public function testDataEventDoesEmitOneChunkMatchingBufferSize() + public function testDataEventDoesEmitOneChunkMatchingBufferSize(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $capturedData = null; @@ -232,9 +254,11 @@ public function testDataEventDoesEmitOneChunkMatchingBufferSize() * @covers React\Stream\DuplexResourceStream::__construct * @covers React\Stream\DuplexResourceStream::handleData */ - public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfinite() + public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfinite(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $capturedData = null; @@ -257,9 +281,11 @@ public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfi /** * @covers React\Stream\DuplexResourceStream::handleData */ - public function testEmptyStreamShouldNotEmitData() + public function testEmptyStreamShouldNotEmitData(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new DuplexResourceStream($stream, $loop); @@ -271,9 +297,11 @@ public function testEmptyStreamShouldNotEmitData() /** * @covers React\Stream\DuplexResourceStream::write */ - public function testWrite() + public function testWrite(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createWriteableLoopMock(); $conn = new DuplexResourceStream($stream, $loop); @@ -288,9 +316,11 @@ public function testWrite() * @covers React\Stream\DuplexResourceStream::isReadable * @covers React\Stream\DuplexResourceStream::isWritable */ - public function testEnd() + public function testEnd(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new DuplexResourceStream($stream, $loop); @@ -304,9 +334,11 @@ public function testEnd() /** * @covers React\Stream\DuplexResourceStream::end */ - public function testEndRemovesReadStreamFromLoop() + public function testEndRemovesReadStreamFromLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); $loop->expects($this->once())->method('removeReadStream')->with($stream); @@ -318,9 +350,11 @@ public function testEndRemovesReadStreamFromLoop() /** * @covers React\Stream\DuplexResourceStream::pause */ - public function testPauseRemovesReadStreamFromLoop() + public function testPauseRemovesReadStreamFromLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); $loop->expects($this->once())->method('removeReadStream')->with($stream); @@ -333,9 +367,11 @@ public function testPauseRemovesReadStreamFromLoop() /** * @covers React\Stream\DuplexResourceStream::pause */ - public function testResumeDoesAddStreamToLoopOnlyOnce() + public function testResumeDoesAddStreamToLoopOnlyOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); @@ -347,9 +383,11 @@ public function testResumeDoesAddStreamToLoopOnlyOnce() /** * @covers React\Stream\DuplexResourceStream::close */ - public function testCloseRemovesReadStreamFromLoop() + public function testCloseRemovesReadStreamFromLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); $loop->expects($this->once())->method('removeReadStream')->with($stream); @@ -361,9 +399,11 @@ public function testCloseRemovesReadStreamFromLoop() /** * @covers React\Stream\DuplexResourceStream::close */ - public function testCloseAfterPauseRemovesReadStreamFromLoopOnlyOnce() + public function testCloseAfterPauseRemovesReadStreamFromLoopOnlyOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); $loop->expects($this->once())->method('removeReadStream')->with($stream); @@ -376,9 +416,11 @@ public function testCloseAfterPauseRemovesReadStreamFromLoopOnlyOnce() /** * @covers React\Stream\DuplexResourceStream::close */ - public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce() + public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); @@ -387,10 +429,12 @@ public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce() $conn->resume(); } - public function testEndedStreamsShouldNotWrite() + public function testEndedStreamsShouldNotWrite(): void { - $file = tempnam(sys_get_temp_dir(), 'reactphptest_'); + $file = (string) tempnam(sys_get_temp_dir(), 'reactphptest_'); $stream = fopen($file, 'r+'); + assert(is_resource($stream)); + $loop = $this->createWriteableLoopMock(); $conn = new DuplexResourceStream($stream, $loop); @@ -398,7 +442,9 @@ public function testEndedStreamsShouldNotWrite() $conn->end(); $res = $conn->write("bar\n"); + $stream = fopen($file, 'r'); + assert(is_resource($stream)); $this->assertSame("foo\n", fgets($stream)); $this->assertFalse($res); @@ -406,9 +452,11 @@ public function testEndedStreamsShouldNotWrite() unlink($file); } - public function testPipeShouldReturnDestination() + public function testPipeShouldReturnDestination(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new DuplexResourceStream($stream, $loop); @@ -418,9 +466,11 @@ public function testPipeShouldReturnDestination() $this->assertSame($dest, $conn->pipe($dest)); } - public function testBufferEventsShouldBubbleUp() + public function testBufferEventsShouldBubbleUp(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -436,9 +486,11 @@ public function testBufferEventsShouldBubbleUp() /** * @covers React\Stream\DuplexResourceStream::handleData */ - public function testClosingStreamInDataEventShouldNotTriggerError() + public function testClosingStreamInDataEventShouldNotTriggerError(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new DuplexResourceStream($stream, $loop); @@ -456,9 +508,10 @@ public function testClosingStreamInDataEventShouldNotTriggerError() /** * @covers React\Stream\DuplexResourceStream::handleData */ - public function testDataFiltered() + public function testDataFiltered(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); // add a filter which removes every 'a' when reading filter_append($stream, function ($chunk) { @@ -484,9 +537,10 @@ public function testDataFiltered() /** * @covers React\Stream\DuplexResourceStream::handleData */ - public function testDataErrorShouldEmitErrorAndClose() + public function testDataErrorShouldEmitErrorAndClose(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); // add a filter which returns an error when encountering an 'a' when reading filter_append($stream, function ($chunk) { @@ -509,7 +563,7 @@ public function testDataErrorShouldEmitErrorAndClose() $conn->handleData($stream); } - private function createWriteableLoopMock() + private function createWriteableLoopMock(): LoopInterface { $loop = $this->createLoopMock(); $loop @@ -522,8 +576,10 @@ private function createWriteableLoopMock() return $loop; } - private function createLoopMock() + /** @return LoopInterface&MockObject */ + private function createLoopMock(): MockObject { + /** @var MockObject&LoopInterface */ return $this->createMock(LoopInterface::class); } } diff --git a/tests/EnforceBlockingWrapper.php b/tests/EnforceBlockingWrapper.php index a171b41..cab433b 100644 --- a/tests/EnforceBlockingWrapper.php +++ b/tests/EnforceBlockingWrapper.php @@ -9,24 +9,25 @@ */ class EnforceBlockingWrapper { + /** @var resource */ public $context; - public function stream_open($path, $mode, $options, &$opened_path) + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool { return true; } - public function stream_cast($cast_as) + public function stream_cast(int $cast_as): bool { return false; } - public function stream_eof() + public function stream_eof(): bool { return false; } - public function stream_set_option($option, $arg1, $arg2) + public function stream_set_option(int $option, int $arg1, ?int $arg2): bool { if ($option === STREAM_OPTION_BLOCKING) { return false; diff --git a/tests/FunctionalInternetTest.php b/tests/FunctionalInternetTest.php index 5113e7b..f56eefc 100644 --- a/tests/FunctionalInternetTest.php +++ b/tests/FunctionalInternetTest.php @@ -12,10 +12,11 @@ */ class FunctionalInternetTest extends TestCase { - public function testUploadKilobytePlain() + public function testUploadKilobytePlain(): void { $size = 1000; $stream = stream_socket_client('tcp://httpbin.org:80'); + assert(is_resource($stream)); $loop = Factory::create(); $stream = new DuplexResourceStream($stream, $loop); @@ -34,10 +35,11 @@ public function testUploadKilobytePlain() $this->assertNotEquals('', $buffer); } - public function testUploadBiggerBlockPlain() + public function testUploadBiggerBlockPlain(): void { $size = 50 * 1000; $stream = stream_socket_client('tcp://httpbin.org:80'); + assert(is_resource($stream)); $loop = Factory::create(); $stream = new DuplexResourceStream($stream, $loop); @@ -56,10 +58,11 @@ public function testUploadBiggerBlockPlain() $this->assertNotEquals('', $buffer); } - public function testUploadKilobyteSecure() + public function testUploadKilobyteSecure(): void { $size = 1000; $stream = stream_socket_client('ssl://httpbin.org:443'); + assert(is_resource($stream)); $loop = Factory::create(); $stream = new DuplexResourceStream($stream, $loop); @@ -78,7 +81,7 @@ public function testUploadKilobyteSecure() $this->assertNotEquals('', $buffer); } - public function testUploadBiggerBlockSecure() + public function testUploadBiggerBlockSecure(): void { // A few dozen kilobytes should be enough to verify this works. // Underlying buffer sizes are platform-specific, so let's increase this @@ -86,6 +89,7 @@ public function testUploadBiggerBlockSecure() $size = 136 * 1000; $stream = stream_socket_client('ssl://httpbin.org:443'); + assert(is_resource($stream)); // PHP < 7.1.4 suffers from a bug when writing big chunks of data over // TLS streams at once. @@ -114,7 +118,7 @@ public function testUploadBiggerBlockSecure() $this->assertNotEquals('', $buffer); } - private function awaitStreamClose(DuplexResourceStream $stream, LoopInterface $loop, $timeout = 10.0) + private function awaitStreamClose(DuplexResourceStream $stream, LoopInterface $loop, float $timeout = 10.0): void { $stream->on('close', function () use ($loop) { $loop->stop(); diff --git a/tests/ReadableResourceStreamTest.php b/tests/ReadableResourceStreamTest.php index 365ac0d..3f7c144 100644 --- a/tests/ReadableResourceStreamTest.php +++ b/tests/ReadableResourceStreamTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Stream; +use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; use React\Stream\ReadableResourceStream; use React\Stream\WritableStreamInterface; @@ -13,17 +14,20 @@ class ReadableResourceStreamTest extends TestCase * @covers React\Stream\ReadableResourceStream::__construct * @doesNotPerformAssertions */ - public function testConstructor() + public function testConstructor(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); new ReadableResourceStream($stream, $loop); } - public function testConstructWithoutLoopAssignsLoopAutomatically() + public function testConstructWithoutLoopAssignsLoopAutomatically(): void { $resource = fopen('php://temp', 'r+'); + assert(is_resource($resource)); $stream = new ReadableResourceStream($resource); @@ -38,11 +42,12 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() * @covers React\Stream\ReadableResourceStream::__construct * @doesNotPerformAssertions */ - public function testConstructorWithExcessiveMode() + public function testConstructorWithExcessiveMode(): void { // excessive flags are ignored for temp streams, so we have to use a file stream - $name = tempnam(sys_get_temp_dir(), 'test'); - $stream = @fopen($name, 'r+eANYTHING'); + $name = (string) tempnam(sys_get_temp_dir(), 'test'); + $stream = fopen($name, 'r+eANYTHING'); + assert(is_resource($stream)); unlink($name); $loop = $this->createLoopMock(); @@ -53,7 +58,7 @@ public function testConstructorWithExcessiveMode() /** * @covers React\Stream\ReadableResourceStream::__construct */ - public function testConstructorThrowsExceptionOnInvalidStream() + public function testConstructorThrowsExceptionOnInvalidStream(): void { $loop = $this->createLoopMock(); @@ -64,7 +69,7 @@ public function testConstructorThrowsExceptionOnInvalidStream() /** * @covers React\Stream\ReadableResourceStream::__construct */ - public function testConstructorThrowsExceptionOnWriteOnlyStream() + public function testConstructorThrowsExceptionOnWriteOnlyStream(): void { $loop = $this->createLoopMock(); @@ -75,11 +80,12 @@ public function testConstructorThrowsExceptionOnWriteOnlyStream() /** * @covers React\Stream\ReadableResourceStream::__construct */ - public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode() + public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode(): void { // excessive flags are ignored for temp streams, so we have to use a file stream - $name = tempnam(sys_get_temp_dir(), 'test'); + $name = (string) tempnam(sys_get_temp_dir(), 'test'); $stream = fopen($name, 'weANYTHING'); + assert(is_resource($stream)); unlink($name); $loop = $this->createLoopMock(); @@ -90,23 +96,26 @@ public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode /** * @covers React\Stream\ReadableResourceStream::__construct */ - public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking(): void { if (!in_array('blocking', stream_get_wrappers())) { stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); } $stream = fopen('blocking://test', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $this->expectException(\RuntimeException::class); new ReadableResourceStream($stream, $loop); } - - public function testCloseShouldEmitCloseEvent() + public function testCloseShouldEmitCloseEvent(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); @@ -117,9 +126,11 @@ public function testCloseShouldEmitCloseEvent() $this->assertFalse($conn->isReadable()); } - public function testCloseTwiceShouldEmitCloseEventOnce() + public function testCloseTwiceShouldEmitCloseEventOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); @@ -133,9 +144,11 @@ public function testCloseTwiceShouldEmitCloseEventOnce() * @covers React\Stream\ReadableResourceStream::__construct * @covers React\Stream\ReadableResourceStream::handleData */ - public function testDataEvent() + public function testDataEvent(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $capturedData = null; @@ -156,9 +169,11 @@ public function testDataEvent() * @covers React\Stream\ReadableResourceStream::__construct * @covers React\Stream\ReadableResourceStream::handleData */ - public function testDataEventDoesEmitOneChunkMatchingBufferSize() + public function testDataEventDoesEmitOneChunkMatchingBufferSize(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $capturedData = null; @@ -181,9 +196,11 @@ public function testDataEventDoesEmitOneChunkMatchingBufferSize() * @covers React\Stream\ReadableResourceStream::__construct * @covers React\Stream\ReadableResourceStream::handleData */ - public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfinite() + public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfinite(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $capturedData = null; @@ -206,9 +223,11 @@ public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfi /** * @covers React\Stream\ReadableResourceStream::handleData */ - public function testEmptyStreamShouldNotEmitData() + public function testEmptyStreamShouldNotEmitData(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); @@ -217,9 +236,11 @@ public function testEmptyStreamShouldNotEmitData() $conn->handleData(); } - public function testPipeShouldReturnDestination() + public function testPipeShouldReturnDestination(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); @@ -232,9 +253,11 @@ public function testPipeShouldReturnDestination() /** * @covers React\Stream\ReadableResourceStream::handleData */ - public function testClosingStreamInDataEventShouldNotTriggerError() + public function testClosingStreamInDataEventShouldNotTriggerError(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); @@ -252,9 +275,11 @@ public function testClosingStreamInDataEventShouldNotTriggerError() /** * @covers React\Stream\ReadableResourceStream::pause */ - public function testPauseRemovesReadStreamFromLoop() + public function testPauseRemovesReadStreamFromLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); $loop->expects($this->once())->method('removeReadStream')->with($stream); @@ -267,9 +292,11 @@ public function testPauseRemovesReadStreamFromLoop() /** * @covers React\Stream\ReadableResourceStream::pause */ - public function testResumeDoesAddStreamToLoopOnlyOnce() + public function testResumeDoesAddStreamToLoopOnlyOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); @@ -281,9 +308,11 @@ public function testResumeDoesAddStreamToLoopOnlyOnce() /** * @covers React\Stream\ReadableResourceStream::close */ - public function testCloseRemovesReadStreamFromLoop() + public function testCloseRemovesReadStreamFromLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); $loop->expects($this->once())->method('removeReadStream')->with($stream); @@ -295,9 +324,11 @@ public function testCloseRemovesReadStreamFromLoop() /** * @covers React\Stream\ReadableResourceStream::close */ - public function testCloseAfterPauseRemovesReadStreamFromLoopOnce() + public function testCloseAfterPauseRemovesReadStreamFromLoopOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); $loop->expects($this->once())->method('removeReadStream')->with($stream); @@ -310,9 +341,11 @@ public function testCloseAfterPauseRemovesReadStreamFromLoopOnce() /** * @covers React\Stream\ReadableResourceStream::close */ - public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce() + public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addReadStream')->with($stream); @@ -324,9 +357,10 @@ public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce() /** * @covers React\Stream\ReadableResourceStream::handleData */ - public function testDataFiltered() + public function testDataFiltered(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); // add a filter which removes every 'a' when reading filter_append($stream, function ($chunk) { @@ -352,9 +386,10 @@ public function testDataFiltered() /** * @covers React\Stream\ReadableResourceStream::handleData */ - public function testDataErrorShouldEmitErrorAndClose() + public function testDataErrorShouldEmitErrorAndClose(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); // add a filter which returns an error when encountering an 'a' when reading filter_append($stream, function ($chunk) { @@ -380,9 +415,12 @@ public function testDataErrorShouldEmitErrorAndClose() /** * @covers React\Stream\ReadableResourceStream::handleData */ - public function testEmptyReadShouldntFcloseStream() + public function testEmptyReadShouldntFcloseStream(): void { - list($stream, $_) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + assert(is_array($pair)); + [$stream, $_] = $pair; + $loop = $this->createLoopMock(); $conn = new ReadableResourceStream($stream, $loop); @@ -396,8 +434,10 @@ public function testEmptyReadShouldntFcloseStream() fclose($_); } - private function createLoopMock() + /** @return MockObject&LoopInterface */ + private function createLoopMock(): MockObject { + /** @var MockObject&LoopInterface */ return $this->createMock(LoopInterface::class); } } diff --git a/tests/Stub/ReadableStreamStub.php b/tests/Stub/ReadableStreamStub.php index 669059c..98d0240 100644 --- a/tests/Stub/ReadableStreamStub.php +++ b/tests/Stub/ReadableStreamStub.php @@ -9,7 +9,10 @@ class ReadableStreamStub extends EventEmitter implements ReadableStreamInterface { + /** @var bool */ public $readable = true; + + /** @var bool */ public $paused = false; public function isReadable(): bool @@ -17,20 +20,25 @@ public function isReadable(): bool return true; } - // trigger data event - public function write($data) + /** + * trigger data event + * + * @param mixed $data + * @return void + */ + public function write($data): void { $this->emit('data', [$data]); } // trigger error event - public function error($error) + public function error(\Exception $error): void { $this->emit('error', [$error]); } // trigger end event - public function end() + public function end(): void { $this->emit('end', []); } diff --git a/tests/TestCase.php b/tests/TestCase.php index b161c46..95bc3f1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,11 +2,12 @@ namespace React\Tests\Stream; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase { - protected function expectCallableOnce() + protected function expectCallableOnce(): callable { $mock = $this->createCallableMock(); $mock @@ -16,7 +17,8 @@ protected function expectCallableOnce() return $mock; } - protected function expectCallableOnceWith($value) + /** @param mixed $value */ + protected function expectCallableOnceWith($value): callable { $callback = $this->createCallableMock(); $callback @@ -27,7 +29,7 @@ protected function expectCallableOnceWith($value) return $callback; } - protected function expectCallableNever() + protected function expectCallableNever(): callable { $mock = $this->createCallableMock(); $mock @@ -37,15 +39,19 @@ protected function expectCallableNever() return $mock; } - protected function createCallableMock() + /** @return MockObject&callable */ + protected function createCallableMock(): MockObject { $builder = $this->getMockBuilder(\stdClass::class); if (method_exists($builder, 'addMethods')) { // PHPUnit 9+ - return $builder->addMethods(['__invoke'])->getMock(); + $mock = $builder->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit - return $builder->setMethods(['__invoke'])->getMock(); + $mock = $builder->setMethods(['__invoke'])->getMock(); } + assert($mock instanceof MockObject && is_callable($mock)); + + return $mock; } } diff --git a/tests/ThroughStreamTest.php b/tests/ThroughStreamTest.php index ddd570f..a4b49b0 100644 --- a/tests/ThroughStreamTest.php +++ b/tests/ThroughStreamTest.php @@ -11,7 +11,7 @@ class ThroughStreamTest extends TestCase { /** @test */ - public function itShouldReturnTrueForAnyDataWrittenToIt() + public function itShouldReturnTrueForAnyDataWrittenToIt(): void { $through = new ThroughStream(); $ret = $through->write('foo'); @@ -20,7 +20,7 @@ public function itShouldReturnTrueForAnyDataWrittenToIt() } /** @test */ - public function itShouldEmitAnyDataWrittenToIt() + public function itShouldEmitAnyDataWrittenToIt(): void { $through = new ThroughStream(); $through->on('data', $this->expectCallableOnceWith('foo')); @@ -28,7 +28,7 @@ public function itShouldEmitAnyDataWrittenToIt() } /** @test */ - public function itShouldEmitAnyDataWrittenToItPassedThruFunction() + public function itShouldEmitAnyDataWrittenToItPassedThruFunction(): void { $through = new ThroughStream('strtoupper'); $through->on('data', $this->expectCallableOnceWith('FOO')); @@ -36,7 +36,7 @@ public function itShouldEmitAnyDataWrittenToItPassedThruFunction() } /** @test */ - public function itShouldEmitAnyDataWrittenToItPassedThruCallback() + public function itShouldEmitAnyDataWrittenToItPassedThruCallback(): void { $through = new ThroughStream('strtoupper'); $through->on('data', $this->expectCallableOnceWith('FOO')); @@ -44,7 +44,7 @@ public function itShouldEmitAnyDataWrittenToItPassedThruCallback() } /** @test */ - public function itShouldEmitErrorAndCloseIfCallbackThrowsException() + public function itShouldEmitErrorAndCloseIfCallbackThrowsException(): void { $through = new ThroughStream(function () { throw new \RuntimeException(); @@ -61,7 +61,7 @@ public function itShouldEmitErrorAndCloseIfCallbackThrowsException() } /** @test */ - public function itShouldEmitErrorAndCloseIfCallbackThrowsExceptionOnEnd() + public function itShouldEmitErrorAndCloseIfCallbackThrowsExceptionOnEnd(): void { $through = new ThroughStream(function () { throw new \RuntimeException(); @@ -78,7 +78,7 @@ public function itShouldEmitErrorAndCloseIfCallbackThrowsExceptionOnEnd() } /** @test */ - public function itShouldReturnFalseForAnyDataWrittenToItWhenPaused() + public function itShouldReturnFalseForAnyDataWrittenToItWhenPaused(): void { $through = new ThroughStream(); $through->pause(); @@ -88,7 +88,7 @@ public function itShouldReturnFalseForAnyDataWrittenToItWhenPaused() } /** @test */ - public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventEndsStream() + public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventEndsStream(): void { $through = new ThroughStream(); $through->on('data', function () use ($through) { @@ -100,7 +100,7 @@ public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventEndsStream( } /** @test */ - public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventClosesStream() + public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventClosesStream(): void { $through = new ThroughStream(); $through->on('data', function () use ($through) { @@ -112,7 +112,7 @@ public function itShouldReturnFalseForAnyDataWrittenToItWhenDataEventClosesStrea } /** @test */ - public function itShouldEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenToItWhenPaused() + public function itShouldEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenToItWhenPaused(): void { $through = new ThroughStream(); $through->pause(); @@ -123,7 +123,7 @@ public function itShouldEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenToItWh } /** @test */ - public function itShouldNotEmitDrainOnResumeAfterClose() + public function itShouldNotEmitDrainOnResumeAfterClose(): void { $through = new ThroughStream(); $through->close(); @@ -133,7 +133,7 @@ public function itShouldNotEmitDrainOnResumeAfterClose() } /** @test */ - public function itShouldNotEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenThatCausesStreamToClose() + public function itShouldNotEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenThatCausesStreamToClose(): void { $through = new ThroughStream(); $through->on('data', function () use ($through) { $through->close(); }); @@ -144,7 +144,7 @@ public function itShouldNotEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenTha } /** @test */ - public function itShouldReturnFalseForAnyDataWrittenToItAfterPausingFromDrainEvent() + public function itShouldReturnFalseForAnyDataWrittenToItAfterPausingFromDrainEvent(): void { $through = new ThroughStream(); $through->pause(); @@ -157,7 +157,7 @@ public function itShouldReturnFalseForAnyDataWrittenToItAfterPausingFromDrainEve } /** @test */ - public function itShouldReturnTrueForAnyDataWrittenToItWhenResumedAfterPause() + public function itShouldReturnTrueForAnyDataWrittenToItWhenResumedAfterPause(): void { $through = new ThroughStream(); $through->on('drain', $this->expectCallableNever()); @@ -169,7 +169,7 @@ public function itShouldReturnTrueForAnyDataWrittenToItWhenResumedAfterPause() } /** @test */ - public function pipingStuffIntoItShouldWork() + public function pipingStuffIntoItShouldWork(): void { $readable = new ThroughStream(); @@ -181,7 +181,7 @@ public function pipingStuffIntoItShouldWork() } /** @test */ - public function endShouldEmitEndAndClose() + public function endShouldEmitEndAndClose(): void { $through = new ThroughStream(); $through->on('data', $this->expectCallableNever()); @@ -191,7 +191,7 @@ public function endShouldEmitEndAndClose() } /** @test */ - public function endShouldCloseTheStream() + public function endShouldCloseTheStream(): void { $through = new ThroughStream(); $through->on('data', $this->expectCallableNever()); @@ -202,7 +202,7 @@ public function endShouldCloseTheStream() } /** @test */ - public function endShouldWriteDataBeforeClosing() + public function endShouldWriteDataBeforeClosing(): void { $through = new ThroughStream(); $through->on('data', $this->expectCallableOnceWith('foo')); @@ -213,7 +213,7 @@ public function endShouldWriteDataBeforeClosing() } /** @test */ - public function endTwiceShouldOnlyEmitOnce() + public function endTwiceShouldOnlyEmitOnce(): void { $through = new ThroughStream(); $through->on('data', $this->expectCallableOnceWith('first')); @@ -222,7 +222,7 @@ public function endTwiceShouldOnlyEmitOnce() } /** @test */ - public function writeAfterEndShouldReturnFalse() + public function writeAfterEndShouldReturnFalse(): void { $through = new ThroughStream(); $through->on('data', $this->expectCallableNever()); @@ -232,7 +232,7 @@ public function writeAfterEndShouldReturnFalse() } /** @test */ - public function writeDataWillCloseStreamShouldReturnFalse() + public function writeDataWillCloseStreamShouldReturnFalse(): void { $through = new ThroughStream(); $through->on('data', [$through, 'close']); @@ -241,7 +241,7 @@ public function writeDataWillCloseStreamShouldReturnFalse() } /** @test */ - public function writeDataToPausedShouldReturnFalse() + public function writeDataToPausedShouldReturnFalse(): void { $through = new ThroughStream(); $through->pause(); @@ -250,7 +250,7 @@ public function writeDataToPausedShouldReturnFalse() } /** @test */ - public function writeDataToResumedShouldReturnTrue() + public function writeDataToResumedShouldReturnTrue(): void { $through = new ThroughStream(); $through->pause(); @@ -260,21 +260,21 @@ public function writeDataToResumedShouldReturnTrue() } /** @test */ - public function itShouldBeReadableByDefault() + public function itShouldBeReadableByDefault(): void { $through = new ThroughStream(); $this->assertTrue($through->isReadable()); } /** @test */ - public function itShouldBeWritableByDefault() + public function itShouldBeWritableByDefault(): void { $through = new ThroughStream(); $this->assertTrue($through->isWritable()); } /** @test */ - public function closeShouldCloseOnce() + public function closeShouldCloseOnce(): void { $through = new ThroughStream(); @@ -287,7 +287,7 @@ public function closeShouldCloseOnce() } /** @test */ - public function doubleCloseShouldCloseOnce() + public function doubleCloseShouldCloseOnce(): void { $through = new ThroughStream(); @@ -301,7 +301,7 @@ public function doubleCloseShouldCloseOnce() } /** @test */ - public function pipeShouldPipeCorrectly() + public function pipeShouldPipeCorrectly(): void { $output = $this->createMock(WritableStreamInterface::class); $output->expects($this->any())->method('isWritable')->willReturn(True); diff --git a/tests/UtilTest.php b/tests/UtilTest.php index cd00c8b..6bac04c 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -15,7 +15,7 @@ */ class UtilTest extends TestCase { - public function testPipeReturnsDestinationStream() + public function testPipeReturnsDestinationStream(): void { $readable = $this->createMock(ReadableStreamInterface::class); assert($readable instanceof ReadableStreamInterface); @@ -28,7 +28,7 @@ public function testPipeReturnsDestinationStream() $this->assertSame($writable, $ret); } - public function testPipeNonReadableSourceShouldDoNothing() + public function testPipeNonReadableSourceShouldDoNothing(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -49,7 +49,7 @@ public function testPipeNonReadableSourceShouldDoNothing() Util::pipe($readable, $writable); } - public function testPipeIntoNonWritableDestinationShouldPauseSource() + public function testPipeIntoNonWritableDestinationShouldPauseSource(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -74,7 +74,7 @@ public function testPipeIntoNonWritableDestinationShouldPauseSource() Util::pipe($readable, $writable); } - public function testPipeClosingDestPausesSource() + public function testPipeClosingDestPausesSource(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable @@ -93,7 +93,7 @@ public function testPipeClosingDestPausesSource() $writable->close(); } - public function testPipeWithEnd() + public function testPipeWithEnd(): void { $readable = new Stub\ReadableStreamStub(); @@ -112,7 +112,7 @@ public function testPipeWithEnd() $readable->end(); } - public function testPipeWithoutEnd() + public function testPipeWithoutEnd(): void { $readable = new Stub\ReadableStreamStub(); @@ -131,7 +131,7 @@ public function testPipeWithoutEnd() $readable->end(); } - public function testPipeWithTooSlowWritableShouldPauseReadable() + public function testPipeWithTooSlowWritableShouldPauseReadable(): void { $readable = new Stub\ReadableStreamStub(); @@ -154,7 +154,7 @@ public function testPipeWithTooSlowWritableShouldPauseReadable() $this->assertTrue($readable->paused); } - public function testPipeWithTooSlowWritableShouldResumeOnDrain() + public function testPipeWithTooSlowWritableShouldResumeOnDrain(): void { $readable = new Stub\ReadableStreamStub(); @@ -184,11 +184,13 @@ public function testPipeWithTooSlowWritableShouldResumeOnDrain() $this->assertFalse($readable->paused); } - public function testPipeWithWritableResourceStream() + public function testPipeWithWritableResourceStream(): void { $readable = new Stub\ReadableStreamStub(); $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createMock(LoopInterface::class); assert($loop instanceof LoopInterface); $buffer = new WritableResourceStream($stream, $loop); @@ -203,7 +205,7 @@ public function testPipeWithWritableResourceStream() $this->assertSame('hello, I am some random data', stream_get_contents($stream)); } - public function testPipeSetsUpListeners() + public function testPipeSetsUpListeners(): void { $source = new ThroughStream(); $dest = new ThroughStream(); @@ -219,7 +221,7 @@ public function testPipeSetsUpListeners() $this->assertCount(1, $dest->listeners('drain')); } - public function testPipeClosingSourceRemovesListeners() + public function testPipeClosingSourceRemovesListeners(): void { $source = new ThroughStream(); $dest = new ThroughStream(); @@ -233,7 +235,7 @@ public function testPipeClosingSourceRemovesListeners() $this->assertCount(0, $dest->listeners('drain')); } - public function testPipeClosingDestRemovesListeners() + public function testPipeClosingDestRemovesListeners(): void { $source = new ThroughStream(); $dest = new ThroughStream(); @@ -247,7 +249,7 @@ public function testPipeClosingDestRemovesListeners() $this->assertCount(0, $dest->listeners('drain')); } - public function testPipeDuplexIntoSelfEndsOnEnd() + public function testPipeDuplexIntoSelfEndsOnEnd(): void { $readable = $this->createMock(ReadableStreamInterface::class); $readable->expects($this->any())->method('isReadable')->willReturn(true); @@ -267,7 +269,7 @@ public function testPipeDuplexIntoSelfEndsOnEnd() } /** @test */ - public function forwardEventsShouldSetupForwards() + public function forwardEventsShouldSetupForwards(): void { $source = new ThroughStream(); $target = new ThroughStream(); diff --git a/tests/WritableResourceStreamTest.php b/tests/WritableResourceStreamTest.php index df3e815..bc4fc42 100644 --- a/tests/WritableResourceStreamTest.php +++ b/tests/WritableResourceStreamTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Stream; +use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; use React\Stream\WritableResourceStream; use function Clue\StreamFilter\append as filter_append; @@ -12,17 +13,20 @@ class WritableResourceStreamTest extends TestCase * @covers React\Stream\WritableResourceStream::__construct * @doesNotPerformAssertions */ - public function testConstructor() + public function testConstructor(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); new WritableResourceStream($stream, $loop); } - public function testConstructWithoutLoopAssignsLoopAutomatically() + public function testConstructWithoutLoopAssignsLoopAutomatically(): void { $resource = fopen('php://temp', 'r+'); + assert(is_resource($resource)); $stream = new WritableResourceStream($resource); @@ -37,11 +41,12 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() * @covers React\Stream\WritableResourceStream::__construct * @doesNotPerformAssertions */ - public function testConstructorWithExcessiveMode() + public function testConstructorWithExcessiveMode(): void { // excessive flags are ignored for temp streams, so we have to use a file stream - $name = tempnam(sys_get_temp_dir(), 'test'); - $stream = @fopen($name, 'w+eANYTHING'); + $name = (string) tempnam(sys_get_temp_dir(), 'test'); + $stream = fopen($name, 'w+eANYTHING'); + assert(is_resource($stream)); unlink($name); $loop = $this->createLoopMock(); @@ -52,7 +57,7 @@ public function testConstructorWithExcessiveMode() /** * @covers React\Stream\WritableResourceStream::__construct */ - public function testConstructorThrowsIfNotAValidStreamResource() + public function testConstructorThrowsIfNotAValidStreamResource(): void { $stream = null; $loop = $this->createLoopMock(); @@ -64,9 +69,11 @@ public function testConstructorThrowsIfNotAValidStreamResource() /** * @covers React\Stream\WritableResourceStream::__construct */ - public function testConstructorThrowsExceptionOnReadOnlyStream() + public function testConstructorThrowsExceptionOnReadOnlyStream(): void { $stream = fopen('php://temp', 'r'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $this->expectException(\InvalidArgumentException::class); @@ -76,11 +83,12 @@ public function testConstructorThrowsExceptionOnReadOnlyStream() /** * @covers React\Stream\WritableResourceStream::__construct */ - public function testConstructorThrowsExceptionOnReadOnlyStreamWithExcessiveMode() + public function testConstructorThrowsExceptionOnReadOnlyStreamWithExcessiveMode(): void { // excessive flags are ignored for temp streams, so we have to use a file stream - $name = tempnam(sys_get_temp_dir(), 'test'); + $name = (string) tempnam(sys_get_temp_dir(), 'test'); $stream = fopen($name, 'reANYTHING'); + assert(is_resource($stream)); unlink($name); $loop = $this->createLoopMock(); @@ -91,13 +99,15 @@ public function testConstructorThrowsExceptionOnReadOnlyStreamWithExcessiveMode( /** * @covers React\Stream\WritableResourceStream::__construct */ - public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking(): void { if (!in_array('blocking', stream_get_wrappers())) { stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); } $stream = fopen('blocking://test', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $this->expectException(\RuntimeException::class); @@ -108,9 +118,11 @@ public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking( * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testWrite() + public function testWrite(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createWriteableLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -124,9 +136,11 @@ public function testWrite() /** * @covers React\Stream\WritableResourceStream::write */ - public function testWriteWithDataDoesAddResourceToLoop() + public function testWriteWithDataDoesAddResourceToLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('addWriteStream')->with($this->equalTo($stream)); @@ -139,9 +153,11 @@ public function testWriteWithDataDoesAddResourceToLoop() * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testEmptyWriteDoesNotAddToLoop() + public function testEmptyWriteDoesNotAddToLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->never())->method('addWriteStream'); @@ -155,9 +171,10 @@ public function testEmptyWriteDoesNotAddToLoop() * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testWriteReturnsFalseWhenWritableResourceStreamIsFull() + public function testWriteReturnsFalseWhenWritableResourceStreamIsFull(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); $preventWrites = true; $loop = $this->createLoopMock(); @@ -182,9 +199,11 @@ public function testWriteReturnsFalseWhenWritableResourceStreamIsFull() /** * @covers React\Stream\WritableResourceStream::write */ - public function testWriteReturnsFalseWhenWritableResourceStreamIsExactlyFull() + public function testWriteReturnsFalseWhenWritableResourceStreamIsExactlyFull(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop, 3); @@ -196,9 +215,11 @@ public function testWriteReturnsFalseWhenWritableResourceStreamIsExactlyFull() * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testWriteDetectsWhenOtherSideIsClosed() + public function testWriteDetectsWhenOtherSideIsClosed(): void { - list($a, $b) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + assert(is_array($pair)); + [$a, $b] = $pair; $loop = $this->createWriteableLoopMock(); @@ -214,9 +235,11 @@ public function testWriteDetectsWhenOtherSideIsClosed() * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testEmitsDrainAfterWriteWhichExceedsBuffer() + public function testEmitsDrainAfterWriteWhichExceedsBuffer(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop, 2); @@ -231,9 +254,11 @@ public function testEmitsDrainAfterWriteWhichExceedsBuffer() * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testWriteInDrain() + public function testWriteInDrain(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop, 2); @@ -255,9 +280,11 @@ public function testWriteInDrain() * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testDrainAfterWrite() + public function testDrainAfterWrite(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop, 2); @@ -271,9 +298,11 @@ public function testDrainAfterWrite() /** * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testDrainAfterWriteWillRemoveResourceFromLoopWithoutClosing() + public function testDrainAfterWriteWillRemoveResourceFromLoopWithoutClosing(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('removeWriteStream')->with($stream); @@ -290,9 +319,11 @@ public function testDrainAfterWriteWillRemoveResourceFromLoopWithoutClosing() /** * @covers React\Stream\WritableResourceStream::handleWrite */ - public function testClosingDuringDrainAfterWriteWillRemoveResourceFromLoopOnceAndClose() + public function testClosingDuringDrainAfterWriteWillRemoveResourceFromLoopOnceAndClose(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $loop->expects($this->once())->method('removeWriteStream')->with($stream); @@ -311,9 +342,11 @@ public function testClosingDuringDrainAfterWriteWillRemoveResourceFromLoopOnceAn /** * @covers React\Stream\WritableResourceStream::end */ - public function testEndWithoutDataClosesImmediatelyIfWritableResourceStreamIsEmpty() + public function testEndWithoutDataClosesImmediatelyIfWritableResourceStreamIsEmpty(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -328,9 +361,11 @@ public function testEndWithoutDataClosesImmediatelyIfWritableResourceStreamIsEmp /** * @covers React\Stream\WritableResourceStream::end */ - public function testEndWithoutDataDoesNotCloseIfWritableResourceStreamIsFull() + public function testEndWithoutDataDoesNotCloseIfWritableResourceStreamIsFull(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -347,9 +382,11 @@ public function testEndWithoutDataDoesNotCloseIfWritableResourceStreamIsFull() /** * @covers React\Stream\WritableResourceStream::end */ - public function testEndWithDataClosesImmediatelyIfWritableResourceStreamFlushes() + public function testEndWithDataClosesImmediatelyIfWritableResourceStreamFlushes(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $filterBuffer = ''; $loop = $this->createLoopMock(); @@ -373,9 +410,11 @@ public function testEndWithDataClosesImmediatelyIfWritableResourceStreamFlushes( /** * @covers React\Stream\WritableResourceStream::end */ - public function testEndWithDataDoesNotCloseImmediatelyIfWritableResourceStreamIsFull() + public function testEndWithDataDoesNotCloseImmediatelyIfWritableResourceStreamIsFull(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -396,9 +435,11 @@ public function testEndWithDataDoesNotCloseImmediatelyIfWritableResourceStreamIs * @covers React\Stream\WritableResourceStream::isWritable * @covers React\Stream\WritableResourceStream::close */ - public function testClose() + public function testClose(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -415,9 +456,11 @@ public function testClose() /** * @covers React\Stream\WritableResourceStream::close */ - public function testClosingAfterWriteRemovesStreamFromLoop() + public function testClosingAfterWriteRemovesStreamFromLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -430,9 +473,11 @@ public function testClosingAfterWriteRemovesStreamFromLoop() /** * @covers React\Stream\WritableResourceStream::close */ - public function testClosingWithoutWritingDoesNotRemoveStreamFromLoop() + public function testClosingWithoutWritingDoesNotRemoveStreamFromLoop(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -444,9 +489,11 @@ public function testClosingWithoutWritingDoesNotRemoveStreamFromLoop() /** * @covers React\Stream\WritableResourceStream::close */ - public function testDoubleCloseWillEmitOnlyOnce() + public function testDoubleCloseWillEmitOnlyOnce(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $loop = $this->createLoopMock(); $buffer = new WritableResourceStream($stream, $loop); @@ -460,9 +507,11 @@ public function testDoubleCloseWillEmitOnlyOnce() * @covers React\Stream\WritableResourceStream::write * @covers React\Stream\WritableResourceStream::close */ - public function testWritingToClosedWritableResourceStreamShouldNotWriteToStream() + public function testWritingToClosedWritableResourceStreamShouldNotWriteToStream(): void { $stream = fopen('php://temp', 'r+'); + assert(is_resource($stream)); + $filterBuffer = ''; $loop = $this->createLoopMock(); @@ -481,13 +530,16 @@ public function testWritingToClosedWritableResourceStreamShouldNotWriteToStream( $this->assertSame('', $filterBuffer); } - public function testWritingToClosedStream() + public function testWritingToClosedStream(): void { if ('Darwin' === PHP_OS) { $this->markTestSkipped('OS X issue with shutting down pair for writing'); } - list($a, $b) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + assert(is_array($pair)); + [$a, $b] = $pair; + $loop = $this->createLoopMock(); $error = null; @@ -508,7 +560,7 @@ public function testWritingToClosedStream() $this->assertEqualsIgnoringCase('Unable to write to stream: fwrite(): send of 3 bytes failed with errno=32 Broken pipe', $error->getMessage()); } - private function createWriteableLoopMock() + private function createWriteableLoopMock(): LoopInterface { $loop = $this->createLoopMock(); $loop @@ -521,8 +573,10 @@ private function createWriteableLoopMock() return $loop; } - private function createLoopMock() + /** @return MockObject&LoopInterface */ + private function createLoopMock(): MockObject { + /** @var MockObject&LoopInterface */ return $this->createMock(LoopInterface::class); } } From 58b6951113ac5e8102639b666327f902493a06e6 Mon Sep 17 00:00:00 2001 From: Paul Rotmann Date: Thu, 27 Feb 2025 08:56:19 +0100 Subject: [PATCH 51/51] Run tests on PHP 8.4 and update test environment --- .github/workflows/ci.yml | 8 +++++--- composer.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bb8701..b2645b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,11 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: php: + - 8.4 - 8.3 - 8.2 - 8.1 @@ -35,7 +36,7 @@ jobs: PHPUnit-macOS: name: PHPUnit (macOS) - runs-on: macos-12 + runs-on: macos-14 continue-on-error: true steps: - uses: actions/checkout@v4 @@ -50,10 +51,11 @@ jobs: PHPStan: name: PHPStan (PHP ${{ matrix.php }}) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: php: + - 8.4 - 8.3 - 8.2 - 8.1 diff --git a/composer.json b/composer.json index beb0a56..00c2378 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ }, "require-dev": { "clue/stream-filter": "^1.2", - "phpstan/phpstan": "1.11.1 || 1.4.10", + "phpstan/phpstan": "1.12.19 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5" }, "autoload": {