network-run-0.6.0: test/CoreSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | White box tests for the internals of "Network.Run.Core".
--
-- The transient 'accept' errors and the closer failures handled there
-- cannot be provoked through a real socket, so they are injected.
module CoreSpec (spec) where
import Control.Concurrent
import qualified Control.Exception as E
import Control.Monad (void)
import Data.IORef
import Foreign.C.Error (Errno (..), eCONNABORTED)
import GHC.IO.Exception (
IOErrorType (Interrupted, OtherError),
IOException (..),
)
import Network.Socket
import System.IO.Error (fullErrorType, illegalOperationErrorType, mkIOError)
import System.Timeout (timeout)
import Test.Hspec
import Network.Run.Core
import Helper
spec :: Spec
spec = do
describe "safeAcceptWith" $ do
it "retries after running out of file descriptors" $ limited $ do
(set0, getReports) <- collecting
let set = set0{settingsAcceptRetryDelay = 50000}
withFakeAccept [emfile, emfile] $ \(accept', count) -> do
((_, peer), ms) <- elapsed $ safeAcceptWith set accept'
peer `shouldBe` fakePeer
count `shouldReturn` 3
-- Two retries of 50ms each.
ms `shouldSatisfy` (>= 90)
reports <- getReports
map fst reports `shouldBe` [Nothing, Nothing]
it "retries EINTR at once, without reporting it" $ limited $ do
(set0, getReports) <- collecting
-- A delay which would be obvious if it were taken.
let set = set0{settingsAcceptRetryDelay = 5000000}
withFakeAccept [eintr, eintr] $ \(accept', count) -> do
(_, ms) <- elapsed $ safeAcceptWith set accept'
count `shouldReturn` 3
ms `shouldSatisfy` (< 1000)
getReports `shouldReturn` []
it "retries ECONNABORTED at once, without reporting it" $ limited $ do
(set0, getReports) <- collecting
let set = set0{settingsAcceptRetryDelay = 5000000}
withFakeAccept [aborted] $ \(accept', count) -> do
(_, ms) <- elapsed $ safeAcceptWith set accept'
count `shouldReturn` 2
ms `shouldSatisfy` (< 1000)
getReports `shouldReturn` []
it "rethrows an error of the listening socket" $ limited $ do
(set, getReports) <- collecting
withFakeAccept [bad] $ \(accept', count) -> do
safeAcceptWith set accept'
`shouldThrow` (\e -> ioeGetErrorType' e == ioeGetErrorType' bad)
count `shouldReturn` 1
getReports `shouldReturn` []
it "is still killable while waiting to retry" $ limited $ do
let set = defaultServerSettings{settingsAcceptRetryDelay = 5000000}
withFakeAccept (repeat emfile) $ \(accept', _) -> do
done <- newEmptyMVar
tid <- forkFinally (void $ safeAcceptWith set accept') (putMVar done)
threadDelay 100000
killThread tid
r <- timeout 1000000 $ takeMVar done
case r of
Just (Left _) -> return ()
Just (Right _) -> expectationFailure "accept returned"
Nothing -> expectationFailure "the retry was not interruptible"
describe "report" $ do
it "swallows a synchronous exception of the hook" $ do
let set =
defaultServerSettings
{ settingsOnException = \_ _ -> E.throwIO $ userError "hook"
}
report set Nothing (E.toException $ userError "boom")
`shouldReturn` ()
it "rethrows an asynchronous exception of the hook" $ do
let set =
defaultServerSettings
{ settingsOnException = \_ _ -> E.throwIO E.ThreadKilled
}
report set Nothing (E.toException $ userError "boom")
`shouldThrow` (== E.ThreadKilled)
describe "forkWith" $ do
it "reports an exception which escapes the action" $ limited $ do
(set, getReports) <- collecting
withDummySocket $ \sock -> do
closed <- newEmptyMVar
forkWith set (\_ -> putMVar closed ()) sock fakePeer $
E.throwIO $
userError "boom"
takeMVar closed `shouldReturn` ()
reports <- waitFor 1 getReports
map fst reports `shouldBe` [Just fakePeer]
it "closes the socket even when the hook throws" $ limited $ do
let set =
defaultServerSettings
{ settingsOnException = \_ _ -> E.throwIO $ userError "hook"
}
withDummySocket $ \sock -> do
closed <- newEmptyMVar
forkWith set (\_ -> putMVar closed ()) sock fakePeer $
E.throwIO $
userError "boom"
r <- timeout 1000000 $ takeMVar closed
r `shouldBe` Just ()
it "reports a failure of the closer" $ limited $ do
(set, getReports) <- collecting
withDummySocket $ \sock ->
forkWith set (\_ -> ioError $ userError "close failed") sock fakePeer $
return ()
reports <- waitFor 1 getReports
case reports of
[(mpeer, desc)] -> do
mpeer `shouldBe` Just fakePeer
desc `shouldContain` "close failed"
_ -> expectationFailure $ "unexpected reports: " ++ show reports
describe "gcloseWith" $ do
it "waits for the FIN of the peer if the timeout is positive" $
limited $
withHeldConnection $ \sock -> do
let set = defaultServerSettings{settingsGracefulCloseTimeout = 500}
(_, ms) <- elapsed $ gcloseWith set sock
ms `shouldSatisfy` (>= 300)
it "closes at once if the timeout is not positive" $
limited $
withHeldConnection $ \sock -> do
let set = defaultServerSettings{settingsGracefulCloseTimeout = 0}
(_, ms) <- elapsed $ gcloseWith set sock
ms `shouldSatisfy` (< 300)
----------------------------------------------------------------
fakePeer :: SockAddr
fakePeer = SockAddrInet 12345 $ tupleToHostAddress (127, 0, 0, 1)
-- | An 'accept' which fails with the given errors before succeeding,
-- together with the number of times it has been called.
withFakeAccept
:: [IOError] -> ((IO (Socket, SockAddr), IO Int) -> IO a) -> IO a
withFakeAccept errs body = withDummySocket $ \sock -> do
ref <- newIORef errs
cnt <- newIORef (0 :: Int)
let accept' = do
atomicModifyIORef' cnt $ \n -> (n + 1, ())
me <- atomicModifyIORef' ref $ \es -> case es of
[] -> ([], Nothing)
e : rest -> (rest, Just e)
case me of
Just e -> E.throwIO e
Nothing -> return (sock, fakePeer)
body (accept', readIORef cnt)
-- | A socket which is never connected, standing in for an accepted one.
withDummySocket :: (Socket -> IO a) -> IO a
withDummySocket = E.bracket (socket AF_INET Stream defaultProtocol) close
-- | The server side of a connection whose peer stays open and silent,
-- so that a graceful close has to wait for its timeout.
withHeldConnection :: (Socket -> IO a) -> IO a
withHeldConnection body = withListenSocket $ \lsock port -> do
var <- newEmptyMVar
withServerThread (accept lsock >>= putMVar var . fst) $
client port $
\_held -> takeMVar var >>= body
emfile, eintr, aborted, bad :: IOError
emfile = mkIOError fullErrorType "accept" Nothing Nothing
eintr = emfile{ioe_type = Interrupted}
aborted = emfile{ioe_type = OtherError, ioe_errno = Just connAborted}
where
Errno connAborted = eCONNABORTED
bad = mkIOError illegalOperationErrorType "accept" Nothing Nothing
ioeGetErrorType' :: IOError -> IOErrorType
ioeGetErrorType' = ioe_type