tinyapp-0.2.1.1: src/TinyApp/Interactive/Test.hs
module TinyApp.Interactive.Test
( TestCase,
InteractiveM,
(~>),
runTestsFor,
expectRenderEq,
expectRenderIs,
expectStateEq,
expectStateIs,
expectContinue,
expectExit,
pressKey,
pressKeys,
pressKey',
inputString,
sendEvent,
)
where
import Control.Monad (forM, forM_)
import Control.Monad.Reader qualified as MTL
import Data.IORef
import GHC.Stack qualified as Stack
import System.Exit (exitFailure, exitSuccess)
import System.IO.Error (ioeGetErrorString, tryIOError)
import TinyApp.Interactive
data AppState s = AppState
{ state :: s,
continue :: ContinueExit,
output :: String
}
data Env s = Env
{ app :: Sandbox s,
appState :: IORef (AppState s)
}
newtype InteractiveM s a = InteractiveM (MTL.ReaderT (Env s) IO a)
deriving (Functor, Applicative, Monad, MTL.MonadIO, MonadFail)
data TestCase s = TestCase
{ description :: String,
t :: InteractiveM s ()
}
infix 0 ~>
(~>) :: String -> InteractiveM s () -> TestCase s
(~>) description test = TestCase {description = description, t = test}
runTestsFor :: Sandbox s -> [TestCase s] -> IO ()
runTestsFor app tests = do
res <-
forM
tests
( \TestCase {description, t} -> do
res <- tryIOError (runInteractiveTest app t)
case res of
Left err -> do
putStrLn ("🛑 " <> description <> ": " <> ioeGetErrorString err)
pure False
Right _ -> do
putStrLn ("✅ " <> description)
pure True
)
if and res
then
exitSuccess
else
exitFailure
expectRenderEq :: (Stack.HasCallStack) => String -> InteractiveM s ()
expectRenderEq value = do
AppState {output} <- readAppState
_expectEq Stack.callStack output value
expectRenderIs :: (Stack.HasCallStack, Show s, Eq s) => (String -> Bool) -> InteractiveM s ()
expectRenderIs f = do
AppState {output} <- readAppState
_expectIs Stack.callStack "render" output f
expectStateEq :: (Stack.HasCallStack, Show s, Eq s) => s -> InteractiveM s ()
expectStateEq value = do
AppState {state} <- readAppState
_expectEq Stack.callStack state value
expectStateIs :: (Stack.HasCallStack, Show s, Eq s) => (s -> Bool) -> InteractiveM s ()
expectStateIs f = do
AppState {state} <- readAppState
_expectIs Stack.callStack "state" state f
expectContinue :: (Stack.HasCallStack) => InteractiveM s ()
expectContinue =
_expectContinue Stack.callStack
_expectContinue :: Stack.CallStack -> InteractiveM s ()
_expectContinue callstack = do
AppState {continue} <- readAppState
_expectEq callstack continue Continue
expectExit :: (Stack.HasCallStack) => InteractiveM s ()
expectExit = do
AppState {continue} <- readAppState
_expectEq Stack.callStack continue Exit
inputString :: String -> InteractiveM s ()
inputString str = pressKeys (map KChar str)
pressKey :: (Stack.HasCallStack) => Key -> InteractiveM s ()
pressKey key = Stack.withFrozenCallStack $ pressKey' key []
pressKeys :: (Stack.HasCallStack) => [Key] -> InteractiveM s ()
pressKeys keys = Stack.withFrozenCallStack $ forM_ keys pressKey
pressKey' :: (Stack.HasCallStack) => Key -> [Modifier] -> InteractiveM s ()
pressKey' key modifiers = _sendEvent Stack.callStack (Key key modifiers)
sendEvent :: (Stack.HasCallStack) => Event -> InteractiveM s ()
sendEvent = _sendEvent Stack.callStack
_sendEvent :: Stack.CallStack -> Event -> InteractiveM s ()
_sendEvent callstack event = do
_expectContinue callstack
app' <- getApp
InteractiveM $ do
appState' <- MTL.asks appState
MTL.liftIO $ modifyIORef appState' (updateAppState app' event)
updateAppState :: Sandbox s -> Event -> AppState s -> AppState s
updateAppState app event AppState {state} =
let (state', continue') = app.update event state
output' = app.render state'
in AppState
{ state = state',
continue = continue',
output = output'
}
getApp :: InteractiveM s (Sandbox s)
getApp = InteractiveM $ MTL.asks app
readAppState :: InteractiveM s (AppState s)
readAppState = InteractiveM $ do
appState' <- MTL.asks appState
MTL.liftIO $ readIORef appState'
_expectEq :: (Show a, Eq a) => Stack.CallStack -> a -> a -> InteractiveM s ()
_expectEq callstack actual expected =
if actual == expected
then
pure ()
else
fail $ "Expected " <> show expected <> " but got " <> show actual <> ".\n" <> Stack.prettyCallStack callstack
_expectIs :: Stack.CallStack -> String -> a -> (a -> Bool) -> InteractiveM s ()
_expectIs callstack label actual f =
if f actual
then
pure ()
else
fail $ "Expected " <> label <> " to satisfy predicate.\n" <> Stack.prettyCallStack callstack
runInteractiveTest :: Sandbox s -> InteractiveM s a -> IO a
runInteractiveTest app (InteractiveM m) = do
let state = app.initialize
appState <-
newIORef
AppState
{ state = state,
continue = Continue,
output = app.render state
}
MTL.runReaderT m Env {app = app, appState = appState}