packages feed

memcache-0.4.0.0: test/MockServer.hs

{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Mock Memcached server - just enough for testing client.
module MockServer (
        MockResponse(..), mockMCServer, withMCServer
    ) where

#if __GLASGOW_HASKELL__ < 710
import           Control.Applicative
#endif
import           Control.Concurrent
import           Control.Monad
import qualified Data.ByteString           as B
import qualified Data.ByteString.Char8     as C
import           Data.IORef
import qualified Network.Socket            as N
import qualified Network.Socket.ByteString as N
import           Text.Read                  (readMaybe)
import           UnliftIO.Exception         (SomeException, bracket, handle,
                                            throwIO)

-- | Actions the mock server can take to a request.
data MockResponse
    = MR B.ByteString
    | CloseConnection
    | DelayMS Int MockResponse
    | Noop

-- | Run an IO action with a mock Memcached server running in the background,
-- killing it once done.
withMCServer :: Bool -> [MockResponse] -> IO () -> IO ()
withMCServer loop res m = do
  sem <- newEmptyMVar

  let waitForInitialization = takeMVar sem

  bracket
    (mockMCServer loop res sem)
    (\tid -> killThread tid >> threadDelay 100000)
    (const $ waitForInitialization >> m)

-- | New mock Memcached server that responds to each request with the specified
-- list of responses.
mockMCServer :: Bool -> [MockResponse] -> MVar () -> IO ThreadId
mockMCServer loop resp' sem = forkIO $ bracket
    (N.socket N.AF_INET N.Stream N.defaultProtocol)
    N.close
    $ \sock -> do
        N.setSocketOption sock N.ReuseAddr 1
        let hints = N.defaultHints {
            N.addrFlags = [N.AI_PASSIVE]
          , N.addrSocketType = N.Stream
          , N.addrFamily = N.AF_INET
        }
        addr:_ <- N.getAddrInfo (Just hints) Nothing (Just "11211")
        N.bind sock $ N.addrAddress addr
        N.listen sock 10
        ref <- newIORef resp'

        -- Publish that initialization is done
        putMVar sem ()

        acceptHandler sock ref
        when loop $ forever $ threadDelay 1000000

  where
    acceptHandler sock ref = do
        client <- fst <$> N.accept sock
        resp <- readIORef ref
        cont <- handle allErrors $ clientHandler client ref resp
        when cont $  acceptHandler sock ref

    allErrors :: SomeException -> IO Bool
    allErrors = const $ return True

    clientHandler client _ []       = N.close client >> return False
    clientHandler client ref (r':resp) = do
      void $ recvReq client
      mrHandler r'
      where
        mrHandler r = case r of
            Noop            -> clientHandler client ref resp
            (MR mr)         -> N.sendAll client mr >> clientHandler client ref resp
            (DelayMS ms mr) -> do
                writeIORef ref resp -- client may reset connection
                threadDelay (ms * 1000)
                mrHandler mr
            CloseConnection -> do
                N.close client
                writeIORef ref resp
                return $ not $ null resp

recvReq :: N.Socket -> IO ()
recvReq s = do
  line <- recvLine s []
  case payloadLen (C.words line) of
    Just n  -> void (recvAll s n >> recvLine s [])
    Nothing -> return ()
  where
    -- Only these carry a data block; guessing by token position instead sees
    -- a payload in commands like @stats cachedump 1 100@ and then hangs.
    payloadLen ("ms":_:size:_)      = readMaybe (C.unpack size)
    payloadLen ("set":_:_:_:size:_) = readMaybe (C.unpack size)
    payloadLen _                    = Nothing

recvAll :: N.Socket -> Int -> IO B.ByteString
recvAll _ 0 = return B.empty
recvAll socket n = do
  chunk <- N.recv socket n
  if B.null chunk
    then throwIO eof
    else (chunk <>) <$> recvAll socket (n - B.length chunk)
  where eof = userError "mock server EOF"

recvLine :: N.Socket -> [B.ByteString] -> IO B.ByteString
recvLine socket acc = do
  chunk <- N.recv socket 1
  if B.null chunk then throwIO (userError "mock server EOF")
  else if chunk == "\n" then return (B.concat (reverse acc))
       else recvLine socket (chunk:acc)