grapesy-1.2.0: test-grapesy/Test/Driver/Dialogue/Definition.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Driver.Dialogue.Definition (
-- * Local
LocalStep(..)
, Action(..)
, ClientAction
, ServerAction
, RPC(..)
, TestMetadata(..)
-- * Bird's-eye view
, GlobalSteps(..)
, LocalSteps(..)
-- * Utility
, hasEarlyTermination
) where
import Control.Monad.Catch (MonadThrow)
import Control.Monad.State (StateT, execStateT, modify)
import Data.Bifunctor
import Data.ByteString qualified as Strict (ByteString)
import GHC.Show (appPrec1, showCommaSpace)
import Network.GRPC.Common
import Network.GRPC.Common.Exception
import Test.Driver.ClientServer (DeliberateException)
import Test.Driver.Dialogue.TestClock qualified as TestClock
{-------------------------------------------------------------------------------
Single RPC
-------------------------------------------------------------------------------}
data LocalStep =
ClientAction ClientAction
| ServerAction ServerAction
deriving stock (Show, Eq)
type ClientAction = Action (TestMetadata, RPC) NoMetadata
type ServerAction = Action TestMetadata TestMetadata
data Action a b =
-- | Initiate request and response
--
-- When the client initiates a request, they can specify a timeout, initial
-- metadata for the request, as well as which endpoint to connect to. This
-- must happen before anything else.
--
-- On the server side an explicit 'Initiate' is not required; if not
-- present, there will be an implicit one, with empty metadata, on the first
-- 'Send'.
Initiate a
-- | Send a message to the peer
| Send (StreamElem b Int)
-- | Early termination (cleanly or with an exception)
| Terminate (Maybe DeliberateException)
deriving stock (Show, Eq)
data RPC = RPC1 | RPC2 | RPC3
deriving stock (Show, Eq)
{-------------------------------------------------------------------------------
Metadata
-------------------------------------------------------------------------------}
data TestMetadata = TestMetadata {
metadataAsc1 :: Maybe Strict.ByteString
, metadataAsc2 :: Maybe Strict.ByteString
, metadataBin3 :: Maybe Strict.ByteString
, metadataBin4 :: Maybe Strict.ByteString
}
deriving (Eq)
-- | Hand-written 'Show' instance which shows @def :: TestMetadata@ as @def@
--
-- This is by far the most common value that shows up in test failures, so this
-- improves readability.
instance Show TestMetadata where
showsPrec _ (TestMetadata Nothing Nothing Nothing Nothing) = showString "def"
showsPrec p (TestMetadata asc1 asc2 bin3 bin4) = showParen (p >= appPrec1) $
showString "TestMetadata {"
. showString "metadataAsc1 = "
. showVal asc1
. showCommaSpace
. showString "metadataAsc2 = "
. showVal asc2
. showCommaSpace
. showString "metadataBin3 = "
. showVal bin3
. showCommaSpace
. showString "metadataBin4 = "
. showVal bin4
. showString "}"
where
showVal Nothing = showString "def"
showVal (Just x) = showsPrec 0 (Just x)
instance Default TestMetadata where
def = TestMetadata {
metadataAsc1 = Nothing
, metadataAsc2 = Nothing
, metadataBin3 = Nothing
, metadataBin4 = Nothing
}
instance BuildMetadata TestMetadata where
buildMetadata md = concat [
[ CustomMetadata "md1" x | Just x <- [metadataAsc1 md]]
, [ CustomMetadata "md2" x | Just x <- [metadataAsc2 md]]
, [ CustomMetadata "md3-bin" x | Just x <- [metadataBin3 md]]
, [ CustomMetadata "md4-bin" x | Just x <- [metadataBin4 md]]
]
instance ParseMetadata TestMetadata where
parseMetadata = flip execStateT def . mapM go
where
go :: MonadThrow m => CustomMetadata -> StateT TestMetadata m ()
go md
| customMetadataName md == "md1"
= modify $ \x -> x{metadataAsc1 = Just $ customMetadataValue md}
| customMetadataName md == "md2"
= modify $ \x -> x{metadataAsc2 = Just $ customMetadataValue md}
| customMetadataName md == "md3-bin"
= modify $ \x -> x{metadataBin3 = Just $ customMetadataValue md}
| customMetadataName md == "md4-bin"
= modify $ \x -> x{metadataBin4 = Just $ customMetadataValue md}
| otherwise
= throwM $ UnexpectedMetadata [md]
instance StaticMetadata TestMetadata where
metadataHeaderNames _ = ["md1", "md2", "md3-bin", "md4-bin"]
{-------------------------------------------------------------------------------
Many RPCs (bird's-eye view)
-------------------------------------------------------------------------------}
newtype LocalSteps = LocalSteps {
getLocalSteps :: [(TestClock.Tick, LocalStep)]
}
deriving stock (Show)
newtype GlobalSteps = GlobalSteps {
getGlobalSteps :: [LocalSteps]
}
deriving stock (Show)
deriving ToExceptionDoc via LinesToExceptionDoc GlobalSteps
{-------------------------------------------------------------------------------
Utility
-------------------------------------------------------------------------------}
-- | Check if the client or server terminate early
hasEarlyTermination :: GlobalSteps -> (Bool, Bool)
hasEarlyTermination =
bimap or or
. unzip
. map (isEarlyTermination . snd)
. concatMap getLocalSteps
. getGlobalSteps
where
isEarlyTermination :: LocalStep -> (Bool, Bool)
isEarlyTermination (ClientAction (Terminate _)) = (True, False)
isEarlyTermination (ServerAction (Terminate _)) = (False, True)
isEarlyTermination _ = (False, False)