packages feed

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 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