fs-api-0.3.0.0: test/Test/System/FS/IO.hs
{-# LANGUAGE BangPatterns #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Test.System.FS.IO (tests) where
import Control.Monad.Primitive
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Short.Internal as SBS
import Data.Primitive.ByteArray
import Prelude hiding (read)
import qualified System.FS.API as FS
import qualified System.FS.IO as IO
import System.IO.Temp
import System.Posix.Types (ByteCount)
import Test.Tasty
import Test.Tasty.QuickCheck
tests :: TestTree
tests = testGroup "Test.System.FS.IO" [
testProperty "prop_roundtrip_hPutGetBufSome"
prop_roundtrip_hPutGetBufSome
, testProperty "prop_roundtrip_hPutGetBufSomeAt"
prop_roundtrip_hPutGetBufSomeAt
, testProperty "prop_roundtrip_hPutGetBufExactly"
prop_roundtrip_hPutGetBufExactly
, testProperty "prop_roundtrip_hPutGetBufExactlyAt"
prop_roundtrip_hPutGetBufExactlyAt
]
instance Arbitrary ByteString where
arbitrary = BS.pack <$> arbitrary
shrink = fmap BS.pack . shrink . BS.unpack
instance Arbitrary FS.AbsOffset where
arbitrary = FS.AbsOffset . getSmall <$> arbitrary
shrink (FS.AbsOffset x) = FS.AbsOffset <$> shrink x
fromByteString :: PrimMonad m => ByteString -> m (MutableByteArray (PrimState m))
fromByteString bs = thawByteArray (ByteArray ba) 0 (SBS.length sbs)
where !sbs@(SBS.SBS ba) = SBS.toShort bs
toByteString :: PrimMonad m => Int -> MutableByteArray (PrimState m) -> m ByteString
toByteString n mba = freezeByteArray mba 0 n >>= \(ByteArray ba) -> pure (SBS.fromShort $ SBS.SBS ba)
-- | A write-then-read roundtrip test for buffered I\/O in 'IO'.
--
-- This does not test what happens if we try to write/read more bytes than fits
-- in the buffer, because the behaviour is then undefined.
prop_roundtrip_hPutGetBufSome ::
ByteString
-> Small ByteCount -- ^ Prefix length
-> Property
prop_roundtrip_hPutGetBufSome bs (Small c) =
BS.length bs >= fromIntegral c ==>
ioProperty $ withSystemTempDirectory "prop_roundtrip_hPutGetBufSome" $ \dirPath -> do
let hfs = IO.ioHasFS (FS.MountPoint dirPath)
FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
putBuf <- fromByteString bs
m <- FS.hPutBufSome hfs h putBuf 0 c
let writeTest = counterexample "wrote too many bytes" ((if c > 0 then 1 .<= m else property True) .&&. m .<= c)
FS.hSeek hfs h FS.AbsoluteSeek 0
getBuf <- newPinnedByteArray (fromIntegral m)
o <- FS.hGetBufSome hfs h getBuf 0 m
let readTest = counterexample "read too many bytes" ((if c > 0 then 1 .<= o else property True) .&&. o .<= m)
bs' <- toByteString (fromIntegral o) getBuf
let cmpTest = counterexample "(prefix of) input and output bytestring do not match"
$ BS.take (fromIntegral o) bs === bs'
pure (writeTest .&&. readTest .&&. cmpTest)
-- | Like 'prop_roundtrip_hPutGetBufSome', but reading and writing at a specified offset.
prop_roundtrip_hPutGetBufSomeAt ::
ByteString
-> Small ByteCount -- ^ Prefix length
-> FS.AbsOffset
-> Property
prop_roundtrip_hPutGetBufSomeAt bs (Small c) off =
BS.length bs >= fromIntegral c ==>
ioProperty $ withSystemTempDirectory "prop_roundtrip_hPutGetBufSomeAt" $ \dirPath -> do
let hfs = IO.ioHasFS (FS.MountPoint dirPath)
FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
putBuf <- fromByteString bs
m <- FS.hPutBufSomeAt hfs h putBuf 0 c off
let writeTest = counterexample "wrote too many bytes" ((if c > 0 then 1 .<= m else property True) .&&. m .<= c)
getBuf <- newPinnedByteArray (fromIntegral m)
o <- FS.hGetBufSomeAt hfs h getBuf 0 m off
let readTest = counterexample "read too many bytes" ((if c > 0 then 1 .<= o else property True) .&&. o .<= m)
bs' <- toByteString (fromIntegral o) getBuf
let cmpTest = counterexample "(prefix of) input and output bytestring do not match"
$ BS.take (fromIntegral o) bs === bs'
pure (writeTest .&&. readTest .&&. cmpTest)
-- | Like 'prop_roundtrip_hPutGetBufSome', but ensuring that all bytes are
-- written/read.
prop_roundtrip_hPutGetBufExactly ::
ByteString
-> Small ByteCount -- ^ Prefix length
-> Property
prop_roundtrip_hPutGetBufExactly bs (Small c) =
BS.length bs >= fromIntegral c ==>
ioProperty $ withSystemTempDirectory "prop_roundtrip_hPutGetBufExactly" $ \dirPath -> do
let hfs = IO.ioHasFS (FS.MountPoint dirPath)
FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
putBuf <- fromByteString bs
m <- FS.hPutBufExactly hfs h putBuf 0 c
let writeTest = counterexample "wrote too few bytes" (m === c)
FS.hSeek hfs h FS.AbsoluteSeek 0
getBuf <- newPinnedByteArray (fromIntegral c)
o <- FS.hGetBufExactly hfs h getBuf 0 c
let readTest = counterexample "read too few byes" (o === c)
bs' <- toByteString (fromIntegral c) getBuf
let cmpTest = counterexample "input and output bytestring do not match"
$ BS.take (fromIntegral c) bs === BS.take (fromIntegral c) bs'
pure (writeTest .&&. readTest .&&. cmpTest)
-- | Like 'prop_roundtrip_hPutGetBufSome', but reading and writing at a
-- specified offset, and ensuring that all bytes are written/read.
prop_roundtrip_hPutGetBufExactlyAt ::
ByteString
-> Small ByteCount -- ^ Prefix length
-> FS.AbsOffset
-> Property
prop_roundtrip_hPutGetBufExactlyAt bs (Small c) off =
BS.length bs >= fromIntegral c ==>
ioProperty $ withSystemTempDirectory "prop_roundtrip_hPutGetBufExactlyAt" $ \dirPath -> do
let hfs = IO.ioHasFS (FS.MountPoint dirPath)
FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
putBuf <- fromByteString bs
m <- FS.hPutBufExactlyAt hfs h putBuf 0 c off
let writeTest = counterexample "wrote too few bytes" (m === c)
getBuf <- newPinnedByteArray (fromIntegral c)
o <- FS.hGetBufExactlyAt hfs h getBuf 0 c off
let readTest = counterexample "read too few byes" (o === c)
bs' <- toByteString (fromIntegral c) getBuf
let cmpTest = counterexample "input and output bytestring do not match"
$ BS.take (fromIntegral c) bs === BS.take (fromIntegral c) bs'
pure (writeTest .&&. readTest .&&. cmpTest)
infix 4 .<=
(.<=) :: (Ord a, Show a) => a -> a -> Property
x .<= y = counterexample (show x ++ interpret res ++ show y) res
where
res = x <= y
interpret True = " <= "
interpret False = " > "