atelier-core-1.0.0.0: test/Unit/Atelier/Effects/TallySpec.hs
module Unit.Atelier.Effects.TallySpec (test_Tally) where
import Effectful (IOE, runEff)
import Effectful.Concurrent (Concurrent, runConcurrent)
import Effectful.Reader.Static (Reader, runReader)
import Hedgehog (forAll, property, (===))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Test.Tasty.Hedgehog (testProperty)
import Data.IORef qualified as IORef
import Data.Map.Strict qualified as Map
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Atelier.Effects.Cache.Config (Config (..))
import Atelier.Effects.Clock (Clock)
import Atelier.Effects.Conc (Conc)
import Atelier.Effects.Delay (Delay)
import Atelier.Effects.Log (Log)
import Atelier.Effects.Tally (Tally)
import Atelier.Effects.Clock qualified as Clock
import Atelier.Effects.Conc qualified as Conc
import Atelier.Effects.Delay qualified as Delay
import Atelier.Effects.Log qualified as Log
import Atelier.Effects.Tally qualified as Tally
test_Tally :: TestTree
test_Tally =
testGroup
"Tally"
[ testGroup
"Multiple Keys"
[ testCase "works with integer keys" do
result <- runTallyTest $ checkCountsForKeys [1, 42, 999 :: Int]
result @?= Map.fromList [(1, 1), (42, 1), (999, 1)]
]
, testGroup
"Action Execution"
[ testCase "continuation with side effects executes correctly" do
result <- runTallyTest $ do
ref <- liftIO $ IORef.newIORef ([] :: [Int])
replicateM_ 3
$ Tally.count @Int
1
\count ->
liftIO $ IORef.modifyIORef' ref (count :)
liftIO $ reverse <$> IORef.readIORef ref
result @?= [1, 2, 3]
]
, testGroup
"Properties"
[ testProperty "kth call receives count k (count fidelity)" $ property $ do
n <- forAll $ Gen.int (Range.linear 1 100)
results <- liftIO $ runTallyTest $ replicateM n checkCount
results === [1 .. n]
, testProperty "no lost updates under concurrency" $ property $ do
n <- forAll $ Gen.int (Range.linear 1 50)
finalCount <- liftIO $ runTallyTest $ do
replicateM_ n checkCount
checkCount
finalCount === n + 1
]
]
--------------------------------------------------------------------------------
-- Test Helpers
--------------------------------------------------------------------------------
-- | Run a tally test
runTallyTest :: Eff '[Tally Int, Reader Config, Clock, Delay, Conc, Log, Concurrent, IOE] a -> IO a
runTallyTest action =
runEff
. runConcurrent
. Log.runNoOp
. Conc.runIO
. Delay.runIO
. Clock.runIO
. runReader (Config {entryTtl = 3600, cleanupInterval = 3600})
. Tally.run @Int
$ action
-- | Increment the tally for the default test key and return the raw count
checkCount :: (Tally Int :> es) => Eff es Int
checkCount = Tally.count @Int 1 pure
-- | Increment the tally for a specific key and return the raw count
checkCountForKey :: (Hashable key, Ord key, Tally key :> es) => key -> Eff es Int
checkCountForKey key = Tally.count key pure
-- | Increment the tally for each key and return a map of key → count
checkCountsForKeys :: (Hashable key, Ord key, Tally key :> es) => [key] -> Eff es (Map key Int)
checkCountsForKeys keys = Map.fromList . zip keys <$> traverse checkCountForKey keys