Browse Source

add unit tests from open to iterDir

olinox14 1 year ago
parent
commit
83146a29d4
3 changed files with 556 additions and 34 deletions
  1. 2 0
      TODO.txt
  2. 47 31
      src/Path.php
  3. 507 3
      tests/unit/PathTest.php

+ 2 - 0
TODO.txt

@@ -7,3 +7,5 @@
 - Créer la doc en ligne
 - Faire le guide des contribs
 - Rendre compatible avec windows / mac
+- faire des constantes des modes de fopen?
+- revoir la gestion des erreurs et les traceback

+ 47 - 31
src/Path.php

@@ -770,11 +770,10 @@ class Path
      * Changes the permissions of a file or directory.
      *
      * @param int $permissions The new permissions to set. The value should be an octal number.
-     * @return bool Returns true on success, false on failure.
      * @throws FileNotFoundException
      * @throws IOException
      */
-    public function setPermissions(int $permissions): bool
+    public function setPermissions(int $permissions): void
     {
         if (!$this->isFile()) {
             throw new FileNotFoundException("File or dir does not exist : " . $this->path);
@@ -786,8 +785,6 @@ class Path
         if ($success === false) {
             throw new IOException("Error while setting permissions on " . $this->path);
         }
-
-        return true;
     }
 
     /**
@@ -795,7 +792,6 @@ class Path
      *
      * @param string $user The new owner username.
      * @param string $group The new owner group name.
-     * @return bool
      * @throws FileNotFoundException
      * @throws IOException
      */
@@ -989,26 +985,29 @@ class Path
      * @param string $mode The mode in which to open the file. Defaults to 'r'.
      * @throws Throwable If an exception is thrown within the callback function.
      */
-    public function with(callable $callback, string $mode = 'r')
+    public function with(callable $callback, string $mode = 'r'): mixed
     {
         $handle = $this->open($mode);
         try {
             return $callback($handle);
         } finally {
-            $this->builtin->fclose($handle);
+            $closed = $this->builtin->fclose($handle);
+            if (!$closed) {
+                throw new IOException("Could not close the file stream : " .$this->path);
+            }
         }
     }
 
     /**
      * Retrieves chunks of data from the file.
      *
-     * @param callable $callback The callback function to process each chunk of data.
      * @param int $chunk_size The size of each chunk in bytes. Defaults to 8192.
      * @return Generator Returns a generator that yields each chunk of data read from the file.
      * @throws FileNotFoundException
      * @throws IOException
+     * @throws Throwable
      */
-    public function chunks(callable $callback, int $chunk_size = 8192): Generator
+    public function chunks(int $chunk_size = 8192): Generator
     {
         $handle = $this->open('rb');
         try {
@@ -1016,7 +1015,10 @@ class Path
                 yield $this->builtin->fread($handle, $chunk_size);
             }
         } finally {
-            $this->builtin->fclose($handle);
+            $closed = $this->builtin->fclose($handle);
+            if (!$closed) {
+                throw new IOException("Could not close the file stream : " .$this->path);
+            }
         }
     }
 
@@ -1035,12 +1037,11 @@ class Path
      * Changes permissions of the file.
      *
      * @param int $mode The new permissions (octal).
-     * @return bool
-     * @throws FileNotFoundException
+     * @throws FileNotFoundException|IOException
      */
-    public function chmod(int $mode): bool
+    public function chmod(int $mode): void
     {
-        return $this->setPermissions($mode);
+        $this->setPermissions($mode);
     }
 
     /**
@@ -1049,22 +1050,24 @@ class Path
      *
      * @param string $user The new owner username.
      * @param string $group The new owner group name.
-     * @return bool
-     * @throws FileNotFoundException
+     * @throws FileNotFoundException|IOException
      */
-    public function chown(string $user, string $group): bool
+    public function chown(string $user, string $group): void
     {
-        return $this->setOwner($user, $group);
+        $this->setOwner($user, $group);
     }
 
     /**
      * Changes the root directory of the current process to the specified directory.
      *
-     * @return bool Returns true on success or false on failure.
+     * @throws IOException
      */
-    public function chroot(): bool
+    public function chroot(): void
     {
-        return $this->builtin->chroot($this->path);
+        $success = $this->builtin->chroot($this->path);
+        if (!$success) {
+            throw new IOException("Error changing root directory to " . $this->path);
+        }
     }
 
     /**
@@ -1082,10 +1085,10 @@ class Path
         // TODO: implement https://path.readthedocs.io/en/latest/api.html#path.Path.ismount
     }
 
-    public function getDirectoryIterator()
+    protected function getDirectoryIterator(): \DirectoryIterator
     {
-        // TODO: complete
-        return new \DirectoryIterator($this->path);
+        // TODO: make it public?
+         return new \DirectoryIterator($this->path);
     }
 
     /**
@@ -1100,7 +1103,7 @@ class Path
             throw new FileNotFoundException("{$this->path} is not a directory");
         }
 
-        foreach (new \DirectoryIterator($this->path) as $fileInfo) {
+        foreach ($this->getDirectoryIterator() as $fileInfo) {
             // TODO: use the DirectoryIterator everywhere else?
             if ($fileInfo->isDot()) {
                 continue;
@@ -1118,15 +1121,28 @@ class Path
      * Create a hard link pointing to a path.
      *
      * @param string|Path $target
-     * @return bool
+     * @throws FileExistsException
+     * @throws FileNotFoundException
+     * @throws IOException
      */
-    public function link(string|self $target): bool
+    public function link(string|self $target): void
     {
-        $target = (string)$target;
-        if (!function_exists('link')) {
-            return false;
+        // TODO: manage dirs and files here
+        if (!$this->isFile()) {
+            throw new FileNotFoundException("{$this->path} is not a file");
+        }
+
+        $target = $this->cast($target);
+
+        if ($target->isFile()) {
+            throw new FileExistsException($target . " already exist");
+        }
+
+        $success = $this->builtin->link($this->path, (string)$target);
+
+        if ($success === false) {
+            throw new IOException("Error while creating the link from " . $this->path . " to " . $target);
         }
-        return $this->builtin->link($this->path, $target);
     }
 
     /**

+ 507 - 3
tests/unit/PathTest.php

@@ -2,6 +2,7 @@
 
 namespace Path\Tests\unit;
 
+use http\Params;
 use Path\BuiltinProxy;
 use Path\Exception\FileExistsException;
 use Path\Exception\FileNotFoundException;
@@ -24,6 +25,11 @@ class TestablePath extends Path {
     public function rrmdir(): bool {
         return $this->rrmdir();
     }
+
+    public function getDirectoryIterator(): \DirectoryIterator
+    {
+        return parent::getDirectoryIterator();
+    }
 }
 
 class PathTest extends TestCase
@@ -1726,9 +1732,7 @@ class PathTest extends TestCase
             ->expects(self::once())
             ->method('clearstatcache');
 
-        $this->assertTrue(
-            $path->setPermissions(0777)
-        );
+        $path->setPermissions(0777);
     }
 
     /**
@@ -1778,6 +1782,7 @@ class PathTest extends TestCase
 
     /**
      * @throws FileNotFoundException
+     * @throws IOException
      */
     public function testSetOwner(): void
     {
@@ -1994,4 +1999,503 @@ class PathTest extends TestCase
 
         $path->rmdir(True);
     }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testOpen(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'open');
+        $path->method('isFile')->willReturn(True);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fopen')
+            ->with('/foo/file.ext', 'r')
+            ->willReturn('the_handle');
+
+        $this->assertEquals(
+            'the_handle',
+            $path->open()
+        );
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testOpenWithMode(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'open');
+        $path->method('isFile')->willReturn(True);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fopen')
+            ->with('/foo/file.ext', 'w')
+            ->willReturn('the_handle');
+
+        $this->assertEquals(
+            'the_handle',
+            $path->open('w')
+        );
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testOpenFileDoesNotExist(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'open');
+        $path->method('isFile')->willReturn(False);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('fopen');
+
+        $this->expectException(FileNotFoundException::class);
+
+        $path->open();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testOpenWithError(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'open');
+        $path->method('isFile')->willReturn(True);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fopen')
+            ->with('/foo/file.ext', 'r')
+            ->willReturn(False);
+
+        $this->expectException(IOException::class);
+
+        $path->open();
+    }
+
+    /**
+     * @throws \Throwable
+     */
+    public function testWith(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'with');
+        $path->method('open')->with('r')->willReturn('the_handle');
+
+        $callback = function ($handle) {
+            $this->assertEquals('the_handle', $handle);
+            return 'content';
+        };
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fclose')
+            ->with('the_handle')
+            ->willReturn(True);
+
+        $this->assertEquals(
+            'content',
+            $path->with($callback)
+        );
+    }
+
+    /**
+     * @throws \Throwable
+     */
+    public function testWithWithMode(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'with');
+        $path->method('open')->with('w+')->willReturn('the_handle');
+
+        $callback = function ($handle) {
+            $this->assertEquals('the_handle', $handle);
+            return 'content';
+        };
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fclose')
+            ->with('the_handle')
+            ->willReturn(True);
+
+        $this->assertEquals(
+            'content',
+            $path->with($callback, 'w+')
+        );
+    }
+
+    /**
+     * @throws \Throwable
+     */
+    public function testWithWithCallbackError(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'with');
+        $path->method('open')->with('w+')->willReturn('the_handle');
+
+        $callback = function ($handle) {
+            $this->assertEquals('the_handle', $handle);
+            throw new \Exception('some_error');
+        };
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fclose')
+            ->with('the_handle')
+            ->willReturn(True);
+
+        $this->expectException(\Exception::class);
+
+        $path->with($callback, 'w+');
+    }
+
+    /**
+     * @throws \Throwable
+     */
+    public function testWithWithCallbackErrorAndClosingError(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'with');
+        $path->method('open')->with('w+')->willReturn('the_handle');
+
+        $callback = function ($handle) {
+            $this->assertEquals('the_handle', $handle);
+            throw new \Exception('some_error');
+        };
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fclose')
+            ->with('the_handle')
+            ->willReturn(False);
+
+        $this->expectException(\Exception::class);
+        $this->expectException(IOException::class);
+
+        $path->with($callback, 'w+');
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws \Throwable
+     */
+    public function testChunks(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'chunks');
+        $path->method('open')->with('rb')->willReturn('the_handle');
+
+        $this->builtin
+            ->method('feof')
+            ->with('the_handle')
+            ->willReturnOnConsecutiveCalls(
+                False,
+                False,
+                False,
+                True
+            );
+
+        $this->builtin
+            ->method('fread')
+            ->with('the_handle', 8192)
+            ->willReturnOnConsecutiveCalls(
+                'abc',
+                'def',
+                'ghi'
+            );
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fclose')
+            ->with('the_handle')
+            ->willReturn(True);
+
+        $this->assertEquals(
+            ['abc', 'def', 'ghi'],
+            iterator_to_array($path->chunks())
+        );
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws \Throwable
+     */
+    public function testChunksWithDifferentLength(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'chunks');
+        $path->method('open')->with('rb')->willReturn('the_handle');
+
+        $this->builtin
+            ->method('feof')
+            ->with('the_handle')
+            ->willReturnOnConsecutiveCalls(
+                False,
+                False,
+                False,
+                True
+            );
+
+        $this->builtin
+            ->method('fread')
+            ->with('the_handle', 123)
+            ->willReturnOnConsecutiveCalls(
+                'abc',
+                'def',
+                'ghi'
+            );
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fclose')
+            ->with('the_handle')
+            ->willReturn(True);
+
+        $this->assertEquals(
+            ['abc', 'def', 'ghi'],
+            iterator_to_array($path->chunks(123))
+        );
+    }
+
+    /**
+     * @throws \Throwable
+     */
+    public function testChunksWithClosingError(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'chunks');
+        $path->method('open')->with('rb')->willReturn('the_handle');
+
+        $this->builtin
+            ->method('feof')
+            ->with('the_handle')
+            ->willReturnOnConsecutiveCalls(
+                False,
+                False,
+                False,
+                True
+            );
+
+        $this->builtin
+            ->method('fread')
+            ->with('the_handle', 8192)
+            ->willReturnOnConsecutiveCalls(
+                'abc',
+                'def',
+                'ghi'
+            );
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fclose')
+            ->with('the_handle')
+            ->willReturn(False);
+
+        $this->expectException(IOException::class);
+
+        iterator_to_array($path->chunks());
+    }
+
+    public function testIsAbs(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'isAbs');
+
+        $this->assertTrue(
+            $path->isAbs()
+        );
+    }
+
+    public function testIsAbsWithRelative(): void
+    {
+        $path = $this->getMock('foo/file.ext', 'isAbs');
+
+        $this->assertFalse(
+            $path->isAbs()
+        );
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testChmod(): void
+    {
+        $path = $this->getMock('foo/file.ext', 'chmod');
+
+        $path
+            ->expects(self::once())
+            ->method('setPermissions')
+            ->with('777');
+
+        $path->chmod(777);
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testChown(): void
+    {
+        $path = $this->getMock('foo/file.ext', 'chown');
+
+        $path
+            ->expects(self::once())
+            ->method('setOwner')
+            ->with('user', 'group');
+
+        $path->chown('user', 'group');
+    }
+
+    /**
+     * @throws IOException
+     */
+    public function testChRoot(): void
+    {
+        $path = $this->getMock('/foo', 'chroot');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('chroot')
+            ->with('/foo')
+            ->willReturn(True);
+
+        $path->chroot();
+    }
+
+    /**
+     * @throws IOException
+     */
+    public function testChRootWithError(): void
+    {
+        $path = $this->getMock('/foo', 'chroot');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('chroot')
+            ->with('/foo')
+            ->willReturn(False);
+
+        $this->expectException(IOException::class);
+
+        $path->chroot();
+    }
+
+    public function testIsLink(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'isLink');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_link')
+            ->with('/foo/file.ext')
+            ->willReturn(True);
+
+        $this->assertTrue(
+            $path->isLink()
+        );
+    }
+
+    public function testIsLinkIsNot(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'isLink');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_link')
+            ->with('/foo/file.ext')
+            ->willReturn(False);
+
+        $this->assertFalse(
+            $path->isLink()
+        );
+    }
+
+    /**
+     * @throws FileNotFoundException
+     */
+    public function testIterDir(): void
+    {
+        $path = $this->getMock('/foo', 'iterDir');
+        $path->method('isDir')->willReturn(True);
+
+        $fileInfo1 = $this->getMockBuilder(\DirectoryIterator::class)->disableOriginalConstructor()->getMock();
+        $fileInfo1->expects(self::once())->method('isDot')->willReturn(True);
+        $fileInfo1->expects(self::never())->method('getFilename');
+
+        $fileInfo2 = $this->getMockBuilder(\DirectoryIterator::class)->disableOriginalConstructor()->getMock();
+        $fileInfo2->expects(self::once())->method('isDot')->willReturn(False);
+        $fileInfo2->expects(self::once())->method('getFilename')->willReturn('file1.ext');
+
+        $fileInfo3 = $this->getMockBuilder(\DirectoryIterator::class)->disableOriginalConstructor()->getMock();
+        $fileInfo3->expects(self::once())->method('isDot')->willReturn(False);
+        $fileInfo3->expects(self::once())->method('getFilename')->willReturn('file2.ext');
+
+        $result = [$fileInfo1, $fileInfo2, $fileInfo3];
+
+        $directoryIterator = $this->getMockBuilder(\DirectoryIterator::class)->disableOriginalConstructor()->getMock();
+        $directoryIterator->method('current')->willReturnCallback(function () use (&$result) {
+            return current($result);
+        });
+        $directoryIterator->method('key')->willReturnCallback(function () use (&$result) {
+            return key($result);
+        });
+        $directoryIterator->method('next')->willReturnCallback(function () use (&$result) {
+            return next($result);
+        });
+        $directoryIterator->method('valid')->willReturnCallback(function () use (&$result) {
+            $key = key($result);
+            return ($key !== NULL);
+        });
+
+        $path
+            ->method('getDirectoryIterator')
+            ->willReturn($directoryIterator);
+
+        $this->assertEquals(
+            ['file1.ext', 'file2.ext'],
+            iterator_to_array($path->iterDir())
+        );
+    }
+
+    /**
+     * @throws FileNotFoundException
+     */
+    public function testIterDirDirDoesNotExist(): void
+    {
+        $path = $this->getMock('/foo', 'iterDir');
+        $path->method('isDir')->willReturn(False);
+
+        $this->expectException(FileNotFoundException::class);
+
+        iterator_to_array($path->iterDir());
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws FileExistsException
+     */
+    public function testLink(): void
+    {
+        $path = $this->getMock('/foo/file.ext', 'link');
+        $path->method('isFile')->willReturn(True);
+
+        $target = $this
+            ->getMockBuilder(TestablePath::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $target->method('isFile')->willReturn(False);
+        $target->method('__toString')->willReturn('/bar/link.ext');
+
+        $this->builtin
+                ->expects(self::once())
+                ->method('link')
+                ->with('/foo/file.ext', '/bar/link.ext')
+                ->willReturn(True);
+
+        $path->link($target);
+    }
 }