From a84feedb199a9cb4a405b1408bfcb8a140f54434 Mon Sep 17 00:00:00 2001 From: azjezz Date: Wed, 3 Nov 2021 06:48:32 +0100 Subject: [PATCH] feat(file): introduce File component Signed-off-by: azjezz --- CHANGELOG.md | 3 + config/.phpcs.xml | 2 + config/psalm.xml | 2 +- docs/README.md | 2 + docs/component/file.md | 39 ++++ docs/component/io-stream.md | 35 ++++ docs/documenter.php | 10 +- .../File/Exception/AlreadyLockedException.php | 9 + src/Psl/File/Exception/ExceptionInterface.php | 12 ++ src/Psl/File/Exception/RuntimeException.php | 11 ++ src/Psl/File/HandleInterface.php | 65 +++++++ .../File/Internal/AbstractHandleWrapper.php | 72 +++++++ src/Psl/File/Internal/ResourceHandle.php | 145 ++++++++++++++ src/Psl/File/Internal/open.php | 22 +++ src/Psl/File/Lock.php | 41 ++++ src/Psl/File/LockType.php | 21 +++ src/Psl/File/ReadHandle.php | 48 +++++ src/Psl/File/ReadHandleInterface.php | 11 ++ src/Psl/File/ReadWriteHandle.php | 89 +++++++++ src/Psl/File/ReadWriteHandleInterface.php | 14 ++ src/Psl/File/WriteHandle.php | 68 +++++++ src/Psl/File/WriteHandleInterface.php | 11 ++ src/Psl/File/WriteMode.php | 37 ++++ src/Psl/File/open_read_only.php | 19 ++ src/Psl/File/open_read_write.php | 21 +++ src/Psl/File/open_write_only.php | 21 +++ src/Psl/File/temporary.php | 24 +++ src/Psl/IO/Internal/ResourceHandle.php | 2 +- src/Psl/Internal/Loader.php | 39 +++- tests/unit/Async/RunTest.php | 8 +- tests/unit/File/LockTest.php | 40 ++++ tests/unit/File/ReadHandleTest.php | 29 +++ tests/unit/File/ReadWriteHandleTest.php | 178 ++++++++++++++++++ tests/unit/File/WriteHandleTest.php | 71 +++++++ 34 files changed, 1213 insertions(+), 8 deletions(-) create mode 100644 docs/component/file.md create mode 100644 docs/component/io-stream.md create mode 100644 src/Psl/File/Exception/AlreadyLockedException.php create mode 100644 src/Psl/File/Exception/ExceptionInterface.php create mode 100644 src/Psl/File/Exception/RuntimeException.php create mode 100644 src/Psl/File/HandleInterface.php create mode 100644 src/Psl/File/Internal/AbstractHandleWrapper.php create mode 100644 src/Psl/File/Internal/ResourceHandle.php create mode 100644 src/Psl/File/Internal/open.php create mode 100644 src/Psl/File/Lock.php create mode 100644 src/Psl/File/LockType.php create mode 100644 src/Psl/File/ReadHandle.php create mode 100644 src/Psl/File/ReadHandleInterface.php create mode 100644 src/Psl/File/ReadWriteHandle.php create mode 100644 src/Psl/File/ReadWriteHandleInterface.php create mode 100644 src/Psl/File/WriteHandle.php create mode 100644 src/Psl/File/WriteHandleInterface.php create mode 100644 src/Psl/File/WriteMode.php create mode 100644 src/Psl/File/open_read_only.php create mode 100644 src/Psl/File/open_read_write.php create mode 100644 src/Psl/File/open_write_only.php create mode 100644 src/Psl/File/temporary.php create mode 100644 tests/unit/File/LockTest.php create mode 100644 tests/unit/File/ReadHandleTest.php create mode 100644 tests/unit/File/ReadWriteHandleTest.php create mode 100644 tests/unit/File/WriteHandleTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b7b9e8f..6ab20029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,6 @@ * **BC** - signature of `Psl\Type\object` function changed from `object(classname $classname): TypeInterface` to `object(): TypeInterface` ( to preserve the old behavior, use `Psl\Type\instance_of` ) * introduced `Psl\Type\instance_of` function, with the signature of `instance_of(classname $classname): TypeInterface`. * introduced a new `Psl\Async` component. +* introduced a new `Psl\IO\Stream` component. +* refactored `Psl\IO` handles API. +* introduced a new `Psl\File` component. diff --git a/config/.phpcs.xml b/config/.phpcs.xml index 8354033f..8d3090a5 100644 --- a/config/.phpcs.xml +++ b/config/.phpcs.xml @@ -5,6 +5,8 @@ ../src ../tests + */src/Psl/File/(WriteMode|LockType).php + diff --git a/config/psalm.xml b/config/psalm.xml index 54686195..65d6891d 100644 --- a/config/psalm.xml +++ b/config/psalm.xml @@ -1,5 +1,5 @@ - + diff --git a/docs/README.md b/docs/README.md index c929aa25..4736985e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,11 +15,13 @@ - [Psl\Encoding\Base64](./component/encoding-base64.md) - [Psl\Encoding\Hex](./component/encoding-hex.md) - [Psl\Env](./component/env.md) +- [Psl\File](./component/file.md) - [Psl\Filesystem](./component/filesystem.md) - [Psl\Fun](./component/fun.md) - [Psl\Hash](./component/hash.md) - [Psl\Html](./component/html.md) - [Psl\IO](./component/io.md) +- [Psl\IO\Stream](./component/io-stream.md) - [Psl\Interface](./component/interface.md) - [Psl\Iter](./component/iter.md) - [Psl\Json](./component/json.md) diff --git a/docs/component/file.md b/docs/component/file.md new file mode 100644 index 00000000..307c2f9c --- /dev/null +++ b/docs/component/file.md @@ -0,0 +1,39 @@ + + +[*index](./../README.md) + +--- + +### `Psl\File` Component + +#### `Functions` + +- [open_read_only](./../../src/Psl/File/open_read_only.php#L16) +- [open_read_write](./../../src/Psl/File/open_read_write.php#L18) +- [open_write_only](./../../src/Psl/File/open_write_only.php#L18) +- [temporary](./../../src/Psl/File/temporary.php#L19) + +#### `Interfaces` + +- [HandleInterface](./../../src/Psl/File/HandleInterface.php#L9) +- [ReadHandleInterface](./../../src/Psl/File/ReadHandleInterface.php#L9) +- [ReadWriteHandleInterface](./../../src/Psl/File/ReadWriteHandleInterface.php#L9) +- [WriteHandleInterface](./../../src/Psl/File/WriteHandleInterface.php#L9) + +#### `Classes` + +- [Lock](./../../src/Psl/File/Lock.php#L9) +- [ReadHandle](./../../src/Psl/File/ReadHandle.php#L11) +- [ReadWriteHandle](./../../src/Psl/File/ReadWriteHandle.php#L11) +- [WriteHandle](./../../src/Psl/File/WriteHandle.php#L11) + +#### `Enums` + +- [LockType](./../../src/Psl/File/LockType.php#L7) +- [WriteMode](./../../src/Psl/File/WriteMode.php#L7) + + diff --git a/docs/component/io-stream.md b/docs/component/io-stream.md new file mode 100644 index 00000000..97c7a1dd --- /dev/null +++ b/docs/component/io-stream.md @@ -0,0 +1,35 @@ + + +[*index](./../README.md) + +--- + +### `Psl\IO\Stream` Component + +#### `Functions` + +- [pipe](./../../src/Psl/IO/Stream/pipe.php#L25) + +#### `Classes` + +- [StreamCloseHandle](./../../src/Psl/IO/Stream/StreamCloseHandle.php#L13) +- [StreamCloseReadHandle](./../../src/Psl/IO/Stream/StreamCloseReadHandle.php#L13) +- [StreamCloseReadWriteHandle](./../../src/Psl/IO/Stream/StreamCloseReadWriteHandle.php#L13) +- [StreamCloseSeekHandle](./../../src/Psl/IO/Stream/StreamCloseSeekHandle.php#L13) +- [StreamCloseSeekReadHandle](./../../src/Psl/IO/Stream/StreamCloseSeekReadHandle.php#L13) +- [StreamCloseSeekReadWriteHandle](./../../src/Psl/IO/Stream/StreamCloseSeekReadWriteHandle.php#L13) +- [StreamCloseSeekWriteHandle](./../../src/Psl/IO/Stream/StreamCloseSeekWriteHandle.php#L13) +- [StreamCloseWriteHandle](./../../src/Psl/IO/Stream/StreamCloseWriteHandle.php#L13) +- [StreamReadHandle](./../../src/Psl/IO/Stream/StreamReadHandle.php#L13) +- [StreamReadWriteHandle](./../../src/Psl/IO/Stream/StreamReadWriteHandle.php#L13) +- [StreamSeekHandle](./../../src/Psl/IO/Stream/StreamSeekHandle.php#L13) +- [StreamSeekReadHandle](./../../src/Psl/IO/Stream/StreamSeekReadHandle.php#L13) +- [StreamSeekReadWriteHandle](./../../src/Psl/IO/Stream/StreamSeekReadWriteHandle.php#L13) +- [StreamSeekWriteHandle](./../../src/Psl/IO/Stream/StreamSeekWriteHandle.php#L13) +- [StreamWriteHandle](./../../src/Psl/IO/Stream/StreamWriteHandle.php#L13) + + diff --git a/docs/documenter.php b/docs/documenter.php index f2a5e264..5abfdd48 100644 --- a/docs/documenter.php +++ b/docs/documenter.php @@ -136,6 +136,7 @@ function document_component(string $component, string $index_link): void $generator($directory, $symbols, Loader::TYPE_INTERFACE), $generator($directory, $symbols, Loader::TYPE_CLASS), $generator($directory, $symbols, Loader::TYPE_TRAIT), + $generator($directory, $symbols, Loader::TYPE_ENUM), [''] ), "\n"), ]); @@ -155,7 +156,7 @@ function get_component_members(string $component): array $list, static function (string $member) use ($component): bool { - if (!Str\starts_with_ci($member, $component)) { + if (!Str\starts_with_ci($member, $component . '\\')) { return false; } @@ -171,6 +172,7 @@ static function (string $member) use ($component): bool { Loader::TYPE_INTERFACE => $filter(Loader::INTERFACES), Loader::TYPE_CLASS => $filter(Loader::CLASSES), Loader::TYPE_TRAIT => $filter(Loader::TRAITS), + Loader::TYPE_ENUM => $filter(Loader::ENUMS), ]; } @@ -189,12 +191,14 @@ function get_all_components(): array 'Psl\\Encoding\\Base64', 'Psl\\Encoding\\Hex', 'Psl\\Env', + 'Psl\\File', 'Psl\\Filesystem', 'Psl\\Fun', 'Psl\\Hash', 'Psl\\Html', 'Psl\\Interface', 'Psl\\IO', + 'Psl\\IO\\Stream', 'Psl\\Iter', 'Psl\\Json', 'Psl\\Math', @@ -233,6 +237,8 @@ function get_symbol_type_name(int $type): string return 'Classes'; case Loader::TYPE_TRAIT: return 'Traits'; + case Loader::TYPE_ENUM: + return 'Enums'; } } @@ -247,6 +253,8 @@ function get_symbol_definition_line(string $symbol, int $type): int if (Loader::TYPE_FUNCTION === $type) { $reflection = new ReflectionFunction($symbol); + } else if (Loader::TYPE_ENUM === $type) { + $reflection = new ReflectionEnum($symbol); } else { $reflection = new ReflectionClass($symbol); } diff --git a/src/Psl/File/Exception/AlreadyLockedException.php b/src/Psl/File/Exception/AlreadyLockedException.php new file mode 100644 index 00000000..f42af2b6 --- /dev/null +++ b/src/Psl/File/Exception/AlreadyLockedException.php @@ -0,0 +1,9 @@ +lock(LockType::SHARED); + * // lock has been acquired. + * $lock->release(); + * ``` + */ + public function lock(LockType $type): Lock; + + /** + * Immediately get a shared or exclusive lock on a file, or throw. + * + * @throws IO\Exception\AlreadyClosedException If the handle has been already closed. + * @throws Exception\RuntimeException If an error occurred during the operation. + * @throws Exception\AlreadyLockedException if `lock()` would block. + * + * Example: + * + * ```php + * try { + * $lock = $file->tryLock(LockType::SHARED); + * // lock has been acquired. + * $lock->release(); + * } catch(AlreadyLockedException) { + * // cannot acquire lock. + * } + * ``` + */ + public function tryLock(LockType $type): Lock; +} diff --git a/src/Psl/File/Internal/AbstractHandleWrapper.php b/src/Psl/File/Internal/AbstractHandleWrapper.php new file mode 100644 index 00000000..5754149e --- /dev/null +++ b/src/Psl/File/Internal/AbstractHandleWrapper.php @@ -0,0 +1,72 @@ +handle->getPath(); + } + + /** + * {@inheritDoc} + */ + public function getSize(): int + { + return $this->handle->getSize(); + } + + /** + * {@inheritDoc} + */ + public function lock(LockType $type): Lock + { + return $this->handle->lock($type); + } + + /** + * {@inheritDoc} + */ + public function tryLock(LockType $type): Lock + { + return $this->handle->tryLock($type); + } + + /** + * {@inheritDoc} + */ + public function seek(int $offset): void + { + $this->handle->seek($offset); + } + + /** + * {@inheritDoc} + */ + public function tell(): int + { + return $this->handle->tell(); + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + $this->handle->close(); + } +} diff --git a/src/Psl/File/Internal/ResourceHandle.php b/src/Psl/File/Internal/ResourceHandle.php new file mode 100644 index 00000000..d7f62d9d --- /dev/null +++ b/src/Psl/File/Internal/ResourceHandle.php @@ -0,0 +1,145 @@ +path = $path; + } + + /** + * {@inheritDoc} + */ + public function getPath(): string + { + return $this->path; + } + + /** + * {@inheritDoc} + */ + public function getSize(): int + { + if (null === $this->resource) { + throw new Exception\AlreadyClosedException('Handle has already been closed.'); + } + + // @codeCoverageIgnoreStart + try { + $position = $this->tell(); + } catch (IO\Exception\RuntimeException $previous) { + throw new File\Exception\RuntimeException($previous->getMessage(), previous: $previous); + } + + /** @psalm-suppress PossiblyInvalidArgument */ + $result = @fseek($this->resource, 0, SEEK_END); + if ($result === -1) { + $error = error_get_last(); + + throw new File\Exception\RuntimeException($error['message'] ?? 'unknown error.'); + } + + try { + $size = $this->tell(); + $this->seek($position); + } catch (IO\Exception\RuntimeException $previous) { + throw new File\Exception\RuntimeException($previous->getMessage(), previous: $previous); + } + // @codeCoverageIgnoreEnd + + return $size; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function lock(LockType $type): Lock + { + while (true) { + try { + return $this->tryLock($type); + } catch (File\Exception\AlreadyLockedException) { + Async\later(); + } + } + } + + /** + * {@inheritDoc} + */ + public function tryLock(LockType $type): Lock + { + if (null === $this->resource) { + throw new Exception\AlreadyClosedException('Handle has already been closed.'); + } + + $operations = LOCK_NB | ($type === LockType::EXCLUSIVE ? LOCK_EX : LOCK_SH); + /** @psalm-suppress PossiblyInvalidArgument */ + $success = @flock($this->resource, $operations, $would_block); + // @codeCoverageIgnoreStart + if ($would_block) { + throw new File\Exception\AlreadyLockedException(); + } + + if (!$success) { + throw new File\Exception\RuntimeException(Str\format( + 'Could not acquire %s lock for "%s".', + $type === LockType::EXCLUSIVE ? 'exclusive' : 'shared', + $this->getPath(), + )); + } + + return new Lock($type, function (): void { + if (null === $this->resource) { + // while closing a handle should unlock it, that is not always the case. + // therefore, we should require users to explicitly release the lock before closing the handle. + throw new Exception\AlreadyClosedException('Handle was closed before releasing the lock.'); + } + + /** @psalm-suppress PossiblyInvalidArgument */ + if (!@flock($this->resource, LOCK_UN)) { + throw new File\Exception\RuntimeException(Str\format( + 'Could not release lock for "%s".', + $this->getPath(), + )); + } + }); + // @codeCoverageIgnoreEnd + } +} diff --git a/src/Psl/File/Internal/open.php b/src/Psl/File/Internal/open.php new file mode 100644 index 00000000..e444be10 --- /dev/null +++ b/src/Psl/File/Internal/open.php @@ -0,0 +1,22 @@ +released) { + return; + } + + ($this->releaseCallback)(); + $this->released = true; + } + + public function __destruct() + { + $this->release(); + } +} diff --git a/src/Psl/File/LockType.php b/src/Psl/File/LockType.php new file mode 100644 index 00000000..27532366 --- /dev/null +++ b/src/Psl/File/LockType.php @@ -0,0 +1,21 @@ +readHandle = Internal\open($path, 'r', read: true, write: false); + + parent::__construct($this->readHandle); + } + + /** + * {@inheritDoc} + */ + public function readImmediately(?int $max_bytes = null): string + { + return $this->readHandle->readImmediately($max_bytes); + } + + /** + * {@inheritDoc} + */ + public function read(?int $max_bytes = null, ?int $timeout_ms = null): string + { + return $this->readHandle->read($max_bytes, $timeout_ms); + } +} diff --git a/src/Psl/File/ReadHandleInterface.php b/src/Psl/File/ReadHandleInterface.php new file mode 100644 index 00000000..4b448de0 --- /dev/null +++ b/src/Psl/File/ReadHandleInterface.php @@ -0,0 +1,11 @@ +readWriteHandle = Internal\open($path, 'r' . ((string) $write_mode->value) . '+', read: true, write: false); + + parent::__construct($this->readWriteHandle); + } + + /** + * {@inheritDoc} + */ + public function readImmediately(?int $max_bytes = null): string + { + return $this->readWriteHandle->readImmediately($max_bytes); + } + + /** + * {@inheritDoc} + */ + public function read(?int $max_bytes = null, ?int $timeout_ms = null): string + { + return $this->readWriteHandle->read($max_bytes, $timeout_ms); + } + + /** + * {@inheritDoc} + */ + public function writeImmediately(string $bytes): int + { + return $this->readWriteHandle->writeImmediately($bytes); + } + + /** + * {@inheritDoc} + */ + public function write(string $bytes, ?int $timeout_ms = null): int + { + return $this->readWriteHandle->write($bytes, $timeout_ms); + } +} diff --git a/src/Psl/File/ReadWriteHandleInterface.php b/src/Psl/File/ReadWriteHandleInterface.php new file mode 100644 index 00000000..e65c8e59 --- /dev/null +++ b/src/Psl/File/ReadWriteHandleInterface.php @@ -0,0 +1,14 @@ +writeHandle = Internal\open($path, (string) $write_mode->value, read: false, write: true); + + parent::__construct($this->writeHandle); + } + + /** + * {@inheritDoc} + */ + public function writeImmediately(string $bytes): int + { + return $this->writeHandle->writeImmediately($bytes); + } + + /** + * {@inheritDoc} + */ + public function write(string $bytes, ?int $timeout_ms = null): int + { + return $this->writeHandle->write($bytes, $timeout_ms); + } +} diff --git a/src/Psl/File/WriteHandleInterface.php b/src/Psl/File/WriteHandleInterface.php new file mode 100644 index 00000000..3221bd89 --- /dev/null +++ b/src/Psl/File/WriteHandleInterface.php @@ -0,0 +1,11 @@ + Async\usleep(1000), - static fn() => Async\usleep(1000), - static fn() => Async\usleep(1000), + static fn() => Async\usleep(10_000), + static fn() => Async\usleep(10_000), + static fn() => Async\usleep(10_000), ])); return 'hello'; - }, timeout_ms: 2000); + }, timeout_ms: 20_000); static::assertSame('hello', $awaitable->await()); } diff --git a/tests/unit/File/LockTest.php b/tests/unit/File/LockTest.php new file mode 100644 index 00000000..8dfd0769 --- /dev/null +++ b/tests/unit/File/LockTest.php @@ -0,0 +1,40 @@ +lock(File\LockType::EXCLUSIVE); + + static::assertSame(File\LockType::EXCLUSIVE, $lock->type); + + $lock->release(); + + $lock = $file->tryLock(File\LockType::SHARED); + + static::assertSame(File\LockType::SHARED, $lock->type); + + $lock->release(); + } + + public function testLockingClosedFile(): void + { + $file = File\temporary(); + $file->close(); + + $this->expectException(AlreadyClosedException::class); + $this->expectExceptionMessage('Handle has already been closed.'); + + $file->lock(File\LockType::EXCLUSIVE); + } +} diff --git a/tests/unit/File/ReadHandleTest.php b/tests/unit/File/ReadHandleTest.php new file mode 100644 index 00000000..0fbd5c4a --- /dev/null +++ b/tests/unit/File/ReadHandleTest.php @@ -0,0 +1,29 @@ +writeAll('hello'); + static::assertSame(2, $handle->write(', ')); + static::assertSame(6, $handle->writeImmediately('world!')); + + $handle->close(); + + $handle = File\open_read_only($temporary_file); + $content = $handle->readImmediately(); + + static::assertSame('hello, world!', $content); + } +} diff --git a/tests/unit/File/ReadWriteHandleTest.php b/tests/unit/File/ReadWriteHandleTest.php new file mode 100644 index 00000000..0f95e20e --- /dev/null +++ b/tests/unit/File/ReadWriteHandleTest.php @@ -0,0 +1,178 @@ +getPath(); + $file->close(); + + $file = new File\ReadWriteHandle($path); + + static::assertSame($path, $file->getPath()); + } + + public function testGetSize(): void + { + $file = File\temporary(); + $path = $file->getPath(); + $file->close(); + + $file = new File\ReadWriteHandle($path); + + static::assertSame(0, $file->getSize()); + + $file->writeAll('hello'); + $file->seek(3); + + static::assertSame(3, $file->tell()); + static::assertSame(5, $file->getSize()); + static::assertSame(3, $file->tell()); + } + + public function testReading(): void + { + $file = File\temporary(); + $file->writeAll('herpderp'); + $file->seek(0); + static::assertSame('herp', $file->readFixedSize(4)); + static::assertSame('derp', $file->read()); + static::assertSame('', $file->read()); + static::assertSame('', $file->read()); + static::assertSame(8, $file->tell()); + $file->seek(0); + static::assertSame(0, $file->tell()); + static::assertSame('herpderp', $file->read()); + $file->seek(4); + static::assertSame(4, $file->tell()); + static::assertSame('derp', $file->read()); + } + + public function testMustCreateExistingFile(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$path already exists.'); + + new File\ReadWriteHandle(__FILE__, File\WriteMode::MUST_CREATE); + } + + public function testAppendToNonExistingFile(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$path does not exist.'); + + new File\ReadWriteHandle(__FILE__ . '.fake', File\WriteMode::APPEND); + } + + public function testAppendToANonWritableFile(): void + { + $temporary_file = Filesystem\create_temporary_file(); + Filesystem\change_permissions($temporary_file, 0555); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$path is not writable.'); + + new File\ReadWriteHandle($temporary_file, File\WriteMode::APPEND); + } + + public function testCreateNonExisting(): void + { + $temporary_file = Filesystem\create_temporary_file(); + Filesystem\delete_file($temporary_file); + + static::assertFalse(Filesystem\is_file($temporary_file)); + + $handle = File\open_read_write($temporary_file, File\WriteMode::MUST_CREATE); + $handle->writeImmediately('hello'); + $handle->seek(0); + + $content = $handle->readAll(); + + static::assertSame('hello', $content); + + $handle->close(); + + static::assertTrue(Filesystem\is_file($temporary_file)); + } + + /** + * @param (callable(File\ReadWriteHandleInterface): mixed) $operation + * + * @dataProvider provideOperations + */ + public function testClose(callable $operation): void + { + $file = File\temporary(); + $file->close(); + + $this->expectException(IO\Exception\AlreadyClosedException::class); + $this->expectExceptionMessage('Handle has already been closed.'); + + $operation($file); + } + + /** + * @return iterable<(callable(File\ReadWriteHandleInterface): mixed)> + */ + public function provideOperations(): iterable + { + yield [ + static fn(File\HandleInterface $handle) => $handle->seek(5), + ]; + + yield [ + static fn(File\HandleInterface $handle) => $handle->tell(), + ]; + + yield [ + static fn(File\WriteHandleInterface $handle) => $handle->write('hello'), + ]; + + yield [ + static fn(File\WriteHandleInterface $handle) => $handle->writeAll('hello'), + ]; + + yield [ + static fn(File\ReadHandleInterface $handle) => $handle->read(), + ]; + + yield [ + static fn(File\ReadHandleInterface $handle) => $handle->readAll(), + ]; + + yield [ + static fn(File\ReadHandleInterface $handle) => $handle->readImmediately(), + ]; + + yield [ + static fn(File\HandleInterface $handle) => $handle->close(), + ]; + + yield [ + static fn(File\HandleInterface $handle) => $handle->lock(File\LockType::EXCLUSIVE), + ]; + + yield [ + static fn(File\HandleInterface $handle) => $handle->tryLock(File\LockType::EXCLUSIVE), + ]; + + yield [ + static fn(File\HandleInterface $handle) => $handle->getSize(), + ]; + + yield [ + static fn(File\HandleInterface $handle) => $handle->close(), + ]; + } +} diff --git a/tests/unit/File/WriteHandleTest.php b/tests/unit/File/WriteHandleTest.php new file mode 100644 index 00000000..c694b196 --- /dev/null +++ b/tests/unit/File/WriteHandleTest.php @@ -0,0 +1,71 @@ +expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$path already exists.'); + + new File\WriteHandle(__FILE__, File\WriteMode::MUST_CREATE); + } + + public function testAppendToNonExistingFile(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$path does not exist.'); + + $f = new File\WriteHandle(__FILE__ . '.fake', File\WriteMode::APPEND); + $f->write('g'); + } + + public function testAppendToANonWritableFile(): void + { + $temporary_file = Filesystem\create_temporary_file(); + Filesystem\change_permissions($temporary_file, 0555); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$path is not writable.'); + + new File\WriteHandle($temporary_file, File\WriteMode::APPEND); + } + + public function testWriting(): void + { + $temporary_file = Filesystem\create_temporary_file(); + $handle = File\open_write_only($temporary_file); + + $handle->writeAll('hello'); + static::assertSame(2, $handle->write(', ')); + static::assertSame(6, $handle->writeImmediately('world!')); + + $handle->close(); + + $handle = File\open_read_only($temporary_file); + $content = $handle->readAll(); + + static::assertSame('hello, world!', $content); + } + + public function testCreateNonExisting(): void + { + $temporary_file = Filesystem\create_temporary_file(); + Filesystem\delete_file($temporary_file); + + static::assertFalse(Filesystem\is_file($temporary_file)); + + $handle = new File\WriteHandle($temporary_file, File\WriteMode::MUST_CREATE); + $handle->close(); + + static::assertTrue(Filesystem\is_file($temporary_file)); + } +}