port-utils-0.2.1.0: test/Tests/Network/Socket/WaitSpec.hs
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE CPP #-}
module Tests.Network.Socket.WaitSpec where
import qualified Network.Socket.Wait.Internal as W
import qualified Network.Socket.Free as F
import qualified Network.Socket as N
import qualified Control.Concurrent as C
import qualified Control.Exception as E
import qualified Test.Hspec as H
import qualified System.Timeout as T
import qualified Control.Concurrent.Async as A
import qualified Control.Monad.Trans.State as St
import qualified Control.Monad as M
instance Monad m => Semigroup (St.StateT s m ()) where
(<>) = (>>)
instance Monad m => Monoid (St.StateT s m ()) where
mempty = pure ()
mappend = (<>)
blockPort :: N.Socket -> IO ()
blockPort sock = E.bracket F.openFreePort (N.close . snd) $ \(port1, sock1) -> do
N.listen sock1 1
N.connect sock $ N.SockAddrInet (fromIntegral port1) $ N.tupleToHostAddress (127,0,0,1)
M.forever $ C.threadDelay 100000000
testAtFirstUnavailable :: String -> (IO () -> Int -> IO ()) -> H.SpecWith (Int, N.Socket)
testAtFirstUnavailable message test = H.describe (message ++ " when the port is") $ do
H.it "free" $ \(port, sock) -> N.close sock >> test (pure ()) port
H.it "is bound and CLOSED" $ \(port, sock) -> test (N.close sock) port
H.it "is in a state other than listening" $ \(port, sock) ->
A.withAsync (blockPort sock) $ \thread -> test (A.cancel thread >> N.close sock) port
testUnavailable :: String -> (Int -> IO ()) -> H.SpecWith (Int, N.Socket)
testUnavailable message = testAtFirstUnavailable message . const
withListeningSocket :: Int -> IO () -> IO ()
withListeningSocket port action = E.bracket (N.socket N.AF_INET N.Stream N.defaultProtocol) N.close $ \sock -> do
N.setSocketOption sock N.ReuseAddr 1
(M.void $ N.bind sock $ N.SockAddrInet (fromIntegral port) $ N.tupleToHostAddress (127,0,0,1))
`E.onException` (putStrLn "bad stuff")
N.listen sock 1
action
spec :: H.Spec
spec = do
-- Black box tests of wait with all the caveats. Just a little bit of sanity testing.
H.describe "wait" $ H.before F.openFreePort $ H.after (N.close . snd) $ do
testUnavailable "never returns" $ \port -> do
T.timeout 100000 (W.wait "127.0.0.1" port) >>= \case
Nothing -> pure ()
Just () -> fail "wait returned! I should have blocked forever!"
H.it "does connect if port is available" $ \(port, sock) -> do
N.listen sock 128
W.wait "127.0.0.1" port
testAtFirstUnavailable "at first does not return" $ \unblock port -> do
A.withAsync (W.wait "127.0.0.1" port) $ \thread -> do
C.threadDelay 100000
-- Ensure wait is blocking the thread from completing
A.poll thread >>= \case
Just _ -> fail "wait returned when the port was unavailable!"
Nothing -> pure ()
-- Free the port
unblock
-- Ensure wait is able to connect when the socket is in a listening state.
withListeningSocket port $ A.wait thread
H.it "throw if the port is invalid" $ \(_, _) ->
W.wait "localhost" 0 `H.shouldThrow` (\(_ :: IOError) -> True)
H.it "throws if the host does not exist" $ \(_, _) ->
W.wait "invalid." 3000 `H.shouldThrow` (\(_ :: IOError) -> True)
H.describe "connectAction" $ H.before F.openFreePort $ H.after (N.close . snd) $ do
testUnavailable "return False" $ \port ->
W.connectAction "127.0.0.1" port `H.shouldReturn` False
H.it "returns True if it connects" $ \(port, sock) -> do
N.listen sock 128
W.connectAction "127.0.0.1" port `H.shouldReturn` True
testAtFirstUnavailable "returns True if connects after failing" $ \unblock port-> do
W.connectAction "127.0.0.1" port `H.shouldReturn` False
unblock
withListeningSocket port $
W.connectAction "127.0.0.1" port `H.shouldReturn` True
#ifdef linux_HOST_OS
H.it "returns false if there is a connection timeout" $ \_ -> do
W.connectAction "93.184.216.34" 80 `H.shouldReturn` False
#endif
H.describe "waitM" $ do
H.it "returns immediantly if the action returns True" $
flip St.execState False (W.waitM mempty (St.put True) (pure True)) `H.shouldBe` False
H.it "loops until the action returns True" $ do
let theAction = do
St.get >>= \case
0 -> St.put 1 >> pure False
_ -> St.put 2 >> pure True
flip St.execState 0 (W.waitM mempty (pure ()) theAction) `H.shouldBe` (2 :: Int)
main :: IO ()
main = H.hspec spec