feed-gipeda-0.2.0.0: tests/Unit/CommitQueue.hs
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
module Unit.CommitQueue
( tests
) where
import Control.Concurrent (forkIO, threadDelay)
import qualified Control.Logging as Logging
import Control.Monad.Free
import Control.Monad.IO.Class
import FeedGipeda.GitShell (SHA)
import FeedGipeda.Master.CommitQueue (CommitQueue)
import qualified FeedGipeda.Master.CommitQueue as CommitQueue
import FeedGipeda.Repo (Repo (..))
import qualified FeedGipeda.Repo as Repo
import System.Timeout (timeout)
import Test.Tasty
import Test.Tasty.HUnit
data QueueF a
= Update Repo [SHA] (Bool -> a)
| TryDequeue (Maybe (Repo, SHA) -> a)
| forall r. EmbedIO (IO r) (r -> a)
instance Functor QueueF where
fmap f (Update repo commits g) = Update repo commits (f . g)
fmap f (TryDequeue g) = TryDequeue (f . g)
fmap f (EmbedIO action g) = EmbedIO action (f . g)
type Queue
= Free QueueF
instance MonadIO (Free QueueF) where
liftIO action = liftF (EmbedIO action id)
update :: Repo -> [SHA] -> Queue Bool
update repo commits = liftF (Update repo commits id)
tryDequeue :: Queue (Maybe (Repo, SHA))
tryDequeue = liftF (TryDequeue id)
timeoutDelay :: Int
timeoutDelay = 1000 -- 1 ms
runQueue :: CommitQueue -> Queue r -> IO r
runQueue queue = iterM run
where
run (Update repo commits k) = CommitQueue.updateRepoBacklog queue repo commits >>= k
run (TryDequeue k) = timeout timeoutDelay (CommitQueue.dequeue queue) >>= k
run (EmbedIO action k) = action >>= k
runEmptyQueue :: (CommitQueue -> Queue r) -> IO r
runEmptyQueue st = Logging.withStdoutLogging $ do
Logging.setLogLevel Logging.LevelError
q <- CommitQueue.new
runQueue q (st q)
pipes, conduit, servant :: Repo
pipes = Repo.unsafeFromString "https://github.com/Gabriel439/Haskell-Pipes-Library"
conduit = Repo.unsafeFromString "https://github.com/snoyberg/conduit"
servant = Repo.unsafeFromString "https://github.com/haskell-servant/servant"
assertJust :: String -> Maybe a -> IO a
assertJust _ (Just a) = return a
assertJust msg _ = assertFailure msg >> error "Should never hit this"
assertNothing :: String -> Maybe a -> IO ()
assertNothing _ Nothing = return ()
assertNothing msg _ = assertFailure msg
assertDequeuedCommitFromRepo :: Repo -> SHA -> Queue ()
assertDequeuedCommitFromRepo repo commit = do
(actualRepo, actualCommit) <- tryDequeue >>= liftIO . assertJust "dequeue should not time out"
liftIO $ assertEqual "dequeued from wrong repo" repo actualRepo
liftIO $ assertEqual "dequeued wrong commit" commit actualCommit
updateNonEmpty :: Repo -> [SHA] -> Queue ()
updateNonEmpty repo commits =
update repo commits >>= liftIO . assertBool "queue was empty" . not
updateEmpty :: Repo -> Queue ()
updateEmpty repo =
update repo [] >>= liftIO . assertBool "queue was not empty"
tests :: TestTree
tests = testGroup "CommitQueue"
[ testGroup "single repo"
[ testCase "update then dequeue and empty queue" $ runEmptyQueue $ \_ -> do
updateNonEmpty pipes ["1", "3", "2"]
assertDequeuedCommitFromRepo pipes "1"
assertDequeuedCommitFromRepo pipes "3"
assertDequeuedCommitFromRepo pipes "2"
tryDequeue >>= liftIO . assertNothing "empty queue should lead to blocking"
-- Even if we there aren't any more items to dequeue, the queue might not
-- not be empty, since items are just blacklisted, meaning they are executing
-- right now. We have to clear all items for that repo to effectively have
-- an empty CommitQueue.
updateEmpty pipes
, testCase "concurrent update leads to unblocking of the dequeuer" $ runEmptyQueue $ \q -> do
liftIO . forkIO . runQueue q $ updateNonEmpty pipes ["1"]
assertDequeuedCommitFromRepo pipes "1"
]
, testGroup "multiple repos"
[ testCase "fairness" $ runEmptyQueue $ \_ -> do
updateNonEmpty pipes ["1", "2"]
assertDequeuedCommitFromRepo pipes "1"
updateNonEmpty conduit ["4", "3"]
-- we point now to what comes after pipes entry, so we wrap around
assertDequeuedCommitFromRepo conduit "4"
updateNonEmpty servant ["7", "5", "6"]
-- lastRepo was conduit, so we continue with pipes
assertDequeuedCommitFromRepo pipes "2"
updateNonEmpty pipes ["8", "9"]
-- pipes -> servant
assertDequeuedCommitFromRepo servant "7"
-- servant -> conduit
assertDequeuedCommitFromRepo conduit "3"
-- conduit -> pipes
assertDequeuedCommitFromRepo pipes "8"
-- pipes -> servant
assertDequeuedCommitFromRepo servant "5"
-- servant -> conduit, which is depleted, so -> pipes
assertDequeuedCommitFromRepo pipes "9"
-- pipes -> servant
assertDequeuedCommitFromRepo servant "6"
-- that should be all, see if dequeue blocks
tryDequeue >>= liftIO . assertNothing "empty queue should lead to blocking"
-- now tear down the queue
updateNonEmpty conduit []
updateNonEmpty pipes []
updateEmpty servant
]
]