graphql-engine/server/src-test/Control/Monad/MemoizationSpecDefinition.hs
Samir Talwar f48c882521 server: Share tests between CircularT and MemoizeT.
This abstracts `CircularT`'s test cases to work against "any" memoizer, and then runs them against `MemoizeT` as well.

Surprisingly (or not), this works without issue; `MemoizeT` passes all tests with a couple of extra instances.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5780
GitOrigin-RevId: 461880caf9220dc3f52d622a22e8b8bcd594e404
2022-09-08 19:38:49 +00:00

152 lines
4.9 KiB
Haskell

{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE NoOverloadedStrings #-}
-- | A suite of test cases for memoization monads.
-- Designed to be reusable.
module Control.Monad.MemoizationSpecDefinition (Memoizer (..), memoizationSpec) where
import Control.Monad.TimeLimit
import Data.HashMap.Strict qualified as Map
import Data.Kind (Type)
import Data.Typeable (Typeable)
import Hasura.Prelude
import Language.Haskell.TH (Name)
import Test.Hspec
class
( forall k v. MonadTrans (m k v),
forall k v n. Monad n => Functor (m k v n),
forall k v n. Monad n => Applicative (m k v n),
forall k v n. Monad n => Monad (m k v n),
forall k v n s. MonadState s n => MonadState s (m k v n)
) =>
Memoizer (m :: Type -> Type -> (Type -> Type) -> Type -> Type)
where
runMemoizer ::
forall k v n a.
(MonadFix n, MonadIO n, Eq k, Ord k, Hashable k) =>
m k v n a ->
n a
memoize ::
forall k v n.
(MonadFix n, MonadIO n, Eq k, Ord k, Hashable k, Typeable k, Typeable v) =>
Name ->
k ->
m k v n v ->
m k v n v
memoizationSpec :: forall m. Memoizer m => Spec
memoizationSpec = do
describe "circular graphs" $ checkCircularGraphs @m
describe "infinite lists" $ checkInfiniteLists @m
describe "memoization" $ checkMemoization @m
describe "does not protect against bad code" $ checkFailure @m
--------------------------------------------------------------------------------
-- Graphs
data Node = Node String [Node]
nodeName :: Node -> String
nodeName (Node n _) = n
instance Show Node where
show (Node name succs) = "Node " ++ show name ++ " " ++ show (nodeName <$> succs)
instance Eq Node where
Node n1 s1 == Node n2 s2 = n1 == n2 && map nodeName s1 == map nodeName s2
checkCircularGraphs :: forall m. Memoizer m => Spec
checkCircularGraphs = do
it "builds A -> B -> C -> A" do
(a, b, c) <- succeedsWithinTimeLimit $ runMemoizer @m do
let buildA = memoize 'checkCircularGraphs "a" do
b <- buildB
pure $ Node "a" [b]
buildB = memoize 'checkCircularGraphs "b" do
c <- buildC
pure $ Node "b" [c]
buildC = memoize 'checkCircularGraphs "c" do
a <- buildA
pure $ Node "c" [a]
(,,)
<$> buildA
<*> buildB
<*> buildC
a `shouldBe` Node "a" [b]
b `shouldBe` Node "b" [c]
c `shouldBe` Node "c" [a]
it "builds A -> A" do
a <- succeedsWithinTimeLimit $ runMemoizer @m do
let buildA = memoize 'checkCircularGraphs "a" do
a <- buildA
pure $ Node "a" [a]
buildA
a `shouldBe` Node "a" [a]
--------------------------------------------------------------------------------
-- Infinite lists
checkInfiniteLists :: forall m. Memoizer m => Spec
checkInfiniteLists = do
it "builds `x = 1 : x`" do
l <- succeedsWithinTimeLimit $ runMemoizer @m do
let x = memoize 'checkInfiniteLists () do
y <- x
pure $ (1 :: Int) : y
x
take 5 l `shouldBe` [1, 1, 1, 1, 1]
it "builds `[0,1,2..]`" do
l <- succeedsWithinTimeLimit $ runMemoizer @m do
let x = memoize 'checkInfiniteLists () do
y <- x
pure $ (0 :: Int) : map succ y
x
take 5 l `shouldBe` [0, 1, 2, 3, 4]
--------------------------------------------------------------------------------
-- Memoization
checkMemoization :: forall m. Memoizer m => Spec
checkMemoization = do
it "memoizes fibo" do
(fibos, count) <- succeedsWithinTimeLimit $
flip runStateT (mempty :: HashMap Int Int) $ runMemoizer @m do
let fibo n = memoize 'checkMemoization n do
modify $ Map.insertWith (+) n (1 :: Int)
case n of
0 -> pure 0
1 -> pure 1
_ -> (+) <$> fibo (n - 2) <*> fibo (n - 1)
traverse fibo [0 .. 20]
fibos !! 20 `shouldBe` (6765 :: Int)
count `shouldBe` Map.fromList (zip [0 .. 20] (repeat 1))
--------------------------------------------------------------------------------
-- Failure
checkFailure :: forall m. Memoizer m => Spec
checkFailure = do
it "unsuccessfully attempts to memoize Maybe" do
result <- runWithTimeLimit $ runMemoizer @m do
let buildA :: m String (Maybe Node) IO (Maybe Node)
buildA = memoize 'checkFailure "a" do
mb <- buildB
-- a can only exist if b exists
pure $ mb <&> \b -> Node "a" [b]
buildB :: m String (Maybe Node) IO (Maybe Node)
buildB = memoize 'checkFailure "b" do
ma <- buildA
-- b can only exist if a exists
pure $ ma <&> \a -> Node "b" [a]
buildA
result `shouldBe` Nothing
it "unsuccessfully attempts to build a self-referential int" do
result <- runWithTimeLimit $ runMemoizer @m do
let go = memoize 'checkFailure () do
x <- go
pure $ if odd x then 1 else 0 :: Int
go
result `shouldBe` Nothing