packages feed

grapesy-1.0.0: interop/Interop/Client/Common.hs

-- | Functionality required by many test cases
module Interop.Client.Common (
    -- * Setup
    enableInitCompression
    -- * Construct server inputs
  , mkSimpleRequest
  , mkStreamingInputCallRequest
  , mkStreamingOutputCallRequest
    -- * Verify server outputs
  , verifySimpleResponse
  , verifyStreamingOutputCallResponse
  , verifyStreamingOutputs
  , expectInvalidArgument
  ) where


import Data.ByteString qualified as BS.Strict

import Network.GRPC.Client
import Network.GRPC.Common
import Network.GRPC.Common.Compression qualified as Compr
import Network.GRPC.Common.Protobuf

import Interop.Util.Exceptions
import Interop.Util.Messages

import Proto.API.Interop

{-------------------------------------------------------------------------------
  Config
-------------------------------------------------------------------------------}

-- | Enable compression of the first message
--
-- Some test specifications (@client_compressed_unary@,
-- @client_compressed_streaming@) tell us to
--
-- 1. First send a request that causes the server to send an error response
--    (which will use the gRPC Trailers-Only case)
-- 2. Then send a compressed message
-- 3. Finally, send an uncompressed message
--
-- This is problematic: we don't know which compression algorithms the server
-- supports until we have had a successful response from the server. We could
-- fix this by swapping (2) and (3), but this would be deviating from the test
-- specification. Instead, we override the initial compression to use, and
-- report the test as skipped if the server comes back with 'GrpcUnimplemented'.
enableInitCompression :: ConnParams
enableInitCompression = def {
      connInitCompression = Just Compr.gzip
    }

{-------------------------------------------------------------------------------
  Construct server inputs
-------------------------------------------------------------------------------}

mkSimpleRequest ::
     Bool  -- ^ Should the server expect the message to be compressed?
  -> Proto SimpleRequest
mkSimpleRequest expectCompressed =
    defMessage
      & #expectCompressed .~ boolValue expectCompressed
      & #responseSize     .~ 314159
      & #payload          .~ payloadOfZeroes 271828

mkStreamingInputCallRequest ::
     Bool  -- ^ Should the server expect the message to be compressed?
  -> Int   -- ^ Payload size
  -> Proto StreamingInputCallRequest
mkStreamingInputCallRequest expectCompressed size =
    defMessage
      & #expectCompressed .~ boolValue expectCompressed
      & #payload          .~ payloadOfZeroes size

mkStreamingOutputCallRequest ::
     [(Bool, Int)] -- ^ Size and compression of each expected response
  -> Maybe Int     -- ^ Size of the payload, if any
  -> Proto StreamingOutputCallRequest
mkStreamingOutputCallRequest expectedSizes payload =
    defMessage
      & #responseParameters .~ [
             defMessage
               & #size       .~ fromIntegral sz
               & #compressed .~ boolValue compressed
           | (compressed, sz) <- expectedSizes
           ]
      & ( case payload of
            Nothing -> id
            Just sz -> #payload .~ payloadOfZeroes sz
        )

{-------------------------------------------------------------------------------
  Verify server outputs
-------------------------------------------------------------------------------}

verifySimpleResponse :: HasCallStack => Proto SimpleResponse -> IO ()
verifySimpleResponse resp = do
    assertEqual 314159 $ BS.Strict.length     (resp ^. #payload . #body)
    assertBool         $ BS.Strict.all (== 0) (resp ^. #payload . #body)

verifyStreamingOutputCallResponse ::
     Int -- ^ Expected size
  -> Proto StreamingOutputCallResponse -> IO ()
verifyStreamingOutputCallResponse expectedSize resp = do
    assertEqual expectedSize $ BS.Strict.length     (resp ^. #payload . #body)
    assertBool               $ BS.Strict.all (== 0) (resp ^. #payload . #body)

verifyStreamingOutputs :: forall rpc.
     HasCallStack
  => Call rpc
  -> (ProperTrailers' -> IO ())            -- ^ Verify trailers
  -> [(InboundMeta, Output rpc) -> IO ()]  -- ^ Verifier per expected output
  -> IO ()
verifyStreamingOutputs call verifyTrailers = go
  where
    go :: [(InboundMeta, Output rpc) -> IO ()] -> IO ()
    go verifiers = do
        mResp <- recvOutputWithMeta call
        case (mResp, verifiers) of
          (NoMoreElems trailers, [])     -> verifyTrailers trailers
          (StreamElem{}, [])             -> assertFailure "Too many outputs"
          (FinalElem{}, [])              -> assertFailure "Too many outputs"
          (NoMoreElems{}, _:_)           -> assertFailure "Not enough outputs"
          (FinalElem{}, _:_:_)           -> assertFailure "Not enough outputs"
          (StreamElem resp, v:vs)        -> v resp >> go vs
          (FinalElem resp trailers, [v]) -> v resp >> verifyTrailers trailers

-- | Expect the server to respond with 'InvalidArgument'
--
-- Throws 'TestSkipped' if the server responds with normal response instead
-- (indicating that the server was unable to perform the required check on
-- the inputs we sent it).
--
-- Also throws 'TestSkipped' is the server returns with 'GrpcUnimplemented'.
expectInvalidArgument :: IO a -> IO ()
expectInvalidArgument = assertThrows $ \exception ->
    case grpcError exception of
      GrpcInvalidArgument ->
        return ()
      GrpcUnimplemented ->
        throwIO $ TestSkipped "Server sent UNIMPLEMENTED"
      err ->
        assertFailure $ "Unexpected failure " ++ show err