From d874ba9163f3142ef4153cafd8982951a5cec63b Mon Sep 17 00:00:00 2001 From: Stephen Young Date: Thu, 8 Jun 2023 23:16:49 -0400 Subject: [PATCH] PSR-7 StreamInterface implementation. --- src/Flow/River/DetachedSource.php | 13 + src/Flow/River/InvalidSource.php | 13 + src/Flow/River/Stream.php | 327 ++++ src/Flow/River/StreamExporter.php | 33 + src/Flow/River/StreamResource.php | 134 ++ src/Flow/River/UnreadableStream.php | 13 + src/Flow/River/UnseekableStream.php | 13 + src/Flow/River/UnwritableStream.php | 13 + src/Gather/IgnoreCaseRegistry.php | 32 +- src/Gather/Registry.php | 18 + tests/Flow/River/DetachedSourceTest.php | 24 + tests/Flow/River/IntegrationTest.php | 61 + tests/Flow/River/InvalidSourceTest.php | 24 + tests/Flow/River/StreamExporterTest.php | 92 + tests/Flow/River/StreamResourceTest.php | 82 + tests/Flow/River/StreamTest.php | 2176 +++++++++++++++++++++ tests/Flow/River/UnreadableStreamTest.php | 24 + tests/Flow/River/UnseekableStreamTest.php | 24 + tests/Flow/River/UnwritableStreamTest.php | 24 + tests/Gather/IgnoreCaseRegistryTest.php | 50 + tests/Gather/RegistryTest.php | 25 + 21 files changed, 3211 insertions(+), 4 deletions(-) create mode 100644 src/Flow/River/DetachedSource.php create mode 100644 src/Flow/River/InvalidSource.php create mode 100644 src/Flow/River/Stream.php create mode 100644 src/Flow/River/StreamExporter.php create mode 100644 src/Flow/River/StreamResource.php create mode 100644 src/Flow/River/UnreadableStream.php create mode 100644 src/Flow/River/UnseekableStream.php create mode 100644 src/Flow/River/UnwritableStream.php create mode 100644 tests/Flow/River/DetachedSourceTest.php create mode 100644 tests/Flow/River/IntegrationTest.php create mode 100644 tests/Flow/River/InvalidSourceTest.php create mode 100644 tests/Flow/River/StreamExporterTest.php create mode 100644 tests/Flow/River/StreamResourceTest.php create mode 100644 tests/Flow/River/StreamTest.php create mode 100644 tests/Flow/River/UnreadableStreamTest.php create mode 100644 tests/Flow/River/UnseekableStreamTest.php create mode 100644 tests/Flow/River/UnwritableStreamTest.php diff --git a/src/Flow/River/DetachedSource.php b/src/Flow/River/DetachedSource.php new file mode 100644 index 0000000..814bbc1 --- /dev/null +++ b/src/Flow/River/DetachedSource.php @@ -0,0 +1,13 @@ +source->isResource()) { + throw new InvalidSource('Stream source must be a live resource'); + } + + $meta = $this->getMetadata(); + if (is_array($meta)) { + $this->seekable = $meta['seekable']; + $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']); + $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']); + $this->uri = $meta['uri']; + } + } + + /** + * Close the stream when the object is destroyed. + */ + public function __destruct() + { + $this->close(); + } + + /** + * Return the stream as a string. + */ + public function __toString(): string + { + try { + if ($this->seekable) { + $this->rewind(); + } + return $this->getContents(); + } catch (\Throwable $e) { + return $e->getMessage(); + } + } + + /** + * Returns whether or not the stream is readable. + */ + public function isReadable(): bool + { + return $this->readable; + } + + /** + * Returns whether or not the stream is writable. + */ + public function isWritable(): bool + { + return $this->writable; + } + + /** + * Returns whether or not the stream is seekable. + */ + public function isSeekable(): bool + { + return $this->seekable; + } + + /** + * Returns true if the stream is at the end of the stream. + */ + public function eof(): bool + { + if (!isset($this->source)) { + throw new DetachedSource('Stream is detached'); + } + + return $this->source->feof(); + } + + /** + * Returns the current position of the file read/write pointer + */ + public function tell(): int + { + if (!isset($this->source)) { + throw new DetachedSource('Stream is detached'); + } + + $position = $this->source->ftell(); + if ($position === false) { + throw new UnreadableStream('Unable to determine stream position'); + } + + return $position; + } + + /** + * Rewind the stream to the beginning. + */ + public function rewind(): void + { + $this->seek(0); + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + if (!isset($this->source)) { + throw new DetachedSource('Stream is detached'); + } + + if (!$this->seekable) { + throw new UnseekableStream('Cannot seek a non-seekable stream'); + } + + if ($this->source->fseek($offset, $whence) === -1) { + throw new UnseekableStream("Unable to seek to stream position $offset with whence $whence"); + } + } + + + /** + * Close the stream and any underlying resources. + */ + public function close(): void + { + // if $this->source is not set, the stream has already been closed + if (!isset($this->source)) { + return; + } + + // is_resource is used here to avoid a warning when the stream is already closed + if ($this->source->isResource()) { + $this->source->fclose(); + } + + $this->detach(); + } + + /** + * Detach the stream from any underlying resources, if any. + * + * This will render the stream unusable. + * + * @return resource|null Underlying PHP stream, if any + */ + public function detach() + { + if (!isset($this->source)) { + return null; + } + + $source = $this->source; + unset($this->source); + $this->size = 0; + $this->seekable = false; + $this->readable = false; + $this->writable = false; + $this->uri = ''; + + return $source->export(); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * The keys returned are identical to the keys returned from PHP's + * `stream_get_meta_data()` function. + * + * @param null|MetaDataKey $key Specific metadata to retrieve. + * @return mixed|MetaData + */ + public function getMetadata(string|null $key = null): mixed + { + if (!isset($this->source)) { + throw new DetachedSource('Cannot get metadata from a detached resource'); + } + + $meta = $this->source->streamGetMetaData(); + if (!$key) { + return $meta; + } + + return $meta[$key] ?? null; + } + + public function getContents(): string + { + if (!isset($this->source)) { + throw new DetachedSource('Cannot get contents from a detached resource'); + } + + if (!$this->readable) { + throw new UnreadableStream('Cannot get contents from an unreadable resource'); + } + try { + $contents = $this->source->streamGetContents(); + if ($contents === false) { + throw new \RuntimeException('Could not read stream'); + } + } catch (\Throwable $e) { + throw new UnreadableStream('Could not read stream', 0, $e); + } + return $contents; + } + + public function getSize(): ?int + { + if (!isset($this->source)) { + throw new DetachedSource('Cannot get the size of a detached stream'); + } + + if ($this->size) { + return $this->size; + } + + if ($this->uri) { + $this->source->clearstatcache(true, $this->uri); + } + + $stats = $this->source->fstat(); + if ($stats && $stats['size'] !== false) { + $this->size = $stats['size']; + return $this->size; + } + + return null; + } + + /** + * Read data from the stream. + */ + public function read(int $length): string + { + if (!isset($this->source)) { + throw new DetachedSource('Stream is detached'); + } + + if (!$this->readable) { + throw new UnreadableStream('Stream is not readable'); + } + + if ($length < 0) { + throw new \InvalidArgumentException('Length to read must be greater than or equal to zero'); + } + + if ($length === 0) { + return ''; + } + + try { + $data = $this->source->fread($length); + } catch (\Throwable $e) { + throw new UnreadableStream('Unable to read from stream', 0, $e); + } + if ($data === false) { + throw new UnreadableStream('Unable to read from stream'); + } + + return $data; + } + + /** + * Write data to the stream. + */ + public function write(string $string): int + { + if (!isset($this->source)) { + throw new DetachedSource('Cannot write to a detached stream'); + } + + if (!$this->writable) { + throw new UnwritableStream('Cannot write to a non-writable stream'); + } + + $this->size = null; + + try { + $result = $this->source->fwrite($string); + } catch (\Throwable $e) { + throw new UnwritableStream('Unable to write to stream', 0, $e); + } + if ($result === false) { + throw new UnwritableStream('Unable to write to stream'); + } + + return $result; + } +} diff --git a/src/Flow/River/StreamExporter.php b/src/Flow/River/StreamExporter.php new file mode 100644 index 0000000..60fda77 --- /dev/null +++ b/src/Flow/River/StreamExporter.php @@ -0,0 +1,33 @@ +isResource()) { + throw new InvalidSource('Stream source must be a live resource'); + } + } + + /** + * Return the contents of the stream as a string. + * + * @throws UnreadableStream + */ + public function getContents(): string + { + try { + $contents = $this->stream->streamGetContents(); + if ($contents === false) { + throw new \RuntimeException('Could not read stream'); + } + } catch (\Throwable $e) { + throw new UnreadableStream('Could not read stream', 0, $e); + } + return $contents; + } +} diff --git a/src/Flow/River/StreamResource.php b/src/Flow/River/StreamResource.php new file mode 100644 index 0000000..e0fc6c3 --- /dev/null +++ b/src/Flow/River/StreamResource.php @@ -0,0 +1,134 @@ +isResource()) { + throw new InvalidSource('Stream source must be a live resource'); + } + return $wrapper; + } + + /** + * Return the resource. + * + * @return resource + */ + public function export() + { + return $this->resource; + } + + public function isResource(): bool + { + return is_resource($this->resource); + } + + public function feof(): bool + { + return feof($this->resource); + } + + public function ftell(): int|false + { + return ftell($this->resource); + } + + public function fseek(int $offset, int $whence = SEEK_SET): int + { + return fseek($this->resource, $offset, $whence); + } + + public function fclose(): bool + { + return fclose($this->resource); + } + + /** + * @return array{ + * timed_out: bool, + * blocked: bool, + * eof: bool, + * unread_bytes: int, + * stream_type: string, + * wrapper_type: string, + * wrapper_data?: mixed, + * mode: string, + * seekable: bool, + * uri: string, + * crypto?: array{ + * protocol: string, + * cipher_name: string, + * cipher_bits: int, + * cipher_version: string + * } + * } + */ + public function streamGetMetaData(): array + { + return stream_get_meta_data($this->resource); + } + + public function clearstatcache(bool $clearRealPathCache = false, string $filename = ''): void + { + clearstatcache($clearRealPathCache, $filename); + } + + /** + * @return false|array{ + * dev: int, + * ino: int, + * mode: int, + * nlink: int, + * uid: int, + * gid: int, + * rdev: int, + * size: int, + * atime: int, + * mtime: int, + * ctime: int, + * blksize: int, + * blocks: int + * } + */ + public function fstat(): array|false + { + return fstat($this->resource); + } + + /** + * @param int<0, max> $length + */ + public function fread(int $length): string|false + { + return fread($this->resource, $length); + } + + public function fwrite(string $data): int|false + { + return fwrite($this->resource, $data); + } + public function streamGetContents(): string|false + { + return stream_get_contents($this->resource); + } +} diff --git a/src/Flow/River/UnreadableStream.php b/src/Flow/River/UnreadableStream.php new file mode 100644 index 0000000..c035c4d --- /dev/null +++ b/src/Flow/River/UnreadableStream.php @@ -0,0 +1,13 @@ +keyMap[strtolower($key)] ?? $key; + } + /** * Get the value stored at the given key. The key is case-insensitive. * @@ -49,6 +57,18 @@ public function has(string $id): bool return parent::has($id); } + /** + * Set the value at the given key. The key is case-insensitive. + */ + public function set(string $id, mixed $value): void + { + $lowerID = strtolower($id); + if (!isset($this->keyMap[$lowerID])) { + $this->keyMap[$lowerID] = $id; + } + parent::set($this->keyMap[$lowerID], $value); + } + /** * Check if the given key exists. The key is case-insensitive. */ @@ -76,9 +96,12 @@ public function offsetGet(mixed $offset): mixed */ public function offsetSet(mixed $offset, mixed $value): void { - $offset = $this->keyMap[strtolower((string)$offset)] ?? $offset; + $lowerOffset = strtolower((string)$offset); + if (!isset($this->keyMap[$lowerOffset])) { + $this->keyMap[$lowerOffset] = (string)$offset; + } - parent::offsetSet($offset, $value); + parent::offsetSet($this->keyMap[$lowerOffset], $value); } /** @@ -86,8 +109,9 @@ public function offsetSet(mixed $offset, mixed $value): void */ public function offsetUnset(mixed $offset): void { - $offset = $this->keyMap[strtolower((string)$offset)] ?? $offset; - + $lowerOffset = strtolower((string)$offset); + $offset = $this->keyMap[$lowerOffset] ?? $offset; + unset($this->keyMap[$lowerOffset]); parent::offsetUnset($offset); } diff --git a/src/Gather/Registry.php b/src/Gather/Registry.php index 88dbce7..05a9349 100644 --- a/src/Gather/Registry.php +++ b/src/Gather/Registry.php @@ -88,6 +88,24 @@ public function has(string $id): bool return isset($this->data[$id]); } + /** + * Set the value at the given key. + */ + public function set(string $id, mixed $value): void + { + $this->data[$id] = $value; + } + + /** + * Return the data as an array. + * + * @return array $data + */ + public function toArray(): array + { + return $this->__serialize(); + } + /** * Return the data as an array iterator. */ diff --git a/tests/Flow/River/DetachedSourceTest.php b/tests/Flow/River/DetachedSourceTest.php new file mode 100644 index 0000000..75b2872 --- /dev/null +++ b/tests/Flow/River/DetachedSourceTest.php @@ -0,0 +1,24 @@ +getMessage(); + + // Assert + $this->assertEquals('Detached source: foo', $message); + } +} diff --git a/tests/Flow/River/IntegrationTest.php b/tests/Flow/River/IntegrationTest.php new file mode 100644 index 0000000..9b1c84a --- /dev/null +++ b/tests/Flow/River/IntegrationTest.php @@ -0,0 +1,61 @@ +fail('Could not open memory stream'); + } + $stream = new Stream(StreamResource::wrap($resource)); + $stream->write('Hello World'); + $stream->rewind(); + + $this->assertSame('Hello World', $stream->read(11)); + $this->assertSame(11, $stream->tell()); + $this->assertSame(11, $stream->getSize()); + + $stream->rewind(); + $this->assertSame(0, $stream->tell()); + $this->assertSame('Hello World', $stream->getContents()); + $this->assertSame(11, $stream->tell()); + + $stream->seek(0); + $this->assertSame('Hello World', (string)$stream); + $this->assertSame(11, $stream->tell()); + + $meta = $stream->getMetadata(); + + if (!is_array($meta)) { + $this->fail('Expected metadata to be an array'); + } + + $this->assertSame('php://memory', $meta['uri']); + $this->assertSame('w+b', $meta['mode']); + $this->assertSame('PHP', $meta['wrapper_type']); + $this->assertSame('MEMORY', $meta['stream_type']); + $this->assertNull($meta['size']); + $this->assertTrue($meta['seekable']); + $this->assertFalse($meta['timed_out']); + $this->assertTrue($meta['blocked']); + $this->assertSame(true, $meta['eof']); + + $this->assertSame('php://memory', $stream->getMetadata('uri')); + + $result = $stream->detach(); + $stream->close(); + + $this->assertSame($resource, $result); + } +} diff --git a/tests/Flow/River/InvalidSourceTest.php b/tests/Flow/River/InvalidSourceTest.php new file mode 100644 index 0000000..b131fbe --- /dev/null +++ b/tests/Flow/River/InvalidSourceTest.php @@ -0,0 +1,24 @@ +getMessage(); + + // Assert + $this->assertEquals('Invalid Source: foo', $message); + } +} diff --git a/tests/Flow/River/StreamExporterTest.php b/tests/Flow/River/StreamExporterTest.php new file mode 100644 index 0000000..23143e6 --- /dev/null +++ b/tests/Flow/River/StreamExporterTest.php @@ -0,0 +1,92 @@ +getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource']) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(false); + + // Assert + $this->expectException(InvalidSource::class); + + // Act + new StreamExporter($resource); + } + + public function testGetContents(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetContents']) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetContents') + ->willReturn('Hello World'); + + $streamExporter = new StreamExporter($resource); + + // Act + $contents = $streamExporter->getContents(); + + // Assert + $this->assertSame('Hello World', $contents); + } + + public function testStreamThrowsException(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetContents']) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetContents') + ->willThrowException(new \Exception('Stream error')); + + $streamExporter = new StreamExporter($resource); + + // Assert + $this->expectException(UnreadableStream::class); + $this->expectExceptionMessage('Unreadable stream: Could not read stream'); + + // Act + $streamExporter->getContents(); + } +} diff --git a/tests/Flow/River/StreamResourceTest.php b/tests/Flow/River/StreamResourceTest.php new file mode 100644 index 0000000..457026f --- /dev/null +++ b/tests/Flow/River/StreamResourceTest.php @@ -0,0 +1,82 @@ +isResource(); + + // Assert + $this->assertTrue($isResource); + + // Cleanup + fclose($resource); + } + + public function testWrapComplainsIfResourceIsClosed(): void + { + // Arrange + $resource = fopen('php://memory', 'r'); + if ($resource === false) { + throw new \Exception('Could not open stream'); + } + fclose($resource); + + // Act + $this->expectException(InvalidSource::class); + $this->expectExceptionMessage('Stream source must be a live resource'); + + StreamResource::wrap($resource); + } + + public function testExportReturnsTheResource(): void + { + // Arrange + $resource = fopen('php://memory', 'r'); + if ($resource === false) { + throw new \Exception('Could not open stream'); + } + $streamResource = StreamResource::wrap($resource); + + // Act + $exportedResource = $streamResource->export(); + + // Assert + $this->assertSame($resource, $exportedResource); + + // Cleanup + fclose($resource); + } +} diff --git a/tests/Flow/River/StreamTest.php b/tests/Flow/River/StreamTest.php new file mode 100644 index 0000000..f30357a --- /dev/null +++ b/tests/Flow/River/StreamTest.php @@ -0,0 +1,2176 @@ +getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export']) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_data' => [ + 'HTTP/1.1 200 OK', + 'Age: 244629', + 'Cache-Control: max-age=604800', + 'Content-Type: text/html; charset=UTF-8', + 'Date: Sat, 20 Nov 2021 18:17:57 GMT', + 'Etag: "3147526947+ident"', + 'Expires: Sat, 27 Nov 2021 18:17:57 GMT', + 'Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT', + 'Server: ECS (chb/0286)', + 'Vary: Accept-Encoding', + 'X-Cache: HIT', + 'Content-Length: 1256', + 'Connection: close', + ], + 'wrapper_type' => 'http', + 'stream_type' => 'tcp_socket/ssl', + 'mode' => 'r', + 'unread_bytes' => 1256, + 'seekable' => false, + 'uri' => 'http://www.example.com/', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + + // Act + $stream = new Stream($resource); + + + // Assert + $this->assertInstanceOf(Stream::class, $stream); + } + + public function testNewStreamFailsIfResourceIsNotLive(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export']) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(false); + + $resource->expects($this->never()) + ->method('streamGetMetaData'); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->never()) + ->method('export'); + + // Assert + $this->expectException(InvalidSource::class); + + // Act + new Stream($resource); + } + + public function testDetach(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export']) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + + // Act + $stream = new Stream($resource); + + $result = $stream->detach(); + $again = $stream->detach(); + + $this->assertTrue(is_resource($result)); + $this->assertFalse(is_resource($again)); + $this->assertNull($stream->detach()); + } + + public function testIsReadable(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export']) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->isReadable(); + + // Assert + $this->assertTrue($result); + } + + public function testIsWritable(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export']) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->isWritable(); + + // Assert + $this->assertTrue($result); + } + + public function testIsSeekable(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export']) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->isSeekable(); + + // Assert + $this->assertTrue($result); + } + + public function testEOF(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export', 'feof']) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('feof') + ->willReturn(false); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->eof(); + + // Assert + $this->assertFalse($result); + } + + public function testEOFThrowsDetechedSourceIfStreamIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Stream is detached'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods(['isResource', 'streamGetMetaData', 'fclose', 'export', 'feof']) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('feof'); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->eof(); + } + + public function testSeekAndTell(): void + { + // Arrange + $expected = 10; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'ftell', + 'fseek', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fseek') + ->with($expected) + ->willReturn(0); + + $resource->expects($this->once()) + ->method('ftell') + ->willReturn($expected); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->seek(10); + $result = $stream->tell(); + + // Assert + $this->assertSame($expected, $result); + } + + public function testTellThrowsDetachedSourceIfStreamIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Stream is detached'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'ftell', + 'fseek', + ]) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fseek'); + + $resource->expects($this->never()) + ->method('ftell'); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->tell(); + } + + public function testTellThrowsUnreadableStreamIfResourceFTellReturnsFalse(): void + { + // Arrange + $this->expectException(UnreadableStream::class); + $this->expectExceptionMessage('Unable to determine stream position'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'ftell', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('ftell') + ->willReturn(false); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->tell(); + } + + public function testSeekThrowsDetachedSourceIfStreamIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Stream is detached'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fseek', + ]) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fseek'); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->seek(10); + } + + public function testSeekThrowsUnseekableStreamIfStreamIsNotSeekable(): void + { + // Arrange + $this->expectException(UnseekableStream::class); + $this->expectExceptionMessage('Cannot seek a non-seekable stream'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fseek', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => false, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fseek'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r')); + + $stream = new Stream($resource); + + // Act + $stream->seek(10); + } + + public function testSeekThrowsUnseekableStreamIfResourceFSeekFails(): void + { + // Arrange + $this->expectException(UnseekableStream::class); + $this->expectExceptionMessage('Unable to seek to stream position 10 with whence 0'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fseek', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fseek') + ->with(10, SEEK_SET) + ->willReturn(-1); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->seek(10); + } + + public function testRewind(): void + { + // Arrange + $expected = 0; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fseek' + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fseek') + ->with(0) + ->willReturn(0); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->rewind(); + } + + public function testGetMetadata(): void + { + // Arrange + $expected = [ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->exactly(2)) + ->method('streamGetMetaData') + ->willReturn($expected); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $actual = $stream->getMetadata(); + + // Assert + $this->assertSame($expected, $actual); + } + + public function testGetMetaDataByKey(): void + { + // Arrange + $expected = [ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->exactly(2)) + ->method('streamGetMetaData') + ->willReturn($expected); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $actual = $stream->getMetadata('stream_type'); + + // Assert + $this->assertSame($expected['stream_type'], $actual); + } + + public function testGetMetadataReturnsNullIfKeyDoesNotExist(): void + { + // Arrange + $expected = [ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->exactly(2)) + ->method('streamGetMetaData') + ->willReturn($expected); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $actual = $stream->getMetadata('foo_bar'); /** @phpstan-ignore-line */ + + // Assert + $this->assertNull($actual); + } + + public function testGetMetadataThrowsDetachedSourceIfResourceIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Cannot get metadata from a detached resource'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + ]) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->getMetadata(); + } + + public function testGetContents(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'streamGetContents', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('streamGetContents') + ->willReturn('foo'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->getContents(); + + // Assert + $this->assertSame('foo', $result); + } + + public function testGetContentsThrowsDetachedSourceIfStreamIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Cannot get contents from a detached resource'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'streamGetContents', + ]) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('streamGetContents'); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->getContents(); + } + + public function testGetContentsThrowsUnreadableStreamIfStreamIsUnreadable(): void + { + // Arrange + $this->expectException(UnreadableStream::class); + $this->expectExceptionMessage('Cannot get contents from an unreadable resource'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'streamGetContents', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'STDIO', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://stdin', + ]); + + $resource->expects($this->never()) + ->method('streamGetContents'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://stdin', 'r')); + + $stream = new Stream($resource); + + // Act + $stream->getContents(); + } + + public function testGetContentsThrowsUnreadableStreamIfStreamGetContentsReturnsFalse(): void + { + // Arrange + $this->expectException(UnreadableStream::class); + $this->expectExceptionMessage('Unreadable stream: Could not read stream'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'streamGetContents', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'STDIO', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://stdin', + ]); + + $resource->expects($this->once()) + ->method('streamGetContents') + ->willReturn(false); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://stdin', 'r')); + + $stream = new Stream($resource); + + // Act + $stream->getContents(); + } + + public function testToString(): void + { + // Arrange + $expected = 'foo'; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fseek', + 'streamGetContents', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fseek') + ->with(0) + ->willReturn(0); + + $resource->expects($this->once()) + ->method('streamGetContents') + ->willReturn($expected); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = (string) $stream; + + // Assert + $this->assertSame($expected, $result); + } + + public function testToStringReturnsExceptionMessageIfGetContentsFails(): void + { + // Arrange + $expected = 'Unreadable stream: Cannot get contents from an unreadable resource'; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fseek', + 'streamGetContents', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'STDIO', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://stdin', + ]); + + $resource->expects($this->once()) + ->method('fseek') + ->with(0) + ->willReturn(0); + + $resource->expects($this->never()) + ->method('streamGetContents'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://stdin', 'r')); + + $stream = new Stream($resource); + + // Act + $result = (string) $stream; + + // Assert + $this->assertSame($expected, $result); + } + + public function testGetSize(): void + { + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'clearstatcache', + 'fstat', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('clearstatcache') + ->with(true, 'php://memory'); + + $resource->expects($this->once()) + ->method('fstat') + ->willReturn([ + 'dev' => 16777220, + 'ino' => 0, + 'mode' => 33206, + 'nlink' => 1, + 'uid' => 501, + 'gid' => 20, + 'rdev' => 0, + 'size' => 42, + 'atime' => 1610612740, + 'mtime' => 1610612740, + 'ctime' => 1610612740, + 'blksize' => 4096, + 'blocks' => 0, + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->getSize(); + $second = $stream->getSize(); // Cached, so should not call fstat again. + + // Assert + $this->assertSame(42, $result); + $this->assertSame(42, $second); + } + + public function testGetSizeReturnsNullIfSizeIsFalse(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'clearstatcache', + 'fstat', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('clearstatcache') + ->with(true, 'php://memory'); + + $resource->expects($this->once()) + ->method('fstat') + ->willReturn([ + 'dev' => 16777220, + 'ino' => 0, + 'mode' => 33206, + 'nlink' => 1, + 'uid' => 501, + 'gid' => 20, + 'rdev' => 0, + 'size' => false, + 'atime' => 1610612740, + 'mtime' => 1610612740, + 'ctime' => 1610612740, + 'blksize' => 4096, + 'blocks' => 0, + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->getSize(); + + // Assert + $this->assertNull($result); + } + + public function testGetSizeThrowsDetachedSourceIfStreamIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Cannot get the size of a detached stream'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'clearstatcache', + 'fstat', + ]) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w+b', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('clearstatcache'); + + $resource->expects($this->never()) + ->method('fstat'); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->getSize(); + } + + public function testRead(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fread', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => false, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fread') + ->with(42) + ->willReturn('Hello World'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->read(42); + + // Assert + $this->assertSame('Hello World', $result); + } + + public function testReadThrowsUnreadableStreamIfFReadReturnsFalse(): void + { + // Arrange + $this->expectException(UnreadableStream::class); + $this->expectExceptionMessage('Unable to read from stream'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fread', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fread') + ->with(42) + ->willReturn(false); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->read(42); + } + + public function testReadThrowsUnreadableStreamIfFReadThrowsAnException(): void + { + // Arrange + $this->expectException(UnreadableStream::class); + $this->expectExceptionMessage('Unable to read from stream'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fread', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fread') + ->with(42) + ->willThrowException(new \RuntimeException('Failed to read from stream')); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->read(42); + } + + public function testReadReturnsEmptyStringIfLengthToReadIsZero(): void + { + // Arrange + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fread', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fread'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $result = $stream->read(0); + + // Assert + $this->assertSame('', $result); + } + + public function testReadThrowsInvalidArgumentExceptionIfLengthToReadIsLessThanZero(): void + { + // Arrange + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Length to read must be greater than or equal to zero'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fread', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fread'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r+')); + + $stream = new Stream($resource); + + // Act + $stream->read(-1); + } + + public function testReadThrowsUnreadableStreamIfStreamIsUnreadable(): void + { + // Arrange + $this->expectException(UnreadableStream::class); + $this->expectExceptionMessage('Stream is not readable'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fread', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fread'); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'w+')); + + $stream = new Stream($resource); + + // Act + $stream->read(42); + } + + public function testReadThrowsDetachedSourceIfStreamIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Stream is detached'); + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fread', + ]) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fread'); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'w+')); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->read(42); + } + + public function testWrite(): void + { + // Arrange + $data = 'Hello World!'; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fwrite', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'w+')); + + $resource->expects($this->once()) + ->method('fwrite') + ->with($data) + ->willReturn(strlen($data)); + + $stream = new Stream($resource); + + // Act + $result = $stream->write($data); + + // Assert + $this->assertSame(strlen($data), $result); + } + + public function testWroteThrowsUnwritableStreamIfFWriteReturnsFalse(): void + { + // Arrange + $this->expectException(UnwritableStream::class); + $this->expectExceptionMessage('Unable to write to stream'); + + $data = 'Hello World!'; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fwrite', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'w+')); + + $resource->expects($this->once()) + ->method('fwrite') + ->with($data) + ->willReturn(false); + + $stream = new Stream($resource); + + // Act + $stream->write($data); + } + + public function testWriteThrowsUnwritableStreamIfFWriteThrowsAnException(): void + { + // Arrange + $this->expectException(UnwritableStream::class); + $this->expectExceptionMessage('Unable to write to stream'); + + $data = 'Hello World!'; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fwrite', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'w+')); + + $resource->expects($this->once()) + ->method('fwrite') + ->with($data) + ->willThrowException(new \Exception()); + + $stream = new Stream($resource); + + // Act + $stream->write($data); + } + + public function testWriteThrowsUnwritableStreamIfResourceIsNotWritable(): void + { + // Arrange + $this->expectException(UnwritableStream::class); + $this->expectExceptionMessage('Unwritable stream: Cannot write to a non-writable stream'); + + $data = 'Hello World!'; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fwrite', + ]) + ->getMock(); + + $resource->expects($this->exactly(2)) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'r', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'r')); + + $resource->expects($this->never()) + ->method('fwrite'); + + $stream = new Stream($resource); + + // Act + $stream->write($data); + } + + public function testWriteThrowsDetachedSourceIfStreamIsDetached(): void + { + // Arrange + $this->expectException(DetachedSource::class); + $this->expectExceptionMessage('Cannot write to a detached stream'); + + $data = 'Hello World!'; + + /** @var StreamResource&\PHPUnit\Framework\MockObject\MockObject */ + $resource = $this->getMockBuilder(StreamResource::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'isResource', + 'streamGetMetaData', + 'fclose', + 'export', + 'fwrite', + ]) + ->getMock(); + + $resource->expects($this->once()) + ->method('isResource') + ->willReturn(true); + + $resource->expects($this->once()) + ->method('streamGetMetaData') + ->willReturn([ + 'timed_out' => false, + 'blocked' => true, + 'eof' => true, + 'wrapper_type' => 'PHP', + 'stream_type' => 'MEMORY', + 'mode' => 'w', + 'unread_bytes' => 0, + 'seekable' => true, + 'uri' => 'php://memory', + ]); + + $resource->expects($this->never()) + ->method('fclose'); + + $resource->expects($this->once()) + ->method('export') + ->willReturn(fopen('php://memory', 'w+')); + + $resource->expects($this->never()) + ->method('fwrite'); + + $stream = new Stream($resource); + + // Act + $stream->detach(); + $stream->write($data); + } +} diff --git a/tests/Flow/River/UnreadableStreamTest.php b/tests/Flow/River/UnreadableStreamTest.php new file mode 100644 index 0000000..8129230 --- /dev/null +++ b/tests/Flow/River/UnreadableStreamTest.php @@ -0,0 +1,24 @@ +getMessage(); + + // Assert + $this->assertEquals('Unreadable stream: foo', $message); + } +} diff --git a/tests/Flow/River/UnseekableStreamTest.php b/tests/Flow/River/UnseekableStreamTest.php new file mode 100644 index 0000000..808e67d --- /dev/null +++ b/tests/Flow/River/UnseekableStreamTest.php @@ -0,0 +1,24 @@ +getMessage(); + + // Assert + $this->assertEquals('Unseekable stream: foo', $message); + } +} diff --git a/tests/Flow/River/UnwritableStreamTest.php b/tests/Flow/River/UnwritableStreamTest.php new file mode 100644 index 0000000..c93fd95 --- /dev/null +++ b/tests/Flow/River/UnwritableStreamTest.php @@ -0,0 +1,24 @@ +getMessage(); + + // Assert + $this->assertEquals('Unwritable stream: foo', $message); + } +} diff --git a/tests/Gather/IgnoreCaseRegistryTest.php b/tests/Gather/IgnoreCaseRegistryTest.php index 389d16a..df171a4 100644 --- a/tests/Gather/IgnoreCaseRegistryTest.php +++ b/tests/Gather/IgnoreCaseRegistryTest.php @@ -66,6 +66,19 @@ public function testOffsetSetGetExistsUnset(): void $this->assertNull($empty); } + public function testNewKeysArePreservedIfNotAddedByConstructor(): void + { + // Arrange + $registry = IgnoreCaseRegistry::create(); + + // Act + $registry['baz'] = 'qux'; + $get = $registry['bAz']; + + // Assert + $this->assertSame('qux', $get); + } + public function testGetString(): void { // Arrange @@ -113,4 +126,41 @@ public function testGetBool(): void // Assert $this->assertTrue($get); } + + public function testSet(): void + { + // Arrange + $registry = IgnoreCaseRegistry::create(); + + // Act + $registry->set('foo', 'bar'); + $get = $registry->get('FOO'); + + // Assert + $this->assertSame('bar', $get); + } + + public function testGetKey(): void + { + // Arrange + $registry = IgnoreCaseRegistry::fromData(['foo' => 'bar']); + + // Act + $get = $registry->getKey('fOo'); + + // Assert + $this->assertSame('foo', $get); + } + + public function testGetKeyNotSet(): void + { + // Arrange + $registry = IgnoreCaseRegistry::fromData(['foo' => 'bar']); + + // Act + $get = $registry->getKey('nope'); + + // Assert + $this->assertSame('nope', $get); + } } diff --git a/tests/Gather/RegistryTest.php b/tests/Gather/RegistryTest.php index e1bb14d..16c1751 100644 --- a/tests/Gather/RegistryTest.php +++ b/tests/Gather/RegistryTest.php @@ -56,6 +56,18 @@ public function testSerializeAndUnserialize(): void $this->assertEquals($registry->get('foo'), $unserialized->get('foo')); } + public function testToArray(): void + { + // Arrange + $registry = Registry::fromData(['foo' => 'bar']); + + // Act + $array = $registry->toArray(); + + // Assert + $this->assertEquals(['foo' => 'bar'], $array); + } + public function testToString(): void { // Arrange @@ -106,6 +118,19 @@ public function testCount(): void $this->assertSame(1, $count); } + public function testSet(): void + { + // Arrange + $registry = Registry::fromData(['foo' => 'bar']); + + // Act + $registry->set('foo', 'baz'); + $get = $registry->get('foo'); + + // Assert + $this->assertSame('baz', $get); + } + public function assertGetSetViaArrayAccess(): void { // Arrange