packages feed

streamly-0.11.0: test/Streamly/Test/FileSystem/Handle.hs

-- |
-- Module      : Streamly.Test.FileSystem.Handle
-- Copyright   : (c) 2020 Composewell technologies
-- License     : BSD-3-Clause
-- Maintainer  : streamly@composewell.com
-- Stability   : experimental
-- Portability : GHC

module Streamly.Test.FileSystem.Handle (main) where

import Data.Functor.Identity (runIdentity)
import Data.Word (Word8)
import Streamly.Internal.Data.Stream (Stream)
import Streamly.Internal.System.IO (defaultChunkSize)
import System.FilePath ((</>))
import System.IO
    ( Handle
    , IOMode(..)
    , SeekMode(..)
    , hClose
    , hFlush
    , hSeek
    , hPutStr
    )
import System.IO.Temp (withSystemTempDirectory)
#if !defined(mingw32_HOST_OS) && !defined(__MINGW32__)
import Streamly.Internal.FileSystem.Posix.File (openFile, withFile)
#else
import Streamly.Internal.FileSystem.Windows.File (openFile, withFile)
#endif
import Test.QuickCheck (Property, forAll, Gen, vectorOf, choose)
import Test.QuickCheck.Monadic (monadicIO, assert, run)
import Streamly.Internal.FileSystem.Path (Path)

import qualified Streamly.Data.Fold as Fold
import qualified Streamly.Internal.FileSystem.Handle as Handle
import qualified Streamly.Internal.Data.Stream as Stream
import qualified Streamly.Internal.Data.Array as Array
import qualified Streamly.Internal.Unicode.Stream as Unicode
import qualified Streamly.Internal.FileSystem.Path as Path

import Prelude hiding (writeFile)
import Test.Hspec as H
import Test.Hspec.QuickCheck

writeFile :: Path -> String -> IO ()
writeFile fpath val = withFile fpath WriteMode (`hPutStr` val)

maxArrLen :: Int
maxArrLen = defaultChunkSize * 8

maxTestCount :: Int
maxTestCount = 10

chooseWord8 :: (Word8, Word8) -> Gen Word8
chooseWord8 = choose

toList :: Monad m => Stream m a -> m [a]
toList = Stream.fold Fold.toList

utf8ToString :: Array.Array Word8 -> String
utf8ToString =
    runIdentity . toList . Unicode.decodeUtf8' . Array.read

testData :: String
testData = "This is the test data for FileSystem.Handle ??`!@#$%^&*~~))`]"

testDataLarge :: String
testDataLarge = concat $ replicate 6000 testData

testBinData :: String
testBinData = "01234567890123456789012345678901234567890123456789"

executor :: (Handle -> Stream IO Char) -> IO (Stream IO Char)
executor f =
    withSystemTempDirectory "fs_handle" $ \fp -> do
        fpath <- Path.fromString $ fp </> "tmp_read.txt"
        writeFile fpath testDataLarge
        h <- openFile fpath ReadMode
        return $ f h

readFromHandle :: IO (Stream IO Char)
readFromHandle =
    let f = Unicode.decodeUtf8 . Stream.unfold Handle.reader
    in executor f

readWithBufferFromHandle :: IO (Stream IO Char)
readWithBufferFromHandle =
    let f1 h = (1024, h)
        f2 = Unicode.decodeUtf8 . Stream.unfold Handle.readerWith . f1
    in executor f2

readChunksFromHandle :: IO (Stream IO Char)
readChunksFromHandle =
    let f =   Unicode.decodeUtf8
            . Stream.concatMap Array.read
            . Stream.unfold Handle.chunkReader
    in executor f

readChunksWithBuffer :: IO (Stream IO Char)
readChunksWithBuffer =
    let f1 h = (1024, h)
        f2 =
              Unicode.decodeUtf8
            . Stream.concatMap Array.read
            . Stream.unfold Handle.chunkReaderWith
            . f1
    in executor f2

testRead :: IO (Stream IO Char) -> Property
testRead fn = monadicIO $ do
    let v2 = Stream.fromList testDataLarge
    v1 <- run fn
    res <- run $ Stream.eqBy (==) v1 v2
    assert res

testWrite :: (Handle -> Fold.Fold IO Word8 ()) -> Property
testWrite hfold =
    forAll (choose (0, maxArrLen)) $ \len ->
        forAll (vectorOf len $ chooseWord8 (0, 255)) $ \list0 ->
            monadicIO $ do
                res <- run $ go list0
                assert res

                where

                go list =
                    withSystemTempDirectory "fs_handle" $ \fp -> do
                        fpathWrite <- Path.fromString $ fp </> "tmp_write.txt"
                        writeFile fpathWrite ""
                        h <- openFile fpathWrite ReadWriteMode
                        hSeek h AbsoluteSeek 0
                        _ <- Stream.fold (hfold h) $ Stream.fromList list
                        hFlush h
                        hSeek h AbsoluteSeek 0
                        ls <- toList $ Stream.unfold Handle.reader h
                        hClose h
                        return (ls == list)

testWriteWithChunk :: Property
testWriteWithChunk =
    monadicIO $ do
        res <- run go
        assert res

        where

        go =
            withSystemTempDirectory "fs_handle" $ \fp -> do
                fpathRead <- Path.fromString $ fp </> "tmp_read.txt"
                fpathWrite <- Path.fromString $ fp </> "tmp_write.txt"
                writeFile fpathRead testDataLarge
                writeFile fpathWrite ""
                hr <- openFile fpathRead ReadMode
                hw <- openFile fpathWrite ReadWriteMode
                hSeek hw AbsoluteSeek 0
                _ <- Stream.fold (Handle.writeChunks hw)
                    $ Stream.unfold Handle.chunkReaderWith (1024, hr)
                hFlush hw
                hSeek hw AbsoluteSeek 0
                ls <- toList $ Stream.unfold Handle.reader hw
                let arr = Array.fromList ls
                return (testDataLarge == utf8ToString arr)

testReadChunksFromToWith :: Int -> Int -> Int -> [[Word8]] -> Property
testReadChunksFromToWith from to buffSize res = monadicIO $ run go

    where

    go =
        withSystemTempDirectory "fs_handle" $ \fp -> do
            fpathRead <- Path.fromString $ fp </> "tmp_read.txt"
            writeFile fpathRead testBinData
            h <- openFile fpathRead ReadMode
            ls <-
                toList
                    $ Stream.unfold
                        Handle.chunkReaderFromToWith (from, to, buffSize, h)
            return (res `shouldBe` fmap Array.toList ls)

-- Test for first byte
testReadChunksFromToWithFirstByte :: Property
testReadChunksFromToWithFirstByte = testReadChunksFromToWith 0 0 15 [[48]]

-- Test for second byte
testReadChunksFromToWithSecondByte :: Property
testReadChunksFromToWithSecondByte = testReadChunksFromToWith 1 1 15 [[49]]

-- Test for second to 10th bytes
testReadChunksFromToWithSecondToTenthBytes :: Property
testReadChunksFromToWithSecondToTenthBytes =
    testReadChunksFromToWith 1 10 15 [[49,50,51,52,53,54,55,56,57,48]]

-- Test for offset of buffer size
testReadChunksFromToWithBuffSizeOffset :: Property
testReadChunksFromToWithBuffSizeOffset =
    testReadChunksFromToWith 15 25 15 [[53,54,55,56,57,48,49,50,51,52,53]]

-- Test with multi buffer size
testReadChunksFromToWithMultiBuff :: Property
testReadChunksFromToWithMultiBuff =
    testReadChunksFromToWith
        5 22 5 [[53,54,55,56,57],[48,49,50,51,52],[53,54,55,56,57],[48,49,50]]

testReadChunksFromToWithRangeInvalid :: Property
testReadChunksFromToWithRangeInvalid = testReadChunksFromToWith 15 5 15 []

moduleName :: String
moduleName = "FileSystem.Handle"

main :: IO ()
main =
    hspec $
    H.parallel $
    modifyMaxSuccess (const maxTestCount) $ do
      describe moduleName $ do
        describe "Read From Handle" $ do
            prop "read" $ testRead readFromHandle
            prop "readWith" $ testRead readWithBufferFromHandle
            prop "readChunks" $ testRead readChunksFromHandle
            prop "readChunksWith" $ testRead readChunksWithBuffer
            prop "readChunksFromToWith (0,0,n)"
                testReadChunksFromToWithFirstByte
            prop "readChunksFromToWith (1,1,n)"
                testReadChunksFromToWithSecondByte
            prop "readChunksFromToWith (1,10,n)"
                testReadChunksFromToWithSecondToTenthBytes
            prop "readChunksFromToWith (n,<2n,n)"
                testReadChunksFromToWithBuffSizeOffset
            prop "readChunksFromToWith (n,>2n,n)"
                testReadChunksFromToWithMultiBuff
            prop "readChunksFromToWith (n,<n,n)"
                testReadChunksFromToWithRangeInvalid
        describe "Write To Handle" $ do
            prop "write" $ testWrite Handle.write
            prop "writeWith"
                $ testWrite $ Handle.writeWith 1024
            -- XXX This test needs a lot of stack when built with -O0
            prop "writeChunks" testWriteWithChunk