packages feed

utxorpc-server-0.0.4.1: test/logged_test.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}

module Main (main) where

import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Writer.CPS (WriterT, runWriterT, tell)
import Data.List (foldl')
import Data.ProtoLens.Message (Message, defMessage)
import Data.UUID (UUID)
import Data.UUID.V4 (nextRandom)
import Network.GRPC.HTTP2.ProtoLens (RPC (RPC))
import Network.GRPC.Server (ServerStream (..), ServerStreamHandler, UnaryHandler)
import Proto.Utxorpc.V1alpha.Sync.Sync
import System.Directory (removeFile)
import System.IO (Handle, IOMode (..), hPutStr, readFile', withFile)
import Test.Hspec
import Utxorpc.Logged
  ( ReplyLogger,
    RequestLogger,
    ServerStreamEndLogger,
    ServerStreamLogger,
    UtxorpcServiceLogger (..),
    loggedSStreamHandler,
    loggedUnaryHandler,
  )

logFilePath :: String
logFilePath = "test/test-logs.txt"

mockRequestLogger :: (String -> IO ()) -> RequestLogger (WriterT String IO)
mockRequestLogger logF _ _ uuid msg = do
  lift . logF $ requestOut uuid msg
  tell $ requestOut uuid msg

requestOut :: (Show a) => UUID -> a -> String
requestOut uuid msg = "REQUEST_LOGGER: " ++ "[" ++ show uuid ++ "]" ++ show msg ++ "\n"

mockReplyLogger :: (String -> IO ()) -> ReplyLogger (WriterT String IO)
mockReplyLogger logF _ _ uuid msg = do
  lift . logF $ replyOut uuid msg
  tell $ replyOut uuid msg

replyOut :: (Show a) => UUID -> a -> String
replyOut uuid msg = "REPLY_LOGGER: [" ++ show uuid ++ "] " ++ show msg ++ "\n"

mockServerStreamLogger :: (String -> IO ()) -> ServerStreamLogger (WriterT String IO)
mockServerStreamLogger logF _ _ (uuid, index) _ = do
  lift . logF $ serverStreamOut (uuid, index)
  tell $ serverStreamOut (uuid, index)

serverStreamOut :: (UUID, Int) -> String
serverStreamOut (uuid, int) = "SERVER_STREAM_LOGGER: " ++ show (uuid, int) ++ "\n"

mockServerStreamEndLogger :: (String -> IO ()) -> ServerStreamEndLogger (WriterT String IO)
mockServerStreamEndLogger logF _ _ (uuid, index) = do
  lift . logF $ serverStreamEndOut (uuid, index)
  tell $ serverStreamEndOut (uuid, index)

serverStreamEndOut :: (UUID, Int) -> String
serverStreamEndOut (uuid, index) = "SERVER_STREAM_END_LOGGER: " ++ show (uuid, index) ++ "\n"

mockLogger :: (String -> IO ()) -> UtxorpcServiceLogger (WriterT String IO)
mockLogger logF =
  UtxorpcServiceLogger
    (mockRequestLogger logF)
    (mockReplyLogger logF)
    (mockServerStreamLogger logF)
    (mockServerStreamEndLogger logF)

mockUnaryHandler :: (Message o) => (String -> IO ()) -> UnaryHandler (WriterT String IO) i o
mockUnaryHandler logF _ _ = do
  lift $ logF unaryHandlerOut
  tell unaryHandlerOut
  return defMessage

unaryHandlerOut :: String
unaryHandlerOut = "UNARY_HANDLER\n"

unaryRpc :: RPC SyncService "fetchBlock"
unaryRpc = RPC

unaryReq :: FetchBlockRequest
unaryReq = defMessage

type UnaryReply = FetchBlockResponse

mockSStreamHandler :: (Message o) => (String -> IO ()) -> ServerStreamHandler (WriterT String IO) i o Int
mockSStreamHandler logF _ _ = do
  lift $ logF generateStreamOut
  tell generateStreamOut
  return (0, stream)
  where
    stream = ServerStream $ \index ->
      if index > 3
        then do
          lift $ logF $ streamHandlerEndOut index
          tell $ streamHandlerEndOut index
          return Nothing
        else do
          lift $ logF $ streamHandlerOut index
          tell $ streamHandlerOut index
          return $ Just (index + 1, defMessage)

generateStreamOut :: String
generateStreamOut = "GENERATE_STREAM\n"

streamHandlerOut :: Int -> String
streamHandlerOut index = "STREAM_HANDLER " ++ show index ++ "\n"

streamHandlerEndOut :: Int -> String
streamHandlerEndOut index = "STREAM_HANDLER_END " ++ show index ++ "\n"

sStreamRpc :: RPC SyncService "followTip"
sStreamRpc = RPC

type SStreamReq = FollowTipRequest

sStreamReq :: SStreamReq
sStreamReq = defMessage

type SStreamReply = FollowTipResponse

testUnary :: IO ()
testUnary = do
  uuid <- nextRandom
  (reply, _mState) <- withFile logFilePath WriteMode (runWriterT . handleReq)
  out <- readFile' logFilePath

  (loggedReply, loggedMState) <- withFile logFilePath WriteMode (runWriterT . handleReqLogged uuid)
  loggedOut <- readFile' logFilePath

  hspec $ describe "Unary Logging" $ do
    it "Logged reply should equal unlogged reply" $ do
      reply `shouldBe` loggedReply
    -- We know that the IO effects are accumulating, so if the WriterT state equals the IO effect,
    -- we know the monadic state was passed from one logger/handler to the next
    it "Monad state should accumulate" $ do
      loggedOut `shouldBe` loggedMState
    it "Execution should occur in the right order" $ do
      targetLoggedOut uuid out reply `shouldBe` loggedOut

  removeFile logFilePath
  where
    handleReq :: Handle -> WriterT String IO UnaryReply
    handleReq fileHandle = mockUnaryHandler (hPutStr fileHandle) undefined unaryReq

    handleReqLogged :: UUID -> Handle -> WriterT String IO UnaryReply
    handleReqLogged uuid fileHandle = loggedHandler uuid (hPutStr fileHandle) undefined unaryReq
    loggedHandler uuid logF = loggedUnaryHandler unaryRpc (mockUnaryHandler logF) uuid (mockLogger logF)

    targetLoggedOut uuid unloggedOut reply =
      requestOut uuid unaryReq ++ unloggedOut ++ replyOut uuid reply

testSStream :: IO ()
testSStream = do
  -- Run unlogged
  let withLogFile = withFile logFilePath WriteMode
  ((_streamStates, msgs), _monadState) <-
    withLogFile $ runWriterT . run . handlerWith
  _out <- readFile' logFilePath

  -- Run logged
  uuid <- nextRandom
  ((loggedStreamStates, loggedMsgs), loggedMonadState) <-
    withLogFile $ runWriterT . run . loggedHandlerWith uuid
  loggedOut <- readFile' logFilePath

  hspec $ describe "Stream Logging" $ do
    it "Logged messages should equal unlogged messages" $ do
      msgs `shouldBe` loggedMsgs
    -- We know that the IO effects are accumulating, so if the WriterT state equals the IO effect,
    -- we know the monadic state was passed from one logger/handler to the next
    it "Monad state should accumulate" $ do
      loggedOut `shouldBe` loggedMonadState
    it "Execution should occur in the right order" $ do
      targetLoggedOut uuid loggedStreamStates `shouldBe` loggedOut

  removeFile logFilePath
  where
    handlerWith ::
      Handle ->
      ServerStreamHandler (WriterT String IO) SStreamReq SStreamReply Int
    handlerWith = mockSStreamHandler . hPutStr

    loggedHandlerWith ::
      UUID ->
      Handle ->
      ServerStreamHandler (WriterT String IO) SStreamReq SStreamReply (Int, Int)
    loggedHandlerWith uuid h =
      loggedSStreamHandler
        sStreamRpc
        (mockSStreamHandler $ hPutStr h)
        uuid
        (mockLogger $ hPutStr h)

    run :: (Monad m) => ServerStreamHandler m SStreamReq SStreamReply a -> m ([a], [SStreamReply])
    run handler = do
      (initState, ServerStream {serverStreamNext}) <- handler undefined sStreamReq
      runStream serverStreamNext initState
      where
        runStream stream state = do
          next <- stream state
          case next of
            Just (state', msg) -> do
              (states, msgs) <- runStream stream state'
              return (state : states, msg : msgs)
            Nothing ->
              return ([], [])

    targetLoggedOut uuid loggedStreamStates =
      requestOut uuid unaryReq -- log request
        ++ generateStreamOut -- generate stream
        -- Output for all stream handling and logs
        ++ foldl' (\out streamState -> out ++ targetOut streamState) "" loggedStreamStates
        -- end of stream handler
        ++ streamHandlerEndOut (length loggedStreamStates)
        -- end of stream logger
        ++ serverStreamEndOut (uuid, length loggedStreamStates)
      where
        -- output for a single stream message
        targetOut (index, _) =
          streamHandlerOut index
            ++ serverStreamOut (uuid, index)

main :: IO ()
main = do
  testUnary
  testSStream