grapesy-1.0.0: test-grapesy/Test/Regression/Issue102.hs
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}
-- | Handling of exceptions occurring in an RPC on a shared connection.
--
-- These tests check for the behavior described in
-- <https://github.com/well-typed/grapesy/issues/102>. In particular, there are
-- two conditions that should hold when an exception occurs in the scope of a
-- call (on either the server or client, e.g. inside either 'mkRpcHandler' or
-- 'withRPC'):
--
-- 1. Other ongoing calls on that connection are not terminated (client), and
-- handlers dealing with other calls on that connection are not terminated
-- (server), and
-- 2. future calls are still possible (client), and more handlers can be started
-- to deal with future calls (server).
module Test.Regression.Issue102 (tests) where
import Control.Concurrent.Async
import Control.Exception
import Control.Monad
import Data.Either
import Data.IORef
import Data.Text qualified as Text
import Data.Word
import Test.Tasty
import Test.Tasty.HUnit
import Network.GRPC.Client qualified as Client
import Network.GRPC.Client.Binary qualified as Binary
import Network.GRPC.Common
import Network.GRPC.Server qualified as Server
import Network.GRPC.Server.Binary qualified as Binary
import Proto.API.Trivial
import Test.Driver.ClientServer
import Test.Util.Exception
tests :: TestTree
tests = testGroup "Issue102" [
testCase "client" test_clientException
, testCase "server" test_serverException
, testCase "earlyTerminationNoWait" test_earlyTerminationNoWait
]
-- | Client makes many concurrent calls, throws an exception during one of them.
test_clientException :: IO ()
test_clientException = testClientServer $ ClientServerTest {
config = def {
isExpectedClientException = isDeliberateException
, isExpectedServerException = isClientDisconnected
}
, client = simpleTestClient $ \conn -> do
-- Make 100 concurrent calls. 99 of them counting to 50, and one
-- more that throws an exception once it reaches 10.
let
predicate = (> 50)
predicates =
replicate 99 predicate ++
[ \n ->
(n > 10)
&& throw (DeliberateException $ SomeClientException 1)
]
results <-
mapConcurrently
( try @DeliberateException
. Client.withRPC conn def (Proxy @Trivial)
. countUntil
)
predicates
-- Only one of the calls failed
assertEqual "" (length $ lefts results) 1
-- All others terminated with results satisfying the predicate
assertBool "" (all predicate $ rights results)
-- New calls still succeed
assertBool "" . predicate
=<< Client.withRPC conn def (Proxy @Trivial) (countUntil predicate)
, server = [
Server.someRpcHandler $
Server.mkRpcHandler @Trivial incUntilFinal
]
}
-- | Client makes many concurrent calls, the handler throws an exception during
-- one of them.
test_serverException :: IO ()
test_serverException = do
handlerCounter <- newIORef @Int 0
testClientServer $ ClientServerTest {
config = def { isExpectedServerException = isDeliberateException }
, client = simpleTestClient $ \conn -> do
-- Make 100 concurrent calls counting to 50.
let predicate = (> 50)
results <-
replicateConcurrently 100 $
try @GrpcException
. Client.withRPC conn def (Proxy @Trivial)
$ countUntil predicate
-- Only one of the calls failed, and we got the appropriate
-- exception
case lefts results of
[GrpcException GrpcUnknown (Just msg) Nothing []] -> do
assertBool "" $ "DeliberateException" `Text.isInfixOf` msg
assertBool "" $ "SomeServerException 1" `Text.isInfixOf` msg
_ ->
assertFailure ""
-- All others terminated with results satisfying the predicate
assertBool "" (all predicate $ rights results)
-- New calls still succeed
assertBool "" . predicate
=<< Client.withRPC conn def (Proxy @Trivial) (countUntil predicate)
, server = [
Server.someRpcHandler $
Server.mkRpcHandler @Trivial $ \call -> do
handlerCount <-
atomicModifyIORef' handlerCounter (\n -> (n + 1, n))
when (handlerCount == 25) $
throwIO $
DeliberateException $ SomeServerException 1
incUntilFinal call
]
}
-- | This is essentially 'Test.Prop.Dialogue.earlyTermination15', but the server
-- does not wait for client termination.
test_earlyTerminationNoWait :: IO ()
test_earlyTerminationNoWait = testClientServer $ ClientServerTest {
config = def {
isExpectedClientException = isDeliberateException
, isExpectedServerException = isClientDisconnected
}
, client = simpleTestClient $ \conn -> do
_mResult <-
try @DeliberateException $
Client.withRPC conn def (Proxy @Trivial) $ \_call ->
throwIO (DeliberateException $ SomeServerException 0)
result <- Client.withRPC conn def (Proxy @Trivial) $ \call -> do
Binary.sendFinalInput @Word8 call 0
Binary.recvFinalOutput @Word8 call
assertEqual "" (1, NoMetadata) result
, server = [
Server.someRpcHandler $
Server.mkRpcHandler @Trivial $ \call ->
Binary.recvInput @Word8 call >>= \case
_ -> Binary.sendFinalOutput @Word8 call (1, NoMetadata)
]
}
{-------------------------------------------------------------------------------
Client and handler functions
-------------------------------------------------------------------------------}
-- | Send numbers to the server until it responds with one that satisfies the
-- given predicate.
countUntil :: (Word64 -> Bool) -> Client.Call Trivial -> IO Word64
countUntil = go 0
where
go :: Word64 -> (Word64 -> Bool) -> Client.Call Trivial -> IO Word64
go next p call
| p next
= do
Binary.sendFinalInput @Word64 call next
(_final, NoMetadata) <- Binary.recvFinalOutput @Word64 call
return next
| otherwise
= do
Binary.sendNextInput @Word64 call next
next' <- Binary.recvNextOutput @Word64 call
go next' p call
-- | Reads numbers from the client and sends them back incremented by one.
incUntilFinal :: Server.Call Trivial -> IO ()
incUntilFinal call = do
Binary.recvInput call >>= \case
StreamElem n -> do
Binary.sendNextOutput @Word64 call $ succ n
incUntilFinal call
FinalElem n _ -> do
Binary.sendFinalOutput @Word64 call (succ n, NoMetadata)
NoMoreElems _ -> do
-- This is never hit, since our client never sends 'NoMoreElems'.
error "Test.Sanity.Exception.incUntilFinal: unexpected NoMoreElems"