content-store-0.2.0: tests/Data/ContentStoreSpec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.ContentStoreSpec(spec)
where
import Conduit((.|), ConduitM, runConduitRes, sinkList, yield, yieldMany)
import Control.Monad.Except(ExceptT, runExceptT)
import Control.Monad.Trans.Resource(ResourceT, runResourceT)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Either(isRight)
import Data.Maybe(fromJust)
import Data.Void(Void)
import System.Directory(createDirectory, doesFileExist, getTemporaryDirectory, removeFile)
import System.FilePath.Posix((</>))
import System.IO(Handle, hClose)
import System.IO.Temp(openTempFile, withSystemTempDirectory)
import Test.Hspec
import Data.ContentStore
import Data.ContentStore.Digest
{-# ANN module ("HLint: ignore Eta reduce" :: String) #-}
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
invalidChecksum :: BS.ByteString
invalidChecksum = "\x82\xe4\x69\x8a\x1e\xbe\x82\xed\xae\xc8\xe8\xa6\xdc\xf1\x8d\x57\xbb\x5a\xc6\x71\x8b\x6b\xf2\xbb\xc3\x8a\x00\xda\x2e\x29\x34\xe6"
anyDigest :: Either CsError ObjectDigest -> Bool
anyDigest = isRight
openSystemTempFile :: String -> IO (FilePath, Handle)
openSystemTempFile template = do
dir <- getTemporaryDirectory
openTempFile dir template
runCs :: ResourceT (ExceptT CsError IO) a -> IO (Either CsError a)
runCs = runExceptT . runResourceT
runCsConduit :: ConduitM () Void (ResourceT (ExceptT CsError IO)) a -> IO (Either CsError a)
runCsConduit = runExceptT . runConduitRes
withContentStore :: ActionWith ContentStore -> IO ()
withContentStore action = withTempDir $ \d ->
runExceptT (mkContentStore d) >>= either (expectationFailure . show) action
withStoredFile :: FilePath -> ActionWith (ContentStore, ObjectDigest) -> IO ()
withStoredFile f action =
withContentStore $ \cs ->
runCs (storeFile cs f) >>= \case
Left e -> expectationFailure $ show e
Right digest -> action (cs, digest)
withTempDir :: ActionWith FilePath -> IO ()
withTempDir action =
withSystemTempDirectory "cs.repo" action
contentStoreValidSpec :: Spec
contentStoreValidSpec =
describe "contentStoreValid" $ do
it "raises CsErrorMissing when directory doesn't exist" $
runExceptT (contentStoreValid "") >>= (`shouldBe` Left CsErrorMissing)
around withTempDir $
it "raises CsErrorInvalid when config file doesn't exist" $ \repo ->
runExceptT (contentStoreValid repo) >>= (`shouldBe` Left (CsErrorInvalid "config"))
around withTempDir $
it "raises CsErrorInvalid with objects subdir doesn't exist" $ \repo -> do
appendFile (repo </> "config") ""
runExceptT (contentStoreValid repo) >>= (`shouldBe` Left (CsErrorInvalid "objects"))
around withTempDir $
it "raises CsErrorInvalid with tmp subdir doesn't exist" $ \repo -> do
appendFile (repo </> "config") ""
createDirectory $ repo </> "objects"
runExceptT (contentStoreValid repo) >>= (`shouldBe` Left (CsErrorInvalid "tmp"))
around withTempDir $
it "raises CsErrorInvalid with lock subdir doesn't exist" $ \repo -> do
appendFile (repo </> "config") ""
createDirectory $ repo </> "objects"
createDirectory $ repo </> "tmp"
runExceptT (contentStoreValid repo) >>= (`shouldBe` Left (CsErrorInvalid "lock"))
around withTempDir $
it "repo should be valid" $ \repo -> do
appendFile (repo </> "config") ""
createDirectory $ repo </> "objects"
createDirectory $ repo </> "tmp"
createDirectory $ repo </> "lock"
runExceptT (contentStoreValid repo) >>= (`shouldBe` Right True)
contentStoreDigestSpec :: Spec
contentStoreDigestSpec =
describe "contentStoreDigest" $
around withContentStore $
it "default digest is BLAKE2b256" $ \cs ->
-- The constructors of DigestAlgorithm are not exported, so we can only
-- compare names.
digestName (contentStoreDigest cs) `shouldBe` "Blake2b_256"
fetchByteStringSpec :: Spec
fetchByteStringSpec =
describe "fetchByteString" $ do
around withContentStore $
it "raises CsErrorNoSuchObject when the object doesn't exist" $ \cs -> do
let od = fromJust $ fromByteString (contentStoreDigest cs) invalidChecksum
runCs (fetchByteString cs od) >>= (`shouldBe` Left (CsErrorNoSuchObject $ toHex od))
around (withStoredFile __FILE__) $
it "stored file and fetched file are the same" $ \(cs, digest) ->
runCs (fetchByteString cs digest) >>= \case
Left e -> expectationFailure (show e)
Right bs2 -> do
bs1 <- BS.readFile __FILE__
bs2 `shouldBe` bs1
fetchByteStringCSpec :: Spec
fetchByteStringCSpec =
describe "fetchByteStringC" $ do
around withContentStore $
it "raises CsErrorNoSuchObject when the object doesn't exist" $ \cs -> do
let od = fromJust $ fromByteString (contentStoreDigest cs) invalidChecksum
runCsConduit (yield od .| fetchByteStringC cs .| sinkList) >>= (`shouldBe` Left (CsErrorNoSuchObject $ toHex od))
around (withStoredFile __FILE__) $
it "stored file and fetched file are the same" $ \(cs, digest) ->
runCsConduit (yield digest .| fetchByteStringC cs .| sinkList) >>= \case
Right [bs2] -> do
bs1 <- BS.readFile __FILE__
bs2 `shouldBe` bs1
Right l -> expectationFailure $ "fetchByteStringC returned " ++ show (length l) ++ " elements"
Left e -> expectationFailure (show e)
around (withStoredFile __FILE__) $
it "requesting two digests returns two files" $ \(cs, digest) ->
runCsConduit (yieldMany [digest, digest] .| fetchByteStringC cs .| sinkList) >>= \case
Left e -> expectationFailure (show e)
Right lst -> length lst `shouldBe` 2
storeByteStringSpec :: Spec
storeByteStringSpec =
describe "storeByteString" $ do
around withContentStore $
it "storing a bytestring should return a digest" $ \cs ->
runCs (storeByteString cs "The spice must flow.") >>= (`shouldSatisfy` anyDigest)
around withContentStore $
it "duplicate stores do not raise an error" $ \cs -> do
first <- runCs $ storeByteString cs "blahblahblah"
second <- runCs $ storeByteString cs "blahblahblah"
first `shouldSatisfy` anyDigest
first `shouldBe` second
storeByteStringCSpec :: Spec
storeByteStringCSpec =
describe "storeByteStringC" $ do
around withContentStore $
it "storing a bytestring should return a digest" $ \cs ->
runCsConduit (yield "The spice must flow." .| storeByteStringC cs .| sinkList) >>= \case
Left e -> expectationFailure (show e)
Right lst -> length lst `shouldBe` 1
around withContentStore $
it "storing multiple bytestrings should return multiple digests" $ \cs -> do
let strs = ["I must not fear.",
"Fear is the mind-killer.",
"Fear is the little-death that brings total obliteration."]
runCsConduit (yieldMany strs .| storeByteStringC cs .| sinkList) >>= \case
Left e -> expectationFailure (show e)
Right lst -> length lst `shouldBe` 3
storeByteStringSinkSpec :: Spec
storeByteStringSinkSpec =
describe "storeByteStringSink" $
around withContentStore $
it "storing multiple bytestrings should return a single digest" $ \cs -> do
let strs = ["I must not fear.",
"Fear is the mind-killer.",
"Fear is the little-death that brings total obliteration."]
runCsConduit (yieldMany strs .| storeByteStringSink cs) >>= (`shouldSatisfy` anyDigest)
fetchLazyByteStringSpec :: Spec
fetchLazyByteStringSpec =
describe "fetchLazyByteString" $ do
around withContentStore $
it "raises CsErrorNoSuchObject when the object doesn't exist" $ \cs -> do
let od = fromJust $ fromByteString (contentStoreDigest cs) invalidChecksum
runCs (fetchLazyByteString cs od) >>= (`shouldBe` Left (CsErrorNoSuchObject $ toHex od))
around (withStoredFile __FILE__) $
it "stored file and fetched file are the same" $ \(cs, digest) ->
runCs (fetchLazyByteString cs digest) >>= \case
Left e -> expectationFailure (show e)
Right bs2 -> do
bs1 <- LBS.readFile __FILE__
bs2 `shouldBe` bs1
fetchLazyByteStringCSpec :: Spec
fetchLazyByteStringCSpec =
describe "fetchLazyByteStringC" $ do
around withContentStore $
it "raises CsErrorNoSuchObject when the object doesn't exist" $ \cs -> do
let od = fromJust $ fromByteString (contentStoreDigest cs) invalidChecksum
runCsConduit (yield od .| fetchLazyByteStringC cs .| sinkList) >>= (`shouldBe` Left (CsErrorNoSuchObject $ toHex od))
around (withStoredFile __FILE__) $
it "stored file and fetched file are the same" $ \(cs, digest) ->
runCsConduit (yield digest .| fetchLazyByteStringC cs .| sinkList) >>= \case
Right [bs2] -> do
bs1 <- LBS.readFile __FILE__
bs2 `shouldBe` bs1
Right _ -> expectationFailure "fetchByteStringC returned more than one element"
Left e -> expectationFailure (show e)
around (withStoredFile __FILE__) $
it "requesting two digests returns two files" $ \(cs, digest) ->
runCsConduit (yieldMany [digest, digest] .| fetchLazyByteStringC cs .| sinkList) >>= \case
Left e -> expectationFailure (show e)
Right lst -> length lst `shouldBe` 2
storeLazyByteStringSpec :: Spec
storeLazyByteStringSpec =
describe "storeLazyByteString" $ do
around withContentStore $
it "storing a lazy bytestring should return a digest" $ \cs ->
runCs (storeLazyByteString cs "The spice must flow.") >>= (`shouldSatisfy` anyDigest)
around withContentStore $
it "duplicate stores do not raise an error" $ \cs -> do
first <- runCs $ storeLazyByteString cs "blahblahblah"
second <- runCs $ storeLazyByteString cs "blahblahblah"
first `shouldSatisfy` anyDigest
first `shouldBe` second
storeLazyByteStringCSpec :: Spec
storeLazyByteStringCSpec =
describe "storeLazyByteStringC" $ do
around withContentStore $
it "storing a lazy bytestring should return a digest" $ \cs ->
runCsConduit (yield "The spice must flow." .| storeLazyByteStringC cs .| sinkList) >>= \case
Left e -> expectationFailure (show e)
Right lst -> length lst `shouldBe` 1
around withContentStore $
it "storing multiple lazy bytestrings should return multiple digests" $ \cs -> do
let strs = ["I must not fear.",
"Fear is the mind-killer.",
"Fear is the little-death that brings total obliteration."]
runCsConduit (yieldMany strs .| storeLazyByteStringC cs .| sinkList) >>= \case
Left e -> expectationFailure (show e)
Right lst -> length lst `shouldBe` 3
storeLazyByteStringSinkSpec :: Spec
storeLazyByteStringSinkSpec =
describe "storeLazyByteStringSink" $
around withContentStore $
it "storing multiple bytestrings should return a single digest" $ \cs -> do
let strs = ["I must not fear.",
"Fear is the mind-killer.",
"Fear is the little-death that brings total obliteration."]
runCsConduit (yieldMany strs .| storeLazyByteStringSink cs) >>= (`shouldSatisfy` anyDigest)
fetchFileSpec :: Spec
fetchFileSpec =
describe "fetchFile" $ do
around withContentStore $
it "raises CsErrorNoSuchObject when the object doesn't exist" $ \cs -> do
let od = fromJust $ fromByteString (contentStoreDigest cs) invalidChecksum
dest = "yyyyyyyyyyyyyyy"
runCs (fetchFile cs od dest) >>= (`shouldBe` Left (CsErrorNoSuchObject $ toHex od))
-- Verify the destination file was not created by conduit. This can happen
-- if sinkFile is used instead of sinkFileCautious.
doesFileExist dest >>= (`shouldBe` False)
around (withStoredFile __FILE__) $
it "stored file and fetched file are the same" $ \(cs, digest) -> do
(dest, h) <- openSystemTempFile "dest"
hClose h
-- Second, pull it back out of the store.
runCs (fetchFile cs digest dest) >>= \case
Left e -> removeFile dest >> expectationFailure (show e)
Right _ -> do
-- Third, compare the two files. They should be the same.
bs1 <- BS.readFile __FILE__
bs2 <- BS.readFile dest
removeFile dest
bs2 `shouldBe` bs1
storeFileSpec :: Spec
storeFileSpec = do
describe "storeFile" $
around withContentStore $ do
it "raises an IOException when trying to store a missing file" $ \cs ->
runCs (storeFile cs "xxxxxxxxxxxxxxx") `shouldThrow` anyIOException
it "storing an existing file should return a digest" $ \cs ->
runCs (storeFile cs __FILE__) >>= (`shouldSatisfy` anyDigest)
describe "storeFile duplicates" $
around withContentStore $
it "duplicate stores do not raise an error" $ \cs -> do
first <- runCs $ storeFile cs __FILE__
second <- runCs $ storeFile cs __FILE__
first `shouldSatisfy` anyDigest
first `shouldBe` second
spec :: Spec
spec =
describe "ContentStore" $ do
contentStoreDigestSpec
contentStoreValidSpec
fetchByteStringSpec
fetchByteStringCSpec
fetchFileSpec
fetchLazyByteStringSpec
fetchLazyByteStringCSpec
-- mkContentStoreSpec
-- openContentStoreSpec
storeByteStringSpec
storeByteStringCSpec
storeByteStringSinkSpec
-- storeDirectorySpec
storeFileSpec
storeLazyByteStringSpec
storeLazyByteStringCSpec
storeLazyByteStringSinkSpec