rhine-1.8: test/Clock/Except.hs
{-# LANGUAGE OverloadedStrings #-}
module Clock.Except where
-- base
import Control.Applicative (Alternative (empty))
import Data.Either (isLeft)
import GHC.IO.Handle (hDuplicateTo)
import System.IO (IOMode (ReadMode), stdin, withFile)
import System.IO.Error (isEOFError)
-- mtl
import Control.Monad.Writer.Class
-- transformers
-- Replace Strict by CPS when bumping mtl to 2.3
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe (MaybeT (..))
import Control.Monad.Trans.Writer.Strict hiding (tell)
-- text
import Data.Text (Text)
-- tasty
import Test.Tasty (TestTree, testGroup)
-- tasty-hunit
import Test.Tasty.HUnit (testCase, (@?), (@?=))
-- rhine
import FRP.Rhine
import FRP.Rhine.Clock.Except (
CatchClock (CatchClock),
DelayIOError,
DelayMonadIOError,
ExceptClock (ExceptClock),
catchClSF,
delayIOError,
delayMonadIOError',
)
import Paths_rhine
tests :: TestTree
tests =
testGroup
"Except"
[exceptClockTests, catchClockTests, delayedClockTests, innerWriterTests]
-- * 'Except'
type E = ExceptT IOError IO
type EClock = ExceptClock StdinClock IOError
exceptClock :: EClock
exceptClock = ExceptClock StdinClock
exceptClockTests :: TestTree
exceptClockTests =
testGroup
"ExceptClock"
[ testCase "Raises the exception in ExceptT on EOF" $ withTestStdin $ do
Left result <- runExceptT $ flow $ clId @@ exceptClock
isEOFError result @? "It's an EOF error"
]
-- ** 'CatchClock'
type TestCatchClock = CatchClock EClock IOError EClock
testClock :: TestCatchClock
testClock = CatchClock exceptClock $ const exceptClock
type ME = MaybeT E
type TestCatchClockMaybe = CatchClock EClock IOError (LiftClock E MaybeT (LiftClock IO (ExceptT IOError) Busy))
testClockMaybe :: TestCatchClockMaybe
testClockMaybe = CatchClock exceptClock (const (liftClock (liftClock Busy)))
catchClockTests :: TestTree
catchClockTests =
testGroup
"CatchClock"
[ testCase "Outputs the exception of the second clock as well" $ withTestStdin $ do
Left result <- runExceptT $ flow $ clId @@ testClock
isEOFError result @? "It's an EOF error"
, testCase "Can recover from an exception" $ withTestStdin $ do
let stopInClsf :: ClSF ME TestCatchClockMaybe () ()
stopInClsf = catchClSF clId $ constMCl empty
result <- runExceptT $ runMaybeT $ flow_ $ stopInClsf @@ testClockMaybe
result @?= Right Nothing
]
-- ** Clock failing at init
{- | This clock throws an exception at initialization.
Useful for testing clock initialization.
-}
data FailingClock = FailingClock
instance (Monad m) => Clock (ExceptT () m) FailingClock where
type Time FailingClock = UTCTime
type Tag FailingClock = ()
initClock FailingClock = throwE ()
{-# INLINE initClock #-}
instance GetClockProxy FailingClock
type CatchFailingClock = CatchClock FailingClock () Busy
catchFailingClock :: CatchFailingClock
catchFailingClock = CatchClock FailingClock $ const Busy
failingClockTests :: TestTree
failingClockTests =
testGroup
"FailingClock"
[ testCase "flow fails immediately" $ do
result <- runExceptT $ flow_ $ clId @@ FailingClock
result @?= Left ()
, testCase "CatchClock recovers from failure at init" $ do
let
clsfStops :: ClSF (MaybeT IO) CatchFailingClock () ()
clsfStops = catchClSF clId $ constM $ lift empty
result <- runMaybeT $ flow_ $ clsfStops @@ catchFailingClock
result @?= Nothing -- The ClSF stopped the execution, not the clock
]
-- ** 'DelayException'
type DelayedClock = DelayIOError StdinClock (Maybe [Text])
delayedClock :: DelayedClock
delayedClock = delayIOError StdinClock $ const Nothing
delayedClockTests :: TestTree
delayedClockTests =
testGroup
"DelayedClock"
[ testCase "DelayException delays error by 1 step" $ withTestStdin $ do
let
throwCollectedText :: ClSF (ExceptT (Maybe [Text]) IO) DelayedClock () ()
throwCollectedText = proc () -> do
tag <- tagS -< ()
textSoFar <- mappendS -< either (const []) pure tag
throwOn' -< (isLeft tag, Just textSoFar)
result <- runExceptT $ flow_ $ throwCollectedText @@ delayedClock
result @?= Left (Just ["data", "test"])
, testCase "DelayException throws error after 1 step" $ withTestStdin $ do
let
dontThrow :: ClSF (ExceptT (Maybe [Text]) IO) DelayedClock () ()
dontThrow = clId
result <- runExceptT $ flow_ $ dontThrow @@ delayedClock
result @?= Left Nothing
]
-- ** Inner writer
{- | 'WriterT' is now the inner monad, meaning that the log survives exceptions.
This way, the state is not lost.
-}
type ClWriterExcept = DelayMonadIOError (ExceptT IOError (WriterT [Text] IO)) StdinClock IOError
clWriterExcept :: ClWriterExcept
clWriterExcept = delayMonadIOError' StdinClock
innerWriterTests :: TestTree
innerWriterTests = testCase "DelayException throws error after 1 step, but can write down results" $ withTestStdin $ do
let
tellStdin :: (MonadWriter [Text] m) => ClSF m ClWriterExcept () ()
tellStdin = catchClSF (tagS >>> arrMCl (tell . pure)) clId
(Left e, result) <- runWriterT $ runExceptT $ flow $ tellStdin @@ clWriterExcept
isEOFError e @? "is EOF"
result @?= ["test", "data"]
-- * Test helpers
-- | Emulate test standard input
withTestStdin :: IO a -> IO a
withTestStdin action = do
testdataFile <- getDataFileName "test/assets/testdata.txt"
withFile testdataFile ReadMode $ \h -> do
hDuplicateTo h stdin
action