plan-b-0.2.1: tests/Main.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TemplateHaskell #-}
module Main (main) where
import Control.Arrow ((***))
import Control.Exception
import Control.Monad
import Data.List ((\\), delete, sort)
import Data.Monoid
import Data.Typeable (Typeable)
import Path
import System.IO.Error
import System.PlanB
import Test.Hspec
import qualified Path.IO as P
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (<$))
#endif
main :: IO ()
main = hspec . around withSandbox $ do
describe "withNewFile" withNewFileSpec
describe "withExistingFile" withExistingFileSpec
describe "withNewDir" withNewDirSpec
describe "withExistingDir" withExistingDirSpec
describe "withNewContainer" withNewContainerSpec
describe "withExistingConcainer" withExistingContainerSpec
----------------------------------------------------------------------------
-- Operations on files
withNewFileSpec :: SpecWith (Path Abs Dir)
withNewFileSpec = do
context "when such file already exists" . beforeWith populatedFile $ do
context "when we want to error" $
it "throws the right exception" $ \file -> do
withNewFile' mempty file pncFile `shouldThrow` isAlreadyExistsError
detectFile (Just oldFileCont) file
context "when we want to override it" $
it "overrides it" $ \file -> do
withNewFile' overrideIfExists file pncFile
detectFile (Just newFileCont) file
context "when we want to use it" $
it "makes it available for editing" $ \file -> do
withNewFile' useIfExists file getFileCont `shouldReturn` oldFileCont
detectFile (Just oldFileCont) file
context "when such file does not exist" . beforeWith missingFile $ do
context "when we throw" $ do
context "when we don't want corpse" $
it "propagates exception, the corpse is removed" $ \file -> do
withNewFile' mempty file (pncFile >=> throwExc)
`shouldThrow` isExc
detectFile Nothing file
detectCorpse False file
context "when we want corpse" $
it "propagates exception, the corpse is there" $ \file -> do
withNewFile' preserveCorpse file (pncFile >=> throwExc)
`shouldThrow` isExc
detectFile Nothing file
detectCorpse True file
context "when we finish successfully" $
it "creates the right file, corpse is always removed" $ \file -> do
withNewFile' preserveCorpse file pncFile
detectFile (Just newFileCont) file
detectCorpse False file
where
withNewFile' pbc p = withNewFile (tempDir (parent p) <> pbc) p
withExistingFileSpec :: SpecWith (Path Abs Dir)
withExistingFileSpec = do
context "when target file exists" . beforeWith populatedFile $ do
context "when we throw" $ do
context "when we don't want corpse" $
it "propagates exception, the corpse is removed" $ \file -> do
withExistingFile' mempty file (pncFile >=> throwExc)
`shouldThrow` isExc
detectFile (Just oldFileCont) file
detectCorpse False file
context "when we want corpse" $
it "propagates exception, the corpse is there" $ \file -> do
withExistingFile' preserveCorpse file (pncFile >=> throwExc)
`shouldThrow` isExc
detectFile (Just oldFileCont) file
detectCorpse True file
context "when we finish successfully" $
it "updates the right file, corpse is always removed" $ \file -> do
withExistingFile' preserveCorpse file pncFile
detectFile (Just newFileCont) file
detectCorpse False file
context "when target file is missing" . beforeWith missingFile $
it "throws the right exception" $ \file -> do
withExistingFile' mempty file pncFile `shouldThrow` isDoesNotExistError
detectFile Nothing file
where
withExistingFile' pbc p = withExistingFile (tempDir (parent p) <> pbc) p
----------------------------------------------------------------------------
-- Operations on directories
withNewDirSpec :: SpecWith (Path Abs Dir)
withNewDirSpec = do
context "when such directory already exists" . beforeWith populatedDir $ do
context "when we want to error" $
it "throws the right exception" $ \dir -> do
withNewDir' mempty dir pncDir `shouldThrow` isAlreadyExistsError
detectDir (Just oldDirCont) dir
context "when we want to override it" $
it "overrides it" $ \dir -> do
withNewDir' overrideIfExists dir pncDir
detectDir (Just newDirCont) dir
context "when we want to use it" $
it "makes it available for editing" $ \dir -> do
withNewDir' useIfExists dir getDirCont `shouldReturn` oldDirCont
detectDir (Just oldDirCont) dir
context "when such directory does not exist" . beforeWith missingDir $ do
context "when we throw" $ do
context "when we don't want corpse" $
it "propagates exception, the corpse is removed" $ \dir -> do
withNewDir' mempty dir (pncDir >=> throwExc)
`shouldThrow` isExc
detectDir Nothing dir
detectCorpse False dir
context "when we want corpse" $
it "propagates exception, the corpse is there" $ \dir -> do
withNewDir' preserveCorpse dir (pncDir >=> throwExc)
`shouldThrow` isExc
detectDir Nothing dir
detectCorpse True dir
context "when we finish successfully" $
it "creates the right directory, corpse is always removed" $ \dir -> do
withNewDir' preserveCorpse dir pncDir
detectDir (Just newDirCont) dir
detectCorpse False dir
where
withNewDir' pbc p = withNewDir (tempDir (parent p) <> pbc) p
withExistingDirSpec :: SpecWith (Path Abs Dir)
withExistingDirSpec = do
context "when target directory exists" . beforeWith populatedDir $ do
context "when we throw" $ do
context "when we don't want corpse" $
it "propagates exception, the corpse is removed" $ \dir -> do
withExistingDir' mempty dir (pncDir >=> throwExc)
`shouldThrow` isExc
detectDir (Just oldDirCont) dir
detectCorpse False dir
context "when we want corpse" $
it "propagates exception, the corpse is there" $ \dir -> do
withExistingDir' preserveCorpse dir (pncDir >=> throwExc)
`shouldThrow` isExc
detectDir (Just oldDirCont) dir
detectCorpse True dir
context "when we finish successfully" $
it "updates the right directory, corpse is always removed" $ \dir -> do
withExistingDir' preserveCorpse dir pncDir
detectDir (Just newDirCont) dir
detectCorpse False dir
context "when target directory is missing" . beforeWith missingDir $
it "throws the right exception" $ \dir -> do
withExistingDir' mempty dir pncDir `shouldThrow` isDoesNotExistError
detectDir Nothing dir
where
withExistingDir' pbc p = withExistingDir (tempDir (parent p) <> pbc) p
----------------------------------------------------------------------------
-- Operations on containers
withNewContainerSpec :: SpecWith (Path Abs Dir)
withNewContainerSpec = do
context "when container already exists" . beforeWith populatedFile $ do
context "when we want to error" $
it "throws the right exception" $ \file -> do
withNewContainer' mempty file pncDir `shouldThrow` isAlreadyExistsError
detectFile (Just oldFileCont) file
context "when we want to override it" $
it "overrides it" $ \file -> do
withNewContainer' overrideIfExists file pncDir
detectFile (Just newFileCont) file
context "when we want to use it" $
it "makes it available for editing" $ \file -> do
withNewContainer' useIfExists file getDirCont `shouldReturn` oldDirCont
detectFile (Just oldFileCont) file
context "when container does not exist" . beforeWith missingFile $ do
context "when we throw" $ do
context "when we don't want corpse" $
it "propagates exception, the corpse is removed" $ \file -> do
withNewContainer' mempty file (pncDir >=> throwExc)
`shouldThrow` isExc
detectFile Nothing file
detectCorpse False file
context "when we want corpse" $
it "propagates exception, the corpse is there" $ \file -> do
withNewContainer' preserveCorpse file (pncDir >=> throwExc)
`shouldThrow` isExc
detectFile Nothing file
detectCorpse True file
context "when we finish successfully" $
it "creates the right file, corpse is always removed" $ \file -> do
withNewContainer' preserveCorpse file pncDir
detectFile (Just newFileCont) file
detectCorpse False file
where
withNewContainer' pbc p =
withNewContainer unpack pack (tempDir (parent p) <> pbc) p
withExistingContainerSpec :: SpecWith (Path Abs Dir)
withExistingContainerSpec = do
context "when container exists" . beforeWith populatedFile $ do
context "when we throw" $ do
context "when we don't want corpse" $
it "propagates exception, the corpse is removed" $ \file -> do
withExistingContainer' mempty file (pncDir >=> throwExc)
`shouldThrow` isExc
detectFile (Just oldFileCont) file
detectCorpse False file
context "when we want corpse" $
it "propagates exception, the corpse is there" $ \file -> do
withExistingContainer' preserveCorpse file (pncDir >=> throwExc)
`shouldThrow` isExc
detectFile (Just oldFileCont) file
detectCorpse True file
context "when we finish successfully" $
it "updates the container, corpse is always removed" $ \file -> do
withExistingContainer' preserveCorpse file pncDir
detectFile (Just newFileCont) file
detectCorpse False file
context "when container is missing" . beforeWith missingFile $
it "throws the right exception" $ \file -> do
withExistingContainer' mempty file pncDir
`shouldThrow` isDoesNotExistError
detectFile Nothing file
where
withExistingContainer' pbc p =
withExistingContainer unpack pack (tempDir (parent p) <> pbc) p
----------------------------------------------------------------------------
-- Helpers
-- | Create sandbox directory to model some situation in it and run some
-- tests. Note that we're using new unique sandbox directory for each test
-- case to avoid contamination and it's unconditionally deleted after test
-- case finishes.
withSandbox :: ActionWith (Path Abs Dir) -> IO ()
withSandbox = P.withSystemTempDir "plan-b-sandbox"
-- | Create “old” file in the sandbox directory (its location is passed as
-- the first parameter). Return location of the file to be used in tests.
populatedFile :: Path Abs Dir -> IO (Path Abs File)
populatedFile dir = path <$ writeFile (toFilePath path) oldFileCont
where path = dir </> preExistingFile
-- | Create “old” directory in the sandbox directory. Similar to
-- 'populatedFile', but this thing is a bit more complex because it has
-- several files in it.
populatedDir :: Path Abs Dir -> IO (Path Abs Dir)
populatedDir dir = do
P.createDirIfMissing True path
forM_ oldDirCont $ \file ->
writeFile (toFilePath $ path </> file) ""
return path
where path = dir </> preExistingDir
-- | Don't create any file, but generate its name and return it, so it can
-- be used in tests.
missingFile :: Path Abs Dir -> IO (Path Abs File)
missingFile dir = return (dir </> preExistingFile)
-- | Similarly, don't create any directory, but generate its name.
missingDir :: Path Abs Dir -> IO (Path Abs Dir)
missingDir dir = return (dir </> preExistingDir)
-- | Unique type of exception that we throw in tests.
data PlanBException = PlanBException
deriving (Show, Typeable)
instance Exception PlanBException
-- | Throw exception of type 'PlanBException'.
throwExc :: a
throwExc = throw PlanBException
-- | Selects only 'PlanBException'.
isExc :: Selector PlanBException
isExc = const True
-- | If the first argument is 'Nothing', file should not exist, otherwise
-- its content should match given 'String'. Name of file is taken as the
-- second argument.
detectFile :: Maybe String -> Path Abs File -> Expectation
detectFile mcont file = do
exists <- P.doesFileExist file
case mcont of
Nothing ->
when exists . expectationFailure $
"target file should not exist, but it does"
Just cont -> do
unless exists . expectationFailure $
"target file does not exist, but it should"
acont <- readFile (toFilePath file)
unless (cont == acont) . expectationFailure $
"contents of target file are incorrect:\n" ++ show acont
-- | If the first argument is 'Nothing', directory should not exist,
-- otherwise its contents should match given collection of files (but no
-- directories allowed in any case). For the sake of simplicity, we do not
-- check contents of those files. Name of directory is taken as the second
-- argument.
detectDir :: Maybe [Path Rel File] -> Path Abs Dir -> Expectation
detectDir mcont dir = do
exists <- P.doesDirExist dir
case mcont of
Nothing ->
when exists . expectationFailure $
"target dir should not exist, but it does"
Just cont -> do
unless exists . expectationFailure $
"target dir does not exist, but it should"
(dirs, files) <- P.listDirRecur dir
files' <- mapM (P.canonicalizePath >=> stripDir dir) files
unless (null dirs && null (cont \\ files')) . expectationFailure $
"contents of target directory are incorrect:\n" ++
unlines (show <$> files')
-- | First argument specifies whether there should be anything in the
-- sandbox directory except for given object. The action checks that
-- expectation. Since we put temporary files (in case of failure these are
-- called corpse and are normally deleted) in the same sandbox as target
-- file, this is a reliable way to check if those temporary files have been
-- deleted.
detectCorpse :: Bool -> Path Abs t -> Expectation
detectCorpse must file = do
items <- uncurry mappend . (fmap toFilePath *** fmap toFilePath)
<$> P.listDir (parent file)
if null (delete (toFilePath file) items)
then when must . expectationFailure $
"no corpse detected, but it should exist:\n" ++
unlines (show <$> items)
else unless must . expectationFailure $
"there is corpse, although it should not exist:\n" ++
unlines (show <$> items)
-- | Get contents of file.
getFileCont :: Path Abs File -> IO String
getFileCont = readFile . toFilePath
-- | Get contents of directory, only files and they are sorted.
getDirCont :: Path Abs Dir -> IO [Path Rel File]
getDirCont dir = sort <$> (P.listDir dir >>= mapM (stripDir dir) . snd)
-- | “Edit” file — overwrite it with new content.
pncFile :: Path Abs File -> IO ()
pncFile file = writeFile (toFilePath file) newFileCont
-- | “Edit” directory — write/overwrite it with new content. This is done by
-- deletion of files listed in 'oldDirCont' and creation of files listed in
-- 'newDirCont'.
pncDir :: Path Abs Dir -> IO ()
pncDir dir = do
forM_ ((dir </>) <$> oldDirCont) $ \file -> do
exists <- P.doesFileExist file
when exists (P.removeFile file)
forM_ ((dir </>) <$> newDirCont) $ \file ->
writeFile (toFilePath file) ""
-- | When given file has 'oldFileCont', put 'oldDirCont' into specified
-- directory. Otherwise put 'newDirCont' there.
unpack :: Path Abs File -> Path Abs Dir -> IO ()
unpack file dir = do
content <- getFileCont file
forM_ (if content == oldFileCont then oldDirCont else newDirCont) $ \f ->
writeFile (toFilePath $ dir </> f) ""
-- | When given directory has 'oldDirCont', write file with 'oldFileCont' to
-- specified path. Otherwise put 'newFileCont' there.
pack :: Path Abs Dir -> Path Abs File -> IO ()
pack dir file = do
content <- getDirCont dir
writeFile (toFilePath file) $
if content == oldDirCont then oldFileCont else newFileCont
----------------------------------------------------------------------------
-- Constants
preExistingFile :: Path Rel File
preExistingFile = $(mkRelFile "file.txt")
preExistingDir :: Path Rel Dir
preExistingDir = $(mkRelDir "dir")
oldFileCont :: String
oldFileCont = "old"
newFileCont :: String
newFileCont = "new"
oldDirCont :: [Path Rel File]
oldDirCont = [$(mkRelFile "old-file-0.txt"), $(mkRelFile "old-file-1.txt")]
newDirCont :: [Path Rel File]
newDirCont = [$(mkRelFile "new-file-0.txt"), $(mkRelFile "new-file-1.txt")]