packages feed

hedis-0.9.2: test/PubSubTest.hs

{-# LANGUAGE CPP, OverloadedStrings #-}
module PubSubTest (testPubSubThreaded) where

#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
import Data.Monoid (mappend)
#endif
import Control.Concurrent
import Control.Monad
import Control.Concurrent.Async
import Control.Exception
import Data.Typeable
--import Control.Monad.Trans
--import Data.Time
import qualified Data.List
import Data.Text
import Data.ByteString
import Control.Concurrent.STM
--import Data.Time.Clock.POSIX
import qualified Test.Framework as Test
import qualified Test.Framework.Providers.HUnit as Test (testCase)
import qualified Test.HUnit as HUnit

import Database.Redis

testPubSubThreaded :: [Connection -> Test.Test]
testPubSubThreaded = [removeAllTest, callbackErrorTest, removeFromUnregister]

-- | A handler label to be able to distinguish the handlers from one another
-- to help make sure we unregister the correct handler.
type HandlerLabel = Text

data TestMsg = MsgFromChannel HandlerLabel ByteString
             | MsgFromPChannel HandlerLabel RedisChannel ByteString
  deriving (Show, Eq)

type MsgVar = TVar [TestMsg]

-- | A handler that just writes the message into the TVar
handler :: HandlerLabel -> MsgVar -> MessageCallback
handler label ref msg = atomically $
  modifyTVar ref $ \x -> x ++ [MsgFromChannel label msg]

-- | A pattern handler that just writes the message into the TVar
phandler :: HandlerLabel -> MsgVar -> PMessageCallback
phandler label ref chan msg = atomically $
  modifyTVar ref $ \x -> x ++ [MsgFromPChannel label chan msg]

-- | Wait for a given message to be received
waitForMessage :: MsgVar -> HandlerLabel -> ByteString -> IO ()
waitForMessage ref label msg = atomically $ do
  let expected = MsgFromChannel label msg
  lst <- readTVar ref
  unless (expected `Prelude.elem` lst) retry
  writeTVar ref $ Prelude.filter (/= expected) lst

-- | Wait for a given pattern message to be received
waitForPMessage :: MsgVar -> HandlerLabel -> RedisChannel -> ByteString -> IO ()
waitForPMessage ref label chan msg = atomically $ do
  let expected = MsgFromPChannel label chan msg
  lst <- readTVar ref
  unless (expected `Prelude.elem` lst) retry
  writeTVar ref $ Prelude.filter (/= expected) lst

expectRedisChannels :: Connection -> [RedisChannel] -> IO ()
expectRedisChannels conn expected = do
  actual <- runRedis conn $ sendRequest ["PUBSUB", "CHANNELS"]
  case actual of
    Left err -> HUnit.assertFailure $ "Error geting channels: " ++ show err
    Right s -> HUnit.assertEqual "redis channels" (Data.List.sort s) (Data.List.sort expected)

-- | Test basic messages, plus using removeChannels
removeAllTest :: Connection -> Test.Test
removeAllTest conn = Test.testCase "Multithreaded Pub/Sub - basic" $ do
  msgVar <- newTVarIO []
  initialComplete <- newTVarIO False
  ctrl <- newPubSubController [("foo1", handler "InitialFoo1" msgVar), ("foo2", handler "InitialFoo2" msgVar)]
                              [("bar1:*", phandler "InitialBar1" msgVar), ("bar2:*", phandler "InitialBar2" msgVar)]
  withAsync (pubSubForever conn ctrl (atomically $ writeTVar initialComplete True)) $ \_ -> do

  -- wait for initial
  atomically $ readTVar initialComplete >>= \b -> if b then return () else retry
  expectRedisChannels conn ["foo1", "foo2"]

  runRedis conn $ publish "foo1" "Hello"
  waitForMessage msgVar "InitialFoo1" "Hello"

  runRedis conn $ publish "bar2:zzz" "World"
  waitForPMessage msgVar "InitialBar2" "bar2:zzz" "World"

  -- subscribe to foo1 and bar1 again
  addChannelsAndWait ctrl [("foo1", handler "NewFoo1" msgVar)] [("bar1:*", phandler "NewBar1" msgVar)]
  expectRedisChannels conn ["foo1", "foo2"]

  runRedis conn $ publish "foo1" "abcdef"
  waitForMessage msgVar "InitialFoo1" "abcdef"
  waitForMessage msgVar "NewFoo1" "abcdef"

  -- unsubscribe from foo1 and bar1
  removeChannelsAndWait ctrl ["foo1", "unusued"] ["bar1:*", "unused:*"]
  expectRedisChannels conn ["foo2"]

  -- foo2 and bar2 are still subscribed
  runRedis conn $ publish "foo2" "12345"
  waitForMessage msgVar "InitialFoo2" "12345"

  runRedis conn $ publish "bar2:aaa" "0987"
  waitForPMessage msgVar "InitialBar2" "bar2:aaa" "0987"

data TestError = TestError ByteString
  deriving (Eq, Show, Typeable)
instance Exception TestError

-- | Test an error thrown from a message handler
callbackErrorTest :: Connection -> Test.Test
callbackErrorTest conn = Test.testCase "Multithreaded Pub/Sub - error in handler" $ do
  initialComplete <- newTVarIO False
  ctrl <- newPubSubController [("foo", throwIO . TestError)] []

  thread <- async (pubSubForever conn ctrl (atomically $ writeTVar initialComplete True))
  atomically $ readTVar initialComplete >>= \b -> if b then return () else retry

  runRedis conn $ publish "foo" "Hello"

  ret <- waitCatch thread
  case ret of
    Left (SomeException e) | cast e == Just (TestError "Hello") -> return ()
    _ -> HUnit.assertFailure $ "Did not properly throw error from message thread " ++ show ret

-- | Test removing channels by using the return value of 'addHandlersAndWait'.
removeFromUnregister :: Connection -> Test.Test
removeFromUnregister conn = Test.testCase "Multithreaded Pub/Sub - unregister handlers" $ do
  msgVar <- newTVarIO []
  initialComplete <- newTVarIO False
  ctrl <- newPubSubController [] []
  withAsync (pubSubForever conn ctrl (atomically $ writeTVar initialComplete True)) $ \_ -> do
  atomically $ readTVar initialComplete >>= \b -> if b then return () else retry

  -- register to some channels
  void $ addChannelsAndWait ctrl
      [("abc", handler "InitialAbc" msgVar), ("xyz", handler "InitialXyz" msgVar)]
      [("def:*", phandler "InitialDef" msgVar), ("uvw", phandler "InitialUvw" msgVar)]
  expectRedisChannels conn ["abc", "xyz"]

  runRedis conn $ publish "abc" "Hello"
  waitForMessage msgVar "InitialAbc" "Hello"

  -- register to some more channels
  unreg <- addChannelsAndWait ctrl
      [("abc", handler "SecondAbc"  msgVar), ("123", handler "Second123" msgVar)]
      [("def:*", phandler "SecondDef" msgVar), ("890:*", phandler "Second890" msgVar)]
  expectRedisChannels conn ["abc", "xyz", "123"]

  -- check messages on all channels
  runRedis conn $ publish "abc" "World"
  waitForMessage msgVar "InitialAbc" "World"
  waitForMessage msgVar "SecondAbc" "World"

  runRedis conn $ publish "123" "World2"
  waitForMessage msgVar "Second123" "World2"

  runRedis conn $ publish "def:bbbb" "World3"
  waitForPMessage msgVar "InitialDef" "def:bbbb" "World3"
  waitForPMessage msgVar "SecondDef" "def:bbbb" "World3"

  runRedis conn $ publish "890:tttt" "World4"
  waitForPMessage msgVar "Second890" "890:tttt" "World4"

  -- unregister
  unreg

  -- we have no way of waiting until unregister actually happened, so just delay and hope
  threadDelay $ 1000*1000 -- 1 second
  expectRedisChannels conn ["abc", "xyz"]

  -- now only initial should be around. In particular, abc should still be subscribed
  runRedis conn $ publish "abc" "World5"
  waitForMessage msgVar "InitialAbc" "World5"

  runRedis conn $ publish "def:cccc" "World6"
  waitForPMessage msgVar "InitialDef" "def:cccc" "World6"