kafka-client-0.7.0.1: integration-test/KafkaSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module KafkaSpec (spec) where
import Control.Applicative ((<$>))
import Control.Concurrent (threadDelay)
import Control.Monad (replicateM)
import Data.IORef (modifyIORef', newIORef, readIORef)
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
import qualified Data.Set as Set
import Kafka
import Test.Kafka.Arbitrary
-- | Keep running the given generator until a value that satisfies the given
-- predicate is produced.
generateSuchThat :: Gen a -> (a -> IO Bool) -> IO a
generateSuchThat gen f = loop
where
loop = do
a <- generate gen
match <- f a
if match
then return a
else loop
-- | Returns an IO action that will always return unique values from the given
-- generator.
uniqueGenerator :: Ord a => Gen a -> IO (IO a)
uniqueGenerator gen = do
generated <- newIORef Set.empty
let isNew a = Set.notMember a <$> readIORef generated
return $ do
a <- generateSuchThat gen isNew
modifyIORef' generated (Set.insert a)
return a
spec :: Socket -> Spec
spec conn = describe "Kafka" $ do
-- This is hacky but it will ensure that none of the tests run under the
-- same Kafka topic.
getNewTopic <- runIO $ uniqueGenerator arbitrary
prop "returns zero offset for empty topic" $ do
topic <- getNewTopic
let req = Offsets topic 0 OffsetsLatest 10
offsets conn req `shouldReturn` Right [0]
prop "can produce and fetch empty payloads" $ do
topic <- getNewTopic
produce conn [Produce topic 0 [""]]
Right resp <- pollMessages conn $ Fetch topic 0 0 (1024 * 1024)
fetchMessages resp `shouldBe` [""]
prop "can produce and fetch empty lists of payloads" $ do
topic <- getNewTopic
produce conn [Produce topic 0 []]
fetch conn [fetchRequest topic 0]
`shouldReturn` Right [FetchResponse [] 0]
prop "can fetch and produce" $ \(NonEmptyPayloadList messages) -> do
topic <- getNewTopic
produce conn [Produce topic 0 messages]
Right resp <- pollMessages conn (fetchRequest topic 0)
fetchMessages resp `shouldBe` messages
let newOffset = fetchNewOffset resp
offsets conn (Offsets topic 0 OffsetsLatest 10)
`shouldReturn` Right [newOffset, 0]
fetch conn [fetchRequest topic newOffset]
`shouldReturn` Right [FetchResponse [] newOffset]
prop "can multi-fetch and multi-produce" $ \(NonEmpty ps) -> do
topics <- replicateM (length ps) getNewTopic
let messageSets = map getPayloads ps
produceReqs = zipWith
(\t ms -> Produce t 0 ms) topics messageSets
fetchReqs = map (`fetchRequest` 0) topics
receivedAll (Left _) = False
receivedAll (Right responses) = all isSuccess responses
where
isSuccess = not . null . fetchMessages
produce conn produceReqs
responses <- do
result <- retryUntil receivedAll 10 500 (fetch conn fetchReqs)
case result of
Just (Right responses) -> return responses
Just (Left e) -> fail (show e)
Nothing -> fail "Failed to fetch messages."
map fetchMessages responses `shouldBe` messageSets
where
fetchRequest topic offset = Fetch topic 0 offset (1024 * 1024)
-- | Polls the given transport with the given Fetch request until a non-empty
-- list of messages is available.
--
-- Throws an error if a non-empty list of messages is not received after
-- trying for 5 seconds.
pollMessages :: Transport t => t -> Fetch -> IO (Response FetchResponse)
pollMessages t req = do
result <- retryUntil isSuccess 10 500 (fetch t [req])
case result of
Nothing -> fail "Failed to fetch messages."
Just xs -> return $ head <$> xs
where
isSuccess (Left _) = False
isSuccess (Right [resp]) = not . null . fetchMessages $ resp
isSuccess (Right xs) =
error $ "Too many messages in response: " ++ show xs
-- | Retries the given IO operation with the specified delay until the given
-- predicate on the result returns True.
--
-- Return Nothing if the maximum number of attempts was exceeded.
retryUntil :: (a -> Bool) -> Int -> Int -> IO a -> IO (Maybe a)
retryUntil predicate maxAttempts delayMillis m = loop 0
where
loop attempt
| attempt >= maxAttempts = return Nothing
| otherwise = do
a <- m
if predicate a
then return (Just a)
else threadDelay (delayMillis * 1000) >> loop (attempt + 1)