packages feed

acid-state-0.17.0: test/Data/Acid/CheckpointSpec.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

-- | Tests that 'createCheckpointAndClose' does not write a checkpoint
-- identical to one this process has already made durable.
module Data.Acid.CheckpointSpec where

import Test.Hspec

import Control.Exception (bracket_)
import Control.Monad.Reader (ask)
import Control.Monad.State (modify)
import Data.SafeCopy (base, deriveSafeCopy)
import System.Directory (getTemporaryDirectory, removePathForcibly)
import System.FilePath ((</>))

import Data.Acid
import Data.Acid.Local (Checkpoint, createCheckpointAndClose,
                        defaultSerialisationLayer, mkCheckpointsLogKey)
import Data.Acid.Log (FileLog, askCurrentEntryId, closeFileLog, openFileLog)

newtype Counter = Counter Int

$(deriveSafeCopy 0 'base ''Counter)

increment :: Update Counter ()
increment = modify (\(Counter n) -> Counter (n + 1))

current :: Query Counter Int
current = do Counter n <- ask
             return n

$(makeAcidic ''Counter ['increment, 'current])

spec :: Spec
spec = around withStateDir $
    describe "createCheckpointAndClose" $ do
        it "writes a checkpoint when events were logged" $ \dir -> do
            n <- closeAfter dir $ \acid -> update acid Increment
            n `shouldBe` 1
            valueIn dir `shouldReturn` 1

        it "writes a checkpoint for a fresh state with no events" $ \dir -> do
            n <- closeAfter dir $ \_ -> return ()
            n `shouldBe` 1
            -- The initial value must have been saved, so a different
            -- initial value at reopen is ignored.
            reopened <- openLocalStateFrom dir (Counter 42)
            query reopened Current `shouldReturn` 0
            closeAcidState reopened

        it "rewrites the checkpoint it loaded, even with no new events" $ \dir -> do
            _ <- closeAfter dir $ \acid -> update acid Increment
            n <- closeAfter dir $ \_ -> return ()
            -- The loaded checkpoint may need a SafeCopy migration, so
            -- closing must still write one.
            n `shouldBe` 1
            valueIn dir `shouldReturn` 1

        it "skips the checkpoint right after createCheckpoint" $ \dir -> do
            n <- closeAfter dir $ \acid -> do
                update acid Increment
                createCheckpoint acid
            -- Only the explicit checkpoint; the close wrote nothing.
            n `shouldBe` 1
            valueIn dir `shouldReturn` 1

        it "skips the checkpoint after repeated checkpoints with no events" $ \dir -> do
            n <- closeAfter dir $ \acid -> do
                update acid Increment
                createCheckpoint acid
                createCheckpoint acid
            n `shouldBe` 2
            valueIn dir `shouldReturn` 1

        it "writes a checkpoint when events follow the last checkpoint" $ \dir -> do
            n <- closeAfter dir $ \acid -> do
                update acid Increment
                createCheckpoint acid
                update acid Increment
            n `shouldBe` 2
            valueIn dir `shouldReturn` 2

        it "keeps the state recoverable across repeated skipped closes" $ \dir -> do
            let cycle_ = closeAfter dir $ \acid -> do
                    update acid Increment
                    createCheckpoint acid
            counts <- sequence [cycle_, cycle_, cycle_]
            counts `shouldBe` [1, 1, 1]
            valueIn dir `shouldReturn` 3

-- | Open the state in @dir@, run the action, then 'createCheckpointAndClose'.
-- Returns the number of checkpoints written while the state was open.
closeAfter :: FilePath -> (AcidState Counter -> IO ()) -> IO Int
closeAfter dir action = do
    countBefore <- checkpointCount dir
    acid <- openLocalStateFrom dir (Counter 0)
    action acid
    createCheckpointAndClose acid
    countAfter <- checkpointCount dir
    return (countAfter - countBefore)

valueIn :: FilePath -> IO Int
valueIn dir = do
    acid <- openLocalStateFrom dir (Counter 0)
    n <- query acid Current
    closeAcidState acid
    return n

-- | Number of entries in the checkpoint log.  Only valid while the state
-- is closed.
checkpointCount :: FilePath -> IO Int
checkpointCount dir = do
    fLog <- openFileLog (mkCheckpointsLogKey dir defaultSerialisationLayer)
              :: IO (FileLog (Checkpoint Counter))
    n <- askCurrentEntryId fLog
    closeFileLog fLog
    return n

withStateDir :: (FilePath -> IO ()) -> IO ()
withStateDir action = do
    tmp <- getTemporaryDirectory
    let dir = tmp </> "acid-state-checkpoint-spec"
    bracket_ (removePathForcibly dir) (removePathForcibly dir) (action dir)