binary-io-0.0.1: test/Data/Binary/IOSpec.hs
{-# LANGUAGE DeriveAnyClass #-}
module Data.Binary.IOSpec (spec) where
import Prelude hiding (read)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad (join)
import Control.Exception (Exception, throw)
import Data.Typeable (typeOf)
import Data.List (isInfixOf)
import Data.Binary.IO
import Data.Binary (Binary (..))
import Data.Bifoldable (bitraverse_)
import qualified Test.Hspec as Hspec
import System.Process (createPipe)
import qualified System.IO as IO
import System.IO.Error (isIllegalOperation, ioeGetErrorString)
-- | Create a pipe with no buffering on read and write side.
createUnbufferedPipe :: IO (IO.Handle, IO.Handle)
createUnbufferedPipe = do
handles <- createPipe
join bitraverse_ (`IO.hSetBuffering` IO.NoBuffering) handles
pure handles
-- | The 'Binary' instance of this type implements a 'get' that always fails
data BadGet
instance Binary BadGet where
put = error "Not implemented"
get = fail "get for BadGet will always"
data ExampleException = ExampleException
deriving (Show, Exception)
-- | Check that a read from the 'IO.Handle' yields the given value.
shouldRead :: (Show a, Eq a, Binary a) => Reader -> a -> Hspec.Expectation
shouldRead reader expectedValue = do
value <- read reader
Hspec.shouldBe value expectedValue
-- | Close a handle and verify.
closeHandle :: IO.Handle -> Hspec.Expectation
closeHandle handle = do
IO.hClose handle
closed <- IO.hIsClosed handle
Hspec.shouldBe closed True
spec :: Hspec.Spec
spec = Hspec.before createUnbufferedPipe $ do
Hspec.describe "Reader" $ do
let
testReads value =
Hspec.it ("reads " <> show (typeOf value)) $ \(handleRead, handleWrite) -> do
reader <- liftIO (newReader handleRead)
write handleWrite value
shouldRead reader value
write handleWrite value
write handleWrite value
shouldRead reader value
shouldRead reader value
-- Test something with 0 length
testReads ()
-- Test something with fixed non-zero length
testReads (1337 :: Int)
-- Test something with variable length
testReads "Hello World"
-- When the read handle has reached its end, reading from it should not throw an error.
-- However, no more input can be read therefore the underling 'Get' parser should fail.
Hspec.it "throws ReaderGetError when Handle is EOF" $ \(handleRead, handleWrite) -> do
reader <- liftIO (newReader handleRead)
IO.hClose handleWrite
eof <- IO.hIsEOF handleRead
Hspec.shouldBe eof True
Hspec.shouldThrow (read reader :: IO String) (\ReaderGetError{} -> True)
-- Reading from a closed handle should throw. That exception needs to surface.
Hspec.it "throws IllegalOperation when read Handle is closed" $ \(handleRead, _handleWrite) -> do
reader <- liftIO (newReader handleRead)
closeHandle handleRead
Hspec.shouldThrow (read reader :: IO String) isIllegalOperation
-- Failing 'Get' operations should not advance the stream position.
Hspec.it "preserves the stream position when Get operation fails" $ \(handleRead, handleWrite) -> do
reader <- liftIO (newReader handleRead)
write handleWrite "Hello World"
Hspec.shouldThrow (read reader :: IO BadGet) (\ReaderGetError{} -> True)
"Hello World" <- read reader
pure ()
-- Failing continuations should not advance the stream position.
Hspec.it "preserves the stream position when continuation fails" $ \(handleRead, handleWrite) -> do
reader <- liftIO (newReader handleRead)
write handleWrite "Hello World"
Hspec.shouldThrow
(readWith reader (\() -> throw ExampleException))
(\ExampleException -> True)
"Hello World" <- read reader
pure ()
Hspec.describe "Writer" $ do
let
testWrites value =
Hspec.it ("writes " <> show (typeOf value)) $ \(handleRead, handleWrite) -> do
let writer = newWriter handleWrite
reader <- newReader handleRead
write writer value
shouldRead reader value
write writer value
write writer value
shouldRead reader value
shouldRead reader value
-- Test something with 0 length
testWrites ()
-- Test something with fixed non-zero length
testWrites (1337 :: Int)
-- Test something with variable length
testWrites "Hello World"
Hspec.it "throws ResourceVanished when read Handle is closed" $ \(handleRead, handleWrite) -> do
let writer = newWriter handleWrite
closeHandle handleRead
Hspec.shouldThrow (write writer "Hello World") $ \exception ->
isInfixOf "resource vanished" (ioeGetErrorString exception)
Hspec.it "throws IllegalOperation when write Handle is closed" $ \(_handleRead, handleWrite) -> do
let writer = newWriter handleWrite
closeHandle handleWrite
Hspec.shouldThrow (write writer "Hello World") isIllegalOperation