packages feed

dojang-0.2.0: test/Dojang/MonadFileSystemSpec.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}

module Dojang.MonadFileSystemSpec (spec) where

import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.List (sort, sortOn)
import GHC.IO.Exception (IOErrorType (InappropriateType))
import System.IO.Error
  ( alreadyExistsErrorType
  , doesNotExistErrorType
  , ioeGetErrorType
  , ioeGetFileName
  , ioeGetLocation
  , isDoesNotExistError
  )
import Prelude hiding (readFile, writeFile)
import Prelude qualified (readFile, writeFile)

import Control.Monad.Except (MonadError (catchError), tryError)
import Data.ByteString qualified (readFile, writeFile)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range (constantFrom)
import System.Directory.OsPath
  ( createDirectoryLink
  , createFileLink
  , doesDirectoryExist
  , doesFileExist
  , doesPathExist
  )
import System.Directory.OsPath qualified (createDirectory)
import System.FilePath (combine)
import System.OsPath (dropFileName, encodeFS, (</>))
import Test.Hspec (Spec, describe, it, runIO, specify, xit, xspecify)
import Test.Hspec.Expectations.Pretty
  ( shouldBe
  , shouldContain
  , shouldReturn
  , shouldSatisfy
  , shouldStartWith
  , shouldThrow
  )
import Test.Hspec.Hedgehog (forAll, hedgehog, (===))

import Dojang.MonadFileSystem
  ( FileType (..)
  , MonadFileSystem (..)
  , dryRunIO
  , tryDryRunIO
  )
import Dojang.TestUtils (withTempDir)


packageYamlFP :: FilePath
packageYamlFP = "package.yaml"


testFP :: FilePath
testFP = "test"


nonExistentFP :: FilePath
nonExistentFP = "---non-existent---"


nonExistentFP' :: FilePath
nonExistentFP' = "---non-existent-2---"


spec :: Spec
spec = do
  packageYamlP <- runIO $ encodeFS packageYamlFP
  testP <- runIO $ encodeFS testFP
  nonExistentP <- runIO $ encodeFS nonExistentFP
  nonExistentP' <- runIO $ encodeFS nonExistentFP'

  -- cSpell:ignore quux corge
  foo <- runIO $ encodeFS "foo"
  bar <- runIO $ encodeFS "bar"
  baz <- runIO $ encodeFS "baz"
  qux <- runIO $ encodeFS "qux"
  quux <- runIO $ encodeFS "quux"
  corge <- runIO $ encodeFS "corge"

  symlinkAvailable <- runIO $ withTempDir $ \tmpDir tmpDir' ->
    ( do
        Prelude.writeFile (tmpDir' `combine` "foo") ""
        createFileLink foo (tmpDir </> bar)
        return True
    )
      `catchError` const (return False)
  let symIt = if symlinkAvailable then it else xit
  let symSpecify = if symlinkAvailable then specify else xspecify

  let withFixture action = withTempDir $ \tmpDir tmpDir' -> do
        () <- Prelude.writeFile (tmpDir' `combine` "foo") ""
        () <- System.Directory.OsPath.createDirectory $ tmpDir </> bar
        () <- System.Directory.OsPath.createDirectory $ tmpDir </> baz
        () <-
          System.Directory.OsPath.createDirectory
            (tmpDir </> baz </> qux)
        action tmpDir tmpDir'

  describe "MonadFileSystem IO" $ do
    specify "encodePath" $ hedgehog $ do
      filePath <- forAll $ Gen.string (constantFrom 0 0 256) Gen.unicode
      filePath' <- liftIO $ encodePath filePath >>= decodePath
      filePath' === filePath

    specify "exists" $ do
      exists packageYamlP `shouldReturn` True
      exists testP `shouldReturn` True
      exists nonExistentP `shouldReturn` False

    specify "isFile" $ do
      isFile packageYamlP `shouldReturn` True
      isFile testP `shouldReturn` False
      isFile nonExistentP `shouldReturn` False

    specify "isDirectory" $ do
      isDirectory packageYamlP `shouldReturn` False
      isDirectory testP `shouldReturn` True
      isDirectory nonExistentP `shouldReturn` False

    symSpecify "isSymlink" $ do
      isSymlink packageYamlP `shouldReturn` False
      isSymlink testP `shouldReturn` False
      isSymlink nonExistentP `shouldReturn` False
      withTempDir $ \tmpDir tmpDir' -> do
        Prelude.writeFile (tmpDir' `combine` "foo") ""
        createFileLink foo (tmpDir </> bar)
        isSymlink (tmpDir </> bar) `shouldReturn` True

    specify "readFile" $ withTempDir $ \tmpDir tmpDir' -> do
      () <- Prelude.writeFile (tmpDir' `combine` "foo") "Foo contents"
      contents <- readFile $ tmpDir </> foo
      contents `shouldBe` "Foo contents"
      Left error' <- tryError $ readFile nonExistentP
      ioeGetErrorType error' `shouldBe` doesNotExistErrorType
      ioeGetFileName error' `shouldBe` Just nonExistentFP

    specify "writeFile" $ withTempDir $ \tmpDirP tmpDirFP -> do
      () <- writeFile (tmpDirP </> nonExistentP) "foo"
      Data.ByteString.readFile (tmpDirFP `combine` nonExistentFP)
        `shouldReturn` "foo"

    describe "readSymlinkTarget" $ do
      symIt "tells the target path of a symbolic link" $ withTempDir $ \tmpDir tmpDir' -> do
        () <- Prelude.writeFile (tmpDir' `combine` "foo") ""
        createFileLink foo (tmpDir </> bar)
        readSymlinkTarget (tmpDir </> bar) `shouldReturn` foo

      it "fails with IOError if the path does not exist" $ do
        readSymlinkTarget nonExistentP `shouldThrow` \e -> do
          isDoesNotExistError e && ioeGetFileName e == Just nonExistentFP

    specify "copyFile" $ withTempDir $ \tmpDirP tmpDirFP -> do
      () <- copyFile packageYamlP (tmpDirP </> nonExistentP)
      contents <- Data.ByteString.readFile (tmpDirFP `combine` nonExistentFP)
      original <- Data.ByteString.readFile packageYamlFP
      contents `shouldBe` original

    specify "createDirectory" $ withTempDir $ \tmpDirP _ -> do
      () <- createDirectory (tmpDirP </> nonExistentP)
      doesDirectoryExist (tmpDirP </> nonExistentP)
        `shouldReturn` True

    specify "removeFile" $ withTempDir $ \tmpDirP tmpDirFP -> do
      Data.ByteString.writeFile (tmpDirFP `combine` nonExistentFP) ""
      doesFileExist (tmpDirP </> nonExistentP) `shouldReturn` True
      () <- removeFile (tmpDirP </> nonExistentP)
      doesFileExist (tmpDirP </> nonExistentP) `shouldReturn` False

    specify "removeDirectory" $ withTempDir $ \tmpDirP tmpDirFP -> do
      () <- createDirectory (tmpDirP </> nonExistentP)
      () <- removeDirectory (tmpDirP </> nonExistentP)
      doesDirectoryExist (tmpDirP </> nonExistentP)
        `shouldReturn` False
      removeDirectory (tmpDirP </> nonExistentP) `shouldThrow` \e ->
        isDoesNotExistError e
          && (ioeGetFileName e == Just (tmpDirFP `combine` nonExistentFP))

    specify "removeDirectoryRecursively" $ withFixture $ \tmpDirP tmpDirFP -> do
      removeDirectoryRecursively tmpDirP
      doesPathExist tmpDirP `shouldReturn` False
      removeDirectoryRecursively tmpDirP `shouldThrow` \e ->
        isDoesNotExistError e && ioeGetFileName e == Just tmpDirFP

    specify "listDirectory" $ withFixture $ \tmpDir _ -> do
      result <- listDirectory tmpDir
      sort result `shouldBe` [bar, baz, foo]

    describe "listDirectoryRecursively" $ do
      specify "basic behavior" $ withFixture $ \tmpDir tmpDir' -> do
        () <- Prelude.writeFile (tmpDir' `combine` "bar" `combine` "quux") ""
        () <- Prelude.writeFile (tmpDir' `combine` "baz" `combine` "corge") ""
        result <- listDirectoryRecursively tmpDir []
        sortOn snd result
          `shouldBe` [ (Directory, bar)
                     , (File, bar </> quux)
                     , (Directory, baz)
                     , (File, baz </> corge)
                     , (Directory, baz </> qux)
                     , (File, foo)
                     ]

      it "filters out files by ignorePatterns" $ withFixture $ \tmpDir tmpDir' -> do
        () <- Prelude.writeFile (tmpDir' `combine` "bar" `combine` "quux") ""
        () <- Prelude.writeFile (tmpDir' `combine` "baz" `combine` "corge") ""
        result <- listDirectoryRecursively tmpDir ["bar/q*", "baz"]
        sortOn snd result `shouldBe` [(Directory, bar), (File, foo)]

      symIt "distinguishes symlinks from regular files and directories" $
        withFixture $
          \tmpDir tmpDir' -> do
            () <- Prelude.writeFile (tmpDir' `combine` "bar" `combine` "quux") ""
            () <- createFileLink quux (tmpDir </> baz </> corge)
            () <- createDirectoryLink baz (tmpDir </> corge)
            result <- listDirectoryRecursively tmpDir []
            sortOn snd result
              `shouldBe` [ (Directory, bar)
                         , (File, bar </> quux)
                         , (Directory, baz)
                         , (Symlink, baz </> corge)
                         , (Directory, baz </> qux)
                         , (Symlink, corge)
                         , (File, foo)
                         ]

    specify "getFileSize" $ withFixture $ \tmpDir tmpDirFP -> do
      Data.ByteString.writeFile (tmpDirFP `combine` "foo") "asdf"
      getFileSize (tmpDir </> foo) `shouldReturn` 4
      Left e <- tryError $ getFileSize (tmpDir </> nonExistentP)
      ioeGetErrorType e `shouldBe` doesNotExistErrorType
      ioeGetFileName e `shouldBe` Just (tmpDirFP `combine` nonExistentFP)
      Left e' <- tryError $ getFileSize tmpDir
      ioeGetErrorType e' `shouldBe` InappropriateType
      ioeGetLocation e' `shouldBe` "getFileSize"
      ioeGetFileName e' `shouldBe` Just tmpDirFP
      show e' `shouldContain` "it is a directory"

  describe "DryRunIO" $ do
    specify "encodePath" $ hedgehog $ do
      filePath <- forAll $ Gen.string (constantFrom 0 0 256) Gen.unicode
      filePath' <- liftIO $ dryRunIO (encodePath filePath >>= decodePath)
      filePath' === filePath

    describe "isFile" $ do
      it "checks an actual file that exists on the real file system" $ do
        packageYamlIsFile <- dryRunIO $ isFile packageYamlP
        packageYamlIsFile `shouldBe` True

      it "checks an actual directory that exists on the real file system" $ do
        testIsFile <- dryRunIO $ isFile testP
        testIsFile `shouldBe` False

      it "checks a non-existent file" $ do
        nonExistentIsFile <- dryRunIO $ isFile nonExistentP
        nonExistentIsFile `shouldBe` False

      it "checks a virtual file (Contents) that exists in memory" $ do
        nonExistentIsFile <- dryRunIO $ do
          () <- writeFile nonExistentP ""
          isFile nonExistentP
        nonExistentIsFile `shouldBe` True

      it "checks a virtual file (Copied) that exists in memory" $ do
        nonExistentIsFile <- dryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          isFile nonExistentP
        nonExistentIsFile `shouldBe` True

      it "checks a virtual directory that exists in memory" $ do
        nonExistentIsFile <- dryRunIO $ do
          () <- createDirectory nonExistentP
          isFile nonExistentP
        nonExistentIsFile `shouldBe` False

      it "checks a virtual non-existent file" $ do
        packageYamlIsFile <- dryRunIO $ do
          () <- removeFile packageYamlP
          isFile packageYamlP
        packageYamlIsFile `shouldBe` False

    describe "isDirectory" $ do
      it "check an actual file that exists on the real file system" $ do
        packageYamlIsDir <- dryRunIO $ isDirectory packageYamlP
        packageYamlIsDir `shouldBe` False

      it "checks an actual directory that exists on the real file system" $ do
        testIsDir <- dryRunIO $ isDirectory testP
        testIsDir `shouldBe` True

      it "checks a non-existent directory" $ do
        nonExistentIsDir <- dryRunIO $ isDirectory nonExistentP
        nonExistentIsDir `shouldBe` False

      it "checks a virtual directory that exists in memory" $ do
        nonExistentIsDir' <- dryRunIO $ do
          () <- createDirectory nonExistentP
          isDirectory nonExistentP
        nonExistentIsDir' `shouldBe` True

      it "checks a virtual file (Copied) that exists in memory" $ do
        nonExistentIsDir'' <- dryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          isDirectory nonExistentP
        nonExistentIsDir'' `shouldBe` False

      it "checks a virtual file (Contents) that exists in memory" $ do
        nonExistentIsDir''' <- dryRunIO $ do
          () <- writeFile nonExistentP ""
          isDirectory nonExistentP
        nonExistentIsDir''' `shouldBe` False

      it "checks a virtual non-existent directory" $ do
        packageYamlIsFile <- dryRunIO $ do
          () <- removeFile packageYamlP
          isDirectory packageYamlP
        packageYamlIsFile `shouldBe` False

    describe "isSymlink" $ do
      symIt "checks an actual symlink that exists on the real file system" $
        withTempDir $
          \tmpDir tmpDir' -> do
            () <- Prelude.writeFile (tmpDir' `combine` "foo") ""
            createFileLink foo (tmpDir </> bar)
            dryRunIO (isSymlink $ tmpDir </> bar) `shouldReturn` True

      it "return False if a path does not exist" $ do
        dryRunIO (isSymlink nonExistentP) `shouldReturn` False

      it "checks a virtual file that exists in memory" $ do
        nonExistentExists' <- dryRunIO $ do
          () <- writeFile nonExistentP ""
          isSymlink nonExistentP
        nonExistentExists' `shouldBe` False

    describe "exists" $ do
      it "checks an actual file that exists on the real file system" $ do
        packageYamlExists <- dryRunIO $ exists packageYamlP
        packageYamlExists `shouldBe` True

      it "checks an actual directory that exists on the real file system" $ do
        testExists <- dryRunIO $ exists testP
        testExists `shouldBe` True

      it "checks a non-existent file" $ do
        nonExistentExists <- dryRunIO $ exists nonExistentP
        nonExistentExists `shouldBe` False

      it "checks a virtual file (Contents) that exists in memory" $ do
        nonExistentExists' <- dryRunIO $ do
          () <- writeFile nonExistentP ""
          exists nonExistentP
        nonExistentExists' `shouldBe` True

      it "checks a virtual file (Copied) that exists in memory" $ do
        nonExistentExists'' <- dryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          exists nonExistentP
        nonExistentExists'' `shouldBe` True

      it "checks a virtual directory that exists in memory" $ do
        nonExistentExists''' <- dryRunIO $ do
          () <- createDirectory nonExistentP
          exists nonExistentP
        nonExistentExists''' `shouldBe` True

      it "checks a virtual non-existent file" $ do
        packageYamlIsFile <- dryRunIO $ do
          () <- removeFile packageYamlP
          exists packageYamlP
        packageYamlIsFile `shouldBe` False

    describe "readFile" $ do
      it "can read an actual file that exists on the real file system" $
        withTempDir $
          \tmpDir tmpDir' -> do
            () <- Prelude.writeFile (tmpDir' `combine` "foo") "Foo contents"
            packageYamlData <- dryRunIO $ readFile $ tmpDir </> foo
            packageYamlData `shouldBe` "Foo contents"

      it "can't read a directory that exists on the real file system" $ do
        Left failToReadTest <- tryDryRunIO $ readFile testP
        ioeGetErrorType failToReadTest `shouldBe` InappropriateType
        ioeGetFileName failToReadTest `shouldBe` Just testFP

      it "can't read a non-existent file" $ do
        Left failToReadNonExistent <- tryDryRunIO $ readFile nonExistentP
        ioeGetErrorType failToReadNonExistent `shouldBe` doesNotExistErrorType
        ioeGetFileName failToReadNonExistent
          `shouldBe` Just nonExistentFP

      it "can read a virtual file (Contents) that exists in memory" $ do
        nonExistentData <- dryRunIO $ do
          () <- writeFile nonExistentP "foo"
          readFile nonExistentP
        nonExistentData `shouldBe` "foo"

      it "can read a virtual file (Copied) that exists in memory" $
        withTempDir $
          \tmpDir tmpDir' -> do
            Prelude.writeFile (tmpDir' `combine` "foo") "Foo contents"
            nonExistentData <- dryRunIO $ do
              () <- copyFile (tmpDir </> foo) nonExistentP
              () <- writeFile packageYamlP "foo" -- must not affect the result
              () <- copyFile nonExistentP nonExistentP'
              () <- writeFile nonExistentP "bar" -- must not affect the result
              readFile nonExistentP'
            nonExistentData `shouldBe` "Foo contents"

      it "can't read a virtual directory that exists in memory" $ do
        Left failToReadNonExistent <- tryDryRunIO $ do
          () <- createDirectory nonExistentP
          readFile nonExistentP
        ioeGetErrorType failToReadNonExistent `shouldBe` InappropriateType
        ioeGetFileName failToReadNonExistent
          `shouldBe` Just nonExistentFP
        ioeGetLocation failToReadNonExistent `shouldBe` "readFile"
        show failToReadNonExistent `shouldContain` "is a directory"

      it "can't read a virtual non-existent file" $ do
        Left failToReadNonExistent <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          readFile packageYamlP
        ioeGetErrorType failToReadNonExistent `shouldBe` doesNotExistErrorType
        ioeGetFileName failToReadNonExistent `shouldBe` Just packageYamlFP
        ioeGetLocation failToReadNonExistent `shouldBe` "readFile"
        show failToReadNonExistent `shouldContain` "no such file"

    describe "writeFile" $ do
      it "can write data to an non-existent file" $ do
        data' <- dryRunIO $ do
          () <- writeFile nonExistentP "foo"
          readFile nonExistentP
        data' `shouldBe` "foo"
        withTempDir $ \tmpDir tmpDir' -> do
          () <- Prelude.writeFile (tmpDir' `combine` "foo") "Foo contents"
          data'' <- dryRunIO $ do
            () <- removeFile $ tmpDir </> foo
            () <- writeFile (tmpDir </> foo) "bar"
            readFile $ tmpDir </> foo
          data'' `shouldBe` "bar"
          Prelude.readFile (tmpDir' `combine` "foo")
            `shouldReturn` "Foo contents"

      it "can write data to an existing file" $
        withTempDir $
          \tmpDir tmpDir' -> do
            () <- Prelude.writeFile (tmpDir' `combine` "foo") "Foo contents"
            data' <- dryRunIO $ do
              () <- writeFile (tmpDir </> foo) "modified"
              readFile (tmpDir </> foo)
            data' `shouldBe` "modified"
            Prelude.readFile (tmpDir' `combine` "foo")
              `shouldReturn` "Foo contents"
            data'' <- dryRunIO $ do
              () <- writeFile nonExistentP "bar"
              () <- writeFile nonExistentP "baz"
              readFile nonExistentP
            data'' `shouldBe` "baz"
            doesFileExist nonExistentP `shouldReturn` False

      it "can't write data to a directory" $ do
        Left failToWriteToTest <- tryDryRunIO $ writeFile testP "foo"
        ioeGetErrorType failToWriteToTest `shouldBe` InappropriateType
        ioeGetFileName failToWriteToTest `shouldBe` Just testFP
        ioeGetLocation failToWriteToTest `shouldBe` "writeFile"
        show failToWriteToTest `shouldContain` "is a directory"
        Left failToWriteToTest' <- tryDryRunIO $ do
          () <- createDirectory nonExistentP
          writeFile nonExistentP "foo"
        ioeGetErrorType failToWriteToTest' `shouldBe` InappropriateType
        ioeGetFileName failToWriteToTest' `shouldBe` Just nonExistentFP
        ioeGetLocation failToWriteToTest' `shouldBe` "writeFile"
        show failToWriteToTest' `shouldContain` "is a directory"

      it "can't write data to a file path in a non-existent directory" $ do
        Left failToWrite <- tryDryRunIO $ do
          writeFile (nonExistentP </> nonExistentP) "foo"
        ioeGetErrorType failToWrite `shouldBe` doesNotExistErrorType
        ioeGetFileName failToWrite
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToWrite `shouldBe` "writeFile"
        show failToWrite `shouldContain` "no parent directory"
        Left failToWrite' <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          writeFile (packageYamlP </> nonExistentP) "foo"
        ioeGetErrorType failToWrite' `shouldBe` doesNotExistErrorType
        ioeGetFileName failToWrite'
          `shouldBe` Just (packageYamlFP `combine` nonExistentFP)
        ioeGetLocation failToWrite' `shouldBe` "writeFile"
        show failToWrite' `shouldContain` "no parent directory"

      it "can't write data to a file path inside a non-directory file" $ do
        Left failToWrite <- tryDryRunIO $ do
          writeFile (packageYamlP </> nonExistentP) "bar"
        ioeGetErrorType failToWrite `shouldBe` InappropriateType
        ioeGetFileName failToWrite
          `shouldBe` Just (packageYamlFP `combine` nonExistentFP)
        ioeGetLocation failToWrite `shouldBe` "writeFile"
        show failToWrite `shouldContain` "not inside a directory"
        Left failToWrite' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          writeFile (nonExistentP </> nonExistentP) "bar"
        ioeGetErrorType failToWrite' `shouldBe` InappropriateType
        ioeGetFileName failToWrite'
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToWrite' `shouldBe` "writeFile"
        show failToWrite' `shouldContain` "not inside a directory"
        Left failToWrite'' <- tryDryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          writeFile (nonExistentP </> nonExistentP) "bar"
        ioeGetErrorType failToWrite'' `shouldBe` InappropriateType
        ioeGetFileName failToWrite''
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToWrite'' `shouldBe` "writeFile"
        show failToWrite'' `shouldContain` "not inside a directory"

    describe "readSymlinkTarget" $ do
      symIt "returns the target path of a symbolic link" $
        withTempDir $
          \tmpDir tmpDir' -> do
            () <- Prelude.writeFile (tmpDir' `combine` "foo") ""
            createFileLink foo (tmpDir </> bar)
            dryRunIO (readSymlinkTarget $ tmpDir </> bar) `shouldReturn` foo

      it "fails with non-existent file" $ do
        dryRunIO (readSymlinkTarget nonExistentP) `shouldThrow` \e ->
          isDoesNotExistError e && ioeGetFileName e == Just nonExistentFP

      it "just fails with any virtual files as they can't be a symlink" $ do
        Left failToReadSymlink <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          readSymlinkTarget nonExistentP
        ioeGetErrorType failToReadSymlink `shouldBe` InappropriateType
        ioeGetFileName failToReadSymlink `shouldBe` Just nonExistentFP
        ioeGetLocation failToReadSymlink `shouldBe` "readSymlinkTarget"
        show failToReadSymlink `shouldContain` "not a symbolic link"

      it "fails with an overlaid removal tag" $ do
        Left failToReadSymlink <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          () <- removeFile nonExistentP
          readSymlinkTarget nonExistentP
        ioeGetErrorType failToReadSymlink `shouldBe` doesNotExistErrorType
        ioeGetFileName failToReadSymlink `shouldBe` Just nonExistentFP
        ioeGetLocation failToReadSymlink `shouldBe` "readSymlinkTarget"
        show failToReadSymlink `shouldContain` "no such file"

    describe "copyFile" $ do
      it "copies a source file to a destination path" $ do
        data' <- dryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          readFile nonExistentP
        packageYamlData <- Data.ByteString.readFile packageYamlFP
        data' `shouldBe` packageYamlData
        doesFileExist nonExistentP `shouldReturn` False
        data'' <- dryRunIO $ do
          () <- writeFile nonExistentP "foo"
          () <- copyFile nonExistentP nonExistentP'
          readFile nonExistentP'
        data'' `shouldBe` "foo"
        doesFileExist nonExistentP' `shouldReturn` False
        data''' <- dryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          () <- copyFile nonExistentP nonExistentP'
          readFile nonExistentP'
        data''' `shouldBe` packageYamlData
        doesFileExist nonExistentP' `shouldReturn` False

      it "can copy a source file to an existing file by overwriting it" $ do
        data' <- dryRunIO $ do
          () <- writeFile nonExistentP "foo"
          () <- copyFile packageYamlP nonExistentP
          readFile nonExistentP
        packageYamlData <- Data.ByteString.readFile packageYamlFP
        data' `shouldBe` packageYamlData
        doesPathExist nonExistentP `shouldReturn` False

      it "can't copy a non-existent file" $ do
        Left failToCopy <- tryDryRunIO $ copyFile nonExistentP nonExistentP'
        ioeGetErrorType failToCopy `shouldBe` doesNotExistErrorType
        ioeGetFileName failToCopy `shouldBe` Just nonExistentFP
        ioeGetLocation failToCopy `shouldBe` "copyFile"
        show failToCopy `shouldContain` "source does not exist"
        Left failToCopy' <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          copyFile packageYamlP nonExistentP
        ioeGetErrorType failToCopy' `shouldBe` doesNotExistErrorType
        ioeGetFileName failToCopy' `shouldBe` Just packageYamlFP
        ioeGetLocation failToCopy' `shouldBe` "copyFile"
        show failToCopy' `shouldContain` "source does not exist"

      it "can't copy a directory" $ do
        Left failToCopy <- tryDryRunIO $ copyFile testP nonExistentP
        ioeGetErrorType failToCopy `shouldBe` InappropriateType
        ioeGetFileName failToCopy `shouldBe` Just testFP
        ioeGetLocation failToCopy `shouldBe` "copyFile"
        show failToCopy `shouldContain` "source is a directory"
        doesPathExist nonExistentP `shouldReturn` False
        Left failToCopy' <- tryDryRunIO $ do
          () <- createDirectory nonExistentP
          copyFile nonExistentP nonExistentP'
        ioeGetErrorType failToCopy' `shouldBe` InappropriateType
        ioeGetFileName failToCopy' `shouldBe` Just nonExistentFP
        ioeGetLocation failToCopy' `shouldBe` "copyFile"
        show failToCopy' `shouldContain` "source is a directory"
        doesPathExist nonExistentP' `shouldReturn` False

      it "can't copy a file to a file path that has no parent directory" $ do
        Left failToCopy <- tryDryRunIO $ do
          copyFile packageYamlP (nonExistentP </> nonExistentP)
        failToCopy `shouldSatisfy` isDoesNotExistError
        ioeGetFileName failToCopy
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCopy `shouldBe` "copyFile"
        show failToCopy `shouldContain` "no parent directory"
        Left failToCopy' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          () <- removeFile nonExistentP
          copyFile packageYamlP (nonExistentP </> nonExistentP)
        failToCopy' `shouldSatisfy` isDoesNotExistError
        ioeGetFileName failToCopy'
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCopy' `shouldBe` "copyFile"
        show failToCopy' `shouldContain` "no parent directory"

      it "can't copy a file to a file path inside a non-directory path" $ do
        Left failToCopy <- tryDryRunIO $ do
          copyFile packageYamlP (packageYamlP </> nonExistentP)
        ioeGetErrorType failToCopy `shouldBe` InappropriateType
        ioeGetFileName failToCopy
          `shouldBe` Just (packageYamlFP `combine` nonExistentFP)
        ioeGetLocation failToCopy `shouldBe` "copyFile"
        show failToCopy `shouldContain` "not inside a directory"
        Left failToCopy' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          copyFile packageYamlP (nonExistentP </> nonExistentP)
        ioeGetErrorType failToCopy `shouldBe` InappropriateType
        ioeGetFileName failToCopy'
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCopy' `shouldBe` "copyFile"
        show failToCopy' `shouldContain` "not inside a directory"
        Left failToCopy'' <- tryDryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          copyFile nonExistentP (nonExistentP </> nonExistentP)
        ioeGetErrorType failToCopy `shouldBe` InappropriateType
        ioeGetFileName failToCopy''
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCopy'' `shouldBe` "copyFile"
        show failToCopy'' `shouldContain` "not inside a directory"

      it "can't copy a file to a directory path" $ do
        Left failToCopy <- tryDryRunIO $ copyFile packageYamlP testP
        ioeGetErrorType failToCopy `shouldBe` InappropriateType
        ioeGetFileName failToCopy `shouldBe` Just testFP
        ioeGetLocation failToCopy `shouldBe` "copyFile"
        show failToCopy `shouldContain` "destination is a directory"
        doesDirectoryExist testP `shouldReturn` True
        Left failToCopy' <- tryDryRunIO $ do
          () <- createDirectory nonExistentP
          copyFile packageYamlP nonExistentP
        ioeGetErrorType failToCopy' `shouldBe` InappropriateType
        ioeGetFileName failToCopy' `shouldBe` Just nonExistentFP
        ioeGetLocation failToCopy' `shouldBe` "copyFile"
        show failToCopy' `shouldContain` "destination is a directory"

    describe "createDirectory" $ do
      it "creates an empty directory" $ do
        dryRunIO (createDirectory nonExistentP >> isDirectory nonExistentP)
          `shouldReturn` True

      it "can't create a directory inside a non-existent path" $ do
        Left failToCreate <- tryDryRunIO $ do
          createDirectory (nonExistentP </> nonExistentP)
        ioeGetErrorType failToCreate `shouldBe` doesNotExistErrorType
        ioeGetFileName failToCreate
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCreate `shouldBe` "createDirectory"
        show failToCreate `shouldContain` "no parent directory"
        Left failToCreate' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          () <- removeFile nonExistentP
          createDirectory (nonExistentP </> nonExistentP)
        ioeGetErrorType failToCreate' `shouldBe` doesNotExistErrorType
        ioeGetFileName failToCreate'
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCreate' `shouldBe` "createDirectory"
        show failToCreate' `shouldContain` "no parent directory"

      it "can't create a directory inside a non-directory path" $ do
        Left failToCreate <- tryDryRunIO $ do
          createDirectory (packageYamlP </> nonExistentP)
        ioeGetErrorType failToCreate `shouldBe` InappropriateType
        ioeGetFileName failToCreate
          `shouldBe` Just (packageYamlFP `combine` nonExistentFP)
        ioeGetLocation failToCreate `shouldStartWith` "createDirectory"
        doesFileExist packageYamlP `shouldReturn` True
        Left failToCreate' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          createDirectory (nonExistentP </> nonExistentP)
        ioeGetErrorType failToCreate' `shouldBe` InappropriateType
        ioeGetFileName failToCreate'
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCreate' `shouldStartWith` "createDirectory"
        show failToCreate' `shouldContain` "not inside a directory"
        Left failToCreate'' <- tryDryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          createDirectory (nonExistentP </> nonExistentP)
        ioeGetErrorType failToCreate'' `shouldBe` InappropriateType
        ioeGetFileName failToCreate''
          `shouldBe` Just (nonExistentFP `combine` nonExistentFP)
        ioeGetLocation failToCreate'' `shouldStartWith` "createDirectory"
        show failToCreate'' `shouldContain` "not inside a directory"

      it "can't create a directory by overwriting an existing file" $ do
        Left failToCreate <- tryDryRunIO $ do
          createDirectory packageYamlP
        ioeGetErrorType failToCreate `shouldBe` alreadyExistsErrorType
        ioeGetFileName failToCreate `shouldBe` Just packageYamlFP
        ioeGetLocation failToCreate `shouldBe` "createDirectory"
        show failToCreate `shouldContain` "destination is already a file"
        doesFileExist packageYamlP `shouldReturn` True
        Left failToCreate' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          createDirectory nonExistentP
        ioeGetErrorType failToCreate' `shouldBe` alreadyExistsErrorType
        ioeGetFileName failToCreate' `shouldBe` Just nonExistentFP
        ioeGetLocation failToCreate' `shouldBe` "createDirectory"
        show failToCreate' `shouldContain` "destination is already a file"
        Left failToCreate'' <- tryDryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          createDirectory nonExistentP
        ioeGetErrorType failToCreate'' `shouldBe` alreadyExistsErrorType
        ioeGetFileName failToCreate'' `shouldBe` Just nonExistentFP
        ioeGetLocation failToCreate'' `shouldBe` "createDirectory"
        show failToCreate'' `shouldContain` "destination is already a file"

      it "can't create a directory if it's already a directory" $ do
        Left failToCreate <- tryDryRunIO $ do
          createDirectory testP
        ioeGetErrorType failToCreate `shouldBe` alreadyExistsErrorType
        ioeGetFileName failToCreate `shouldBe` Just testFP
        ioeGetLocation failToCreate `shouldBe` "createDirectory"
        show failToCreate `shouldContain` "destination is already a directory"
        doesDirectoryExist testP `shouldReturn` True
        Left failToCreate' <- tryDryRunIO $ do
          () <- createDirectory nonExistentP
          createDirectory nonExistentP
        ioeGetErrorType failToCreate' `shouldBe` alreadyExistsErrorType
        ioeGetFileName failToCreate' `shouldBe` Just nonExistentFP
        ioeGetLocation failToCreate' `shouldBe` "createDirectory"
        show failToCreate' `shouldContain` "destination is already a directory"

    describe "removeFile" $ do
      it "removes an existing file" $ do
        dryRunIO (removeFile packageYamlP >> exists packageYamlP)
          `shouldReturn` False
        doesFileExist packageYamlP `shouldReturn` True
        fileExists <- dryRunIO $ do
          () <- writeFile nonExistentP ""
          () <- removeFile nonExistentP
          exists nonExistentP
        fileExists `shouldBe` False
        fileExists' <- dryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          () <- removeFile nonExistentP
          exists nonExistentP
        fileExists' `shouldBe` False

      it "can't remove a non-existent file" $ do
        Left failToRemove <- tryDryRunIO $ removeFile nonExistentP
        ioeGetErrorType failToRemove `shouldBe` doesNotExistErrorType
        ioeGetFileName failToRemove `shouldBe` Just nonExistentFP
        ioeGetLocation failToRemove `shouldBe` "removeFile"
        show failToRemove `shouldContain` "no such file"
        Left failToRemove' <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          removeFile packageYamlP
        ioeGetErrorType failToRemove' `shouldBe` doesNotExistErrorType
        ioeGetFileName failToRemove' `shouldBe` Just packageYamlFP
        ioeGetLocation failToRemove' `shouldBe` "removeFile"
        show failToRemove' `shouldContain` "no such file"

      it "can't remove a directory" $ do
        Left failToRemove <- tryDryRunIO $ removeFile testP
        ioeGetErrorType failToRemove `shouldBe` InappropriateType
        ioeGetFileName failToRemove `shouldBe` Just testFP
        ioeGetLocation failToRemove `shouldBe` "removeFile"
        show failToRemove `shouldContain` "is a directory"
        doesDirectoryExist testP `shouldReturn` True
        Left failToRemove' <- tryDryRunIO $ do
          () <- createDirectory nonExistentP
          removeFile nonExistentP
        ioeGetErrorType failToRemove' `shouldBe` InappropriateType
        ioeGetFileName failToRemove' `shouldBe` Just nonExistentFP
        ioeGetLocation failToRemove' `shouldBe` "removeFile"
        show failToRemove' `shouldContain` "is a directory"

    describe "removeDirectory" $ do
      it "removes an existing directory" $ do
        result <- dryRunIO $ do
          createDirectory nonExistentP
          removeDirectory nonExistentP
          exists nonExistentP
        result `shouldBe` False

      it "can't remove a non-existent directory" $ do
        Left failToRemove <- tryDryRunIO $ removeDirectory nonExistentP
        failToRemove `shouldSatisfy` isDoesNotExistError
        ioeGetFileName failToRemove `shouldBe` Just nonExistentFP
        Left failToRemove' <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          removeDirectory packageYamlP
        failToRemove' `shouldSatisfy` isDoesNotExistError
        ioeGetFileName failToRemove' `shouldBe` Just packageYamlFP

      it "can't remove a non-directory" $ do
        Left failToRemove <- tryDryRunIO $ removeDirectory packageYamlP
        ioeGetErrorType failToRemove `shouldBe` InappropriateType
        ioeGetFileName failToRemove `shouldBe` Just packageYamlFP
        doesFileExist packageYamlP `shouldReturn` True
        Left failToRemove' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          removeDirectory nonExistentP
        ioeGetErrorType failToRemove' `shouldBe` InappropriateType
        ioeGetFileName failToRemove' `shouldBe` Just nonExistentFP
        Left failToRemove'' <- tryDryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          removeDirectory nonExistentP
        ioeGetErrorType failToRemove'' `shouldBe` InappropriateType
        ioeGetFileName failToRemove'' `shouldBe` Just nonExistentFP

    describe "removeDirectoryRecursively" $ do
      it "removes an existing directory recursively" $ do
        result <- dryRunIO $ do
          createDirectory nonExistentP
          writeFile (nonExistentP </> foo) ""
          createDirectory (nonExistentP </> bar)
          writeFile (nonExistentP </> bar </> baz) ""
          removeDirectoryRecursively nonExistentP
          exists nonExistentP
        result `shouldBe` False

      it "can't remove a non-existent directory" $ do
        Left failToRemove <- tryDryRunIO $ do
          removeDirectoryRecursively nonExistentP
        failToRemove `shouldSatisfy` isDoesNotExistError
        ioeGetFileName failToRemove `shouldBe` Just nonExistentFP
        Left failToRemove' <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          removeDirectoryRecursively packageYamlP
        failToRemove' `shouldSatisfy` isDoesNotExistError
        ioeGetFileName failToRemove' `shouldBe` Just packageYamlFP

      it "can't remove a non-directory" $ do
        Left failToRemove <- tryDryRunIO $ removeDirectoryRecursively packageYamlP
        ioeGetErrorType failToRemove `shouldBe` InappropriateType
        ioeGetLocation failToRemove
          `shouldStartWith` "removeDirectoryRecursively"
        ioeGetFileName failToRemove `shouldBe` Just packageYamlFP
        doesFileExist packageYamlP `shouldReturn` True
        Left failToRemove' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          removeDirectoryRecursively nonExistentP
        ioeGetErrorType failToRemove' `shouldBe` InappropriateType
        ioeGetLocation failToRemove'
          `shouldStartWith` "removeDirectoryRecursively"
        ioeGetFileName failToRemove' `shouldBe` Just nonExistentFP
        Left failToRemove'' <- tryDryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          removeDirectoryRecursively nonExistentP
        ioeGetErrorType failToRemove'' `shouldBe` InappropriateType
        ioeGetLocation failToRemove''
          `shouldStartWith` "removeDirectoryRecursively"
        ioeGetFileName failToRemove'' `shouldBe` Just nonExistentFP

    describe "createDirectories" $ do
      it "creates a directory and its ancestors if needed" $ do
        result <- dryRunIO $ do
          () <- createDirectories (nonExistentP </> nonExistentP)
          isDirectory (nonExistentP </> nonExistentP)
        result `shouldBe` True
        doesDirectoryExist nonExistentP `shouldReturn` False

      it "can't create a directory if any of its ancestors is a file" $ do
        Left failToCreate <- tryDryRunIO $ do
          createDirectories (packageYamlP </> nonExistentP </> nonExistentP)
        ioeGetErrorType failToCreate `shouldBe` InappropriateType
        ioeGetFileName failToCreate `shouldBe` Just packageYamlFP
        ioeGetLocation failToCreate `shouldStartWith` "createDirectories"
        show failToCreate
          `shouldContain` "one of its ancestors is a non-directory file"

    describe "listDirectory" $ do
      it "lists direct children in a directory" $ do
        withFixture $ \tmpDir tmpDir' -> do
          result <- dryRunIO $ listDirectory tmpDir
          result `shouldBe` [bar, baz, foo]
          () <- Prelude.writeFile (tmpDir' `combine` "corge") ""
          result' <- dryRunIO $ do
            () <- writeFile (tmpDir </> qux) ""
            () <- createDirectory (tmpDir </> quux)
            () <- writeFile (tmpDir </> corge) ""
            () <- writeFile (dropFileName tmpDir </> foo) ""
            listDirectory tmpDir
          result' `shouldBe` [bar, baz, corge, foo, quux, qux]

      it "lists direct children in a virtual directory" $ do
        result <- dryRunIO $ do
          () <- createDirectory nonExistentP
          () <- writeFile (nonExistentP </> foo) ""
          () <- createDirectory (nonExistentP </> bar)
          () <- createDirectory (nonExistentP </> baz)
          () <- createDirectory (nonExistentP </> baz </> qux)
          listDirectory nonExistentP
        result `shouldBe` [bar, baz, foo]

      it "hides removed files in the sandbox" $ withFixture $ \tmpDir _ -> do
        result <- dryRunIO $ do
          () <- removeFile (tmpDir </> foo)
          listDirectory tmpDir
        result `shouldBe` [bar, baz]

      it "fails if a specified path does not exist" $ do
        Left e <- tryDryRunIO $ listDirectory nonExistentP
        e `shouldSatisfy` isDoesNotExistError
        ioeGetFileName e `shouldBe` Just nonExistentFP
        ioeGetErrorType e `shouldBe` doesNotExistErrorType
        ioeGetLocation e `shouldStartWith` "listDirectory"
        Left e' <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          listDirectory packageYamlP
        e' `shouldSatisfy` isDoesNotExistError
        ioeGetFileName e' `shouldBe` Just packageYamlFP
        ioeGetLocation e' `shouldStartWith` "listDirectory"
        show e' `shouldContain` "no such directory"

      it "fails if a specified path is a regular file" $ do
        Left e <- tryDryRunIO $ listDirectory packageYamlP
        ioeGetErrorType e `shouldBe` InappropriateType
        ioeGetFileName e `shouldBe` Just packageYamlFP
        ioeGetLocation e `shouldBe` "listDirectory"
        Left e' <- tryDryRunIO $ do
          () <- writeFile nonExistentP ""
          listDirectory nonExistentP
        ioeGetErrorType e' `shouldBe` InappropriateType
        ioeGetFileName e' `shouldBe` Just nonExistentFP
        ioeGetLocation e' `shouldBe` "listDirectory"
        show e' `shouldContain` "not a directory"
        Left e'' <- tryDryRunIO $ do
          () <- copyFile packageYamlP nonExistentP
          listDirectory nonExistentP
        ioeGetErrorType e'' `shouldBe` InappropriateType
        ioeGetFileName e'' `shouldBe` Just nonExistentFP
        ioeGetLocation e'' `shouldBe` "listDirectory"
        show e'' `shouldContain` "not a directory"

    describe "getFileSize" $ do
      it "returns the size of a file" $ withTempDir $ \tmpDir tmpDirFP -> do
        Data.ByteString.writeFile (tmpDirFP `combine` "foo") "asdf"
        dryRunIO (getFileSize $ tmpDir </> foo) `shouldReturn` 4
        size <- dryRunIO $ do
          () <- writeFile (tmpDir </> bar) "asdf asdf"
          getFileSize $ tmpDir </> bar
        size `shouldBe` 9
        size' <- dryRunIO $ do
          () <- writeFile (tmpDir </> bar) "asdf"
          () <- copyFile (tmpDir </> bar) (tmpDir </> baz)
          getFileSize $ tmpDir </> baz
        size' `shouldBe` 4

      it "fails if path doesn't exist" $ withTempDir $ \tmpDir tmpDirFP -> do
        Left e <- tryDryRunIO $ getFileSize $ tmpDir </> nonExistentP
        ioeGetErrorType e `shouldBe` doesNotExistErrorType
        ioeGetFileName e `shouldBe` Just (tmpDirFP `combine` nonExistentFP)
        ioeGetLocation e `shouldBe` "getFileSize"
        Left e' <- tryDryRunIO $ do
          () <- removeFile packageYamlP
          getFileSize packageYamlP
        ioeGetErrorType e' `shouldBe` doesNotExistErrorType
        ioeGetFileName e' `shouldBe` Just packageYamlFP
        ioeGetLocation e' `shouldBe` "getFileSize"
        show e' `shouldContain` "does not exist"

      it "fails if path is a directory" $ withTempDir $ \tmpDir tmpDirFP -> do
        Left e <- tryDryRunIO $ getFileSize tmpDir
        ioeGetErrorType e `shouldBe` InappropriateType
        ioeGetFileName e `shouldBe` Just tmpDirFP
        ioeGetLocation e `shouldBe` "getFileSize"
        show e `shouldContain` "not a regular file, but a directory"
        Left e' <- tryDryRunIO $ do
          () <- createDirectory nonExistentP
          getFileSize nonExistentP
        ioeGetErrorType e' `shouldBe` InappropriateType
        ioeGetFileName e' `shouldBe` Just nonExistentFP
        ioeGetLocation e' `shouldBe` "getFileSize"
        show e' `shouldContain` "not a regular file, but a directory"