polysemy/test/BracketSpec.hs
Sandy Maguire b00d451d1e Ensure IO Resource interpreters handle Sem failure
The IO Resource interpreters had a bug in `BracketOnError` that would
fail to run the finalizers if the error was a Sem error (as opposed to
an IO exception.)

This PR also reworks the bracket spec so that it runs each test for
every interpreter, hopefully keeping them all in sync in the future.

Fixes #262
2019-10-22 19:35:22 +02:00

219 lines
5.5 KiB
Haskell

module BracketSpec where
import Control.Monad
import Polysemy
import Polysemy.Error
import Polysemy.Output
import Polysemy.Resource
import Polysemy.State
import Polysemy.Trace
import Test.Hspec
import Unsafe.Coerce
spec :: Spec
spec = parallel $ do
testAllThree "persist state and call the finalizer"
(\(ts, (s, e)) -> do
s `shouldBe` "finalized"
e `shouldBe` Left ()
ts `shouldBe` ["allocated", "starting block"]
) $ do
bracket
(put "allocated" >> pure ())
(\() -> do
get >>= trace
put "finalized"
)
(\() -> do
get >>= trace
put "starting block"
_ <- throw ()
put "don't get here"
)
testAllThree "persist state and call the finalizer with bracketOnError"
(\(ts, (s, e)) -> do
ts `shouldContain` ["allocated"]
ts `shouldContain` ["starting block"]
s `shouldBe` "finalized"
e `shouldBe` Left ()
) $ do
bracketOnError
(put "allocated" >> pure ())
(\() -> do
get >>= trace
put "finalized"
)
(\() -> do
get >>= trace
put "starting block"
_ <- throw ()
put "don't get here"
)
testAllThree "should not call the finalizer if there no error"
(\(ts, (s, e)) -> do
ts `shouldContain` ["allocated"]
ts `shouldNotContain` ["starting block"]
s `shouldBe` "don't get here"
e `shouldBe` Right ()
) $ do
bracketOnError
(put "allocated" >> pure ())
(\() -> do
get >>= trace
put "finalized"
)
(\() -> do
get >>= trace
put "starting block"
put "don't get here"
)
testAllThree "should call the finalizer on Error"
(\(ts, (s, e)) -> do
ts `shouldContain` ["beginning transaction"]
ts `shouldContain` ["rolling back transaction"]
s `shouldBe` ""
e `shouldBe` Left ()
) $ do
withTransaction $ do
void $ throw ()
pure "hello"
testTheIOTwo "io dispatched bracket"
(\(ts, (s, e)) -> do
ts `shouldContain` ["allocated"]
ts `shouldContain` ["starting block"]
s `shouldBe` "finalized"
e `shouldBe` Left ()
) $ do
bracket
(put "allocated" >> pure ())
(\() -> do
get >>= trace
put "finalized"
)
(\() -> do
get >>= trace
put "starting block"
_ <- throw ()
put "don't get here"
)
testTheIOTwo "should not lock when done recursively"
(\(ts, (s, e)) -> do
ts `shouldContain` [ "hello 1"
, "hello 2"
, "RUNNING"
, "goodbye 2"
]
s `shouldBe` "finished"
e `shouldBe` Left ()
) $ do
bracket
(put "hello 1")
(\() -> do
get >>= trace
put "finished"
)
(\() -> do
get >>= trace
void $
bracket (put "hello 2")
(const $ do
get >>= trace
put "goodbye 2"
)
(const $ do
get >>= trace
put "RUNNING"
throw ()
)
-- This doesn't run due to the thrown error above
get >>= trace
put "goodbye 1"
)
------------------------------------------------------------------------------
runTest
:: Sem '[Error (), Resource, State [Char], Trace] a
-> IO ([String], ([Char], Either () a))
runTest = pure
. run
. runTraceList
. runState ""
. runResource
. runError @()
runTest2
:: Sem '[Error (), Resource, State [Char], Trace, Output String, Embed IO] a
-> IO ([String], ([Char], Either () a))
runTest2 = runM
. ignoreOutput
. runTraceList
. runState ""
. resourceToIO
. runError @()
runTest3
:: Sem '[Error (), Resource, State [Char], Trace, Output String, Embed IO, Final IO] a
-> IO ([String], ([Char], Either () a))
runTest3 = runFinal
. embedToFinal
. outputToIOMonoid (:[])
. traceToOutput
. stateToIO ""
. resourceToIOFinal
. runError @()
testAllThree
:: String
-> (([String], ([Char], Either () a)) -> Expectation)
-> (Sem '[Error (), Resource, State [Char], Trace] a)
-> Spec
testAllThree name k m = do
describe name $ do
it "via runResource" $ do
z <- runTest m
k z
-- NOTE(sandy): These unsafeCoerces are safe, because we're just weakening
-- the end of the union
it "via resourceToIO" $ do
z <- runTest2 $ unsafeCoerce m
k z
it "via resourceToIOFinal" $ do
z <- runTest3 $ unsafeCoerce m
k z
testTheIOTwo
:: String
-> (([String], ([Char], Either () a)) -> Expectation)
-> (Sem '[Error (), Resource, State [Char], Trace, Output String, Embed IO] a)
-> Spec
testTheIOTwo name k m = do
describe name $ do
it "via resourceToIO" $ do
z <- runTest2 m
k z
-- NOTE(sandy): This unsafeCoerces are safe, because we're just weakening
-- the end of the union
it "via resourceToIOFinal" $ do
z <- runTest3 $ unsafeCoerce m
k z
withTransaction :: (Member Resource r, Member Trace r) => Sem r a -> Sem r a
withTransaction m =
bracketOnError
(trace "beginning transaction")
(const $ trace "rolling back transaction")
(const $ m <* trace "committing transaction")