Browse Source

add unit tests for methods from construct to copy

olinox14 1 year ago
parent
commit
cfc7a44dac
5 changed files with 1000 additions and 63 deletions
  1. 5 0
      src/BuiltinProxy.php
  2. 59 30
      src/Path.php
  3. 1 28
      tests/functionnal/PathTest.php
  4. 0 1
      tests/tempmy_file.bin
  5. 935 4
      tests/unit/PathTest.php

+ 5 - 0
src/BuiltinProxy.php

@@ -10,6 +10,11 @@ use JetBrains\PhpStorm\ExpectedValues;
  */
 class BuiltinProxy
 {
+    public function date(string $format, int $time): string
+    {
+        return date($format, $time);
+    }
+
     public function is_dir(string $filename): bool
     {
         return is_dir($filename);

+ 59 - 30
src/Path.php

@@ -38,16 +38,6 @@ class Path
     protected mixed $handle;
     protected BuiltinProxy $builtin;
 
-    /**
-     * Returns a new instance of the current class, initialized with the constant __DIR__ as the path.
-     *
-     * @return self A new instance of the current class.
-     */
-    public static function here(): self
-    {
-        return new self(__DIR__);
-    }
-
     /**
      * Joins two or more parts of a path together, inserting '/' as needed.
      * If any component is an absolute path, all previous path components
@@ -87,6 +77,7 @@ class Path
      *
      * @return void
      * TODO: see https://stackoverflow.com/a/12763962/4279120
+     * TODO: en faire une méthode non statique et tester
      *
      * @throws FileNotFoundException
      * @throws FileExistsException
@@ -113,7 +104,7 @@ class Path
 
     /**
      * [internal] Recursively copies a directory from source to destination.
-     *
+     * TODO: en faire une méthode non statique et tester
      * @param string $src The path to the source directory.
      * @param string $dst The path to the destination directory.
      * @return void
@@ -151,7 +142,10 @@ class Path
         }
     }
 
-    /** @noinspection SpellCheckingInspection */
+    /**
+     * TODO: en faire une méthode non statique et tester
+     * @noinspection SpellCheckingInspection
+     */
     private static function _rrmdir(string $dir): void
     {
         if (!is_dir($dir)) {
@@ -173,11 +167,11 @@ class Path
         rmdir($dir);
     }
 
-    public function __construct(string $path)
+    public function __construct(string|self $path)
     {
         $this->builtin = new BuiltinProxy();
         
-        $this->path = $path;
+        $this->path = (string)$path;
         $this->handle = null;
         return $this;
     }
@@ -186,6 +180,11 @@ class Path
         return $this->path;
     }
 
+    protected function cast(string|self $path): self
+    {
+        return new self($path);
+    }
+
     /**
      * Retrieves the current path of the file or directory
      *
@@ -204,7 +203,7 @@ class Path
      * @return bool Returns true if the given path is equal to the current path, false otherwise.
      */
     public function eq(string|self $path): bool {
-        return (string)$path === $this->path;
+        return $this->cast($path)->path() === $this->path();
     }
 
     /**
@@ -224,12 +223,26 @@ class Path
     /**
      * Returns an absolute version of the current path.
      *
-     * @return string
+     * @return self
      * TODO: make an alias `realpath`
+     * @throws IOException
      */
-    public function abspath(): string
+    public function absPath(): self
     {
-        return $this->builtin->realpath($this->path);
+        $absPath = $this->builtin->realpath($this->path);
+        if ($absPath === false) {
+            throw new IOException("Error while getting abspath of `" . $this->path . "`");
+        }
+        return $this->cast($absPath);
+    }
+
+    /**
+     * > Alias for absPath()
+     * @throws IOException
+     */
+    public function realpath(): self
+    {
+        return $this->absPath();
     }
 
     /**
@@ -242,7 +255,6 @@ class Path
      *        - W_OK: checks for write permission.
      *        - X_OK: checks for execute permission.
      * @return bool Returns true if the permission check is successful; otherwise, returns false.
-     * TODO: complete unit tests
      */
     function access(int $mode): bool
     {
@@ -258,7 +270,8 @@ class Path
     /**
      * Retrieves the last access time of a file or directory.
      *
-     * @return string|null The last access time of the file or directory in 'Y-m-d H:i:s' format. Returns null if the file or directory does not exist or on error.
+     * @return string|null The last access time of the file or directory in 'Y-m-d H:i:s' format.
+     * Returns null if the file or directory does not exist or on error.
      */
     function atime(): ?string
     {
@@ -266,13 +279,14 @@ class Path
         if ($time === false) {
             return null;
         }
-        return date('Y-m-d H:i:s', $time);
+        return $this->builtin->date('Y-m-d H:i:s', $time);
     }
 
     /**
      * Retrieves the creation time of a file or directory.
      *
-     * @return string|null The creation time of the file or directory in 'Y-m-d H:i:s' format, or null if the time could not be retrieved.
+     * @return string|null The creation time of the file or directory in 'Y-m-d H:i:s' format,
+     * or null if the time could not be retrieved.
      */
     function ctime(): ?string
     {
@@ -280,7 +294,7 @@ class Path
         if ($time === false) {
             return null;
         }
-        return date('Y-m-d H:i:s', $time);
+        return $this->builtin->date('Y-m-d H:i:s', $time);
     }
 
     /**
@@ -294,7 +308,7 @@ class Path
         if ($time === false) {
             return null;
         }
-        return date('Y-m-d H:i:s', $time);
+        return $this->builtin->date('Y-m-d H:i:s', $time);
     }
 
     /**
@@ -388,6 +402,7 @@ class Path
      *
      * @return void
      * @throws FileExistsException
+     * @throws IOException
      */
     public function mkdir(int $mode = 0777, bool $recursive = false): void
     {
@@ -403,7 +418,11 @@ class Path
             throw new FileExistsException("A file with this name already exists : " . $this);
         }
 
-        $this->builtin->mkdir($this->path, $mode, $recursive);
+        $result = $this->builtin->mkdir($this->path, $mode, $recursive);
+
+        if (!$result) {
+            throw new IOException("Error why creating the new directory : " . $this->path);
+        }
     }
 
     /**
@@ -411,13 +430,22 @@ class Path
      *
      * @return void
      * @throws FileNotFoundException
+     * @throws IOException
      */
     public function delete(): void
     {
         if ($this->isFile()) {
-            $this->builtin->unlink($this->path);
+            $result = $this->builtin->unlink($this->path);
+
+            if (!$result) {
+                throw new IOException("Error why deleting file : " . $this->path);
+            }
         } else if ($this->isDir()) {
-            $this->builtin->rmdir($this->path);
+            $result = $this->builtin->rmdir($this->path);
+
+            if (!$result) {
+                throw new IOException("Error why deleting directory : " . $this->path);
+            }
         } else {
             throw new FileNotFoundException("File does not exist : " . $this);
         }
@@ -427,6 +455,7 @@ class Path
      * Copy data and mode bits (“cp src dst”). Return the file’s destination.
      * The destination may be a directory.
      * If follow_symlinks is false, symlinks won’t be followed. This resembles GNU’s “cp -P src dst”.
+     * TODO: implements the follow_symlinks functionality
      *
      * @param string|self $destination The destination path or object to copy the file to.
      * @throws FileNotFoundException If the source file does not exist or is not a file.
@@ -439,7 +468,7 @@ class Path
             throw new FileNotFoundException("File does not exist or is not a file : " . $this);
         }
 
-        $destination = (string)$destination;
+        $destination = (string)$destination; // TODO: add an absPath method to the dest?
         if ($this->builtin->is_dir($destination)) {
             $destination = self::join($destination, $this->basename());
         }
@@ -453,7 +482,7 @@ class Path
             throw new IOException("Error copying file {$this->path} to {$destination}");
         }
 
-        return new self($destination);
+        return $this->cast($destination);
     }
 
     /**
@@ -1109,7 +1138,7 @@ class Path
             throw new FileNotFoundException("{$this->path} is not a file or directory");
         }
 
-        $path = $this->abspath();
+        $path = $this->absPath();
         $basePath = (string)$basePath;
 
         $realBasePath = $this->builtin->realpath($basePath);

+ 1 - 28
tests/functionnal/PathTest.php

@@ -9,7 +9,7 @@ use Path\Path;
 use PHPUnit\Framework\TestCase;
 
 // TODO: tested args should be both typed Path and string
-class PathTest extends TestCase
+class PathTest
 {
     const TEMP_TEST_DIR = __DIR__ . "/temp";
     // TODO: consider using sys_get_temp_dir()
@@ -39,35 +39,8 @@ class PathTest extends TestCase
         chdir(__DIR__);
     }
 
-    public function testToString(): void
-    {
-        $path = new Path('/foo/bar');
-        $this->assertEquals('/foo/bar', $path->__toString());
-    }
 
-    /**
-     * Test 'join' method.
-     */
-    public function testJoin(): void
-    {
-        // One part
-        $this->assertEquals(
-            '/home/user',
-            Path::join('/home', 'user')
-        );
 
-        // Multiple parts
-        $this->assertEquals(
-            '/home/user/documents',
-            Path::join('/home', 'user', 'documents')
-        );
-
-        // Absolute path passed in $parts
-        $this->assertEquals(
-            '/user/documents',
-            Path::join('home', '/user', 'documents')
-        );
-    }
 
     /**
      * Test 'Path' class 'copy_dir' method to copy a directory

+ 0 - 1
tests/tempmy_file.bin

@@ -1 +0,0 @@
-ba0c1874e2001b9c613c07dfad783b63247d8fc3a8a80b180c2071404b5f952c8600e0e70f9180d6ff03bb33bf8a34aef769

+ 935 - 4
tests/unit/PathTest.php

@@ -3,6 +3,9 @@
 namespace Path\Tests\unit;
 
 use Path\BuiltinProxy;
+use Path\Exception\FileExistsException;
+use Path\Exception\FileNotFoundException;
+use Path\Exception\IOException;
 use Path\Path;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
@@ -12,6 +15,11 @@ class TestablePath extends Path {
     {
         $this->builtin = $builtinProxy;
     }
+
+    public function cast(string|Path $path): Path
+    {
+        return parent::cast($path);
+    }
 }
 
 class PathTest extends TestCase
@@ -23,19 +31,95 @@ class PathTest extends TestCase
         $this->builtin = $this->getMockBuilder(BuiltinProxy::class)->getMock();
     }
 
-    public function getPathMockForMethod(string $path, string $methodName): TestablePath | MockObject
+    public function getMock(string $path, string $methodName): TestablePath | MockObject
     {
         $mock = $this
             ->getMockBuilder(TestablePath::class)
             ->setConstructorArgs([$path])
-            ->setMethodsExcept(['setBuiltin', $methodName])
+            ->setMethodsExcept(['__toString', 'setBuiltin', $methodName])
             ->getMock();
         $mock->setBuiltin($this->builtin);
         return $mock;
     }
 
+    /**
+     * Test 'join' method.
+     */
+    public function testJoin(): void
+    {
+        // One part
+        $this->assertEquals(
+            '/home/user',
+            Path::join('/home', 'user')
+        );
+
+        // Multiple parts
+        $this->assertEquals(
+            '/home/user/documents',
+            Path::join('/home', 'user', 'documents')
+        );
+
+        // Absolute path passed in $parts
+        $this->assertEquals(
+            '/user/documents',
+            Path::join('home', '/user', 'documents')
+        );
+    }
+
+    public function testToString(): void
+    {
+        $path = new Path('/foo/bar');
+        $this->assertEquals('/foo/bar', $path->__toString());
+    }
+
+    public function testPath()
+    {
+        $path = $this->getMock('bar', 'path');
+
+        $this->assertEquals(
+            'bar',
+            $path->path()
+        );
+    }
+
+    public function testEq(): void
+    {
+        $path = $this->getMock('bar', 'eq');
+        $path
+            ->method('path')
+            ->willReturn('bar');
+
+        $path2 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock();
+        $path2
+            ->method('path')
+            ->willReturn('bar');
+
+        $path3 = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock();
+        $path3
+            ->method('path')
+            ->willReturn('/foo/bar');
+
+        $path->method('cast')->willReturnMap(
+            [
+                [$path2, $path2],
+                [$path3, $path3],
+                ['bar', $path2],
+                ['/foo/bar', $path3],
+            ]
+        );
+
+        $this->assertTrue($path->eq($path2));
+        $this->assertFalse($path->eq($path3));
+
+        $this->assertTrue($path->eq('bar'));
+        $this->assertFalse($path->eq('/foo/bar'));
+    }
+
+    /**
+     * @throws IOException
+     */
     public function testAbsPath(): void {
-        $path = $this->getPathMockForMethod('bar', 'absPath');
+        $path = $this->getMock('bar', 'absPath');
 
         $this->builtin
             ->expects(self::once())
@@ -43,6 +127,853 @@ class PathTest extends TestCase
             ->with('bar')
             ->willReturn('/foo/bar');
 
-        $this->assertEquals('/foo/bar', $path->absPath());
+        $newPath = $this->getMockBuilder(TestablePath::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $newPath->method('path')->willReturn('/foo/bar');
+
+        $path->method('cast')->with('/foo/bar')->willReturn($newPath);
+
+        $this->assertEquals(
+            '/foo/bar',
+            $path->absPath()->path()
+        );
+    }
+
+    /**
+     * @throws IOException
+     */
+    public function testRealPath()
+    {
+        $path = $this->getMock('bar', 'realpath');
+
+        $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock();
+        $newPath
+            ->method('eq')
+            ->with('/foo/bar')
+            ->willReturn(True);
+
+        $path
+            ->expects(self::once())
+            ->method('absPath')
+            ->willReturn($newPath);
+
+        $this->assertTrue(
+            $path->realpath()->eq('/foo/bar')
+        );
+    }
+
+    public function testAccessFileExist(): void
+    {
+        $path = $this->getMock('bar', 'access');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('file_exists')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_readable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_writable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_executable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->access(Path::F_OK)
+        );
+    }
+
+    public function testAccessIsReadable(): void
+    {
+        $path = $this->getMock('bar', 'access');
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('file_exists')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_readable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_writable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_executable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->access(Path::R_OK)
+        );
+    }
+
+    public function testAccessIsWritable(): void
+    {
+        $path = $this->getMock('bar', 'access');
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('file_exists')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_readable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_writable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_executable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->access(Path::W_OK)
+        );
+    }
+
+    public function testAccessIsExecutable(): void
+    {
+        $path = $this->getMock('bar', 'access');
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('file_exists')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_readable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_writable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_executable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->access(Path::X_OK)
+        );
+    }
+
+    public function testAccessInvalidMode(): void
+    {
+        $path = $this->getMock('bar', 'access');
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('file_exists')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_readable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_writable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_executable')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->expectException(\RuntimeException::class);
+
+        $path->access(-1);
+    }
+
+    public function testATime(): void
+    {
+        $path = $this->getMock('bar', 'atime');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fileatime')
+            ->with('bar')
+            ->willReturn(1000);
+
+        $date = '2000-01-01';
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('date')
+            ->with('Y-m-d H:i:s', 1000)
+            ->willReturn($date);
+
+        $this->assertEquals(
+            $date,
+            $path->atime()
+        );
+    }
+
+    public function testATimeError(): void
+    {
+        $path = $this->getMock('bar', 'atime');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('fileatime')
+            ->with('bar')
+            ->willReturn(false);
+
+        $this->assertEquals(
+            null,
+            $path->atime()
+        );
+    }
+
+    public function testCTime(): void
+    {
+        $path = $this->getMock('bar', 'ctime');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('filectime')
+            ->with('bar')
+            ->willReturn(1000);
+
+        $date = '2000-01-01';
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('date')
+            ->with('Y-m-d H:i:s', 1000)
+            ->willReturn($date);
+
+        $this->assertEquals(
+            $date,
+            $path->ctime()
+        );
+    }
+
+    public function testCTimeError(): void
+    {
+        $path = $this->getMock('bar', 'ctime');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('filectime')
+            ->with('bar')
+            ->willReturn(false);
+
+        $this->assertEquals(
+            null,
+            $path->ctime()
+        );
+    }
+
+    public function testMTime(): void
+    {
+        $path = $this->getMock('bar', 'mtime');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('filemtime')
+            ->with('bar')
+            ->willReturn(1000);
+
+        $date = '2000-01-01';
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('date')
+            ->with('Y-m-d H:i:s', 1000)
+            ->willReturn($date);
+
+        $this->assertEquals(
+            $date,
+            $path->mtime()
+        );
+    }
+
+    public function testMTimeError(): void
+    {
+        $path = $this->getMock('bar', 'mtime');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('filemtime')
+            ->with('bar')
+            ->willReturn(false);
+
+        $this->assertEquals(
+            null,
+            $path->mtime()
+        );
+    }
+
+    public function testIsFile(): void
+    {
+        $path = $this->getMock('bar', 'isFile');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_file')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->isFile()
+        );
+    }
+
+    public function testIsDir(): void
+    {
+        $path = $this->getMock('bar', 'isDir');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_dir')
+            ->with('bar')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->isDir()
+        );
+    }
+
+    public function testExt(): void
+    {
+        $path = $this->getMock('bar.ext', 'ext');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('pathinfo')
+            ->with('bar.ext', PATHINFO_EXTENSION)
+            ->willReturn('ext');
+
+        $this->assertEquals(
+            'ext',
+            $path->ext()
+        );
+    }
+
+    public function testBaseName(): void
+    {
+        $path = $this->getMock('bar.ext', 'basename');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('pathinfo')
+            ->with('bar.ext', PATHINFO_BASENAME)
+            ->willReturn('bar.ext');
+
+        $this->assertEquals(
+            'bar.ext',
+            $path->basename()
+        );
+    }
+
+    public function testCD(): void
+    {
+        $path = $this->getMock('bar', 'cd');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('chdir')
+            ->with('foo')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->cd('foo')
+        );
+    }
+
+    public function testChDir(): void
+    {
+        $path = $this->getMock('bar', 'chdir');
+
+        $path
+            ->expects(self::once())
+            ->method('cd')
+            ->with('foo')
+            ->willReturn(true);
+
+        $this->assertTrue(
+            $path->cd('foo')
+        );
+    }
+
+    public function testName(): void
+    {
+        $path = $this->getMock('bar.ext', 'name');
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('pathinfo')
+            ->with('bar.ext', PATHINFO_FILENAME)
+            ->willReturn('bar');
+
+        $this->assertEquals(
+            'bar',
+            $path->name()
+        );
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileExistsException
+     */
+    public function testMkDir(): void
+    {
+        $path = $this->getMock('foo', 'mkdir');
+        $path->method('isDir')->willReturn(False);
+        $path->method('isFile')->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('mkdir')
+            ->with('foo', 0777, false)
+            ->willReturn(true);
+
+        $path->mkdir();
+    }
+
+    /**
+     * @throws IOException
+     */
+    public function testMkDirAlreadyExist(): void
+    {
+        $path = $this->getMock('foo', 'mkdir');
+        $path->method('isDir')->willReturn(True);
+
+        $this->expectException(FileExistsException::class);
+
+        $path->mkdir();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileExistsException
+     */
+    public function testMkDirAlreadyExistButRecursive(): void
+    {
+        $path = $this->getMock('foo', 'mkdir');
+        $path->method('isDir')->willReturn(True);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('mkdir');
+
+        $path->mkdir(0777, true);
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileExistsException
+     */
+    public function testMkDirFileExists(): void
+    {
+        $path = $this->getMock('foo', 'mkdir');
+        $path->method('isDir')->willReturn(False);
+        $path->method('isFile')->willReturn(True);
+
+        $this->expectException(FileExistsException::class);
+
+        $path->mkdir();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileExistsException
+     */
+    public function testMkDirFileExistsButRecursive(): void
+    {
+        $path = $this->getMock('foo', 'mkdir');
+        $path->method('isDir')->willReturn(False);
+        $path->method('isFile')->willReturn(True);
+
+        $this->expectException(FileExistsException::class);
+
+        $path->mkdir(0777, true);
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileExistsException
+     */
+    public function testMkDirIoError(): void
+    {
+        $path = $this->getMock('foo', 'mkdir');
+        $path->method('isDir')->willReturn(False);
+        $path->method('isFile')->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('mkdir')
+            ->with('foo', 0777, false)
+            ->willReturn(false);
+
+        $this->expectException(IOException::class);
+
+        $path->mkdir();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testDeleteIsFile(): void
+    {
+        $path = $this->getMock('foo', 'delete');
+        $path->method('isFile')->willReturn(True);
+        $path->method('isDir')->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('unlink')
+            ->with('foo')
+            ->willReturn(true);
+
+        $path->delete();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testDeleteIsFileWithError(): void
+    {
+        $path = $this->getMock('foo', 'delete');
+        $path->method('isFile')->willReturn(True);
+        $path->method('isDir')->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('unlink')
+            ->with('foo')
+            ->willReturn(false);
+
+        $this->expectException(IOException::class);
+
+        $path->delete();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testDeleteIsDir(): void
+    {
+        $path = $this->getMock('foo', 'delete');
+        $path->method('isFile')->willReturn(False);
+        $path->method('isDir')->willReturn(True);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('rmdir')
+            ->with('foo')
+            ->willReturn(true);
+
+        $path->delete();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testDeleteIsDirWithError(): void
+    {
+        $path = $this->getMock('foo', 'delete');
+        $path->method('isFile')->willReturn(False);
+        $path->method('isDir')->willReturn(True);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('rmdir')
+            ->with('foo')
+            ->willReturn(false);
+
+        $this->expectException(IOException::class);
+
+        $path->delete();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    public function testDeleteNonExistentFile(): void
+    {
+        $path = $this->getMock('foo', 'delete');
+        $path->method('isFile')->willReturn(False);
+        $path->method('isDir')->willReturn(False);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('unlink');
+        $this->builtin
+            ->expects(self::never())
+            ->method('rmdir');
+
+        $this->expectException(FileNotFoundException::class);
+
+        $path->delete();
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws FileExistsException
+     */
+    public function testCopy()
+    {
+        $path = $this->getMock('foo.ext', 'copy');
+        $path->method('isFile')->willReturn(True);
+
+        $destination = "/bar/foo2.ext";
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_dir')
+            ->with($destination)
+            ->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('file_exists')
+            ->with($destination)
+            ->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('copy')
+            ->with('foo.ext', $destination)
+            ->willReturn(True);
+
+        $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock();
+        $newPath->method('eq')->with($destination)->willReturn(True);
+
+        $path->method('cast')->with($destination)->willReturn($newPath);
+
+        $result = $path->copy($destination);
+
+        $this->assertTrue(
+            $result->eq($destination)
+        );
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws FileExistsException
+     */
+    public function testCopyFileDoesNotExist()
+    {
+        $path = $this->getMock('foo.ext', 'copy');
+        $path->method('isFile')->willReturn(False);
+
+        $destination = "/bar/foo2.ext";
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('is_dir');
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('file_exists');
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('copy');
+
+        $this->expectException(FileNotFoundException::class);
+
+        $path->copy($destination);
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws FileExistsException
+     */
+    public function testCopyDestIsDir()
+    {
+        $path = $this->getMock('foo.ext', 'copy');
+        $path->method('isFile')->willReturn(True);
+        $path->method('basename')->willReturn('foo.ext');
+
+        $destination = "/bar";
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_dir')
+            ->with($destination)
+            ->willReturn(True);
+
+        $newDest = $destination . "/foo.ext";
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('file_exists')
+            ->with($newDest)
+            ->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('copy')
+            ->with('foo.ext', $newDest)
+            ->willReturn(True);
+
+        $newPath = $this->getMockBuilder(TestablePath::class)->disableOriginalConstructor()->getMock();
+        $newPath->method('eq')->with($newDest)->willReturn(True);
+
+        $path->method('cast')->with($newDest)->willReturn($newPath);
+
+        $result = $path->copy($destination);
+
+        $this->assertTrue(
+            $result->eq($newDest)
+        );
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws FileExistsException
+     */
+    public function testCopyFileAlreadyExists()
+    {
+        $path = $this->getMock('foo.ext', 'copy');
+        $path->method('isFile')->willReturn(True);
+
+        $destination = "/bar/foo2.ext";
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_dir')
+            ->with($destination)
+            ->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('file_exists')
+            ->with($destination)
+            ->willReturn(True);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('copy');
+
+        $this->expectException(FileExistsException::class);
+
+        $path->copy($destination);
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws FileExistsException
+     */
+    public function testCopyFileDestIsDirButFileAlreadyExists()
+    {
+        $path = $this->getMock('foo.ext', 'copy');
+        $path->method('isFile')->willReturn(True);
+        $path->method('basename')->willReturn('foo.ext');
+
+        $destination = "/bar";
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_dir')
+            ->with($destination)
+            ->willReturn(True);
+
+        $newDest = $destination . "/foo.ext";
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('file_exists')
+            ->with($newDest)
+            ->willReturn(True);
+
+        $this->builtin
+            ->expects(self::never())
+            ->method('copy');
+
+        $this->expectException(FileExistsException::class);
+
+        $path->copy($destination);
+    }
+
+    /**
+     * @throws IOException
+     * @throws FileNotFoundException
+     * @throws FileExistsException
+     */
+    public function testCopyFileWithError()
+    {
+        $path = $this->getMock('foo.ext', 'copy');
+        $path->method('isFile')->willReturn(True);
+
+        $destination = "/bar/foo2.ext";
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('is_dir')
+            ->with($destination)
+            ->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('file_exists')
+            ->with($destination)
+            ->willReturn(False);
+
+        $this->builtin
+            ->expects(self::once())
+            ->method('copy')
+            ->with('foo.ext', $destination)
+            ->willReturn(False);
+
+        $this->expectException(IOException::class);
+
+        $path->copy($destination);
     }
 }