{-# LANGUAGE BlockArguments #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE UndecidableInstances #-} module Main (main) where import Control.Exception import Data.IORef import LazyBracket import Test.Tasty import Test.Tasty.HUnit tests :: TestTree tests = testGroup "All" [ testCase "doesAcquire" doesAcquire, testCase "doesAcquireEx" doesAcquireEx, testCase "doesNotAcquireTwice" doesNotAcquireTwice, testCase "noAcquisition" noAcquisition, testCase "noAcquisitionEx" noAcquisitionEx ] data TestResourceState = NotYetAcquired | AlreadyAcquired | Disposed deriving (Show, Eq) data TestOps = TestOpA | TestOpB deriving (Show, Eq) doesAcquire :: Assertion doesAcquire = do ref <- newIORef NotYetAcquired opsRef <- newIORef @[TestOps] [] let refMustBe msg expectedState = do testState <- readIORef ref assertEqual msg expectedState testState let opsRefMustBe msg expectedOpsState = do opsState <- readIORef opsRef assertEqual msg expectedOpsState opsState _ <- lazyBracket (writeIORef ref AlreadyAcquired) (\_ -> writeIORef ref Disposed) ( \Resource {accessResource, controlResource} -> do refMustBe "not acquired at the beginning" NotYetAcquired controlResource (\_ -> modifyIORef opsRef (<> [TestOpA])) refMustBe "control op doesn't trigger acquisition" NotYetAcquired opsRefMustBe "control op not executed before acquisition" [] _ <- accessResource refMustBe "access to resource triggers acquisition" AlreadyAcquired opsRefMustBe "access to resource triggers pending ops" [TestOpA] writeIORef opsRef [] _ <- accessResource opsRefMustBe "re-accessing the resource doesn't re-execute delayed control ops" [] controlResource (\_ -> modifyIORef opsRef (<> [TestOpB])) opsRefMustBe "control ops when already acquired execute immediately" [TestOpB] pure () ) refMustBe "release function must run" Disposed pure () data DummyEx = DummyEx deriving (Show, Eq) instance Exception DummyEx doesAcquireEx :: Assertion doesAcquireEx = do ref <- newIORef NotYetAcquired opsRef <- newIORef @[TestOps] [] let refMustBe msg expectedState = do testState <- readIORef ref assertEqual msg expectedState testState e <- try @DummyEx do _ <- lazyBracket (writeIORef ref AlreadyAcquired) (\_ -> writeIORef ref Disposed) ( \Resource {accessResource, controlResource} -> do _ <- accessResource throwIO DummyEx pure () ) pure () case e of Left DummyEx -> do refMustBe "release function must run" Disposed pure () Right _ -> assertFailure "Exception should have bubbled up" doesNotAcquireTwice :: Assertion doesNotAcquireTwice = do let bombs = pure () : repeat (throwIO $ userError "boom!") bombsRef <- newIORef @[IO ()] bombs let attempt = do action <- atomicModifyIORef bombsRef \(b : bs) -> (bs, b) action lazyBracket attempt (\_ -> pure ()) ( \Resource {accessResource} -> do _ <- accessResource _ <- accessResource pure () ) pure () noAcquisition :: Assertion noAcquisition = do ref <- newIORef NotYetAcquired opsRef <- newIORef @[TestOps] [] let refMustBe msg expectedState = do testState <- readIORef ref assertEqual msg expectedState testState let opsRefMustBe msg expectedOpsState = do opsState <- readIORef opsRef assertEqual msg expectedOpsState opsState _ <- lazyBracket (throwIO (userError "should not run")) (\_ -> writeIORef ref Disposed) ( \Resource {accessResource, controlResource} -> do controlResource (\_ -> modifyIORef opsRef (<> [TestOpA])) pure () ) refMustBe "release function must not run" NotYetAcquired opsRefMustBe "no ops should trigger" [] pure () noAcquisitionEx :: Assertion noAcquisitionEx = do e <- try @DummyEx do ref <- newIORef NotYetAcquired opsRef <- newIORef @[TestOps] [] let refMustBe msg expectedState = do testState <- readIORef ref assertEqual msg expectedState testState let opsRefMustBe msg expectedOpsState = do opsState <- readIORef opsRef assertEqual msg expectedOpsState opsState _ <- lazyBracket (throwIO (userError "should not run")) (\_ -> writeIORef ref Disposed) ( \Resource {accessResource, controlResource} -> do controlResource (\_ -> modifyIORef opsRef (<> [TestOpA])) throwIO DummyEx pure () ) refMustBe "release function must not run" NotYetAcquired opsRefMustBe "no ops should trigger" [] pure () case e of Left DummyEx -> pure () Right _ -> assertFailure "Exception should have bubbled up" main :: IO () main = defaultMain tests