packages feed

lsm-tree-1.0.0.1: test-control/Test/Control/RefCount.hs

{-# LANGUAGE CPP          #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Control.RefCount (tests) where

import           Control.Concurrent.Class.MonadMVar
import           Control.Exception (AssertionFailed (..))
import           Control.Monad
import           Control.Monad.Class.MonadThrow
import           Control.Monad.IOSim (IOSim, runSimOrThrow)
import           Control.Monad.Primitive
import           Control.RefCount
import           Data.Primitive.PrimVar
import           Test.Tasty (TestTree, testGroup)
import           Test.Tasty.QuickCheck

#ifdef NO_IGNORE_ASSERTS
import           Data.Primitive
#endif

tests :: TestTree
tests = testGroup "Test.Control.RefCount" [
      testProperty "prop_RefCounter @IO" $
        ioPropertyOnce prop_RefCounter
#ifndef NO_IGNORE_ASSERTS
      -- prop_RefCounter throws and catches AssertionFailed exceptions, but we
      -- can only catch these exceptions in IO. In IOSim, these uncaught
      -- exceptions will lead to property failures. So, we only run the property
      -- in IOSim if assertions are turned off.
    , testProperty "prop_RefCounter @IOSim" $
        ioSimPropertyOnce prop_RefCounter
#endif
#ifdef NO_IGNORE_ASSERTS
      -- All of these tests below are checking that the debug implementation of
      -- Ref does indeed detect all the violations (double-free, use-after-free,
      -- never-free). But this obviously depends on the debug implementation
      -- being in use. Hence only tested when NO_IGNORE_ASSERTS.
    , testGroup "IO" [
          testProperty "prop_ref_double_free" $
            ioPropertyOnce prop_ref_double_free
        , testProperty "prop_ref_use_after_free"  $
            ioPropertyOnce $ prop_ref_use_after_free True
        , testProperty "prop_ref_never_released0" $
            ioPropertyOnce prop_ref_never_released0
        , testProperty "prop_ref_never_released1" $
            ioPropertyOnce prop_ref_never_released1
        , testProperty "prop_ref_never_released2" $
            ioPropertyOnce prop_ref_never_released2
        , testProperty "prop_release_ref_exception" $
            ioPropertyOnce prop_release_ref_exception
        ]
    , testGroup "IOSim" [
          testProperty "prop_ref_double_free" $
            ioSimPropertyOnce prop_ref_double_free
        , testProperty "prop_ref_use_after_free"  $
            -- Exceptions thrown by the DeRef pattern can only be caught from
            -- IO, so we do not test the DeRef pattern in IOSim.
            ioSimPropertyOnce $ prop_ref_use_after_free False
        , testProperty "prop_ref_never_released0" $
            ioSimPropertyOnce prop_ref_never_released0
        , testProperty "prop_ref_never_released1" $
            ioSimPropertyOnce prop_ref_never_released1
        , testProperty "prop_ref_never_released2" $
            ioSimPropertyOnce prop_ref_never_released2
        , testProperty "prop_release_ref_exception" $
            ioSimPropertyOnce prop_release_ref_exception
        ]
#endif
    ]

ioPropertyOnce :: Testable prop => IO prop -> Property
ioPropertyOnce p = once $ ioProperty p

ioSimPropertyOnce :: Testable prop => (forall s. IOSim s prop) -> Property
ioSimPropertyOnce p = once $ property $ runSimOrThrow p

-- | Test for the low level RefCounter API
prop_RefCounter ::
     (MonadMVar m, PrimMonad m, MonadMask m)
  => m Property
prop_RefCounter = do
    obj <- newMVar False
    ref <- newRefCounter (void $ modifyMVar_ obj (\x -> pure (not x)) )

    incrementRefCounter ref
    n1 <- readRefCount ref -- 2
    b1 <- readMVar obj -- False

    decrementRefCounter ref
    n2 <- readRefCount ref -- 1
    b2 <- readMVar obj -- False

    e2w <- tryIncrementRefCounter ref -- ok, True
    n2w <- readRefCount ref -- 2
    b2w <- readMVar obj -- False
    decrementRefCounter ref

    decrementRefCounter ref
    n3 <- readRefCount ref -- 0
    b3 <- readMVar obj -- True, finaliser ran

    e4 <- try (decrementRefCounter ref) -- error
    n4 <- readRefCount ref -- -1
    b4 <- readMVar obj -- True, finaliser did not run again

    e5 <- try (incrementRefCounter ref) -- error
    n5 <- readRefCount ref -- 0
    b5 <- readMVar obj -- True, finaliser did not run again

    e6 <- tryIncrementRefCounter ref
    n6 <- readRefCount ref -- 0
    b6 <- readMVar obj -- True, finaliser did not run again

    pure $
        counterexample "n1" (n1 == 2) .&&.
        counterexample "b1" (not b1) .&&.

        counterexample "n2" (n2 == 1) .&&.
        counterexample "b2" (not b2) .&&.

        counterexample "e2w" (e2w == True) .&&.
        counterexample "n2w" (n2w == 2) .&&.
        counterexample "b2w" (not b2w) .&&.

        counterexample "n3" (n3 == 0) .&&.
        counterexample "b3" b3 .&&.

        counterexample "e4" (check e4) .&&.
        counterexample "n4" (n4 == (-1)) .&&.
        counterexample "b4" b4 .&&.

        counterexample "e5" (check e5) .&&.
        counterexample "n5" (n5 == 0) .&&.
        counterexample "b5" b5 .&&.

        counterexample "e6" (e6 == False) .&&.
        counterexample "n6" (n6 == 0) .&&.
        counterexample "b6" b6
  where
#ifdef NO_IGNORE_ASSERTS
    check = \case Left (AssertionFailed _) -> True; Right () -> False
#else
    check = \case Left (AssertionFailed _) -> False; Right () -> True
#endif

-- | Warning: reading the current reference count is inherently racy as there
-- is no way to reliably act on the information. It is only useful for tests or
-- debugging.
--
readRefCount :: PrimMonad m => RefCounter m -> m Int
readRefCount (RefCounter countVar _) = readPrimVar countVar

#ifdef NO_IGNORE_ASSERTS
data TestObject m = TestObject !(RefCounter m)

instance RefCounted m (TestObject m) where
    getRefCounter (TestObject rc) = rc

data TestObject2 m = TestObject2 (Ref (TestObject m))

instance RefCounted m (TestObject2 m) where
    getRefCounter (TestObject2 (DeRef to1)) = getRefCounter to1

prop_ref_double_free ::
     (PrimMonad m, MonadMask m, MonadFail m)
  => m Property
prop_ref_double_free = withRefCtx $ \refCtx -> do
    finalised <- newMutVar False
    ref <- newRef refCtx (writeMutVar finalised True) TestObject
    releaseRef ref
    True <- readMutVar finalised
    Left e@RefDoubleRelease{} <- try $ releaseRef ref
    checkForgottenRefs refCtx
    -- Print the displayed exception as an example
    pure $ tabulate "displayException" [displayException e] ()

prop_ref_use_after_free ::
     (PrimMonad m, MonadMask m, MonadFail m)
  => Bool -- ^ Test the DeRef pattern
  -> m Property
prop_ref_use_after_free testDeRef = withRefCtx $ \refCtx -> do
    finalised <- newMutVar False
    ref <- newRef refCtx (writeMutVar finalised True) TestObject
    releaseRef ref
    True <- readMutVar finalised
    Left e@RefUseAfterRelease{} <- try $ withRef ref return
    when testDeRef $ do
      Left RefUseAfterRelease{} <- try $ case ref of DeRef _ -> pure ()
      pure ()
    Left RefUseAfterRelease{} <- try $ dupRef ref
    checkForgottenRefs refCtx
    -- Print the displayed exception as an example
    pure $ tabulate "displayException" [displayException e] ()

prop_ref_never_released0 ::
     (PrimMonad m, MonadMask m)
  => m ()
prop_ref_never_released0 = withRefCtx $ \refCtx -> do
    finalised <- newMutVar False
    ref <- newRef refCtx (writeMutVar finalised True) TestObject
    _ <- case ref of DeRef _ -> pure ()
    checkForgottenRefs refCtx
    -- ref is still being used, so check should not fail
    _ <- case ref of DeRef _ -> pure ()
    releaseRef ref

prop_ref_never_released1 ::
     (PrimMonad m, MonadMask m)
  => m Property
prop_ref_never_released1 = withRefCtx $ \refCtx -> do
    handle expectRefNeverReleased $ do
      finalised <- newMutVar False
      ref <- newRef refCtx (writeMutVar finalised True) TestObject
      _ <- withRef ref return
      _ <- case ref of DeRef _ -> pure ()
      -- ref is never released, so should fail
      checkForgottenRefs refCtx
      pure (counterexample "no forgotten refs detected" $ property False)

prop_ref_never_released2 ::
     (PrimMonad m, MonadMask m)
  => m Property
prop_ref_never_released2 = withRefCtx $ \refCtx -> do
    handle expectRefNeverReleased $ do
      finalised <- newMutVar False
      ref  <- newRef refCtx (writeMutVar finalised True) TestObject
      ref2 <- dupRef ref
      releaseRef ref
      _ <- withRef ref2 return
      _ <- case ref2 of DeRef _ -> pure ()
      -- ref2 is never released, so should fail
      checkForgottenRefs refCtx
      pure (counterexample "no forgotten refs detected" $ property False)

expectRefNeverReleased :: Monad m => RefException -> m Property
expectRefNeverReleased e@RefNeverReleased{} =
    -- Print the displayed exception as an example
    pure (tabulate "displayException" [displayException e] (property True))
expectRefNeverReleased e =
    pure (counterexample (displayException e) $ property False)

-- | If a finaliser throws an exception, then the 'RefTracker' is still released
prop_release_ref_exception ::
     (PrimMonad m, MonadMask m)
  => m ()
prop_release_ref_exception = withRefCtx $ \refCtx -> do
    finalised <- newMutVar False
    ref  <- newRef refCtx (writeMutVar finalised True >> throwIO (userError "oops")) TestObject
    _ <- try @_ @SomeException (releaseRef ref)
    checkForgottenRefs refCtx
#endif