eventsourcing-0.9.0: test/Database/CQRS/InMemoryTest.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
module Database.CQRS.InMemoryTest
( tests
) where
import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Concurrent.MVar
import Control.Monad (forever, replicateM_, void, zipWithM_)
import Control.Monad.Trans (liftIO)
import Data.Foldable (for_, traverse_)
import Data.List (nub)
import Data.Traversable (for)
import Hedgehog hiding (collect)
import Pipes ((<-<))
import System.Mem (performGC)
import Test.Tasty
import Test.Tasty.Hedgehog
import qualified Control.Monad.Except as Exc
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import qualified Pipes
import qualified Pipes.Prelude as Pipes
import Helpers (collect)
import qualified Database.CQRS as CQRS
import qualified Database.CQRS.InMemory as CQRS.InMem
tests :: TestTree
tests = testGroup "In memory"
[ testProperty "All events are written exactly once" $
property writtenExactlyOnce
, testProperty "Writing an event sends a notification" $
property writingSendsNotification
, testProperty
"GC'ing notification stream removes it from the stream family" $
withTests 1 $ property gcRemovesListener
]
writtenExactlyOnce :: PropertyT IO ()
writtenExactlyOnce = do
streamIds <- forAll $
nub <$> Gen.list (Range.linear 1 100) (Gen.int (Range.linear 1 1000))
chunks <- for streamIds $ \streamId -> do
events <- forAll $
Gen.list (Range.linear 1 100) (Gen.int (Range.linear 1 100))
mvar <- liftIO newEmptyMVar
pure (streamId, events, mvar)
streamFamily <- CQRS.InMem.emptyStreamFamily @()
mvars <- liftIO $ for chunks $ \(streamId, events, mvar) -> do
_ <- forkIO $ do
stream <- CQRS.getStream streamFamily streamId
_ <- Exc.runExceptT $ traverse_ (CQRS.writeEvent stream) events
putMVar mvar ()
pure mvar
liftIO $ traverse_ takeMVar mvars
lastEventIdentifiers <-
collect $ CQRS.latestEventIdentifiers streamFamily
length chunks === length lastEventIdentifiers
for_ lastEventIdentifiers $ \(streamId, eventId) -> do
stream <- CQRS.getStream streamFamily streamId
events <- collect $ CQRS.streamEvents stream mempty
annotateShow events
length events === fromInteger eventId
writingSendsNotification :: PropertyT IO ()
writingSendsNotification = do
streamFamily <- CQRS.InMem.emptyStreamFamily @()
notifStream <- CQRS.allNewEvents streamFamily
events <- forAll $ Gen.list (Range.linear 1 1000) $
(,)
<$> Gen.integral (Range.linear 1 10)
<*> Gen.double (Range.linearFrac 100 10000)
_ <- liftIO . forkIO $
for_ events $ \(streamId, event) -> do
stream <- CQRS.getStream streamFamily (streamId :: Int)
void . Exc.runExceptT $ CQRS.writeEvent stream (event :: Double)
notifications <- collect $
Pipes.take (length events)
<-< forever (Pipes.each =<< Pipes.await)
<-< notifStream
(\f -> zipWithM_ f events notifications) $
\(streamId, event) (streamId', ewc) -> do
let Right CQRS.EventWithContext{ CQRS.event = event' } = ewc
streamId === streamId'
event === event'
gcRemovesListener :: PropertyT IO ()
gcRemovesListener = do
streamFamily <- CQRS.InMem.emptyStreamFamily @()
stream <- CQRS.getStream streamFamily ()
notifStreamMVar <- liftIO newEmptyMVar
liftIO $ putMVar notifStreamMVar . Just =<< CQRS.allNewEvents streamFamily
-- Fill the queue.
replicateM_ 100 . Exc.runExceptT $ CQRS.writeEvent stream ()
do
-- Trying to write a new event should fail.
mvar <- liftIO newEmptyMVar
threadId <- liftIO . forkIO $ do
void . Exc.runExceptT $ CQRS.writeEvent stream () -- This should block.
putMVar mvar () -- This should not happen.
liftIO $ threadDelay 1000
failed <- liftIO $ isEmptyMVar mvar
liftIO $ killThread threadId
failed === True
liftIO $ modifyMVar_ notifStreamMVar $ \_ -> pure Nothing
liftIO performGC
do
mvar <- liftIO newEmptyMVar
threadId <- liftIO . forkIO $ do
void . Exc.runExceptT $ CQRS.writeEvent stream () -- This should not block.
putMVar mvar () -- This should happen.
liftIO $ threadDelay 1000
failed <- liftIO $ isEmptyMVar mvar
liftIO $ killThread threadId
failed === False