mirror of
https://github.com/zap-proto/haskell.git
synced 2026-10-30 15:05:00 +00:00
This is specifically testing something that requires triggering this warning. I don't want to disable it, since there's only one place where this is valid (maybe we should move it to its own module). But it can't be allowed to break the build when we're building with -Werror.
467 lines
18 KiB
Haskell
467 lines
18 KiB
Haskell
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# OPTIONS_GHC -Wno-error=missing-methods #-}
|
|
module Module.Capnp.Rpc (rpcTests) where
|
|
|
|
import Control.Concurrent.STM
|
|
import Data.Word
|
|
import Test.Hspec
|
|
|
|
import Control.Concurrent.Async (concurrently_, race_)
|
|
import Control.Exception.Safe (bracket, try)
|
|
import Control.Monad (replicateM, void, (>=>))
|
|
import Control.Monad.Catch (throwM)
|
|
import Control.Monad.IO.Class (liftIO)
|
|
import Data.Foldable (for_)
|
|
import Data.Mutable (freeze)
|
|
import System.Timeout (timeout)
|
|
|
|
import qualified Data.ByteString.Builder as BB
|
|
import qualified Data.Text as T
|
|
import qualified Network.Socket as Socket
|
|
import qualified Supervisors
|
|
|
|
import Capnp
|
|
( createPure
|
|
, def
|
|
, defaultLimit
|
|
, evalLimitT
|
|
, lbsToMsg
|
|
, msgToValue
|
|
, valueToMsg
|
|
)
|
|
import Capnp.Bits (WordCount)
|
|
import Capnp.Rpc.Errors (eFailed)
|
|
|
|
import Capnp.Gen.Aircraft.Pure hiding (Left, Right)
|
|
import Capnp.Gen.Capnp.Rpc.Pure
|
|
import Capnp.Rpc
|
|
import Capnp.Rpc.Untyped
|
|
|
|
import qualified Capnp.Gen.Echo.Pure as E
|
|
import qualified Capnp.Pointer as P
|
|
|
|
rpcTests :: Spec
|
|
rpcTests = do
|
|
echoTests
|
|
aircraftTests
|
|
unusualTests
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Tests using echo.capnp.
|
|
-------------------------------------------------------------------------------
|
|
|
|
echoTests :: Spec
|
|
echoTests = describe "Echo server & client" $
|
|
it "Should echo back the same message." $ runVatPair
|
|
(`E.export_Echo` TestEchoServer)
|
|
(\_sup echoSrv -> do
|
|
let msgs =
|
|
[ def { E.query = "Hello #1" }
|
|
, def { E.query = "Hello #2" }
|
|
]
|
|
rets <- traverse ((E.echo'echo echoSrv ?) >=> wait) msgs
|
|
liftIO $ rets `shouldBe`
|
|
[ def { E.reply = "Hello #1" }
|
|
, def { E.reply = "Hello #2" }
|
|
]
|
|
)
|
|
|
|
data TestEchoServer = TestEchoServer
|
|
|
|
instance Server IO TestEchoServer
|
|
instance E.Echo'server_ IO TestEchoServer where
|
|
echo'echo = pureHandler $ \_ params -> pure def { E.reply = E.query params }
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Tests using aircraft.capnp.
|
|
--
|
|
-- These use the 'CallSequence' interface as a counter.
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- | Bump a counter n times, returning a list of the results.
|
|
bumpN :: CallSequence -> Int -> IO [CallSequence'getNumber'results]
|
|
bumpN ctr n = bumpNPromise ctr n >>= traverse wait
|
|
|
|
-- | Like 'bumpN', but doesn't wait for the results -- returns a list of promises.
|
|
bumpNPromise :: CallSequence -> Int -> IO [Promise CallSequence'getNumber'results]
|
|
bumpNPromise ctr n = replicateM n (callSequence'getNumber ctr ? def)
|
|
|
|
aircraftTests :: Spec
|
|
aircraftTests = describe "aircraft.capnp rpc tests" $ do
|
|
describe "newPromiseClient" $
|
|
it "Should preserve E-order" $
|
|
Supervisors.withSupervisor $ \sup -> do
|
|
(pc, f) <- newPromiseClient
|
|
firsts <- bumpNPromise pc 2
|
|
atomically (newTestCtr 0)
|
|
>>= export_CallSequence sup
|
|
>>= fulfill f
|
|
nexts <- bumpN pc 2
|
|
firstsResolved <- traverse wait firsts
|
|
firstsResolved `shouldBe`
|
|
[ def { n = 1 }
|
|
, def { n = 2 }
|
|
]
|
|
nexts `shouldBe`
|
|
[ def { n = 3 }
|
|
, def { n = 4 }
|
|
]
|
|
it "Should propogate server-side exceptions to client method calls" $ runVatPair
|
|
(`export_CallSequence` ExnCtrServer)
|
|
(\_sup -> expectException
|
|
(\cap -> callSequence'getNumber cap ? def)
|
|
def
|
|
{ type_ = Exception'Type'failed
|
|
, reason = "Something went sideways."
|
|
}
|
|
)
|
|
it "Should receive unimplemented when calling a method on a null cap." $ runVatPair
|
|
(\_sup -> pure $ CallSequence nullClient)
|
|
(\_sup -> expectException
|
|
(\cap -> callSequence'getNumber cap ? def)
|
|
def
|
|
{ type_ = Exception'Type'unimplemented
|
|
, reason = "Method unimplemented"
|
|
}
|
|
)
|
|
it "Should throw an unimplemented exception if the server doesn't implement a method" $ runVatPair
|
|
(`export_CallSequence` NoImplServer)
|
|
(\_sup -> expectException
|
|
(\cap -> callSequence'getNumber cap ? def)
|
|
def
|
|
{ type_ = Exception'Type'unimplemented
|
|
, reason = "Method unimplemented"
|
|
}
|
|
)
|
|
it "Should throw an opaque exception when the server throws a non-rpc exception" $ runVatPair
|
|
(`export_CallSequence` NonRpcExnServer)
|
|
(\_sup -> expectException
|
|
(\cap -> callSequence'getNumber cap ? def)
|
|
def
|
|
{ type_ = Exception'Type'failed
|
|
, reason = "Unhandled exception"
|
|
}
|
|
)
|
|
it "A counter should maintain state" $ runVatPair
|
|
(\sup -> newTestCtr 0 >>= export_CallSequence sup)
|
|
(\_sup ctr -> do
|
|
results <- bumpN ctr 4
|
|
liftIO $ results `shouldBe`
|
|
[ def { n = 1 }
|
|
, def { n = 2 }
|
|
, def { n = 3 }
|
|
, def { n = 4 }
|
|
]
|
|
)
|
|
it "Methods returning interfaces work" $ runVatPair
|
|
(\sup -> export_CounterFactory sup (TestCtrFactory sup))
|
|
(\_sup factory -> do
|
|
let newCounter start = do
|
|
CounterFactory'newCounter'results{counter} <-
|
|
counterFactory'newCounter factory ? def { start }
|
|
>>= wait
|
|
pure counter
|
|
|
|
ctrA <- newCounter 2
|
|
ctrB <- newCounter 0
|
|
|
|
r1 <- bumpN ctrA 4
|
|
liftIO $ r1 `shouldBe`
|
|
[ def { n = 3 }
|
|
, def { n = 4 }
|
|
, def { n = 5 }
|
|
, def { n = 6 }
|
|
]
|
|
|
|
r2 <- bumpN ctrB 2
|
|
liftIO $ r2 `shouldBe`
|
|
[ def { n = 1 }
|
|
, def { n = 2 }
|
|
]
|
|
|
|
ctrC <- newCounter 30
|
|
|
|
r3 <- bumpN ctrA 3
|
|
liftIO $ r3 `shouldBe`
|
|
[ def { n = 7 }
|
|
, def { n = 8 }
|
|
, def { n = 9 }
|
|
]
|
|
|
|
r4 <- bumpN ctrC 1
|
|
liftIO $ r4 `shouldBe` [ def { n = 31 } ]
|
|
)
|
|
it "Methods with interface parameters work" $ do
|
|
ctrA <- atomically $ newTestCtr 2
|
|
ctrB <- atomically $ newTestCtr 0
|
|
ctrC <- atomically $ newTestCtr 30
|
|
runVatPair
|
|
(`export_CounterAcceptor` TestCtrAcceptor)
|
|
(\sup acceptor -> do
|
|
for_ [ctrA, ctrB, ctrC] $ \ctrSrv -> do
|
|
ctr <- atomically $ export_CallSequence sup ctrSrv
|
|
counterAcceptor'accept acceptor ? CounterAcceptor'accept'params { counter = ctr }
|
|
>>= wait
|
|
r <- traverse
|
|
(\(TestCtrServer var) -> liftIO $ readTVarIO var)
|
|
[ctrA, ctrB, ctrC]
|
|
liftIO $ r `shouldBe` [7, 5, 35]
|
|
)
|
|
|
|
data TestCtrAcceptor = TestCtrAcceptor
|
|
|
|
instance Server IO TestCtrAcceptor
|
|
instance CounterAcceptor'server_ IO TestCtrAcceptor where
|
|
counterAcceptor'accept =
|
|
pureHandler $ \_ CounterAcceptor'accept'params{counter} -> do
|
|
[start] <- map n <$> bumpN counter 1
|
|
r <- bumpN counter 4
|
|
liftIO $ r `shouldBe`
|
|
[ def { n = start + 1 }
|
|
, def { n = start + 2 }
|
|
, def { n = start + 3 }
|
|
, def { n = start + 4 }
|
|
]
|
|
pure def
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Implementations of various interfaces for testing purposes.
|
|
-------------------------------------------------------------------------------
|
|
|
|
newtype TestCtrFactory = TestCtrFactory { sup :: Supervisor }
|
|
|
|
instance Server IO TestCtrFactory
|
|
instance CounterFactory'server_ IO TestCtrFactory where
|
|
counterFactory'newCounter =
|
|
pureHandler $ \TestCtrFactory{sup} CounterFactory'newCounter'params{start} -> do
|
|
ctr <- atomically $ newTestCtr start >>= export_CallSequence sup
|
|
pure CounterFactory'newCounter'results { counter = ctr }
|
|
|
|
newTestCtr :: Word32 -> STM TestCtrServer
|
|
newTestCtr n = TestCtrServer <$> newTVar n
|
|
|
|
newtype TestCtrServer = TestCtrServer (TVar Word32)
|
|
|
|
instance Server IO TestCtrServer
|
|
instance CallSequence'server_ IO TestCtrServer where
|
|
callSequence'getNumber = pureHandler $ \(TestCtrServer tvar) _ -> do
|
|
ret <- liftIO $ atomically $ do
|
|
modifyTVar' tvar (+1)
|
|
readTVar tvar
|
|
pure def { n = ret }
|
|
|
|
-- a 'CallSequence' which always throws an exception.
|
|
data ExnCtrServer = ExnCtrServer
|
|
|
|
instance Server IO ExnCtrServer
|
|
instance CallSequence'server_ IO ExnCtrServer where
|
|
callSequence'getNumber = pureHandler $ \_ _ ->
|
|
throwM def
|
|
{ type_ = Exception'Type'failed
|
|
, reason = "Something went sideways."
|
|
}
|
|
|
|
-- a 'CallSequence' which doesn't implement its methods.
|
|
data NoImplServer = NoImplServer
|
|
|
|
instance Server IO NoImplServer
|
|
instance CallSequence'server_ IO NoImplServer -- TODO: can we silence the warning somehow?
|
|
|
|
-- Server that throws some non-rpc exception.
|
|
data NonRpcExnServer = NonRpcExnServer
|
|
|
|
instance Server IO NonRpcExnServer
|
|
instance CallSequence'server_ IO NonRpcExnServer where
|
|
callSequence'getNumber = pureHandler $ \_ _ -> error "OOPS"
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Tests for unusual patterns of messages .
|
|
--
|
|
-- Some of these will never come up when talking to a correct implementation of
|
|
-- capnproto, and others just won't come up when talking to the Haskell
|
|
-- implementation. Accordingly, these tests start a vat in one thread and
|
|
-- directly manipulate the transport in the other.
|
|
-------------------------------------------------------------------------------
|
|
|
|
unusualTests :: Spec
|
|
unusualTests = describe "Tests for unusual message patterns" $ do
|
|
it "Should raise ReceivedAbort in response to an abort message." $ do
|
|
-- Send an abort message to the remote vat, and verify that
|
|
-- the vat actually aborts.
|
|
let exn = def
|
|
{ type_ = Exception'Type'failed
|
|
, reason = "Testing abort"
|
|
}
|
|
withTransportPair $ \(vatTrans, probeTrans) -> do
|
|
ret <- try $ concurrently_
|
|
(handleConn (vatTrans defaultLimit) def { debugMode = True})
|
|
$ do
|
|
msg <- createPure maxBound $ valueToMsg $ Message'abort exn
|
|
sendMsg (probeTrans defaultLimit) msg
|
|
ret `shouldBe` Left (ReceivedAbort exn)
|
|
triggerAbort (Message'unimplemented $ Message'abort def) $
|
|
"Your vat sent an 'unimplemented' message for an abort message " <>
|
|
"that its remote peer never sent. This is likely a bug in your " <>
|
|
"capnproto library."
|
|
triggerAbort
|
|
(Message'call def
|
|
{ target = MessageTarget'importedCap 443
|
|
}
|
|
)
|
|
"No such export: 443"
|
|
triggerAbort
|
|
(Message'call def
|
|
{ target = MessageTarget'promisedAnswer def { questionId=300 }
|
|
}
|
|
)
|
|
"No such answer: 300"
|
|
triggerAbort
|
|
(Message'return def { answerId = 234 })
|
|
"No such question: 234"
|
|
it "Should respond with an abort if sent junk data" $ do
|
|
let wantAbortExn = def
|
|
{ reason = "Unhandled exception: TraversalLimitError"
|
|
, type_ = Exception'Type'failed
|
|
}
|
|
withTransportPair $ \(vatTrans, probeTrans) ->
|
|
concurrently_
|
|
(do
|
|
Left (e :: RpcError) <- try $
|
|
handleConn (vatTrans defaultLimit) def { debugMode = True }
|
|
e `shouldBe` SentAbort wantAbortExn
|
|
)
|
|
(do
|
|
let bb = mconcat
|
|
[ BB.word32LE 0 -- 1 segment - 1 = 0
|
|
, BB.word32LE 2 -- 2 words in first segment
|
|
-- a pair of structs that point to each other:
|
|
, BB.word64LE (P.serializePtr (Just (P.StructPtr 0 0 1)))
|
|
, BB.word64LE (P.serializePtr (Just (P.StructPtr (-1) 0 1)))
|
|
]
|
|
lbs = BB.toLazyByteString bb
|
|
msg <- lbsToMsg lbs
|
|
sendMsg (probeTrans defaultLimit) msg
|
|
msg' <- recvMsg (probeTrans defaultLimit)
|
|
resp <- msgToValue msg'
|
|
resp `shouldBe` Message'abort wantAbortExn
|
|
)
|
|
it "Should respond with an abort if erroneously sent return = resultsSentElsewhere" $
|
|
|
|
withTransportPair $ \(vatTrans, probeTrans) ->
|
|
let wantExn = eFailed $
|
|
"Received Return.resultsSentElswhere for a call "
|
|
<> "with sendResultsTo = caller."
|
|
in concurrently_
|
|
(do
|
|
Left (e :: RpcError) <- try $
|
|
handleConn (vatTrans defaultLimit) def
|
|
{ debugMode = True
|
|
, withBootstrap = Just $ \_sup client ->
|
|
let ctr :: CallSequence = fromClient client
|
|
in void $ (callSequence'getNumber ctr ? def) >>= wait
|
|
}
|
|
e `shouldBe` SentAbort wantExn
|
|
)
|
|
(do
|
|
let send msg =
|
|
evalLimitT maxBound (valueToMsg msg >>= freeze)
|
|
>>= sendMsg (probeTrans defaultLimit)
|
|
recv = recvMsg (probeTrans defaultLimit) >>= msgToValue
|
|
Message'bootstrap Bootstrap{} <- recv
|
|
Message'call Call{questionId} <- recv
|
|
send $ Message'return def
|
|
{ answerId = questionId
|
|
, union' = Return'resultsSentElsewhere
|
|
}
|
|
msg <- recv
|
|
msg `shouldBe` Message'abort wantExn
|
|
)
|
|
it "Should reply with unimplemented when sent a join (level 4 only)." $
|
|
withTransportPair $ \(vatTrans, probeTrans) ->
|
|
race_
|
|
(handleConn (vatTrans defaultLimit) def { debugMode = True })
|
|
$ do
|
|
msg <- createPure maxBound $ valueToMsg $ Message'join def
|
|
sendMsg (probeTrans defaultLimit) msg
|
|
msg' <- recvMsg (probeTrans defaultLimit) >>= msgToValue
|
|
msg' `shouldBe` Message'unimplemented (Message'join def)
|
|
|
|
|
|
-- | Verify that the given message triggers an abort with the specified 'reason'
|
|
-- field.
|
|
triggerAbort :: Message -> T.Text -> Spec
|
|
triggerAbort msg reason =
|
|
it ("Should abort when sent the message " ++ show msg ++ " on startup") $ do
|
|
let wantAbortExn = def
|
|
{ reason = reason
|
|
, type_ = Exception'Type'failed
|
|
}
|
|
withTransportPair $ \(vatTrans, probeTrans) ->
|
|
concurrently_
|
|
(do
|
|
ret <- try $ handleConn (vatTrans defaultLimit) def { debugMode = True }
|
|
ret `shouldBe` Left (SentAbort wantAbortExn)
|
|
)
|
|
(do
|
|
rawMsg <- createPure maxBound $ valueToMsg msg
|
|
sendMsg (probeTrans defaultLimit) rawMsg
|
|
-- 4 second timeout. The remote vat's timeout before killing the
|
|
-- connection is one second, so if this happens we're never going
|
|
-- to receive the message. In theory this is possible, but if it
|
|
-- happens something is very wrong.
|
|
r <- timeout 4000000 $ recvMsg (probeTrans defaultLimit)
|
|
case r of
|
|
Nothing ->
|
|
error "Test timed out waiting on abort message."
|
|
Just rawResp -> do
|
|
resp <- msgToValue rawResp
|
|
resp `shouldBe` Message'abort wantAbortExn
|
|
)
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Utilties used by the tests.
|
|
-------------------------------------------------------------------------------
|
|
|
|
withSocketPair :: ((Socket.Socket, Socket.Socket) -> IO a) -> IO a
|
|
withSocketPair =
|
|
bracket
|
|
(Socket.socketPair Socket.AF_UNIX Socket.Stream 0)
|
|
(\(x, y) -> Socket.close x >> Socket.close y)
|
|
|
|
withTransportPair ::
|
|
( ( WordCount -> Transport
|
|
, WordCount -> Transport
|
|
) -> IO a
|
|
) -> IO a
|
|
withTransportPair f =
|
|
withSocketPair $ \(x, y) -> f (socketTransport x, socketTransport y)
|
|
|
|
-- | @'runVatPair' server client@ runs a pair of vats connected to one another,
|
|
-- using 'server' as the 'offerBootstrap' field in the one vat's config, and
|
|
-- 'client' as the 'withBootstrap' field in the other's.
|
|
runVatPair :: IsClient c => (Supervisor -> STM c) -> (Supervisor -> c -> IO ()) -> IO ()
|
|
runVatPair getBootstrap withBootstrap = withTransportPair $ \(clientTrans, serverTrans) -> do
|
|
let runClient = handleConn (clientTrans defaultLimit) def
|
|
{ debugMode = True
|
|
, withBootstrap = Just $ \sup -> withBootstrap sup . fromClient
|
|
}
|
|
runServer = handleConn (serverTrans defaultLimit) def
|
|
{ debugMode = True
|
|
, getBootstrap = fmap (Just . toClient) . getBootstrap
|
|
}
|
|
race_ runServer runClient
|
|
|
|
expectException :: Show a => (cap -> IO (Promise a)) -> Exception -> cap -> IO ()
|
|
expectException callFn wantExn cap = do
|
|
ret <- try $ callFn cap >>= wait
|
|
case ret of
|
|
Left (e :: Exception) ->
|
|
liftIO $ e `shouldBe` wantExn
|
|
Right val ->
|
|
error $ "Should have received exn, but got " ++ show val
|