packages feed

grapesy-1.0.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(..)
    -- * Exceptions
    -- ** User exceptions
  , SomeClientException(..)
  , SomeServerException(..)
  , ExceptionId
    -- * Utility
  , hasEarlyTermination
  ) where

import Control.Monad.State (StateT, execStateT, modify)
import Data.Bifunctor
import Data.ByteString qualified as Strict (ByteString)

import Network.GRPC.Common

import Test.Driver.Dialogue.TestClock qualified as TestClock
import Test.Util.Exception
import Control.Monad.Catch
import GHC.Show (appPrec1, showCommaSpace)

{-------------------------------------------------------------------------------
  Single RPC
-------------------------------------------------------------------------------}

data LocalStep =
    ClientAction ClientAction
  | ServerAction ServerAction
  deriving stock (Show, Eq)

type ClientAction = Action (TestMetadata, RPC) NoMetadata   SomeClientException
type ServerAction = Action TestMetadata        TestMetadata SomeServerException

data Action a b e =
    -- | 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 e)
  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)

{-------------------------------------------------------------------------------
  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)