{-# LANGUAGE AllowAmbiguousTypes #-}
module WriterSpec where
import Test.Hspec
import qualified Control.Concurrent.Async as A
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Exception (evaluate)
import Control.Effect
import Control.Effect.Conc
import Control.Effect.Error
import Control.Effect.Reader
import Control.Effect.Writer
import Control.Effect.Unlift
censor' :: forall e s a m
. Effs '[Error e, Writer s] m
=> (s -> s)
-> m a
-> m a
censor' f m = do
res <- censor f $ try m
case res of
Right res' -> return res'
Left e -> throw (e :: e)
test1 :: (String, Either () ())
test1 =
run
$ runWriter
$ runError $ do
tell "censoring"
censor @String
(drop 4)
(tell " not applied" *> throw ())
`catch`
(\(_ :: ()) -> pure ())
test2 :: (String, Either () ())
test2 =
run
$ runWriter
$ runError $
do
tell "censoring"
censor' @() @String
(drop 4)
(tell " not applied" *> throw ())
`catch`
(\(_ :: ()) -> pure ())
test3 :: (String, (String, ()))
test3 = run . runListen $ listen (tell "and hear")
test4 :: IO (String, String)
test4 = do
tvar <- newTVarIO ""
(listened, _) <- runM $ concToIO $ runWriterTVar tvar $ do
tell "message "
listen $ do
tell "has been"
a <- async $ tell " received"
wait a
end <- readTVarIO tvar
return (end, listened)
test5 :: IO (String, String)
test5 = do
tvar <- newTVarIO ""
lock <- newEmptyMVar
(listened, a) <- runM $ concToIO $ runWriterTVar tvar $ do
tell "message "
listen $ do
tell "has been"
a <- async $ do
embed $ takeMVar lock
tell " received"
return a
putMVar lock ()
_ <- A.wait a
end <- readTVarIO tvar
return (end, listened)
test6 :: ( Effs '[Error (Async ()), Embed IO] m
, Threaders '[ReaderThreads] m p
, MonadMask m
, MonadBaseControlPure IO m
)
=> m String
test6 = do
tvar <- embed $ newTVarIO ""
lock <- embed $ newEmptyMVar
let
inner = do
tell "message "
fmap snd $ listen @String $ do
tell "has been"
a <- async $ do
embed $ takeMVar lock
tell " received"
throw a
concToIO (runWriterTVar tvar inner) `catch` \a ->
embed $ do
putMVar lock ()
() <- A.wait a
readTVarIO tvar
spec :: Spec
spec = do
describe "writer" $ do
it "should not censor" $ do
test1 `shouldBe` ("censoring not applied", Right ())
it "should censor" $ do
test2 `shouldBe` ("censoring applied", Right ())
it "should have a proper listen" $ do
test3 `shouldBe` ("and hear", ("and hear", ()))
it "should be strict in the output" $
let
t1 :: (Carrier m, Threaders '[WriterThreads] m p) => m (String, ())
t1 = runWriter @String $ do
tell @String (error "strict")
return ()
t2 :: (Carrier m, Threaders '[WriterThreads] m p) => m (String, ())
t2 = runWriter @String $ do
_ <- listen @String (tell @String (error "strict"))
return ()
t3 :: (Carrier m, Threaders '[WriterThreads] m p) => m (String, ())
t3 = runWriter @String $ do
pass @String $ pure (\_ -> error "strict", ())
return ()
in do
runM t1 `shouldThrow` errorCall "strict"
evaluate (run t1) `shouldThrow` errorCall "strict"
runM t2 `shouldThrow` errorCall "strict"
evaluate (run t2) `shouldThrow` errorCall "strict"
runM t3 `shouldThrow` errorCall "strict"
evaluate (run t3) `shouldThrow` errorCall "strict"
describe "runWriterTVar" $ do
it "should listen and commit asyncs spawned and awaited upon in a listen \
\block" $ do
(end, listened) <- test4
end `shouldBe` "message has been received"
listened `shouldBe` "has been received"
it "should commit writes of asyncs spawned inside a listen block even if \
\the block has finished." $ do
(end, listened) <- test5
end `shouldBe` "message has been received"
listened `shouldBe` "has been"
it "should commit writes of asyncs spawned inside a listen block even if \
\the block failed for any reason." $ do
Right end1 <- runM $ errorToIO @(Async ()) $ test6
end1 `shouldBe` "message has been received"
describe "runLazyWriter" $ do
let
runLazily = run . runAskConstSimple () . runWriterLazy @[Int]
runSemiLazily = runLazily . runError @()
runStrictly = run . runError @() . runWriterLazy @[Int]
runStrictlyM = runM . runWriterLazy @[Int]
act :: Eff (Writer [Int]) m => m ()
act = do
tell @[Int] [1]
tell @[Int] [2]
error "strict"
it "should build the final output lazily, if the interpreters after \
\runLazyWriter and the final monad are lazy" $ do
(take 2 . fst . runLazily) act `shouldBe` [1,2]
(take 2 . fst . runSemiLazily) act `shouldBe` [1,2]
evaluate (runStrictly act) `shouldThrow` errorCall "strict"
runStrictlyM act `shouldThrow` errorCall "strict"
it "should listen lazily if all interpreters and final monad are lazy" $ do
let
listenAct :: Eff (Writer [Int]) m => m [Int]
listenAct = do
(end,_) <- listen @[Int] act
return (take 2 end)
(snd . runLazily) listenAct `shouldBe` [1,2]
evaluate ((snd . runSemiLazily) listenAct) `shouldThrow` errorCall "strict"
evaluate (runStrictly listenAct) `shouldThrow` errorCall "strict"
runStrictlyM listenAct `shouldThrow` errorCall "strict"
it "should censor lazily if all interpreters and final monad are lazy" $ do
let
censorAct :: Eff (Writer [Int]) m => m ()
censorAct = censor @[Int] (\(_:y:_) -> [0,y]) act
(fst . runLazily) censorAct `shouldBe` [0,2]
evaluate ((fst . runSemiLazily) censorAct) `shouldThrow` errorCall "strict"
evaluate (runStrictly censorAct) `shouldThrow` errorCall "strict"
runStrictlyM censorAct `shouldThrow` errorCall "strict"