From 9448095e2f44b595247410d0fc497b7b68e5240c Mon Sep 17 00:00:00 2001 From: azjezz Date: Tue, 23 Feb 2021 13:06:12 +0100 Subject: [PATCH] [Filesystem] Introduce filesystem component --- phpunit.xml.dist | 14 ++ .../Exception/ExceptionInterface.php | 11 ++ .../Filesystem/Exception/RuntimeException.php | 11 ++ src/Psl/Filesystem/Internal/write_file.php | 67 +++++++ src/Psl/Filesystem/append_file.php | 21 ++ src/Psl/Filesystem/canonicalize.php | 18 ++ src/Psl/Filesystem/change_group.php | 38 ++++ src/Psl/Filesystem/change_owner.php | 37 ++++ src/Psl/Filesystem/change_permissions.php | 33 ++++ src/Psl/Filesystem/constants.php | 12 ++ src/Psl/Filesystem/copy.php | 72 +++++++ src/Psl/Filesystem/create_directory.php | 32 +++ src/Psl/Filesystem/create_file.php | 51 +++++ src/Psl/Filesystem/create_hard_link.php | 55 ++++++ src/Psl/Filesystem/create_symbolic_link.php | 53 +++++ src/Psl/Filesystem/create_temporary_file.php | 58 ++++++ src/Psl/Filesystem/delete_directory.php | 54 +++++ src/Psl/Filesystem/delete_file.php | 34 ++++ src/Psl/Filesystem/exists.php | 22 +++ src/Psl/Filesystem/file_size.php | 33 ++++ src/Psl/Filesystem/get_access_time.php | 40 ++++ src/Psl/Filesystem/get_basename.php | 30 +++ src/Psl/Filesystem/get_change_time.php | 41 ++++ src/Psl/Filesystem/get_directory.php | 33 ++++ src/Psl/Filesystem/get_extension.php | 19 ++ src/Psl/Filesystem/get_filename.php | 21 ++ src/Psl/Filesystem/get_group.php | 38 ++++ src/Psl/Filesystem/get_inode.php | 40 ++++ src/Psl/Filesystem/get_modification_time.php | 41 ++++ src/Psl/Filesystem/get_owner.php | 38 ++++ src/Psl/Filesystem/get_permissions.php | 40 ++++ src/Psl/Filesystem/is_directory.php | 25 +++ src/Psl/Filesystem/is_executable.php | 23 +++ src/Psl/Filesystem/is_file.php | 25 +++ src/Psl/Filesystem/is_readable.php | 23 +++ src/Psl/Filesystem/is_symbolic_link.php | 22 +++ src/Psl/Filesystem/is_writable.php | 23 +++ src/Psl/Filesystem/read_directory.php | 30 +++ src/Psl/Filesystem/read_file.php | 51 +++++ src/Psl/Filesystem/read_symbolic_link.php | 44 +++++ src/Psl/Filesystem/write_file.php | 21 ++ src/Psl/Internal/Loader.php | 43 ++++ src/Psl/Internal/box.php | 53 +++++ src/Psl/Math/maxva.php | 3 +- tests/.cache/.gitignore | 2 + .../Psl/Filesystem/AbstractFilesystemTest.php | 60 ++++++ tests/Psl/Filesystem/CopyTest.php | 58 ++++++ tests/Psl/Filesystem/FileTest.php | 179 +++++++++++++++++ tests/Psl/Filesystem/LinkTest.php | 187 ++++++++++++++++++ tests/Psl/Filesystem/PathTest.php | 68 +++++++ tests/Psl/Filesystem/PermissionsTest.php | 56 ++++++ tests/Psl/Filesystem/ReadDirectoryTest.php | 73 +++++++ 52 files changed, 2175 insertions(+), 1 deletion(-) create mode 100644 src/Psl/Filesystem/Exception/ExceptionInterface.php create mode 100644 src/Psl/Filesystem/Exception/RuntimeException.php create mode 100644 src/Psl/Filesystem/Internal/write_file.php create mode 100644 src/Psl/Filesystem/append_file.php create mode 100644 src/Psl/Filesystem/canonicalize.php create mode 100644 src/Psl/Filesystem/change_group.php create mode 100644 src/Psl/Filesystem/change_owner.php create mode 100644 src/Psl/Filesystem/change_permissions.php create mode 100644 src/Psl/Filesystem/constants.php create mode 100644 src/Psl/Filesystem/copy.php create mode 100644 src/Psl/Filesystem/create_directory.php create mode 100644 src/Psl/Filesystem/create_file.php create mode 100644 src/Psl/Filesystem/create_hard_link.php create mode 100644 src/Psl/Filesystem/create_symbolic_link.php create mode 100644 src/Psl/Filesystem/create_temporary_file.php create mode 100644 src/Psl/Filesystem/delete_directory.php create mode 100644 src/Psl/Filesystem/delete_file.php create mode 100644 src/Psl/Filesystem/exists.php create mode 100644 src/Psl/Filesystem/file_size.php create mode 100644 src/Psl/Filesystem/get_access_time.php create mode 100644 src/Psl/Filesystem/get_basename.php create mode 100644 src/Psl/Filesystem/get_change_time.php create mode 100644 src/Psl/Filesystem/get_directory.php create mode 100644 src/Psl/Filesystem/get_extension.php create mode 100644 src/Psl/Filesystem/get_filename.php create mode 100644 src/Psl/Filesystem/get_group.php create mode 100644 src/Psl/Filesystem/get_inode.php create mode 100644 src/Psl/Filesystem/get_modification_time.php create mode 100644 src/Psl/Filesystem/get_owner.php create mode 100644 src/Psl/Filesystem/get_permissions.php create mode 100644 src/Psl/Filesystem/is_directory.php create mode 100644 src/Psl/Filesystem/is_executable.php create mode 100644 src/Psl/Filesystem/is_file.php create mode 100644 src/Psl/Filesystem/is_readable.php create mode 100644 src/Psl/Filesystem/is_symbolic_link.php create mode 100644 src/Psl/Filesystem/is_writable.php create mode 100644 src/Psl/Filesystem/read_directory.php create mode 100644 src/Psl/Filesystem/read_file.php create mode 100644 src/Psl/Filesystem/read_symbolic_link.php create mode 100644 src/Psl/Filesystem/write_file.php create mode 100644 src/Psl/Internal/box.php create mode 100644 tests/.cache/.gitignore create mode 100644 tests/Psl/Filesystem/AbstractFilesystemTest.php create mode 100644 tests/Psl/Filesystem/CopyTest.php create mode 100644 tests/Psl/Filesystem/FileTest.php create mode 100644 tests/Psl/Filesystem/LinkTest.php create mode 100644 tests/Psl/Filesystem/PathTest.php create mode 100644 tests/Psl/Filesystem/PermissionsTest.php create mode 100644 tests/Psl/Filesystem/ReadDirectoryTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f02d12eb..91fdd28e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,12 +6,26 @@ src + src/preload.php src/bootstrap.php src/Psl/Internal + + src/Psl/Exception + + src/Psl/Str/constants.php src/Psl/Math/constants.php + src/Psl/Filesystem/constants.php + + + src/Psl/Filesystem/get_group.php + src/Psl/Filesystem/change_group.php + src/Psl/Filesystem/Internal/change_group.php + src/Psl/Filesystem/get_owner.php + src/Psl/Filesystem/change_owner.php + src/Psl/Filesystem/Internal/change_owner.php diff --git a/src/Psl/Filesystem/Exception/ExceptionInterface.php b/src/Psl/Filesystem/Exception/ExceptionInterface.php new file mode 100644 index 00000000..62c9103d --- /dev/null +++ b/src/Psl/Filesystem/Exception/ExceptionInterface.php @@ -0,0 +1,11 @@ + file_put_contents($file, $content, $flags) + ); + + // @codeCoverageIgnoreStart + if (false === $written || null !== $error) { + throw new Exception\RuntimeException(Str\format( + 'Failed to write to file "%s": %s.', + $file, + $error ?? 'internal error', + )); + } + + $length = Str\Byte\length($content); + if ($written !== $length) { + throw new Exception\RuntimeException(Str\format( + 'Failed to write the whole content to "%s" ( %g of %g bytes written ).', + $file, + $written, + $length, + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/append_file.php b/src/Psl/Filesystem/append_file.php new file mode 100644 index 00000000..857bd490 --- /dev/null +++ b/src/Psl/Filesystem/append_file.php @@ -0,0 +1,21 @@ + lchgrp($filename, $group); + } else { + $fun = static fn(): bool => chgrp($filename, $group); + } + + [$success, $error] = Internal\box($fun); + if (!$success) { + throw new Exception\RuntimeException(Str\format( + 'Failed to change the group for file "%s": %s', + $filename, + $error ?? 'internal error.', + )); + } +} diff --git a/src/Psl/Filesystem/change_owner.php b/src/Psl/Filesystem/change_owner.php new file mode 100644 index 00000000..ac305797 --- /dev/null +++ b/src/Psl/Filesystem/change_owner.php @@ -0,0 +1,37 @@ + lchown($filename, $user); + } else { + $fun = static fn(): bool => chown($filename, $user); + } + + [$success, $error] = Internal\box($fun); + if (!$success) { + throw new Exception\RuntimeException(Str\format( + 'Failed to change owner for file "%s": %s', + $filename, + $error ?? 'internal error.', + )); + } +} diff --git a/src/Psl/Filesystem/change_permissions.php b/src/Psl/Filesystem/change_permissions.php new file mode 100644 index 00000000..ed7ae346 --- /dev/null +++ b/src/Psl/Filesystem/change_permissions.php @@ -0,0 +1,33 @@ + chmod($filename, $permissions)); + // @codeCoverageIgnoreStart + if (!$success) { + throw new Exception\RuntimeException(Str\format( + 'Failed to change permissions for file "%s": %s', + $filename, + $error ?? 'internal error.', + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/constants.php b/src/Psl/Filesystem/constants.php new file mode 100644 index 00000000..70af81d4 --- /dev/null +++ b/src/Psl/Filesystem/constants.php @@ -0,0 +1,12 @@ + fopen($source, 'rb')); + // @codeCoverageIgnoreStart + if (false === $source_stream) { + throw new Exception\RuntimeException('Failed to open $source file for reading'); + } + // @codeCoverageIgnoreEnd + + /** + * @psalm-suppress InvalidArgument - callable is not pure.. + */ + $destination_stream = Internal\suppress(static fn() => fopen($destination, 'wb')); + // @codeCoverageIgnoreStart + if (false === $destination_stream) { + throw new Exception\RuntimeException('Failed to open $destination file for writing.'); + } + // @codeCoverageIgnoreEnd + + $copied_bytes = stream_copy_to_stream($source_stream, $destination_stream); + fclose($source_stream); + fclose($destination_stream); + + $total_bytes = file_size($source); + + // preserve executable permission bits + change_permissions( + $destination, + get_permissions($destination) | (get_permissions($source) & 0111) + ); + + // @codeCoverageIgnoreStart + if ($copied_bytes !== $total_bytes) { + throw new Exception\RuntimeException(Str\format( + 'Failed to copy the whole content of "%s" to "%s" ( %g of %g bytes copied ).', + $source, + $destination, + $copied_bytes, + $total_bytes + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_directory.php b/src/Psl/Filesystem/create_directory.php new file mode 100644 index 00000000..f9756a95 --- /dev/null +++ b/src/Psl/Filesystem/create_directory.php @@ -0,0 +1,32 @@ + mkdir($directory, $permissions, true) + ); + + // @codeCoverageIgnoreStart + if (false === $result && !is_directory($directory)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create directory "%s": %s.', + $directory, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_file.php b/src/Psl/Filesystem/create_file.php new file mode 100644 index 00000000..ba8f9a7f --- /dev/null +++ b/src/Psl/Filesystem/create_file.php @@ -0,0 +1,51 @@ + touch($filename); + } elseif (null === $access_time) { + $fun = static fn(): bool => touch($filename, $time); + } else { + $time = $time ?? $access_time; + + $fun = static fn(): bool => touch($filename, $time, Math\maxva($access_time, $time)); + } + + /** @psalm-suppress MissingThrowsDocblock */ + $directory = get_directory($filename); + if (!is_directory($directory)) { + create_directory($directory); + } + + [$result, $error_message] = Internal\box($fun); + // @codeCoverageIgnoreStart + if (false === $result && !is_file($filename)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create file "%s": %s.', + $filename, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_hard_link.php b/src/Psl/Filesystem/create_hard_link.php new file mode 100644 index 00000000..e918ad2e --- /dev/null +++ b/src/Psl/Filesystem/create_hard_link.php @@ -0,0 +1,55 @@ + link($source, $destination)); + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create hard link "%s" from "%s": %s.', + $destination, + $source, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_symbolic_link.php b/src/Psl/Filesystem/create_symbolic_link.php new file mode 100644 index 00000000..4069ae07 --- /dev/null +++ b/src/Psl/Filesystem/create_symbolic_link.php @@ -0,0 +1,53 @@ + symlink($source, $destination)); + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to create symbolic link "%s" from "%s": %s.', + $destination, + $source, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/create_temporary_file.php b/src/Psl/Filesystem/create_temporary_file.php new file mode 100644 index 00000000..91220f2a --- /dev/null +++ b/src/Psl/Filesystem/create_temporary_file.php @@ -0,0 +1,58 @@ + is_symbolic_link($node) + ); + + Iter\apply($symbolic_nodes, static fn(string $node) => delete_file($node)); + Iter\apply( + $nodes, + Fun\when( + static fn(string $node) => is_directory($node), + static fn(string $node) => delete_directory($node, true), + static fn(string $node) => delete_file($node), + ) + ); + } + + [$result, $error_message] = Internal\box(static fn() => rmdir($directory)); + // @codeCoverageIgnoreStart + if (false === $result && is_directory($directory)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to delete directory "%s": %s.', + $directory, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/delete_file.php b/src/Psl/Filesystem/delete_file.php new file mode 100644 index 00000000..938799d9 --- /dev/null +++ b/src/Psl/Filesystem/delete_file.php @@ -0,0 +1,34 @@ + unlink($filename)); + // @codeCoverageIgnoreStart + if (false === $result && is_file($filename)) { + throw new Exception\RuntimeException(Str\format( + 'Failed to delete file "%s": %s.', + $filename, + $error_message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd +} diff --git a/src/Psl/Filesystem/exists.php b/src/Psl/Filesystem/exists.php new file mode 100644 index 00000000..dab93a2d --- /dev/null +++ b/src/Psl/Filesystem/exists.php @@ -0,0 +1,22 @@ + filesize($filename)); + if (false === $size) { + throw new Exception\RuntimeException(Str\format( + 'Error reading the size of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $size; +} diff --git a/src/Psl/Filesystem/get_access_time.php b/src/Psl/Filesystem/get_access_time.php new file mode 100644 index 00000000..49b710fe --- /dev/null +++ b/src/Psl/Filesystem/get_access_time.php @@ -0,0 +1,40 @@ + fileatime($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the access time of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_basename.php b/src/Psl/Filesystem/get_basename.php new file mode 100644 index 00000000..e526c81e --- /dev/null +++ b/src/Psl/Filesystem/get_basename.php @@ -0,0 +1,30 @@ + filectime($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the change time of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_directory.php b/src/Psl/Filesystem/get_directory.php new file mode 100644 index 00000000..52f165bb --- /dev/null +++ b/src/Psl/Filesystem/get_directory.php @@ -0,0 +1,33 @@ + 0, '$levels must be a positive integer, %d given.', $levels); + + return dirname($path, $levels); +} diff --git a/src/Psl/Filesystem/get_extension.php b/src/Psl/Filesystem/get_extension.php new file mode 100644 index 00000000..9d7bfb36 --- /dev/null +++ b/src/Psl/Filesystem/get_extension.php @@ -0,0 +1,19 @@ + filegroup($filename) + ); + + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve group of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + + return $result; +} diff --git a/src/Psl/Filesystem/get_inode.php b/src/Psl/Filesystem/get_inode.php new file mode 100644 index 00000000..02773c2e --- /dev/null +++ b/src/Psl/Filesystem/get_inode.php @@ -0,0 +1,40 @@ + fileinode($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the inode of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_modification_time.php b/src/Psl/Filesystem/get_modification_time.php new file mode 100644 index 00000000..f86dab1f --- /dev/null +++ b/src/Psl/Filesystem/get_modification_time.php @@ -0,0 +1,41 @@ + filemtime($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the modification time of "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/get_owner.php b/src/Psl/Filesystem/get_owner.php new file mode 100644 index 00000000..321eea22 --- /dev/null +++ b/src/Psl/Filesystem/get_owner.php @@ -0,0 +1,38 @@ + fileowner($filename) + ); + + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve owner of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + + return $result; +} diff --git a/src/Psl/Filesystem/get_permissions.php b/src/Psl/Filesystem/get_permissions.php new file mode 100644 index 00000000..68244171 --- /dev/null +++ b/src/Psl/Filesystem/get_permissions.php @@ -0,0 +1,40 @@ + fileperms($filename) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve permissions of file "%s": %s', + $filename, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/is_directory.php b/src/Psl/Filesystem/is_directory.php new file mode 100644 index 00000000..fee1fa92 --- /dev/null +++ b/src/Psl/Filesystem/is_directory.php @@ -0,0 +1,25 @@ + + * + * @throws Psl\Exception\InvariantViolationException If the directory specified by + * $directory does not exist, or is not readable. + */ +function read_directory(string $directory): array +{ + Psl\invariant(exists($directory), '$directory does not exists.'); + Psl\invariant(is_directory($directory), '$directory is not a directory.'); + Psl\invariant(is_readable($directory), '$directory is not readable.'); + + /** @var list */ + return Vec\values(new FilesystemIterator( + $directory, + FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS + )); +} diff --git a/src/Psl/Filesystem/read_file.php b/src/Psl/Filesystem/read_file.php new file mode 100644 index 00000000..3e04c196 --- /dev/null +++ b/src/Psl/Filesystem/read_file.php @@ -0,0 +1,51 @@ + file_get_contents($file, false, null, $offset) + ); + } else { + [$content, $error] = Internal\box( + static fn() => file_get_contents($file, false, null, $offset, $length) + ); + } + + // @codeCoverageIgnoreStart + if (false === $content || null !== $error) { + throw new Exception\RuntimeException(Str\format( + 'Failed to read file "%s": %s.', + $file, + $error ?? 'internal error', + )); + } + // @codeCoverageIgnoreEnd + + return $content; +} diff --git a/src/Psl/Filesystem/read_symbolic_link.php b/src/Psl/Filesystem/read_symbolic_link.php new file mode 100644 index 00000000..da7e0588 --- /dev/null +++ b/src/Psl/Filesystem/read_symbolic_link.php @@ -0,0 +1,44 @@ + readlink($symbolic_link) + ); + + // @codeCoverageIgnoreStart + if (false === $result) { + throw new Exception\RuntimeException(Str\format( + 'Failed to retrieve the target of symbolic link "%s": %s', + $symbolic_link, + $message ?? 'internal error' + )); + } + // @codeCoverageIgnoreEnd + + return $result; +} diff --git a/src/Psl/Filesystem/write_file.php b/src/Psl/Filesystem/write_file.php new file mode 100644 index 00000000..75bd5ace --- /dev/null +++ b/src/Psl/Filesystem/write_file.php @@ -0,0 +1,21 @@ +cacheDirectory = Type\string()->assert(Filesystem\canonicalize(Str\join([ + __DIR__, '..', '..', '.cache' + ], Filesystem\SEPARATOR))); + + $this->directory = Str\join([$this->cacheDirectory, $this->function], Filesystem\SEPARATOR); + Filesystem\create_directory($this->directory); + $this->directoryPermissions = Filesystem\get_permissions($this->directory) & 0777; + + static::assertTrue(Filesystem\exists($this->directory)); + static::assertTrue(Filesystem\is_directory($this->directory)); + } + + protected function tearDown(): void + { + Filesystem\change_permissions($this->directory, $this->directoryPermissions); + Filesystem\delete_directory($this->directory, true); + + static::assertFalse(Filesystem\is_directory($this->directory)); + } + + protected static function runOnlyOnLinux(): void + { + if ('Linux' !== PHP_OS_FAMILY) { + static::markTestSkipped('Test can only be executed on linux.'); + } + } + + protected static function runOnlyUsingRoot(): void + { + $user = Env\get_var('USER'); + if (null === $user || 'root' === $user) { + return; + } + + static::markTestSkipped('Test can only be executed by a superuser.'); + } +} diff --git a/tests/Psl/Filesystem/CopyTest.php b/tests/Psl/Filesystem/CopyTest.php new file mode 100644 index 00000000..4a3b6689 --- /dev/null +++ b/tests/Psl/Filesystem/CopyTest.php @@ -0,0 +1,58 @@ +directory, 'hello.txt'], Filesystem\SEPARATOR); + $markdown_file = Str\join([$this->directory, 'hello.md'], Filesystem\SEPARATOR); + + Filesystem\write_file($text_file, 'Hello, World!'); + Filesystem\copy($text_file, $markdown_file); + + static::assertSame('Hello, World!', Filesystem\read_file($markdown_file)); + } + + public function testCopyOverwrite(): void + { + $text_file = Str\join([$this->directory, 'hello.txt'], Filesystem\SEPARATOR); + $markdown_file = Str\join([$this->directory, 'hello.md'], Filesystem\SEPARATOR); + + Filesystem\write_file($text_file, 'Hello, World!'); + Filesystem\write_file($markdown_file, '# Hello, World!'); + Filesystem\copy($text_file, $markdown_file); + + static::assertSame('Hello, World!', Filesystem\read_file($text_file)); + static::assertSame('# Hello, World!', Filesystem\read_file($markdown_file)); + + Filesystem\copy($text_file, $markdown_file, true); + + static::assertSame('Hello, World!', Filesystem\read_file($text_file)); + static::assertSame('Hello, World!', Filesystem\read_file($markdown_file)); + } + + public function testCopyExecutableBits(): void + { + $shell_file = Str\join([$this->directory, 'hello.sh'], Filesystem\SEPARATOR); + + Filesystem\create_file($shell_file); + Filesystem\change_permissions($shell_file, 0557); + + static::assertTrue(Filesystem\is_executable($shell_file)); + + $shell_file_copy = Str\join([$this->directory, 'hey.sh'], Filesystem\SEPARATOR); + + Filesystem\copy($shell_file, $shell_file_copy); + + static::assertTrue(Filesystem\is_executable($shell_file_copy)); + } +} diff --git a/tests/Psl/Filesystem/FileTest.php b/tests/Psl/Filesystem/FileTest.php new file mode 100644 index 00000000..93909d07 --- /dev/null +++ b/tests/Psl/Filesystem/FileTest.php @@ -0,0 +1,179 @@ +directory); + + static::assertTrue(Filesystem\is_file($file)); + static::assertSame($this->directory, Filesystem\get_directory($file)); + } + + public function testTemporaryFileWithPrefix(): void + { + $file = Filesystem\create_temporary_file($this->directory, 'foo'); + + static::assertTrue(Filesystem\is_file($file)); + static::assertSame($this->directory, Filesystem\get_directory($file)); + static::assertStringContainsString('foo', Filesystem\get_filename($file)); + } + + public function testTemporaryFileIsCreateInTempDirectoryByDefault(): void + { + $file = Filesystem\create_temporary_file(); + + static::assertSame(Env\temp_dir(), Filesystem\get_directory($file)); + } + + public function testTemporaryFileThrowsForNonExistingDirectory(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory is not a directory.'); + + Filesystem\create_temporary_file(__FILE__); + } + + public function testTemporaryFileThrowsForPrefixWithSeparator(): void + { + $prefix = Str\join(['a', 'b'], Filesystem\SEPARATOR); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage( + '$prefix should not contain a directory separator ( "' . Filesystem\SEPARATOR . '" ).' + ); + + Filesystem\create_temporary_file($this->directory, $prefix); + } + + public function testCreateFileAndParentDirectory(): void + { + $directory = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + $file = Str\join([$directory, 'write.txt'], Filesystem\SEPARATOR); + + static::assertFalse(Filesystem\is_directory($directory)); + + Filesystem\create_file($file); + + static::assertTrue(Filesystem\is_directory($directory)); + static::assertTrue(Filesystem\is_file($file)); + } + + public function testWriteFile(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + static::assertFileDoesNotExist($file); + + Filesystem\write_file($file, 'Hello!'); + + static::assertFileExists($file); + + static::assertStringEqualsFile($file, 'Hello!'); + + Filesystem\write_file($file, 'Hello'); + + static::assertStringEqualsFile($file, 'Hello'); + + Filesystem\append_file($file, ', World!'); + + static::assertStringEqualsFile($file, 'Hello, World!'); + + Filesystem\delete_file($file); + } + + public function testWriteFileThrowsForDirectories(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$file is not a file.'); + + Filesystem\write_file($this->directory, 'hello'); + } + + public function testWriteFileThrowsForNonWritableFiles(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + Filesystem\create_file($file); + Filesystem\change_permissions($file, 0111); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$file is not writeable.'); + + Filesystem\write_file($file, 'hello'); + } + + public function testReadFile(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + Filesystem\write_file($file, 'PHP Standard Library'); + Filesystem\append_file($file, ' - a modern, consistent, centralized'); + Filesystem\append_file($file, ' well-typed set of APIs for PHP programmers.'); + + $content = Filesystem\read_file($file, 0, 20); + + static::assertSame('PHP Standard Library', $content); + + $content = Filesystem\read_file($file, 84, 16); + + static::assertSame('PHP programmers.', $content); + } + + public function testFileModificationAndAccessTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + $modification_time = time() - 3600; + $access_time = time() - 1800; + + Filesystem\create_file($file, $modification_time, $access_time); + + static::assertSame($modification_time, Filesystem\get_modification_time($file)); + static::assertSame($access_time, Filesystem\get_access_time($file)); + } + + public function testFileAccessTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + $access_time = time() - 1800; + + Filesystem\create_file($file, null, $access_time); + + static::assertSame($access_time, Filesystem\get_modification_time($file)); + static::assertSame($access_time, Filesystem\get_access_time($file)); + } + + public function testFileModificationTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + $modification_time = time() - 3600; + + Filesystem\create_file($file, $modification_time); + + static::assertSame($modification_time, Filesystem\get_modification_time($file)); + static::assertSame($modification_time, Filesystem\get_access_time($file)); + } + + public function testFileChangeTime(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + static::assertEqualsWithDelta(time(), Filesystem\get_change_time($file), 1.0); + } +} diff --git a/tests/Psl/Filesystem/LinkTest.php b/tests/Psl/Filesystem/LinkTest.php new file mode 100644 index 00000000..7179160c --- /dev/null +++ b/tests/Psl/Filesystem/LinkTest.php @@ -0,0 +1,187 @@ +directory, 'write.txt'], Filesystem\SEPARATOR); + $symlink = Str\join([$this->directory, 'symlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_symbolic_link($file, $symlink); + + static::assertTrue(Filesystem\exists($symlink)); + static::assertTrue(Filesystem\is_symbolic_link($symlink)); + + static::assertSame($file, Filesystem\read_symbolic_link($symlink)); + } + + public function testSymbolicLinkAlreadyExists(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $symlink = Str\join([$this->directory, 'symlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_symbolic_link($file, $symlink); + + static::assertTrue(Filesystem\exists($symlink)); + static::assertTrue(Filesystem\is_symbolic_link($symlink)); + + static::assertSame($file, Filesystem\read_symbolic_link($symlink)); + + Filesystem\create_symbolic_link($file, $symlink); + + static::assertTrue(Filesystem\exists($symlink)); + static::assertTrue(Filesystem\is_symbolic_link($symlink)); + + static::assertSame($file, Filesystem\read_symbolic_link($symlink)); + } + + public function testSymbolicLinkOverwrite(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $symbolic_link = Str\join([$this->directory, 'symbolic_link.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_file($symbolic_link); + + static::assertFalse(Filesystem\is_symbolic_link($symbolic_link)); + + Filesystem\create_symbolic_link($file, $symbolic_link); + + static::assertTrue(Filesystem\is_symbolic_link($symbolic_link)); + static::assertSame($file, Filesystem\read_symbolic_link($symbolic_link)); + + $file = Str\join([$this->directory, 'foo', 'bar'], Filesystem\SEPARATOR); + $symbolic_link = Str\join([$this->directory, 'foo', 'baz'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_directory($symbolic_link); + + static::assertFalse(Filesystem\is_symbolic_link($symbolic_link)); + + Filesystem\create_symbolic_link($file, $symbolic_link); + + static::assertTrue(Filesystem\is_symbolic_link($symbolic_link)); + static::assertSame($file, Filesystem\read_symbolic_link($symbolic_link)); + } + + public function testSymbolicLinkCreatesDestinationsDirectory(): void + { + self::runOnlyOnLinux(); + + $directory = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $symbolic_link = Str\join([$directory, 'symbolic.txt'], Filesystem\SEPARATOR); + + static::assertFalse(Filesystem\is_directory($directory)); + + Filesystem\create_file($file); + Filesystem\create_symbolic_link($file, $symbolic_link); + + static::assertTrue(Filesystem\is_directory($directory)); + } + + public function testHardLink(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertTrue(Filesystem\exists($hardlink)); + static::assertFalse(Filesystem\is_symbolic_link($hardlink)); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkAlreadyExists(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertTrue(Filesystem\exists($hardlink)); + static::assertFalse(Filesystem\is_symbolic_link($hardlink)); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkCreatesDestinationDirectory(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $destination_directory = Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR); + $hardlink = Str\join([$destination_directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + static::assertFalse(Filesystem\is_directory($destination_directory)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertTrue(Filesystem\is_directory($destination_directory)); + static::assertTrue(Filesystem\exists($hardlink)); + static::assertFalse(Filesystem\is_symbolic_link($hardlink)); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkOverwrite(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_file($hardlink); + + static::assertNotSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + $file = Str\join([$this->directory, 'foo', 'bar'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'foo', 'baz'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + Filesystem\create_directory($hardlink); + + static::assertNotSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } + + public function testHardLinkCreatesDestinationsDirectory(): void + { + $file = Str\join([$this->directory, 'write.txt'], Filesystem\SEPARATOR); + $hardlink = Str\join([$this->directory, 'baz', 'hardlink.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($file); + + Filesystem\create_hard_link($file, $hardlink); + + static::assertSame(Filesystem\get_inode($file), Filesystem\get_inode($hardlink)); + } +} diff --git a/tests/Psl/Filesystem/PathTest.php b/tests/Psl/Filesystem/PathTest.php new file mode 100644 index 00000000..d47a31aa --- /dev/null +++ b/tests/Psl/Filesystem/PathTest.php @@ -0,0 +1,68 @@ +expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$levels must be a positive integer, -3 given.'); + + Filesystem\get_directory('/home/azjezz/Projects/psl/src/Psl', -3); + } +} diff --git a/tests/Psl/Filesystem/PermissionsTest.php b/tests/Psl/Filesystem/PermissionsTest.php new file mode 100644 index 00000000..8162cc0d --- /dev/null +++ b/tests/Psl/Filesystem/PermissionsTest.php @@ -0,0 +1,56 @@ +directory, 'foo.txt'], Filesystem\SEPARATOR); + + Filesystem\create_file($filename); + + $permissions = Filesystem\get_permissions($filename) & 0777; + + try { + Filesystem\change_permissions($filename, 0444); + + static::assertTrue(Filesystem\is_readable($filename)); + static::assertFalse(Filesystem\is_writable($filename)); + static::assertFalse(Filesystem\is_executable($filename)); + + Filesystem\change_permissions($filename, 0222); + + static::assertTrue(Filesystem\is_writable($filename)); + static::assertFalse(Filesystem\is_readable($filename)); + static::assertFalse(Filesystem\is_executable($filename)); + + Filesystem\change_permissions($filename, 0111); + + static::assertTrue(Filesystem\is_executable($filename)); + static::assertFalse(Filesystem\is_writable($filename)); + static::assertFalse(Filesystem\is_readable($filename)); + + Filesystem\change_permissions($filename, 0666); + + static::assertTrue(Filesystem\is_writable($filename)); + static::assertTrue(Filesystem\is_readable($filename)); + static::assertFalse(Filesystem\is_executable($filename)); + + Filesystem\change_permissions($filename, 0777); + + static::assertTrue(Filesystem\is_writable($filename)); + static::assertTrue(Filesystem\is_readable($filename)); + static::assertTrue(Filesystem\is_executable($filename)); + } finally { + Filesystem\change_permissions($filename, $permissions); + } + } +} diff --git a/tests/Psl/Filesystem/ReadDirectoryTest.php b/tests/Psl/Filesystem/ReadDirectoryTest.php new file mode 100644 index 00000000..f9842920 --- /dev/null +++ b/tests/Psl/Filesystem/ReadDirectoryTest.php @@ -0,0 +1,73 @@ +directory, 'hello.txt' + ], Filesystem\SEPARATOR)); + + Filesystem\create_directory(Str\join([ + $this->directory, 'foo' + ], Filesystem\SEPARATOR)); + + $children = Filesystem\read_directory($this->directory); + + static::assertCount(2, $children); + static::assertSame([ + Str\join([$this->directory, 'foo'], Filesystem\SEPARATOR), + Str\join([$this->directory, 'hello.txt'], Filesystem\SEPARATOR), + ], Vec\sort($children)); + } + + public function testReadDirectoryThrowsIfDirectoryDoesNotExist(): void + { + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory does not exists.'); + + Filesystem\read_directory(Env\temp_dir() . '/foo-bar-baz'); + } + + public function testReadDirectoryThrowsIfNotDirectory(): void + { + $filename = Str\join([ + $this->directory, 'hello.txt' + ], Filesystem\SEPARATOR); + + Filesystem\create_file($filename); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory is not a directory.'); + + Filesystem\read_directory($filename); + } + + public function testReadDirectoryThrowsIfNotReadable(): void + { + Filesystem\change_permissions($this->directory, 0077); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('$directory is not readable.'); + + try { + Filesystem\read_directory($this->directory); + } finally { + // restore $this->directory permissions, otherwise we won't + // be able to delete it. + Filesystem\change_permissions($this->directory, 0777); + } + } +}