packages feed

rdkafka-hs-1.1.0.0: test-integration/Kafka/FixtureSupportTest.hs

module Kafka.FixtureSupportTest (tests) where

import Control.Concurrent (forkFinally, killThread)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
import Control.Exception (IOException, bracket, throwIO, try)
import Data.IORef (IORef, atomicModifyIORef', mkWeakIORef, newIORef, readIORef, writeIORef)
import Foreign.StablePtr (StablePtr, freeStablePtr, newStablePtr)
import Kafka.CallbackTestSupport (assertRetained, awaitCollected)
import Kafka.Consumer (Offset (..))
import Kafka.FaultTestSupport (awaitBarrier, newBarrier, reachBarrier)
import Kafka.FixtureSupport
import Kafka.Producer
import System.Mem.Weak (Weak)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Fixture error controls"
    [ testCase "immediate enqueue rejection propagates" $ do
        result <- sendAndConfirm 100000 (\_ _ -> pure (Left (ImmediateError failure))) (const (pure Nothing)) [record]
        result @?= Left failure,
      testCase "delivery failure propagates" $ do
        result <- sendAndConfirm 100000 (\r cb -> cb (DeliveryFailure r failure) >> pure (Right ())) (const (pure Nothing)) [record]
        result @?= Left failure,
      testCase "flush failure propagates" $ do
        result <- sendAndConfirm 100000 (\_ _ -> pure (Right ())) (const (pure (Just failure))) [record]
        result @?= Left failure,
      testCase "missing delivery callback fails" $ do
        result <- sendAndConfirm 1000 (\_ _ -> pure (Right ())) (const (pure Nothing)) [record]
        requireFailure result,
      testCase "wrong delivery identity fails" $ do
        result <- sendAndConfirm 100000 (\r cb -> cb (DeliverySuccess (r {prValue = Just "wrong"}) (Offset 0)) >> pure (Right ())) (const (pure Nothing)) [record]
        requireFailure result,
      testCase "matching delivery succeeds" $ do
        result <- sendAndConfirm 100000 (\r cb -> cb (DeliverySuccess r (Offset 0)) >> pure (Right ())) (const (pure Nothing)) [record]
        result @?= Right (),
      testCase "unexpected poll error propagates" $ do
        collect [Left failure] 1 >>= (@?= Left failure),
      testCase "missing record fails" $ do
        collect [Right 1] 2 >>= requireFailure,
      testCase "extra record fails" $ do
        collect [Right 1, Right 2] 1 >>= requireFailure,
      testCase "idle before exact records is retried" $ do
        collect [Left idle, Right 1, Left idle, Right 2] 2 >>= (@?= Right [1, 2]),
      testCase "partial setup releases first resource" $ do
        released <- newIORef False
        result <- try @IOException (acquirePair (pure ()) (const (writeIORef released True)) (throwIO (userError "injected setup failure") :: IO ()))
        case result of
          Left _ -> pure ()
          Right _ -> assertFailure "injected setup failure was swallowed"
        readIORef released >>= (@?= True),
      testCase "interrupted partial setup releases first resource" $ do
        released <- newIORef False
        stage <- newBarrier
        finished <- newEmptyMVar
        worker <-
          forkFinally
            (acquirePair (pure ()) (const (writeIORef released True)) (reachBarrier stage))
            (putMVar finished)
        awaitBarrier stage
        killThread worker
        takeMVar finished >>= \case
          Left _ -> pure ()
          Right _ -> assertFailure "interrupted acquisition unexpectedly completed"
        readIORef released >>= (@?= True),
      testCase "weak control retains while rooted and collects after release" $
        do
          weak <- bracket capturedRoot (freeStablePtr . fst) $ \(_, weak) -> do
            assertRetained weak
            pure weak
          awaitCollected weak
    ]
  where
    failure = KafkaError "injected failure"
    idle = KafkaResponseError RdKafkaRespErrTimedOut
    record = ProducerRecord (TopicName "fixture") (SpecifiedPartition 0) Nothing (Just "payload") mempty
    collect :: [Either KafkaError Int] -> Int -> IO (Either KafkaError [Int])
    collect events count = do
      remaining <- newIORef events
      ticks <- newIORef 0
      let clock = atomicModifyIORef' ticks (\n -> (n + 1, n))
          poll = atomicModifyIORef' remaining $ \case
            [] -> ([], Left idle)
            e : es -> (es, e)
      collectExactly clock 20 count poll
    requireFailure :: (Show a) => Either KafkaError a -> IO ()
    requireFailure = \case
      Left _ -> pure ()
      Right value -> assertFailure ("expected failure, got " <> show value)

{-# NOINLINE capturedRoot #-}
capturedRoot :: IO (StablePtr (IO Int), Weak (IORef Int))
capturedRoot = do
  value <- newIORef (42 :: Int)
  weak <- mkWeakIORef value (pure ())
  root <- newStablePtr (readIORef value)
  pure (root, weak)