packages feed

network-run-0.6.0: test/UDPSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module UDPSpec (spec) where

import Control.Concurrent
import qualified Control.Exception as E
import Control.Monad
import Data.ByteString (ByteString)
import Data.IORef
import qualified Data.List.NonEmpty as NE
import Data.Maybe (isJust)
import Network.Socket
import Network.Socket.ByteString
import System.Timeout (timeout)
import Test.Hspec

import Network.Run.UDP

import Helper

spec :: Spec
spec = do
    describe "runUDPServer" $
        it "serves a datagram" $
            limited $ do
                port <- freeUDPPort
                let server sock = forever $ do
                        (bs, peer) <- recvFrom sock 2048
                        void $ sendTo sock bs peer
                withServerThread (runUDPServer (Just loopback) port server) $
                    udpRequest loopback port "hello" `shouldReturn` Just "hello"

    describe "runUDPServerFork" $ do
        it "serves a datagram" $
            limited $
                withUDPServerFork defaultServerSettings [loopback] echoDatagram $ \port ->
                    udpRequest loopback port "hello" `shouldReturn` Just "hello"

        it "returns at once when no host is given" $ limited $ do
            r <- timeout 1000000 $ runUDPServerFork [] "0" $ \_ _ -> return ()
            r `shouldBe` Just ()

        it "keeps serving after a handler throws" $ limited $ do
            (set, getReports) <- collecting
            ref <- newIORef (0 :: Int)
            let server sock bs = do
                    n <- atomicModifyIORef' ref $ \n -> (n + 1, n)
                    if n == 0
                        then ioError $ userError "boom"
                        else sendAll sock bs
            withUDPServerFork set [loopback] server $ \port -> do
                udpRequest loopback port "hello" `shouldReturn` Just "hello"
                reports <- waitFor 1 getReports
                case reports of
                    [] -> expectationFailure "the failure was not reported"
                    (mpeer, _) : _ -> mpeer `shouldSatisfy` isJust

        it "serves every given host" $ limited $ do
            ok <- hasIPv6
            if not ok
                then pendingWith "no IPv6 on this machine"
                else withUDPServerFork
                    defaultServerSettings
                    [loopback, "::1"]
                    echoDatagram
                    $ \port -> do
                        udpRequest loopback port "v4" `shouldReturn` Just "v4"
                        udpRequest "::1" port "v6" `shouldReturn` Just "v6"

        it "does not leak sockets" $
            limited $
                withUDPServerFork defaultServerSettings [loopback] echoDatagram $ \port -> do
                    -- Warming up first, so that one-off descriptors of the
                    -- runtime are not counted.
                    replicateM_ 5 $ udpRequest loopback port "warm"
                    threadDelay 200000
                    mbefore <- openFds
                    replicateM_ 40 $ udpRequest loopback port "hello"
                    threadDelay 300000
                    mafter <- openFds
                    case (mbefore, mafter) of
                        (Just n0, Just n1) ->
                            n1 - n0 `shouldSatisfy` (< 10)
                        _ -> pendingWith "file descriptors are not observable here"

    describe "runUDPClient" $
        it "closes the socket when the action throws" $
            limited $ do
                port <- freeUDPPort
                ref <- newIORef Nothing
                let action = runUDPClient loopback port $ \sock _ -> do
                        writeIORef ref $ Just sock
                        E.throwIO $ userError "boom"
                (action :: IO ()) `shouldThrow` anyIOException
                msock <- readIORef ref
                case msock of
                    Nothing -> expectationFailure "the socket was never opened"
                    Just sock -> do
                        r <- E.try $ getSocketOption sock ReuseAddr
                        case r :: Either E.IOException Int of
                            Left _ -> return ()
                            Right _ -> expectationFailure "the client socket is still open"

----------------------------------------------------------------

echoDatagram :: Socket -> ByteString -> IO ()
echoDatagram sock bs = sendAll sock bs

-- | A UDP port which is free at the time of the call.  Unlike a TCP
-- server, a UDP server here binds the port itself, so it cannot be
-- discovered afterwards.
freeUDPPort :: IO ServiceName
freeUDPPort = do
    addr <- resolve Datagram (Just loopback) "0" [AI_PASSIVE] NE.head
    E.bracket (openServerSocket addr) close $ \sock ->
        show . portOf <$> getSocketName sock

withUDPServerFork
    :: ServerSettings
    -> [HostName]
    -> (Socket -> ByteString -> IO ())
    -> (ServiceName -> IO a)
    -> IO a
withUDPServerFork set hosts server body = do
    port <- freeUDPPort
    withServerThread (runUDPServerForkWithSettings set hosts port server) $ do
        threadDelay 200000
        body port

-- | Sending a datagram until a reply comes back.  UDP may drop it, and
-- the server may not have bound its port yet.
udpRequest :: HostName -> ServiceName -> ByteString -> IO (Maybe ByteString)
udpRequest host port bs = runUDPClient host port $ \sock server -> go (20 :: Int) sock server
  where
    go 0 _ _ = return Nothing
    go n sock server = do
        void $ sendTo sock bs server
        mr <- timeout 200000 $ fst <$> recvFrom sock 2048
        case mr of
            Just r -> return $ Just r
            Nothing -> go (n - 1) sock server