packages feed

natskell-0.0.0.1: test/Unit/StreamingSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module StreamingSpec where

import           Control.Concurrent
import           Control.Concurrent.STM
import           Control.Exception
import qualified Control.Monad
import           Data.ByteString
    ( ByteString
    , append
    , empty
    , hGetSome
    , hPut
    , head
    , isPrefixOf
    , pack
    , tail
    )
import qualified Data.ByteString.Lazy      as LBS
import           Data.Char                 (chr)
import           Data.Word8                (isUpper)
import           GHC.IO.Handle
    ( BufferMode (NoBuffering)
    , Handle
    , hClose
    , hSetBuffering
    )
import           GHC.IO.IOMode
import           Lib.Logger
import           Network.ConnectionAPI     (ReaderAPI (..), WriterAPI (..))
import           Network.Socket            hiding (Debug, close)
import           Parser.API
    ( ParseStep (DropPrefix, Emit, NeedMore)
    , ParserAPI (ParserAPI)
    )
import           Pipeline.Broadcasting     (broadcastingApi)
import           Pipeline.Broadcasting.API (BroadcastingAPI (BroadcastingAPI))
import           Pipeline.Streaming        (streamingApi)
import           Pipeline.Streaming.API    (StreamingAPI (StreamingAPI))
import           Prelude                   hiding
    ( head
    , last
    , length
    , null
    , replicate
    , tail
    , take
    )
import           Queue.API                 (QueueItem (..), close, enqueue)
import           Queue.TransactionalQueue  (newQueue)
import           Test.Hspec
import qualified Types.Pub                 as Pub

defaultLogger' :: IO LoggerConfig
defaultLogger' = do
  lock <- newTMVarIO ()
  pure $ LoggerConfig Debug (putStrLn . renderLogEntry) lock

bufferLimit :: Int
bufferLimit = 4096

newtype CaptureWriter = CaptureWriter (TVar [ByteString])

captureWriterApi :: WriterAPI CaptureWriter
captureWriterApi =
  WriterAPI
    { writeData = \(CaptureWriter output) bytes -> do
        atomically $ modifyTVar' output (<> [bytes])
        pure (Right ())
    , writeDataLazy = \writer bytes ->
        writeData captureWriterApi writer (LBS.toStrict bytes)
    , closeWriter = \_ -> pure ()
    , openWriter = \_ -> pure ()
    }

handleReaderApi :: ReaderAPI Handle
handleReaderApi =
  ReaderAPI
    { readData = \h n -> do
        result <- try (hGetSome h n) :: IO (Either SomeException ByteString)
        case result of
          Left err    -> return $ Left (show err)
          Right bytes -> return $ Right bytes
    , closeReader = hClose
    , openReader = \_ -> pure ()
    }

spec :: Spec
spec = do
  describe "Streaming" $ do
      it "reads from source and writes to sink" $ do
        (server, client) <- makeSocketPair
        result <- newTVarIO "" :: IO (TVar ByteString)
        let sink curr = atomically $ modifyTVar' result (`append` curr)
        dl <- defaultLogger'
        ctx <- newLogContext
        let StreamingAPI runStreaming = streamingApi
        (forkIO . runWithLogger dl ctx) (runStreaming bufferLimit handleReaderApi client singleByteParser sink :: AppM ())
        hPut server "Hello, World"
        atomically $ assertTVarWithRetry result "Hello, World"
        hClose server
        hClose client
      it "continues reading from the handle after reaching end" $ do
        (server, client) <- makeSocketPair
        result <- newTVarIO "" :: IO (TVar ByteString)
        let sink curr = atomically $ modifyTVar' result (`append` curr)
        dl <- defaultLogger'
        ctx <- newLogContext
        let StreamingAPI runStreaming = streamingApi
        (forkIO . runWithLogger dl ctx) (runStreaming bufferLimit handleReaderApi client singleByteParser sink :: AppM ())
        hPut server "Hello, World"
        atomically $ assertTVarWithRetry result "Hello, World"
        hPut server "Hello, again"
        atomically $ assertTVarWithRetry result "Hello, WorldHello, again"
        hClose server
        hClose client
      it "applies the parser healing" $ do
        (server, client) <- makeSocketPair
        result <- newTVarIO "" :: IO (TVar ByteString)
        let sink curr = atomically $ modifyTVar' result (`append` curr)
        dl <- defaultLogger'
        ctx <- newLogContext
        let StreamingAPI runStreaming = streamingApi
        (forkIO . runWithLogger dl ctx) (runStreaming bufferLimit handleReaderApi client excludingUpperParser sink :: AppM ())
        hPut server "HELLO WORLD hello, world"
        atomically $ assertTVarWithRetry result "  hello, world"
        hClose server
        hClose client
      it "waits for more data" $ do
        (server, client) <- makeSocketPair
        result <- newTVarIO "" :: IO (TVar ByteString)
        let sink curr = atomically $ modifyTVar' result (`append` curr)
        dl <- defaultLogger'
        ctx <- newLogContext
        let StreamingAPI runStreaming = streamingApi
        (forkIO . runWithLogger dl ctx) (runStreaming bufferLimit handleReaderApi client explicitWordParser sink :: AppM ())
        hPut server "part1"
        threadDelay 100000
        atomically $ ensureTVarIsEmpty result
        hPut server "part2"
        atomically $ assertTVarWithRetry result "part1part2"
        hClose server
        hClose client
  describe "Broadcasting" $ do
      it "drops messages larger than the buffer limit" $ do
        dl <- defaultLogger'
        ctx <- newLogContext
        q <- newQueue
        output <- newTVarIO []
        let pub = Pub.Pub "FOO" Nothing Nothing (Just "0123456789")
        let BroadcastingAPI runBroadcasting = broadcastingApi
        enqueue q (QueueItem pub) `shouldReturn` Right ()
        close q
        runWithLogger dl ctx (runBroadcasting 5 q captureWriterApi (CaptureWriter output) :: AppM ())
        readTVarIO output `shouldReturn` []

ensureTVarIsEmpty :: TVar ByteString -> STM ()
ensureTVarIsEmpty tvar = do
  content <- readTVar tvar
  Control.Monad.when (content /= empty) retry

assertTVarWithRetry :: Eq a => TVar a -> a -> STM ()
assertTVarWithRetry tvar expected = do
  actual <- readTVar tvar
  Control.Monad.unless (actual == expected) retry

singleByteParser :: ParserAPI ByteString
singleByteParser = ParserAPI (\bs -> Emit (pack [head bs]) (tail bs))

excludingUpperParser :: ParserAPI ByteString
excludingUpperParser =
  ParserAPI $ \bs ->
    case isUpper (head bs) of
      False ->
        Emit (pack [head bs]) (tail bs)
      True ->
        DropPrefix 1 [chr . fromIntegral . head $ bs]

explicitWordParser :: ParserAPI ByteString
explicitWordParser = ParserAPI parseWord
  where
    parseWord bs
      | bs == "part1part2" =
          Emit bs empty
      | bs `isPrefixOf` "part1part2" =
          NeedMore
      | otherwise =
          DropPrefix 1 [chr . fromIntegral . head $ bs]

makeSocketPair :: IO (Handle, Handle)
makeSocketPair = do
  serverSock <- socket AF_INET Stream defaultProtocol
  setSocketOption serverSock ReuseAddr 1
  bind serverSock (SockAddrInet 0 (tupleToHostAddress (127,0,0,1)))
  listen serverSock 1
  SockAddrInet port _ <- getSocketName serverSock

  -- initiate client connection
  clientSock <- socket AF_INET Stream defaultProtocol
  connect clientSock (SockAddrInet port (tupleToHostAddress (127,0,0,1)))

  -- accept connection from server side
  (serverConn, _) <- accept serverSock

  -- convert both sides to handles
  serverHandle <- socketToHandle serverConn ReadWriteMode
  clientHandle <- socketToHandle clientSock ReadWriteMode
  hSetBuffering serverHandle NoBuffering
  hSetBuffering clientHandle NoBuffering

  return (serverHandle, clientHandle)