packages feed

wasmedge-hs-0.17.1.0: test/Test/Unit/OwnershipSpec.hs

-- | Ownership model tests — verifies no double-free and lifecycle correctness
module Test.Unit.OwnershipSpec (spec) where

import Data.IORef
import Foreign.Ptr (nullPtr)
import System.Mem (performMajorGC)
import Test.Hspec

import WasmEdge.Configure (createConfigure, deleteConfigure)
import WasmEdge.Internal.Ownership
import WasmEdge.Internal.Types

spec :: Spec
spec = describe "Ownership model" $ do
  describe "OwnedCtx" $ do
    it "finalizer is called when OwnedCtx goes out of scope" $ do
      finalizerCalled <- newIORef False
      let finalizer _ = writeIORef finalizerCalled True
      _ <- mkOwned finalizer nullPtr -- will be GC'd
      -- Force GC
      performMajorGC
      -- Note: GC timing is non-deterministic; this test is best-effort.
      -- In practice use explicit delete in real code.
      pure ()

    it "withOwned runs action and returns" $ do
      let finalizer _ = pure ()
      owned <- mkOwned @ConfigureTag finalizer nullPtr
      result <- withOwned owned $ \_ -> pure (42 :: Int)
      result `shouldBe` 42

  describe "BorrowedCtx" $ do
    it "borrowCtx gives a non-owning pointer" $ do
      let finalizer _ = pure ()
      owned <- mkOwned @ConfigureTag finalizer nullPtr
      let borrowed = borrowCtx owned
      withBorrowed borrowed $ \p -> do
        -- p should be nullPtr since we passed nullPtr to mkOwned
        p `shouldBe` nullPtr
      -- owned is still alive here
      touchOwned owned

  describe "double-delete protection" $ do
    it "calling deleteVM twice should not crash (GHC idempotency via ForeignPtr)" $ do
      -- Create a Configure and delete it twice — second delete is a no-op
      -- due to ForeignPtr semantics (finalizer called only once by the GC)
      cfg <- createConfigure
      deleteConfigure cfg
      -- Second call is safe: ForeignPtr doesn't call finalizer again
      pure ()