marionette-1.1.0: test/ClientSpec.hs
module Main where
import Data.Aeson qualified as Aeson
import Data.Binary qualified as Binary
import Network.Simple.TCP
( HostPreference (Host)
, Socket
, accept
, closeSock
, listen
, sendLazy
)
import Network.Socket (PortNumber, socketPort)
import Test.Hspec (describe, hspec, it)
import Test.Hspec.Expectations.Lifted (shouldReturn, shouldSatisfy)
import Test.Marionette.Class (sendCommand)
import Test.Marionette.Client
( MarionetteTimeout (..)
, SocketClosed (..)
, incoming
, runMarionetteTWith
)
import Test.Marionette.Protocol
import UnliftIO (TQueue, async, cancel, finally, timeout, try)
import UnliftIO.STM (atomically, readTQueue)
import Prelude
withServer :: (Socket -> TQueue MarionetteMessage -> IO ()) -> (PortNumber -> IO a) -> IO a
withServer serve body =
listen (Host "127.0.0.1") "0" \(listening, _) -> do
port <- socketPort listening
handling <-
async $ accept listening \(socket, _) ->
serve socket =<< incoming socket \_ -> pure ()
body port `finally` cancel handling
send :: (Aeson.ToJSON a) => Socket -> a -> IO ()
send socket = sendLazy socket . Binary.encode . MarionetteMessage . Aeson.encode
recv :: TQueue MarionetteMessage -> IO (Message Command)
recv frames = do
MarionetteMessage lbs <- atomically $ readTQueue frames
either fail pure $ Aeson.eitherDecode lbs
greeting :: Greeting
greeting = Greeting{applicationType = "fake", marionetteProtocol = 3}
reply :: Socket -> Message Command -> Result -> IO ()
reply socket Message{messageId} result =
send socket Message{messageId, messageContent = result}
testCommandTimeout :: Int
testCommandTimeout = 200_000
main :: IO ()
main = hspec do
describe "incoming" do
it "reports SocketClosed when the server hangs up" $
withServer
( \socket _frames -> do
send socket greeting
closeSock socket
)
\port -> do
outcome <-
timeout 2_000_000 . try $
runMarionetteTWith
"127.0.0.1"
port
testCommandTimeout
(sendCommand @_ @Aeson.Value "noop")
outcome `shouldSatisfy` \case
Just (Left SocketClosed) -> True
_ -> False
describe "sendCommand" do
it "throws MarionetteTimeout without killing the session" $
withServer
( \socket frames -> do
send socket greeting
_silent <- recv frames
echoed <- recv frames
reply socket echoed $ Right (Aeson.String "ok")
)
\port ->
runMarionetteTWith "127.0.0.1" port testCommandTimeout do
silent <- try $ sendCommand @_ @Aeson.Value "silent"
silent `shouldSatisfy` \case
Left MarionetteTimeout{} -> True
_ -> False
sendCommand "echo" `shouldReturn` Aeson.String "ok"
it "reports connection loss promptly, waking a command already in flight" $
withServer
( \socket frames -> do
send socket greeting
_abandoned <- recv frames
closeSock socket
)
\port -> do
outcome <-
timeout 2_000_000 . try $
runMarionetteTWith
"127.0.0.1"
port
testCommandTimeout
(sendCommand @_ @Aeson.Value "abandoned")
outcome `shouldSatisfy` \case
Just (Left SocketClosed) -> True
_ -> False
it "surfaces a server-reported Error rather than a transport failure" $
withServer
( \socket frames -> do
send socket greeting
command <- recv frames
reply socket command $
Left Error{error = "no such element", message = "nope", stacktrace = ""}
)
\port -> do
outcome <-
try $
runMarionetteTWith
"127.0.0.1"
port
testCommandTimeout
(sendCommand @_ @Aeson.Value "find")
outcome `shouldSatisfy` \case
Left Error{message = "nope"} -> True
_ -> False
it "discards a reply that arrives after its command already timed out" $
withServer
( \socket frames -> do
send socket greeting
slow <- recv frames
echoed <- recv frames -- only received once the client gives up on "slow"
reply socket slow $ Right (Aeson.String "too late")
reply socket echoed $ Right (Aeson.String "ok")
)
\port ->
runMarionetteTWith "127.0.0.1" port testCommandTimeout do
slow <- try $ sendCommand @_ @Aeson.Value "slow"
slow `shouldSatisfy` \case
Left MarionetteTimeout{} -> True
_ -> False
sendCommand "echo" `shouldReturn` Aeson.String "ok"