stm-ringbuffer-0.1.0.0: test/ReferenceTest.hs
-- Simulation that ensures that TRingBuffer.TBQueue matches the behavior of TBQueue
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
module ReferenceTest (referenceTest) where
import Control.Concurrent.STM.TBQueue qualified as A
import Control.Concurrent.STM.TRingBuffer as RB
( TRingBuffer (..),
flushBack,
flushFront,
isEmpty,
isFull,
length,
newIO,
peekBack,
peekFront,
popBack,
popFront,
pushBack,
pushFront,
)
import Control.Concurrent.STM.TRingBuffer qualified as RB
import Control.Concurrent.STM.TRingBuffer.TBQueue qualified as B
import Control.Monad (forM_)
import Control.Monad.STM (STM, atomically, orElse)
import Data.Functor (($>))
import Data.List (singleton)
import GHC.Generics (Generic)
import Generic.Random (genericArbitrary, uniform)
import Numeric.Natural (Natural)
import Test.Hspec (SpecWith, describe, shouldBe)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck (Arbitrary (..), genericShrink)
referenceTest :: SpecWith ()
referenceTest = describe "Compare to reference behavior" $ do
describe "TBQueue" $ do
forM_ [0, 1, 2, 4, 8, 16] $ \size -> do
let title = "matches reference behavior for size == " <> show size
prop title $ testImplsMatch size implReferenceTBQueue implRBTBQueue
describe "TRingBuffer" $ do
forM_ [0, 1, 2, 4, 8, 16] $ \size -> do
let title = "matches reference behavior for size == " <> show size
prop title $ testImplsMatch size implReferenceTBQueue implTRingBuffer
forM_ [0, 1, 2, 4, 8, 16] $ \size -> do
let titleR = "(reverse order) matches reference behavior for size == " <> show size
prop titleR $ testImplsMatch size implReferenceTBQueue implTRingBufferReverse
testImplsMatch :: Natural -> Impl a -> Impl b -> [Command] -> IO ()
testImplsMatch size a b commands = do
bufA <- newBuf a size
capacity a bufA `shouldBe` size
bufB <- newBuf b size
capacity b bufB `shouldBe` size
resA <- traverse (runCommand a bufA) commands
resB <- traverse (runCommand b bufB) commands
resA `shouldBe` resB
data Command
= Read
| TryRead
| Flush
| Peek
| TryPeek
| Write !Int
| UnGet !Int
| Length
| IsEmpty
| IsFull
deriving (Show, Generic)
instance Arbitrary Command where
arbitrary = genericArbitrary uniform
shrink = genericShrink
-- using this instead of a typeclass lets us test a ringbuffer both
-- forwards and backwards
data Impl a = MkImpl
{ newBuf :: Natural -> IO (a Int),
runCommand :: a Int -> Command -> IO [Int],
capacity :: a Int -> Natural
}
implReferenceTBQueue :: Impl A.TBQueue
implReferenceTBQueue =
let newBuf = A.newTBQueueIO
runCommand q =
atomically . \case
Read -> attempt $ A.readTBQueue q
TryRead -> attempt' $ A.tryReadTBQueue q
Flush -> A.flushTBQueue q
Peek -> attempt $ A.peekTBQueue q
TryPeek -> attempt' $ A.tryPeekTBQueue q
Write e -> attempt $ A.writeTBQueue q e $> 0
UnGet e -> attempt $ A.unGetTBQueue q e $> 0
Length -> singleton . fromIntegral <$> A.lengthTBQueue q
IsEmpty -> fromBool <$> A.isEmptyTBQueue q
IsFull -> fromBool <$> A.isFullTBQueue q
capacity = A.capacityTBQueue
in MkImpl {..}
implRBTBQueue :: Impl B.TBQueue
implRBTBQueue =
let newBuf = B.newTBQueueIO
runCommand q =
atomically . \case
Read -> attempt $ B.readTBQueue q
TryRead -> attempt' $ B.tryReadTBQueue q
Flush -> B.flushTBQueue q
Peek -> attempt $ B.peekTBQueue q
TryPeek -> attempt' $ B.tryPeekTBQueue q
Write e -> attempt $ B.writeTBQueue q e $> 0
UnGet e -> attempt $ B.unGetTBQueue q e $> 0
Length -> singleton . fromIntegral <$> B.lengthTBQueue q
IsEmpty -> fromBool <$> B.isEmptyTBQueue q
IsFull -> fromBool <$> B.isFullTBQueue q
capacity = B.capacityTBQueue
in MkImpl {..}
implTRingBuffer :: Impl RB.TRingBuffer
implTRingBuffer =
let newBuf = RB.newIO . fromIntegral
runCommand q =
atomically . \case
Read -> attempt $ RB.popFront q
TryRead -> attempt' $ try $ RB.popFront q
Flush -> RB.flushFront q
Peek -> attempt $ RB.peekFront q
TryPeek -> attempt' $ try $ RB.peekFront q
Write e -> attempt $ RB.pushBack q e $> 0
UnGet e -> attempt $ RB.pushFront q e $> 0
Length -> singleton . fromIntegral <$> RB.length q
IsEmpty -> fromBool <$> RB.isEmpty q
IsFull -> fromBool <$> RB.isFull q
capacity = fromIntegral . RB.size
in MkImpl {..}
implTRingBufferReverse :: Impl RB.TRingBuffer
implTRingBufferReverse =
let newBuf = RB.newIO . fromIntegral
runCommand q =
atomically . \case
Read -> attempt $ RB.popBack q
TryRead -> attempt' $ try $ RB.popBack q
Flush -> RB.flushBack q
Peek -> attempt $ RB.peekBack q
TryPeek -> attempt' $ try $ RB.peekBack q
Write e -> attempt $ RB.pushFront q e $> 0
UnGet e -> attempt $ RB.pushBack q e $> 0
Length -> singleton . fromIntegral <$> RB.length q
IsEmpty -> fromBool <$> RB.isEmpty q
IsFull -> fromBool <$> RB.isFull q
capacity = fromIntegral . RB.size
in MkImpl {..}
fromBool :: (Num a) => Bool -> [a]
fromBool b = [if b then 1 else 0]
attempt :: STM a -> STM [a]
attempt m = fmap singleton m `orElse` pure []
attempt' :: (Functor f) => f (Maybe a) -> f [a]
attempt' m = maybe [] singleton <$> m
try :: STM a -> STM (Maybe a)
try m = fmap Just m `orElse` pure Nothing