milena-0.5.4.0: test/tests.hs
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Prelude
-- base
import Data.Either (isRight, isLeft)
import qualified Data.List.NonEmpty as NE
-- Hackage
import Control.Lens
import Control.Monad.Except (catchError, throwError)
import Control.Monad.Trans (liftIO)
import Test.Tasty
import Test.Tasty.Hspec
import Test.Tasty.QuickCheck
import qualified Data.ByteString.Char8 as B
-- local
import Network.Kafka
import Network.Kafka.Consumer
import Network.Kafka.Producer
import Network.Kafka.Protocol (ProduceResponse(..),
KafkaError(..),
CompressionCodec(..),
CreateTopicsResponse(..),
DeleteTopicsResponse(..),
Offset(..),
OffsetCommitResponse(..),
OffsetFetchResponse(..),
ConsumerGroup(..),
Partition(..),
KafkaString(..),
Metadata(..),
TopicName(..),
)
main :: IO ()
main = testSpec "the specs" specs >>= defaultMain
specs :: Spec
specs = do
let topic = "milena-test"
run = runKafka $ mkKafkaState "milena-test-client" ("localhost", 9092)
requireAllAcks = do
stateRequiredAcks .= -1
stateWaitSize .= 1
stateWaitTime .= 1000
byteMessages = fmap (TopicAndMessage topic . makeMessage . B.pack)
let cleanup :: TopicName -> IO ()
cleanup topicName = do
run $ do
stateAddresses %= NE.cons ("localhost", 9092)
deleteTopic (deleteTopicsRequest topicName)
pure ()
describe "can talk to local Kafka server" $ do
prop "can produce messages" $ \ms -> do
result <- run . produceMessages $ byteMessages ms
result `shouldSatisfy` isRight
prop "can produce compressed messages" $ \ms -> do
result <- run . produceCompressedMessages Gzip $ byteMessages ms
result `shouldSatisfy` isRight
prop "can produce multiple messages" $ \(ms, ms', ms'') -> do
result <- run $ do
r1 <- produceMessages $ byteMessages ms
r2 <- produceMessages $ byteMessages ms'
r3 <- produceCompressedMessages Gzip $ byteMessages ms''
return $ r1 ++ r2 ++ r3
result `shouldSatisfy` isRight
prop "can fetch messages" $ do
result <- run $ do
offset <- getLastOffset EarliestTime 0 topic
withAnyHandle (\handle -> fetch' handle =<< fetchRequest offset 0 topic)
result `shouldSatisfy` isRight
prop "can roundtrip messages" $ \ms key -> do
let messages = byteMessages ms
result <- run $ do
requireAllAcks
info <- brokerPartitionInfo topic
case getPartitionByKey (B.pack key) info of
Just PartitionAndLeader { _palLeader = leader, _palPartition = partition } -> do
let payload = [(TopicAndPartition topic partition, groupMessagesToSet NoCompression messages)]
s = stateBrokers . at leader
[(_topicName, [(_, NoError, offset)])] <- _produceResponseFields <$> send leader payload
broker <- findMetadataOrElse [topic] s (KafkaInvalidBroker leader)
resp <- withBrokerHandle broker (\handle -> fetch' handle =<< fetchRequest offset partition topic)
return $ fmap tamPayload . fetchMessages $ resp
Nothing -> fail "Could not deduce partition"
result `shouldBe` Right (tamPayload <$> messages)
prop "can roundtrip compressed messages" $ \(NonEmpty ms) -> do
let messages = byteMessages ms
result <- run $ do
requireAllAcks
produceResps <- produceCompressedMessages Gzip messages
case map _produceResponseFields produceResps of
[[(_topicName, [(partition, NoError, offset)])]] -> do
resp <- fetch offset partition topic
return $ fmap tamPayload . fetchMessages $ resp
_ -> fail "Unexpected produce response"
result `shouldBe` Right (tamPayload <$> messages)
prop "can roundtrip keyed messages" $ \(NonEmpty ms) key -> do
let keyBytes = B.pack key
messages = fmap (TopicAndMessage topic . makeKeyedMessage keyBytes . B.pack) ms
result <- run $ do
requireAllAcks
produceResps <- produceMessages messages
case map _produceResponseFields produceResps of
[[(_topicName, [(partition, NoError, offset)])]] -> do
resp <- fetch offset partition topic
return $ fmap tamPayload . fetchMessages $ resp
_ -> fail "Unexpected produce response"
result `shouldBe` Right (tamPayload <$> messages)
describe "withAddressHandle" $ do
it "turns 'IOException's into 'KafkaClientError's" $ do
result <- run $ withAddressHandle ("localhost", 9092) (\_ -> liftIO $ ioError $ userError "SOMETHING WENT WRONG!") :: IO (Either KafkaClientError ())
result `shouldSatisfy` isLeft
it "discards monadic effects when exceptions are thrown" $ do
result <- run $ do
stateName .= "expected"
_ <- flip catchError (return . Left) $ withAddressHandle ("localhost", 9092) $ \_ -> do
stateName .= "changed"
_ <- throwError KafkaFailedToFetchMetadata
n <- use stateName
return (Right n)
use stateName
result `shouldBe` Right "expected"
describe "updateMetadatas" $
it "de-dupes _stateAddresses" $ do
result <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
updateMetadatas []
use stateAddresses
result `shouldBe` fmap NE.nub result
let newTopicName = "milena-test-13-partitions"
describe "create topics" $ do
it "create topics with multiple partitions" $ do
result <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
createTopic (createTopicsRequest newTopicName 13 1 [] [])
result `shouldBe` (Right $ TopicsResp [(newTopicName, NoError)])
it "create already existing topic" $ do
result <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
createTopic (createTopicsRequest newTopicName 13 1 [] [])
result `shouldBe` (Right $ TopicsResp [(newTopicName, TopicAlreadyExists)])
describe "delete topics" $
it "delete topics" $ do
result <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
deleteTopic (deleteTopicsRequest newTopicName)
result `shouldBe` (Right $ DeleteTopicsResp [(newTopicName, NoError)])
let commitOffsetTopicName = "commit-offset"
t = commitOffsetTopicName
Test.Tasty.Hspec.afterAll_ (cleanup commitOffsetTopicName) $ do
describe "can commit messages" $ do
it "create a topic" $ do
topicCreation <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
createTopic (createTopicsRequest t 3 1 [] [])
topicCreation `shouldBe` (Right $ TopicsResp [(t, NoError)])
it "commit offset 5 to partition 0 for consumer group \"group1\"" $ do
commitOff <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
commitOffset (commitOffsetRequest (ConsumerGroup "group1") t 0 (Offset 5))
commitOff `shouldBe` Right (OffsetCommitResp [(t,[(Partition 0,NoError)])])
it "commit offset 15 to partition 1 for consumer group \"group1\"" $ do
commitOff <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
commitOffset (commitOffsetRequest (ConsumerGroup "group1") t 1 (Offset 15))
commitOff `shouldBe` Right (OffsetCommitResp [(t,[(Partition 1,NoError)])])
it "commit offset 10 to partition 2 for consumer group \"group2\"" $ do
commitOff <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
commitOffset (commitOffsetRequest (ConsumerGroup "group2") t 2 10)
commitOff `shouldSatisfy` isRight
it "fetch offset from partition 0 for \"group1\"" $ do
fetchOff <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
fetchOffset (fetchOffsetRequest (ConsumerGroup "group1") t 0)
fetchOff `shouldBe` Right (OffsetFetchResp [(t,[(Partition 0, Offset 5,Metadata (KString {_kString = ""}),NoError)])])
it "fetch offset from partition 0 for \"group2\"" $ do
fetchOff <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
fetchOffset (fetchOffsetRequest (ConsumerGroup "group2") t 0)
fetchOff `shouldBe` Right (OffsetFetchResp [(t,[(Partition 0, Offset (-1),Metadata (KString {_kString = ""}),UnknownTopicOrPartition)])])
it "note that getLastOffset is unchanged" $ do
getLastOff <- run $ do
stateAddresses %= NE.cons ("localhost", 9092)
getLastOffset EarliestTime 0 t
getLastOff `shouldSatisfy` isRight
getLastOff `shouldBe` (Right $ Offset 0)
prop :: Testable prop => String -> prop -> SpecWith ()
prop s = it s . property