packages feed

recv-0.1.3: test/BufferPoolSpec.hs

{-# LANGUAGE OverloadedStrings #-}
module BufferPoolSpec where

import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
import Control.Concurrent.STM
import Control.Monad (void)
import qualified Data.ByteString as B
import qualified Data.ByteString.Internal as B (ByteString (PS))
import Data.IORef (newIORef, readIORef, writeIORef)
import Foreign.ForeignPtr (withForeignPtr)
import Foreign.Marshal.Utils (copyBytes)
import Foreign.Ptr (plusPtr)

import Network.Socket
import qualified Network.Socket.ByteString as NSB
import Network.Socket.BufferPool
import Test.Hspec (Spec, describe, hspec, it, shouldBe, shouldReturn)

main :: IO ()
main = hspec spec

-- Two ByteStrings each big enough to fill a buffer (16K).
wantData, otherData :: B.ByteString
wantData = B.replicate 16384 0xac
otherData = B.replicate 16384 0x77

spec :: Spec
spec = do
    describe "withBufferPool" $ do
        it "does not clobber buffers" $ do
            pool <- newBufferPool 2048 16384
            -- 'pool' contains B.empty; prime it to contain a real buffer.
            _ <- withBufferPool pool $ \_ _ -> return 0
            -- 'pool' contains a 16K buffer; fill it with \xac and keep the result.
            got <- withBufferPool pool $ blitBuffer wantData
            got `shouldBe` wantData
            -- 'pool' should now be empty and reallocate, rather than clobber the
            -- previous buffer.
            _ <- withBufferPool pool $ blitBuffer otherData
            got `shouldBe` wantData

    describe "tryWithBufferPool" $ do
        it "returns the filled prefix when the filler fills" $ do
            pool <- newBufferPool 2048 16384
            tryWithBufferPool pool (blitBuffer (B.take 10 wantData))
                `shouldReturn` Just (B.take 10 wantData)

        it "returns an empty ByteString when the filler consumes nothing" $ do
            -- 'receiveNoWait' reports EOF this way, so it must not be 'Nothing'.
            pool <- newBufferPool 2048 16384
            tryWithBufferPool pool (\_ _ -> return 0) `shouldReturn` Just B.empty

        it "returns Nothing when the filler declines" $ do
            pool <- newBufferPool 2048 16384
            tryWithBufferPool pool (\_ _ -> return (-1)) `shouldReturn` Nothing

        it "keeps the leftover buffer when the filler declines" $ do
            pool <- newBufferPool 2048 16384
            -- Consume 10000 of the 16384 bytes, leaving 6384 in the pool.
            _ <- tryWithBufferPool pool $ \_ _ -> return 10000
            tryWithBufferPool pool (\_ _ -> return (-1)) `shouldReturn` Nothing
            -- The declined call must neither consume, drop nor reallocate the
            -- leftover: the next filler is offered exactly those 6384 bytes.
            offered <- offeredSize pool
            offered `shouldBe` 6384

        it "does not corrupt buffered data across a decline" $ do
            pool <- newBufferPool 2048 16384
            _ <- tryWithBufferPool pool $ \_ _ -> return 0
            tryWithBufferPool pool (\_ _ -> return (-1)) `shouldReturn` Nothing
            tryWithBufferPool pool (blitBuffer wantData)
                `shouldReturn` Just wantData

    describe "receiveWith" $ do
        it "gives up while blocked, without closing the socket" $ do
            withPair $ \_ srv pool -> do
                stop <- newTVarIO False
                done <- newEmptyMVar
                void $ forkIO $ do
                    r <- receiveWith srv pool $ readTVar stop >>= check
                    putMVar done r
                threadDelay 100000
                atomically $ writeTVar stop True
                takeMVar done `shouldReturn` Left ()
                -- The socket is the caller's, and still theirs to read.
                void $ getSocketName srv

        -- Data already there wins over a give-up, as it does in
        -- withReadable: answering Left would leave it unread.
        it "takes what is there even once the caller has given up" $ do
            withPair $ \cli srv pool -> do
                void $ NSB.send cli "unread"
                threadDelay 100000
                gone <- newTVarIO True
                receiveWith srv pool (readTVar gone >>= check)
                    `shouldReturn` Right "unread"

        it "reads nothing once the caller has given up and none is there" $ do
            withPair $ \_cli srv pool -> do
                gone <- newTVarIO True
                receiveWith srv pool (readTVar gone >>= check)
                    `shouldReturn` Left ()

        it "takes what is already there without waiting" $ do
            withPair $ \cli srv pool -> do
                void $ NSB.send cli "hello"
                threadDelay 100000
                receiveWith srv pool (retry :: STM ()) `shouldReturn` Right "hello"

-- A connected pair on the loopback, and a pool to receive with.
withPair :: (Socket -> Socket -> BufferPool -> IO a) -> IO a
withPair action = do
    lsock <- socket AF_INET Stream defaultProtocol
    setSocketOption lsock ReuseAddr 1
    bind lsock $ SockAddrInet 0 $ tupleToHostAddress (127, 0, 0, 1)
    listen lsock 1
    port <- socketPort lsock
    cli <- socket AF_INET Stream defaultProtocol
    connect cli $ SockAddrInet port $ tupleToHostAddress (127, 0, 0, 1)
    (srv, _) <- accept lsock
    close lsock
    pool <- newBufferPool 2048 16384
    r <- action cli srv pool
    close cli
    close srv
    return r

-- The 'BufSize' the pool offers to the next filler, leaving the pool as it was.
offeredSize :: BufferPool -> IO Int
offeredSize pool = do
    ref <- newIORef 0
    _ <- tryWithBufferPool pool $ \_ size -> writeIORef ref size >> return 0
    readIORef ref

-- Fill the Buffer with the contents of the ByteString and return the number of
-- bytes written.  To be used with 'withBufferPool'.
blitBuffer :: B.ByteString -> Buffer -> BufSize -> IO Int
blitBuffer (B.PS fp off len) dst len' = withForeignPtr fp $ \ptr -> do
    let src = ptr `plusPtr` off
        n = min len len'
    copyBytes dst src n
    return n