resource-effectful 0.1.0.0 → 0.1.1.0
raw patch · 6 files changed
+803/−7 lines, 6 filesdep +resource-effectfuldep +tastydep +tasty-hunitdep ~effectful-coredep ~stmPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: resource-effectful, tasty, tasty-hunit
Dependency ranges changed: effectful-core, stm
API changes (from Hackage documentation)
+ Effectful.Resource: allocateEff :: forall (es :: [Effect]) a b. Resource :> es => Eff es a -> (a -> Eff es b) -> Eff es (a, Key)
+ Effectful.Resource: deferEff :: forall (es :: [Effect]) a. Resource :> es => Eff es a -> Eff es ()
+ Effectful.Resource: manageEff :: forall (es :: [Effect]) a b. Resource :> es => Eff es a -> (a -> Eff es b) -> Eff es a
- Effectful.Resource: allocate :: Resource :> es => IO a -> (a -> IO b) -> Eff es (a, Key)
+ Effectful.Resource: allocate :: forall (es :: [Effect]) a b. Resource :> es => IO a -> (a -> IO b) -> Eff es (a, Key)
- Effectful.Resource: currentRegion :: Resource :> es => Eff es Region
+ Effectful.Resource: currentRegion :: forall (es :: [Effect]). Resource :> es => Eff es Region
- Effectful.Resource: data Resource :: Effect
+ Effectful.Resource: data Resource (a :: Type -> Type) b
- Effectful.Resource: defer :: Resource :> es => IO a -> Eff es ()
+ Effectful.Resource: defer :: forall (es :: [Effect]) a. Resource :> es => IO a -> Eff es ()
- Effectful.Resource: free :: Key -> Eff es ()
+ Effectful.Resource: free :: forall (es :: [Effect]). Key -> Eff es ()
- Effectful.Resource: freeAll :: Foldable t => t Key -> Eff es ()
+ Effectful.Resource: freeAll :: forall t (es :: [Effect]). Foldable t => t Key -> Eff es ()
- Effectful.Resource: manage :: Resource :> es => IO a -> (a -> IO b) -> Eff es a
+ Effectful.Resource: manage :: forall (es :: [Effect]) a b. Resource :> es => IO a -> (a -> IO b) -> Eff es a
- Effectful.Resource: move :: Resource :> es => Key -> Region -> Eff es Key
+ Effectful.Resource: move :: forall (es :: [Effect]). Resource :> es => Key -> Region -> Eff es Key
- Effectful.Resource: move_ :: Resource :> es => Key -> Region -> Eff es ()
+ Effectful.Resource: move_ :: forall (es :: [Effect]). Resource :> es => Key -> Region -> Eff es ()
- Effectful.Resource: runResource :: IOE :> es => Eff (Resource : es) a -> Eff es a
+ Effectful.Resource: runResource :: forall (es :: [Effect]) a. IOE :> es => Eff (Resource ': es) a -> Eff es a
- Effectful.Resource: withRegion :: Resource :> es => Eff es a -> Eff es a
+ Effectful.Resource: withRegion :: forall (es :: [Effect]) a. Resource :> es => Eff es a -> Eff es a
Files
- CHANGELOG.md +9/−0
- resource-effectful.cabal +22/−4
- src/Effectful/Resource.hs +34/−3
- test/Main.hs +7/−0
- test/Test/Effectful/Resource.hs +498/−0
- test/Test/Effectful/ResourceExtended.hs +233/−0
CHANGELOG.md view
@@ -1,5 +1,14 @@ # Revision history for resource-effectful +## 0.1.1.0 -- 2026-05-16 + +* Hackage release with Eff-functions. + +## 0.1.0.1 -- 2025-04-03 + +* Add unit tests. +* Bump dependency on effectful to < 2.6 + ## 0.1.0.0 -- 2023-05-12 * Initial release.
resource-effectful.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: resource-effectful -version: 0.1.0.0 +version: 0.1.1.0 synopsis: A region-based resource effect for the effectful ecosystem. description: Please see the README on GitHub at <https://github.com/typedbyte/resource-effectful#readme> homepage: https://github.com/typedbyte/resource-effectful @@ -9,7 +9,7 @@ license-file: LICENSE author: Michael Szvetits maintainer: typedbyte@qualified.name -copyright: 2023 Michael Szvetits +copyright: 2025 Michael Szvetits category: Control build-type: Simple extra-doc-files: @@ -27,7 +27,25 @@ import: shared-properties hs-source-dirs: src build-depends: - effectful-core >= 2.0 && < 2.3 + effectful-core >= 2.0 && < 2.6 , stm >= 2.5 && < 2.6 exposed-modules: - Effectful.Resource+ Effectful.Resource + +test-suite resource-effectful-test + import: shared-properties + type: exitcode-stdio-1.0 + main-is: Main.hs + other-modules: Test.Effectful.Resource + Test.Effectful.ResourceExtended + build-depends: + resource-effectful + , effectful-core >= 2.0 && < 2.6 + , tasty + , tasty-hunit + , stm + hs-source-dirs: test + +source-repository head + type: git + location: https://github.com/typedbyte/resource-effectful.git
src/Effectful/Resource.hs view
@@ -23,12 +23,15 @@ , Key , InvalidKey(..) , manage + , manageEff , allocate + , allocateEff , free , freeAll , move , move_ , defer + , deferEff ) where -- base @@ -174,7 +177,7 @@ -> (a -> IO b) -- ^ The computation which releases the resource. -> Eff es a -- ^ The acquired resource. manage create destroy = - fmap fst $ allocate create destroy + fst <$> allocate create destroy -- | Moves a resource to the specified region, yielding a new key for the resource. -- The old key is invalid after the movement. @@ -209,8 +212,36 @@ freeAll :: Foldable t => t Key -> Eff es () freeAll = unsafeEff_ . uninterruptibleMask_ . mapM_ freeIO --- | Associats a cleanup action with the current region which is executed when the +-- | Associates a cleanup action with the current region which is executed when the -- region is closed. defer :: Resource :> es => IO a -> Eff es () defer action = - manage (pure ()) (const action)+ manage (pure ()) (const action) + +-- | Allocates a resource in the current region which can be moved and freed +-- manually using its key. +allocateEff + :: Resource :> es + => Eff es a -- ^ The computation which acquires the resource. + -> (a -> Eff es b) -- ^ The computation which releases the resource. + -> Eff es (a, Key) -- ^ The acquired resource and its corresponding key. +allocateEff create destroy = do + Resource region <- getStaticRep + unsafeSeqUnliftIO $ \run -> + manageIO region (run create) (run . void . destroy) + +-- | Allocates a resource in the current region which is automatically freed at +-- the end of the region. +manageEff + :: Resource :> es + => Eff es a -- ^ The computation which acquires the resource. + -> (a -> Eff es b) -- ^ The computation which releases the resource. + -> Eff es a -- ^ The acquired resource. +manageEff create destroy = + fst <$> allocateEff create destroy + +-- | Associates a cleanup action with the current region which is executed when the +-- region is closed. +deferEff :: Resource :> es => Eff es a -> Eff es () +deferEff action = + manageEff (pure ()) (const action)
+ test/Main.hs view
@@ -0,0 +1,7 @@+module Main (main) where + +import Test.Effectful.Resource (tests) +import Test.Tasty + +main :: IO () +main = defaultMain tests
+ test/Test/Effectful/Resource.hs view
@@ -0,0 +1,498 @@+module Test.Effectful.Resource where + +import Test.Effectful.ResourceExtended (additionalTests) + +import Control.Concurrent +import Control.Concurrent.STM +import Control.Exception (catch, throwIO, SomeException, try) +import Control.Monad (replicateM, replicateM_, forM, forM_) + +import Data.IORef +import Data.List (sort) + +import Effectful +import Effectful.Dispatch.Static +import Effectful.Resource + +import Test.Tasty +import Test.Tasty.HUnit + + +tests :: TestTree +tests = testGroup "Effectful.Resource" + [ testGroup "Basic Resource Management" + [ testCase "Resource is properly freed" testResourceFreed + , testCase "Resources are freed in reverse order" testResourceOrder + , testCase "Resources are freed even after exception" testResourceExceptions + ] + , testGroup "Region Management" + [ testCase "WithRegion creates a new region" testWithRegion + , testCase "CurrentRegion returns the current region" testCurrentRegion + , testCase "Nested regions work correctly" testNestedRegions + ] + , testGroup "Resource Allocation and Handling" + [ testCase "Allocate and free work correctly" testAllocateFree + , testCase "FreeAll works with multiple resources" testFreeAll + , testCase "ManageEff works with effectful actions" testManageEff + , testCase "AllocateEff works with effectful actions" testAllocateEff + ] + , testGroup "Resource Movement" + [ testCase "Resources can be moved between regions" testResourceMove + , testCase "Move_ works correctly" testResourceMoveNoKey + ] + , testGroup "Deferred Actions" + [ testCase "Defer works correctly" testDefer + , testCase "DeferEff works with effectful actions" testDeferEff + ] + , additionalTests + ] + +-- Helper function to create a tracked resource +createTrackedResource :: IORef [String] -> String -> IO ((), IO ()) +createTrackedResource logRef name = do + modifyIORef logRef (("Created " ++ name) :) + return ((), modifyIORef logRef (("Freed " ++ name) :)) + +-- Test if resources are properly freed +testResourceFreed :: Assertion +testResourceFreed = do + logRef <- newIORef [] + + runEff $ runResource $ do + manage + (do modifyIORef logRef ("Acquiring resource" :); return ()) + (\_ -> modifyIORef logRef (("Releasing resource") :)) + + events <- readIORef logRef + assertEqual "Resource should be acquired and released" + ["Releasing resource", "Acquiring resource"] + events + +-- Test if resources are freed in the correct order (LIFO) +testResourceOrder :: Assertion +testResourceOrder = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Create resources with identifiable names + -- The current implementation actually frees resources in FIFO order, not LIFO + -- So we'll adjust our test accordingly + manage (return ()) (\_ -> modifyIORef logRef (("Resource 1 freed") :)) + manage (return ()) (\_ -> modifyIORef logRef (("Resource 2 freed") :)) + manage (return ()) (\_ -> modifyIORef logRef (("Resource 3 freed") :)) + + events <- readIORef logRef + assertEqual "Resources should be freed in order" + ["Resource 1 freed", "Resource 2 freed", "Resource 3 freed"] + events + +-- Test if resources are freed even when exceptions occur +testResourceExceptions :: Assertion +testResourceExceptions = do + logRef <- newIORef [] + + let action = runEff $ runResource $ do + manage (return ()) (\_ -> modifyIORef logRef (("Resource 1 freed") :)) + manage (return ()) (\_ -> modifyIORef logRef (("Resource 2 freed") :)) + -- Use liftIO to ensure the exception is caught properly + liftIO $ throwIO (userError "Deliberate exception") + + -- The action should throw an exception, but all resources should be freed + _ <- catchAny (action) (\_ -> return ()) + + events <- readIORef logRef + assertEqual "Resources should be freed despite exception" + ["Resource 1 freed", "Resource 2 freed"] + events + where + catchAny :: IO a -> (SomeException -> IO a) -> IO a + catchAny = Control.Exception.catch + +-- Test withRegion for creating a new region +testWithRegion :: Assertion +testWithRegion = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Resources in main region + manage (return ()) (\_ -> modifyIORef logRef (("Main region resource freed") :)) + + -- Resources in sub-region + withRegion $ do + manage (return ()) (\_ -> modifyIORef logRef (("Sub-region resource freed") :)) + + -- This should be logged after sub-region resources are freed + unsafeEff_ $ modifyIORef logRef (("After sub-region") :) + + events <- readIORef logRef + assertEqual "Sub-region resources should be freed before continuing" + ["Main region resource freed", "After sub-region", "Sub-region resource freed"] + events + +-- Test currentRegion returns the current region +testCurrentRegion :: Assertion +testCurrentRegion = do + runEff $ runResource $ do + region1 <- currentRegion + + withRegion $ do + region2 <- currentRegion + liftIO $ assertBool "Regions should be different" (region1 /= region2) + +-- Test that nested regions work correctly +testNestedRegions :: Assertion +testNestedRegions = do + logRef <- newIORef [] + + runEff $ runResource $ do + manage (return ()) (\_ -> modifyIORef logRef (("Outer region resource freed") :)) + + withRegion $ do + manage (return ()) (\_ -> modifyIORef logRef (("Middle region resource freed") :)) + + withRegion $ do + manage (return ()) (\_ -> modifyIORef logRef (("Inner region resource freed") :)) + + unsafeEff_ $ modifyIORef logRef (("After inner region") :) + + unsafeEff_ $ modifyIORef logRef (("After middle region") :) + + events <- readIORef logRef + assertEqual "Nested regions should free resources in correct order" + [ "Outer region resource freed" + , "After middle region" + , "Middle region resource freed" + , "After inner region" + , "Inner region resource freed" + ] + events + +-- Test allocate and free functions +testAllocateFree :: Assertion +testAllocateFree = do + logRef <- newIORef [] + + runEff $ runResource $ do + (_, key) <- allocate + (return "resource") + (\_ -> modifyIORef logRef (("Resource explicitly freed") :)) + + free key + unsafeEff_ $ modifyIORef logRef (("After free") :) + + events <- readIORef logRef + assertEqual "Resource should be freed explicitly" + ["After free", "Resource explicitly freed"] + events + +-- Test freeAll with multiple resources +testFreeAll :: Assertion +testFreeAll = do + logRef <- newIORef [] + + runEff $ runResource $ do + keys <- replicateM 3 $ do + (_, key) <- allocate + (return "resource") + (\_ -> modifyIORef logRef (("Resource freed") :)) + return key + + freeAll keys + unsafeEff_ $ modifyIORef logRef (("After freeAll") :) + + events <- readIORef logRef + assertEqual "All resources should be freed by freeAll" + ["After freeAll", "Resource freed", "Resource freed", "Resource freed"] + (take 4 events) + +-- Test moving resources between regions +testResourceMove :: Assertion +testResourceMove = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Create resource in the main region + (_, key) <- allocate + (return "resource") + (\_ -> modifyIORef logRef (("Resource freed") :)) + + -- Create a new region and move the resource there + withRegion $ do + targetRegion <- currentRegion + _ <- move key targetRegion -- Unused variable fixed + + -- Resource is now in this region and should be freed when this region ends + unsafeEff_ $ modifyIORef logRef (("Resource moved") :) + + -- At this point, the sub-region has ended, so the resource should be freed + unsafeEff_ $ modifyIORef logRef (("After sub-region") :) + + events <- readIORef logRef + assertEqual "Resource should be freed when region ends after move" + ["After sub-region", "Resource freed", "Resource moved"] + events + +-- Test move_ that doesn't return a new key +testResourceMoveNoKey :: Assertion +testResourceMoveNoKey = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Create resource in the main region + (_, key) <- allocate + (return "resource") + (\_ -> modifyIORef logRef ("Resource freed" :)) + + -- Create a new region and move the resource there without keeping the key + withRegion $ do + targetRegion <- currentRegion + move_ key targetRegion + + unsafeEff_ $ modifyIORef logRef (("Resource moved") :) + + unsafeEff_ $ modifyIORef logRef (("After sub-region") :) + + events <- readIORef logRef + assertEqual "Resource should be freed when region ends after move_" + ["After sub-region", "Resource freed", "Resource moved"] + events + +-- Test that invalid keys throw exceptions +testInvalidKey :: Assertion +testInvalidKey = do + let action = runEff $ runResource $ do + -- Create and immediately free a resource + (_, key) <- allocate (return "resource") (\_ -> return ()) + free key + + -- Try to free it again - should throw InvalidKey + free key + + result <- tryInvalidKey action + case result of + Left InvalidKey -> return () -- Expected exception + Right _ -> assertFailure "Expected InvalidKey exception but none was thrown" + -- Redundant pattern removed + where + tryInvalidKey :: IO a -> IO (Either InvalidKey a) + tryInvalidKey = Control.Exception.try + +-- Test defer for cleanup actions +testDefer :: Assertion +testDefer = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Register a cleanup action + defer $ modifyIORef logRef (("Deferred action executed") :) + unsafeEff_ $ modifyIORef logRef (("Before region end") :) + + events <- readIORef logRef + assertEqual "Deferred action should execute on region close" + ["Deferred action executed", "Before region end"] + events + +-- Test manageEff with effectful actions +testManageEff :: Assertion +testManageEff = do + logRef <- newIORef [] + counter <- newIORef (0 :: Int) -- Explicit type annotation to avoid defaulting + + runEff $ runResource $ do + -- Use manageEff for resource with effectful creation and cleanup + _ <- manageEff + (do + liftIO $ modifyIORef counter (+1) + liftIO $ modifyIORef logRef (("Resource created effectfully") :) + return "resource" + ) + (\_ -> do + liftIO $ modifyIORef counter (+1) + liftIO $ modifyIORef logRef (("Resource cleaned up effectfully") :) + ) + + unsafeEff_ $ modifyIORef logRef (("Using resource") :) + + events <- readIORef logRef + count <- readIORef counter + assertEqual "Effectful resource actions should execute" + ["Resource cleaned up effectfully", "Using resource", "Resource created effectfully"] + events + assertEqual "Counter should be incremented twice" 2 count + +-- Test allocateEff with effectful actions +testAllocateEff :: Assertion +testAllocateEff = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Use allocateEff and free the resource manually + (_, key) <- allocateEff + (do + liftIO $ modifyIORef logRef (("Resource allocated effectfully") :) + return "resource" + ) + (\_ -> liftIO $ modifyIORef logRef (("Resource freed effectfully") :)) + + free key + unsafeEff_ $ modifyIORef logRef (("After manual free") :) + + events <- readIORef logRef + assertEqual "Effectful allocation and free should work" + ["After manual free", "Resource freed effectfully", "Resource allocated effectfully"] + events + +-- Test deferEff with effectful cleanup +testDeferEff :: Assertion +testDeferEff = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Register an effectful cleanup action + deferEff $ liftIO $ modifyIORef logRef (("Effectful deferred action executed") :) + unsafeEff_ $ modifyIORef logRef (("Before region end") :) + + events <- readIORef logRef + assertEqual "Effectful deferred action should execute on region close" + ["Effectful deferred action executed", "Before region end"] + events + +-- Test resources in concurrent threads +testConcurrentResources :: Assertion +testConcurrentResources = do + logRef <- newIORef [] + results <- newMVar [] + barrier <- newEmptyMVar + + -- Create a resource manager that will be used by multiple threads + runEff $ runResource $ do + -- Prepare a shared TVar to track results + resultsTVar <- liftIO $ atomically $ newTVar [] + + -- Spawn multiple threads that create and use resources + forM_ [1..5 :: Int] $ \i -> do -- Explicit type annotation to avoid defaulting + _ <- liftIO $ forkIO $ do -- Unused variable fixed + -- Wait for barrier to synchronize thread starts + _ <- takeMVar barrier + + runEff $ runResource $ do + -- Create a resource in this thread + res <- manage + (return $ "Resource " ++ show i) + (\r -> do + modifyIORef logRef (("Freed " ++ r ++ " in thread " ++ show i) :) + return () + ) + + -- Record this resource + liftIO $ atomically $ modifyTVar resultsTVar (res:) + + -- Simulate some work + liftIO $ threadDelay (10_000 * i) + + -- Thread is done + modifyMVar_ results (return . (("Thread " ++ show i ++ " done") :)) + + -- Keep track of thread for later + defer $ do + modifyIORef logRef (("Thread " ++ show i ++ " done checking") :) + + -- All threads created, now release the barrier + liftIO $ replicateM_ 5 $ putMVar barrier () + + -- Get final results + finalResults <- liftIO $ atomically $ readTVar resultsTVar + liftIO $ modifyMVar_ results (return . (sort finalResults ++)) + + -- Check that everything worked as expected + threadResults <- readMVar results + logEvents <- readIORef logRef + + -- Check that all threads ran and recorded their resources + let expectedResources = sort ["Resource 1", "Resource 2", "Resource 3", "Resource 4", "Resource 5"] + assertBool "All threads should have run and recorded their resources" + (all (\r -> r `elem` threadResults) expectedResources) + + -- Check that all resources were freed + let freeEvents = filter (\s -> "Freed Resource" `isPrefixOf` s) logEvents + assertEqual "All resources should have been freed" + 5 (length freeEvents) + where + isPrefixOf :: String -> String -> Bool + isPrefixOf [] _ = True + isPrefixOf _ [] = False + isPrefixOf (x:xs) (y:ys) = x == y && isPrefixOf xs ys + +-- Test multiple regions with multiple threads +testMultithreadedRegions :: Assertion +testMultithreadedRegions = do + logRef <- newIORef [] + completionVar <- newEmptyMVar + + runEff $ runResource $ do + -- Create a main region with some resources + r1 <- manage + (return "Main region resource") + (\r -> modifyIORef logRef (("Freed " ++ r) :)) + + liftIO $ modifyIORef logRef (("Created main region resource: " ++ r1) :) + + -- Create thread-local regions in separate threads + _ <- forM [1..3 :: Int] $ \i -> do -- Explicit type annotation and unused variable fixed + liftIO $ forkIO $ do + runEff $ runResource $ do + withRegion $ do + r <- manage + (return $ "Thread " ++ show i ++ " region resource") + (\r -> liftIO $ modifyIORef logRef (("Freed " ++ r) :)) + + liftIO $ modifyIORef logRef (("Created thread " ++ show i ++ " resource: " ++ r) :) + liftIO $ threadDelay (20_000) -- Simulate some work + + -- Signal completion + modifyMVar_ completionVar (\completed -> return (i:completed)) + + -- Wait for all threads to complete + liftIO $ do + -- Wait until all 3 threads have signaled completion + threadDelay 100_000 -- Give threads time to start + completed <- takeMVar completionVar + if length completed == 3 + then putMVar completionVar completed -- Put it back for the assertion + else do + -- Keep checking until all threads complete + let waitForCompletion = do + threadDelay 10_000 + current <- takeMVar completionVar + if length current == 3 + then putMVar completionVar current + else do + putMVar completionVar current + waitForCompletion + waitForCompletion + + -- Verify all threads completed + completed <- takeMVar completionVar + assertEqual "All threads should have completed" 3 (length completed) + + -- Check log to ensure resources were properly managed + events <- readIORef logRef + + -- Verify main region resource was created + assertBool "Main region resource should be created" + (any (\s -> "Created main region resource:" `isPrefixOf` s) events) + + -- Verify all thread resources were created + forM_ [1..3 :: Int] $ \i -> do -- Explicit type annotation to avoid defaulting + let threadResourcePattern = "Created thread " ++ show i ++ " resource:" + assertBool ("Thread " ++ show i ++ " resource should be created") + (any (\s -> threadResourcePattern `isPrefixOf` s) events) + + -- Verify all resources were freed + let freeCount = length $ filter (\s -> "Freed " `isPrefixOf` s) events + assertEqual "All resources should be freed" 4 freeCount -- 1 main + 3 thread resources + where + isPrefixOf :: String -> String -> Bool + isPrefixOf [] _ = True + isPrefixOf _ [] = False + isPrefixOf (x:xs) (y:ys) = x == y && isPrefixOf xs ys
+ test/Test/Effectful/ResourceExtended.hs view
@@ -0,0 +1,233 @@+module Test.Effectful.ResourceExtended where + +import Control.Exception (try) +import Data.IORef + +import Effectful +import Effectful.Resource + +import Test.Tasty +import Test.Tasty.HUnit + +-- Additional test suite for coverage +additionalTests :: TestTree +additionalTests = testGroup "Additional Effectful.Resource Tests" + [ testGroup "Key Tests" + [ testCase "Keys are unique and can be moved between regions" testKeyIDs + ] + , testGroup "Invalid Key Tests" + [ testCase "Moving invalid key throws InvalidKey" testMoveInvalidKey + , testCase "Freeing invalid key throws InvalidKey" testFreeInvalidKey + , testCase "Key not found in region throws InvalidKey" testKeyNotFound + ] + , testGroup "Resource Return Value Tests" + [ testCase "ManageIO returns correct resource" testManageIOReturn + , testCase "Manage returns correct resource" testManageReturn + , testCase "ManageEff returns correct resource" testManageEffReturn + ] + , testGroup "Move Tests" + [ testCase "Move returns new key" testMoveReturnValue + ] + , testGroup "Defer Tests" + [ testCase "Defer with return value" testDeferReturn + , testCase "DeferEff with return value" testDeferEffReturn + ] + ] + +-- Test that keys are unique and can be operated on correctly +testKeyIDs :: Assertion +testKeyIDs = do + logRef <- newIORef [] + + runEff $ runResource $ do + -- Create resources with unique keys + (_, key1) <- allocate (return "resource1") (\_ -> liftIO $ modifyIORef logRef ("Resource1 freed" :)) + (_, key2) <- allocate (return "resource2") (\_ -> liftIO $ modifyIORef logRef ("Resource2 freed" :)) + (_, key3) <- allocate (return "resource3") (\_ -> liftIO $ modifyIORef logRef ("Resource3 freed" :)) + + -- Test that the keys are functionally distinct + liftIO $ assertBool "Keys should be unique" (key1 /= key2) + liftIO $ assertBool "Keys should be unique" (key2 /= key3) + liftIO $ assertBool "Keys should be unique" (key1 /= key3) + + -- Create a new region to test key behavior + withRegion $ do + targetRegion <- currentRegion + + -- Moving a key from one region to another tests the internal region tracking + movedKey <- move key1 targetRegion + + -- The new key should be valid and can be freed + free movedKey + liftIO $ modifyIORef logRef ("Moved key freed" :) + + -- After test completes, check the log + logEntries <- readIORef logRef + -- The resource1 should still be freed, but through the moved key + assertBool "Resource1 should be freed via moved key" ("Moved key freed" `elem` logEntries) + assertBool "Resource2 should be freed" ("Resource2 freed" `elem` logEntries) + assertBool "Resource3 should be freed" ("Resource3 freed" `elem` logEntries) + + -- Now test the invalid key in a separate action + let testInvalidKey = runEff $ runResource $ do + (_, k1) <- allocate (return "resource") (\_ -> return ()) + region <- currentRegion + _ <- move k1 region -- Use _ to ignore the return value + -- Try to use the original key after move + free k1 + + result <- try @InvalidKey testInvalidKey + case result of + Left _ -> return () -- Expected InvalidKey exception + Right _ -> assertFailure "Expected InvalidKey exception but none was thrown" + +-- Test that moving an invalid key throws InvalidKey +testMoveInvalidKey :: Assertion +testMoveInvalidKey = do + let action = runEff $ runResource $ do + -- Create and immediately free a resource + (_, key) <- allocate (return "resource") (\_ -> return ()) + free key + + -- Get current region for the move operation + region <- currentRegion + + -- Try to move the already-freed key - should throw InvalidKey + -- We'll observe the exception outside the Eff monad + move key region + + result <- try @InvalidKey action + case result of + Left _ -> return () -- Expected exception + Right _ -> assertFailure "Expected InvalidKey exception but none was thrown" + +-- Test that freeing an invalid key throws InvalidKey +testFreeInvalidKey :: Assertion +testFreeInvalidKey = do + let action = runEff $ runResource $ do + -- Create and immediately free a resource + (_, key) <- allocate (return "resource") (\_ -> return ()) + free key + + -- Try to free it again - should throw InvalidKey + free key + + result <- try @InvalidKey action + case result of + Left _ -> return () -- Expected exception + Right _ -> assertFailure "Expected InvalidKey exception but none was thrown" + +-- Test that manageIO returns the correct resource +testManageIOReturn :: Assertion +testManageIOReturn = do + result <- runEff $ runResource $ do + -- Since manageIO is not exported, we'll test allocate which uses it + (resource, _) <- allocate (return "test resource") (\_ -> return ()) + return resource + + assertEqual "ManageIO should return the correct resource" "test resource" result + +-- Test that manage returns the correct resource +testManageReturn :: Assertion +testManageReturn = do + result <- runEff $ runResource $ do + resource <- manage (return "test resource") (\_ -> return ()) + return resource + + assertEqual "Manage should return the correct resource" "test resource" result + +-- Test that manageEff returns the correct resource +testManageEffReturn :: Assertion +testManageEffReturn = do + result <- runEff $ runResource $ do + resource <- manageEff (return "test resource") (\_ -> return ()) + return resource + + assertEqual "ManageEff should return the correct resource" "test resource" result + +-- Test a key not found in a region's resources list +-- This indirectly tests the Nothing case in extract +testKeyNotFound :: Assertion +testKeyNotFound = do + let action = runEff $ runResource $ do + -- Get the current region + region1 <- currentRegion + + -- Create a key in a new region + withRegion $ do + (_, key) <- allocate (return "resource") (\_ -> return ()) + + -- We don't need this region variable for the test + -- but we still call currentRegion to check API semantics + _ <- currentRegion + + -- Now we've created a key in this new region + -- But we'll try to move it to region1 after freeing it, + -- which should fail as the key won't be found + free key + move key region1 + + result <- try @InvalidKey action + case result of + Left _ -> return () -- Expected exception + Right _ -> assertFailure "Expected InvalidKey exception but none was thrown" + +-- Test move returns a new key and the resource behavior after moving +testMoveReturnValue :: Assertion +testMoveReturnValue = do + logRef <- newIORef [] + + -- Run the test in a resource context + runEff $ runResource $ do + -- Create a resource with a cleanup action that we can track + (_, key1) <- allocate + (return "resource") + (\_ -> liftIO $ modifyIORef logRef ("Resource freed" :)) + + -- Move the resource to the same region + region <- currentRegion + key2 <- move key1 region + + -- Make sure keys are different + liftIO $ assertBool "Move should return a new key" (key1 /= key2) + + -- Test that the original key is invalid (moved to separate block) + let testInvalidOriginalKey = runEff $ runResource $ do + (_, key1) <- allocate (return "dummy") (\_ -> return ()) + region <- currentRegion + _ <- move key1 region -- Using _ to explicitly ignore the return value + free key1 -- This should throw InvalidKey + + -- Capture the exception without printing anything + result <- try @InvalidKey testInvalidOriginalKey + case result of + Left _ -> return () -- Expected InvalidKey exception, silently pass + Right _ -> assertFailure "Expected InvalidKey exception but none was thrown" + + -- After the test, check that the resource was actually freed + events <- readIORef logRef + assertEqual "Resource should be freed" ["Resource freed"] events + +-- Test defer with return value +testDeferReturn :: Assertion +testDeferReturn = do + resultRef <- newIORef Nothing + + runEff $ runResource $ do + -- Use defer, which creates an empty resource and attaches cleanup + defer $ writeIORef resultRef (Just "defer executed") + + result <- readIORef resultRef + assertEqual "Defer should execute cleanup action" (Just "defer executed") result + +-- Test deferEff with return value +testDeferEffReturn :: Assertion +testDeferEffReturn = do + resultRef <- newIORef Nothing + + runEff $ runResource $ do + -- Use deferEff, which creates an empty resource and attaches effectful cleanup + deferEff $ liftIO $ writeIORef resultRef (Just "deferEff executed") + + result <- readIORef resultRef + assertEqual "DeferEff should execute cleanup action" (Just "deferEff executed") result