unliftio-0.2.12: test/UnliftIO/IO/FileSpec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module UnliftIO.IO.FileSpec where
import Test.Hspec
-- Atomic/durable file writing is not supported on Windows.
#ifndef WINDOWS
import Control.Monad (forM_)
import Data.Bool (bool)
import System.FilePath ((</>))
import Test.QuickCheck
import UnliftIO.Directory
import UnliftIO.Exception
import UnliftIO.IO
import UnliftIO.IO.File as File
import UnliftIO.Temporary (withSystemTempDirectory)
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
#if __GLASGOW_HASKELL__ < 820
import Data.Monoid
#endif
data ExpectedException =
ExpectedException
deriving (Show)
instance Exception ExpectedException
spec :: Spec
spec = do
describe "ensureFileDurable" $
it "ensures a file is durable with an fsync" $
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> "ensure_file_durable"
writeFile fp "Hello World"
File.ensureFileDurable fp
contents <- B.readFile fp
contents `shouldBe` "Hello World"
withBinaryFileSpec False "withBinaryFile" withBinaryFile
writeBinaryFileSpec "writeBinaryFile" writeBinaryFile
-- Above two specs are validating the specs behavior by applying to
-- known good implementations
withBinaryFileSpec True "withBinaryFileAtomic" File.withBinaryFileAtomic
writeBinaryFileSpec "writeBinaryFileAtomic" File.writeBinaryFileAtomic
withBinaryFileSpec False "withBinaryFileDurable" File.withBinaryFileDurable
writeBinaryFileSpec "writeBinaryFileDurable" File.writeBinaryFileDurable
withBinaryFileSpec True "withBinaryFileDurableAtomic" File.withBinaryFileDurableAtomic
writeBinaryFileSpec "writeBinaryFileDurableAtomic" File.writeBinaryFileDurableAtomic
writeFileUtf8 fp str = withBinaryFile fp WriteMode (`BB.hPutBuilder` BB.stringUtf8 str)
withBinaryFileSpec ::
Bool -- ^ Should we test atomicity
-> String
-> (forall a. FilePath -> IOMode -> (Handle -> IO a) -> IO a)
-> Spec
withBinaryFileSpec atomic fname withFileTestable = do
let hello = "Hello World"
helloString = "Hello World"
writeHello fp = writeFileUtf8 fp helloString
-- Create a file, write "Hello World" into it and apply the action.
withHelloFileTestable fp iomode action = do
writeHello fp
withFileTestable fp iomode action
goodbye = "Goodbye yall"
modifiedPermissions =
setOwnerExecutable True $
setOwnerReadable True $ setOwnerWritable True emptyPermissions
describe fname $ do
it "read" $
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-read"
withHelloFileTestable fp ReadWriteMode (`B.hGet` B.length hello) `shouldReturn`
hello
it "write" $
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-write"
withHelloFileTestable fp WriteMode (`B.hPut` goodbye)
B.readFile fp `shouldReturn` goodbye
it "read/write" $
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-read-write"
withHelloFileTestable fp ReadWriteMode $ \h -> do
B.hGetLine h `shouldReturn` hello
B.hPut h goodbye
B.readFile fp `shouldReturn` (hello <> goodbye)
it "append" $
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-append"
privet = "Привет Мир" -- some unicode won't hurt
encodeUtf8 = BL.toStrict . BB.toLazyByteString . BB.stringUtf8
writeFileUtf8 fp privet
setPermissions fp modifiedPermissions
withFileTestable fp AppendMode $ \h -> B.hPut h goodbye
B.readFile fp `shouldReturn` (encodeUtf8 privet <> goodbye)
it "sub-directory" $
withSystemTempDirectory "rio" $ \dir -> do
let subDir = dir </> fname ++ "-sub-directory"
fp = subDir </> "test.file"
createDirectoryIfMissing True subDir
withHelloFileTestable fp ReadWriteMode $ \h -> do
B.hGetLine h `shouldReturn` hello
B.hPut h goodbye
B.readFile fp `shouldReturn` (hello <> goodbye)
it "relative-directory" $
withSystemTempDirectory "rio" $ \dir -> do
let relDir = fname ++ "-relative-directory"
subDir = dir </> relDir
fp = relDir </> "test.file"
createDirectoryIfMissing True subDir
withCurrentDirectoryCompat dir $ do
withHelloFileTestable fp ReadWriteMode $ \h -> do
B.hGetLine h `shouldReturn` hello
B.hPut h goodbye
B.readFile fp `shouldReturn` (hello <> goodbye)
it "modified-permissions" $
forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode ->
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-modified-permissions"
writeHello fp
setPermissions fp modifiedPermissions
withFileTestable fp iomode $ \h -> B.hPut h goodbye
getPermissions fp `shouldReturn` modifiedPermissions
it "exception - Does not corrupt files" $
bool expectFailure property atomic $ -- should fail for non-atomic
forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode ->
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-exception"
_ :: Either ExpectedException () <-
try $
withHelloFileTestable fp iomode $ \h -> do
B.hPut h goodbye
throwIO ExpectedException
B.readFile fp `shouldReturn` hello
it "exception - Does not leave files behind" $
bool expectFailure property atomic $ -- should fail for non-atomic
forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode ->
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-exception"
_ :: Either ExpectedException () <-
try $
withFileTestable fp iomode $ \h -> do
B.hPut h goodbye
throwIO ExpectedException
doesFileExist fp `shouldReturn` False
listDirectoryCompat dir `shouldReturn` []
it "delete - file" $
bool expectFailure property atomic $ -- should fail for non-atomic
forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode ->
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-delete"
withHelloFileTestable fp iomode $ \h -> do
removeFile fp
B.hPut h goodbye
doesFileExist fp `shouldReturn` True
writeBinaryFileSpec :: String -> (FilePath -> B.ByteString -> IO ()) -> SpecWith ()
writeBinaryFileSpec fname writeFileTestable = do
let hello = "Hello World"
describe fname $ do
it "write" $
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-write"
writeFileTestable fp hello
B.readFile fp `shouldReturn` hello
it "default-permissions" $
withSystemTempDirectory "rio" $ \dir -> do
let fp = dir </> fname ++ "-default-permissions"
defaultPermissions =
setOwnerReadable True $ setOwnerWritable True emptyPermissions
writeFileTestable fp hello
getPermissions fp `shouldReturn` defaultPermissions
listDirectoryCompat :: FilePath -> IO [FilePath]
#if MIN_VERSION_directory(1,2,5)
listDirectoryCompat = listDirectory
#else
listDirectoryCompat path =
filter f <$> getDirectoryContents path
where f filename = filename /= "." && filename /= ".."
#endif
withCurrentDirectoryCompat :: FilePath -> IO a -> IO a
#if MIN_VERSION_directory(1,2,3)
withCurrentDirectoryCompat = withCurrentDirectory
#else
withCurrentDirectoryCompat dir action =
bracket getCurrentDirectory setCurrentDirectory $ \ _ -> do
setCurrentDirectory dir
action
#endif
#else
spec :: Spec
spec = pure ()
#endif