{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where
import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar)
import Control.Distributed.Process
import Control.Distributed.Process.Node
import Control.Distributed.Process.Serializable()
import Control.Distributed.Process.Extras hiding (__remoteTable, monitor, send)
import qualified Control.Distributed.Process.Extras (__remoteTable)
import Control.Distributed.Process.Extras.Call
import Control.Distributed.Process.Extras.Monitoring
import Control.Distributed.Process.Extras.Time
import Control.Monad (void)
import Control.Rematch hiding (match)
import qualified Network.Transport as NT (Transport)
import Network.Transport.TCP()
#if ! MIN_VERSION_base(4,6,0)
import Prelude hiding (catch)
#endif
import Test.HUnit (Assertion)
import Test.Framework (Test, testGroup, defaultMain)
import Test.Framework.Providers.HUnit (testCase)
import Network.Transport.TCP
import qualified Network.Transport as NT
import Control.Distributed.Process.Tests.Internal.Utils
testLinkingWithNormalExits :: TestResult DiedReason -> Process ()
testLinkingWithNormalExits result = do
testPid <- getSelfPid
pid <- spawnLocal $ do
worker <- spawnLocal $ do
"finish" <- expect
return ()
linkOnFailure worker
send testPid worker
() <- expect
return ()
workerPid <- expect :: Process ProcessId
ref <- monitor workerPid
send workerPid "finish"
receiveWait [
matchIf (\(ProcessMonitorNotification ref' _ _) -> ref == ref')
(\_ -> return ())
]
-- by now, the worker is gone, so we can check that the
-- insulator is still alive and well and that it exits normally
-- when asked to do so
ref2 <- monitor pid
send pid ()
r <- receiveWait [
matchIf (\(ProcessMonitorNotification ref2' _ _) -> ref2 == ref2')
(\(ProcessMonitorNotification _ _ reason) -> return reason)
]
stash result r
testLinkingWithAbnormalExits :: TestResult (Maybe Bool) -> Process ()
testLinkingWithAbnormalExits result = do
testPid <- getSelfPid
pid <- spawnLocal $ do
worker <- spawnLocal $ do
"finish" <- expect
return ()
linkOnFailure worker
send testPid worker
() <- expect
return ()
workerPid <- expect :: Process ProcessId
ref <- monitor pid
kill workerPid "finish" -- note the use of 'kill' instead of send
r <- receiveTimeout (asTimeout $ seconds 20) [
matchIf (\(ProcessMonitorNotification ref' _ _) -> ref == ref')
(\(ProcessMonitorNotification _ _ reason) -> return reason)
]
case r of
Just (DiedException _) -> stash result $ Just True
(Just _) -> stash result $ Just False
Nothing -> stash result Nothing
testMonitorNodeDeath :: NT.Transport -> TestResult () -> Process ()
testMonitorNodeDeath transport result = do
void $ nodeMonitor >> monitorNodes -- start node monitoring
nid1 <- getSelfNode
nid2 <- liftIO $ newEmptyMVar
nid3 <- liftIO $ newEmptyMVar
node2 <- liftIO $ newLocalNode transport initRemoteTable
node3 <- liftIO $ newLocalNode transport initRemoteTable
-- sending to (nodeId, "ignored") is a short cut to force a connection
liftIO $ tryForkProcess node2 $ ensureNodeRunning nid2 (nid1, "ignored")
liftIO $ tryForkProcess node3 $ ensureNodeRunning nid3 (nid1, "ignored")
NodeUp _ <- expect
NodeUp _ <- expect
void $ liftIO $ closeLocalNode node2
void $ liftIO $ closeLocalNode node3
NodeDown n1 <- expect
NodeDown n2 <- expect
mn1 <- liftIO $ takeMVar nid2
mn2 <- liftIO $ takeMVar nid3
[mn1, mn2] `shouldContain` n1
[mn1, mn2] `shouldContain` n2
nid4 <- liftIO $ newEmptyMVar
node4 <- liftIO $ newLocalNode transport initRemoteTable
void $ liftIO $ runProcess node4 $ do
us <- getSelfNode
liftIO $ putMVar nid4 us
monitorNode nid1 >> return ()
mn3 <- liftIO $ takeMVar nid4
NodeUp n3 <- expect
mn3 `shouldBe` (equalTo n3)
liftIO $ closeLocalNode node4
stash result ()
where
ensureNodeRunning mvar nid = do
us <- getSelfNode
liftIO $ putMVar mvar us
sendTo nid "connected"
myRemoteTable :: RemoteTable
myRemoteTable = Control.Distributed.Process.Extras.__remoteTable initRemoteTable
multicallTest :: NT.Transport -> Assertion
multicallTest transport =
do node1 <- newLocalNode transport myRemoteTable
tryRunProcess node1 $
do pid1 <- whereisOrStart "server1" server1
_ <- whereisOrStart "server2" server2
pid2 <- whereisOrStart "server2" server2
tag <- newTagPool
-- First test: expect positives answers from both processes
tag1 <- getTag tag
result1 <- multicall [pid1,pid2] mystr tag1 infiniteWait
case result1 of
[Just reversed, Just doubled] |
reversed == reverse mystr && doubled == mystr ++ mystr -> return ()
_ -> error "Unmatched"
-- Second test: First process works, second thread throws an exception
tag2 <- getTag tag
[Just 10, Nothing] <- multicall [pid1,pid2] (5::Int) tag2 infiniteWait :: Process [Maybe Int]
-- Third test: First process exceeds time limit, second process is still dead
tag3 <- getTag tag
[Nothing, Nothing] <- multicall [pid1,pid2] (23::Int) tag3 (Just 1000000) :: Process [Maybe Int]
return ()
where server1 = receiveWait [callResponse (\str -> mention (str::String) (return (reverse str,())))] >>
receiveWait [callResponse (\i -> mention (i::Int) (return (i*2,())))] >>
receiveWait [callResponse (\i -> liftIO (threadDelay 2000000) >> mention (i::Int) (return (i*10,())))]
server2 = receiveWait [callResponse (\str -> mention (str::String) (return (str++str,())))] >>
receiveWait [callResponse (\i -> error "barf" >> mention (i::Int) (return (i :: Int,())))]
mystr = "hello"
mention :: a -> b -> b
mention _a b = b
--------------------------------------------------------------------------------
-- Utilities and Plumbing --
--------------------------------------------------------------------------------
tests :: NT.Transport -> LocalNode -> [Test]
tests transport localNode = [
testGroup "Linking Tests" [
testCase "testLinkingWithNormalExits"
(delayedAssertion
"normal exit should not terminate the caller"
localNode DiedNormal testLinkingWithNormalExits)
, testCase "testLinkingWithAbnormalExits"
(delayedAssertion
"abnormal exit should terminate the caller"
localNode (Just True) testLinkingWithAbnormalExits)
],
testGroup "Call/RPC" [
testCase "multicallTest" (multicallTest transport)
],
testGroup "Node Monitoring" [
testCase "Death Notifications"
(delayedAssertion
"subscribers should both have received NodeDown twice"
localNode () (testMonitorNodeDeath transport))
]
]
primitivesTests :: NT.Transport -> IO [Test]
primitivesTests transport = do
localNode <- newLocalNode transport initRemoteTable
let testData = tests transport localNode
return testData
-- | Given a @builder@ function, make and run a test suite on a single transport
testMain :: (NT.Transport -> IO [Test]) -> IO ()
testMain builder = do
Right (transport, _) <- createTransportExposeInternals
"127.0.0.1" "10501" defaultTCPParameters
testData <- builder transport
defaultMain testData
main :: IO ()
main = testMain $ primitivesTests