packages feed

blockio-uring-0.2.0.0: test/test.hs

{-# OPTIONS_GHC -Wno-orphans #-}

{- HLINT ignore "Use camelCase" -}

module Main (main) where

import           Control.Exception        (Exception (displayException),
                                           IOException, SomeException, try)
import           Control.Monad            (void)
import           Data.List                (isPrefixOf)
import qualified Data.Primitive.ByteArray as P
import qualified Data.Vector              as V
import           GHC.IO.Exception         (IOException (ioe_location))
import           GHC.IO.FD                (FD (..))
import           GHC.IO.Handle.FD         (handleToFd)
import           System.IO
import           System.IO.BlockIO
import           Test.Tasty
import           Test.Tasty.HUnit
import           Test.Tasty.QuickCheck

main :: IO ()
main = defaultMain tests

tests :: TestTree
tests = testGroup "test"
    [ testCase "example_initClose" example_initClose
    , testCase "example_initReadClose 32" $ example_initReadClose 32
    , testCase "example_initReadClose 96" $ example_initReadClose 96
    , testCase "example_initReadClose 200" $ example_initReadClose 200
    , testCase "example_initEmptyClose" example_initEmptyClose
    , testCase "example_closeIsIdempotent" example_closeIsIdempotent
    , testProperty "prop_ValidIOCtxParams" prop_ValidIOCtxParams
    ]

example_initClose :: Assertion
example_initClose = do
    ctx <- initIOCtx defaultIOCtxParams
    closeIOCtx ctx

example_initReadClose :: Int -> Assertion
example_initReadClose size = do
    ctx <- initIOCtx defaultIOCtxParams
    withFile "blockio-uring.cabal" ReadMode $ \hdl -> do
        -- handleToFd is available since base-4.16.0.0
        FD { fdFD = fromIntegral -> fd } <- handleToFd hdl
        mba <- P.newPinnedByteArray 10 -- TODO: shouldn't use the same array for all ops :)
        void $ submitIO ctx $ V.replicate size $
            IOOpRead fd 0 mba 0 10
    closeIOCtx ctx

example_initEmptyClose :: Assertion
example_initEmptyClose = do
    ctx <- initIOCtx defaultIOCtxParams
    _ <- submitIO ctx V.empty
    closeIOCtx ctx

example_closeIsIdempotent :: Assertion
example_closeIsIdempotent = do
    ctx <- initIOCtx defaultIOCtxParams
    closeIOCtx ctx
    eith <- try (closeIOCtx ctx)
    case eith of
      Left e ->
        assertFailure ("Close on a closed context threw an error : " <> show (e :: SomeException))
      Right () ->
        pure ()

{-------------------------------------------------------------------------------
  Valid IOCtxParams
-------------------------------------------------------------------------------}

-- | We test @validateIOCtxParams@ through 'withIOCtx'. If any params slip
-- through the cracks, then the call to @setupURing@ inside 'withIOCtx' should
-- throw an unexpected exception, which causes the property to fail.
prop_ValidIOCtxParams :: IOCtxParams -> Property
prop_ValidIOCtxParams params@IOCtxParams{..} =
    checkCoverage $
    coverTable "Result" [("Success", 5)] $
    ioProperty $ do
      eith <- try @IOException $ withIOCtx params $ \_ctx -> pure ()
      pure $ case eith of
        Left e
          |  "IOCtxParams are invalid" `isPrefixOf` ioe_location e
            &&
            not (
              inBoundsExcl ioctxBatchSizeLimit batchSizeLimitBoundsExcl &&
              inBoundsExcl ioctxConcurrencyLimit (concurrencyLimitBoundsExcl (ioctxBatchSizeLimit - 1))
            )
          -> tabulate "Result" [displayException e] True
          | otherwise
          -> counterexample ("Unknown exception: " ++ displayException e) False
        Right () -> tabulate "Result" ["Success"] True
  where
    inBoundsExcl x (lb, ub) = lb < x && x < ub

    batchSizeLimitBoundsExcl = (0, 2^(15 :: Int))
    concurrencyLimitBoundsExcl batchSizeLimit =  (batchSizeLimit, 2^(16::Int))

instance Arbitrary IOCtxParams where
  arbitrary = IOCtxParams <$> genLimit <*> genLimit <*> arbitrary
  shrink (IOCtxParams a b i) =
      [ IOCtxParams a' b' i' | (a', (b', i')) <- liftShrink2 shrinkLimit (liftShrink2 shrinkLimit shrink) (a, (b, i)) ]

genLimit :: Gen Int
genLimit = frequency [
      -- Generate powers of 2 to hit the upper bounds on the batch size limit
      -- and concurrency limit
      (10, genPowerOf2)
      -- Get some coverage of the whole range between the lower and upper
      -- bounds on the batch size limit and concurrency limit.
    , (10, chooseNonNegativeInt)
      -- Otherwise, generate integers like normal
    , (10, arbitrary)
    ]

shrinkLimit :: Int -> [Int]
shrinkLimit x = shrinkPowerOf2 x ++ shrinkNonNegativeInt x ++ shrink x

genPowerOf2 :: Gen Int
genPowerOf2 = do
    i <- chooseInt (minExponent, maxExponent)
    pure (2^i)

shrinkPowerOf2 :: Int -> [Int]
shrinkPowerOf2 x = [x' | i <- [ minExponent .. maxExponent ], let x' = 2^i, x' < x]

minExponent, maxExponent :: Int
minExponent = 0
maxExponent = 20

chooseNonNegativeInt :: Gen Int
chooseNonNegativeInt = chooseInt (minValue, maxValue)

shrinkNonNegativeInt :: Int -> [Int]
shrinkNonNegativeInt x = [ x' | NonNegative x' <- shrink (NonNegative x) ]

minValue, maxValue :: Int
minValue = 0
maxValue = 2^maxExponent