tinyapp 0.2.0.0 → 0.2.1.0
raw patch · 2 files changed
+179/−5 lines, 2 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ TinyApp.Interactive.Test: (~>) :: String -> InteractiveM s () -> TestCase s
+ TinyApp.Interactive.Test: data InteractiveM s a
+ TinyApp.Interactive.Test: data TestCase s
+ TinyApp.Interactive.Test: expectContinue :: HasCallStack => InteractiveM s ()
+ TinyApp.Interactive.Test: expectExit :: HasCallStack => InteractiveM s ()
+ TinyApp.Interactive.Test: expectRenderEq :: HasCallStack => String -> InteractiveM s ()
+ TinyApp.Interactive.Test: expectRenderIs :: (HasCallStack, Show s, Eq s) => (String -> Bool) -> InteractiveM s ()
+ TinyApp.Interactive.Test: expectStateEq :: (HasCallStack, Show s, Eq s) => s -> InteractiveM s ()
+ TinyApp.Interactive.Test: expectStateIs :: (HasCallStack, Show s, Eq s) => (s -> Bool) -> InteractiveM s ()
+ TinyApp.Interactive.Test: infix 0 ~>
+ TinyApp.Interactive.Test: inputString :: String -> InteractiveM s ()
+ TinyApp.Interactive.Test: instance Control.Monad.Fail.MonadFail (TinyApp.Interactive.Test.InteractiveM s)
+ TinyApp.Interactive.Test: instance Control.Monad.IO.Class.MonadIO (TinyApp.Interactive.Test.InteractiveM s)
+ TinyApp.Interactive.Test: instance GHC.Base.Applicative (TinyApp.Interactive.Test.InteractiveM s)
+ TinyApp.Interactive.Test: instance GHC.Base.Functor (TinyApp.Interactive.Test.InteractiveM s)
+ TinyApp.Interactive.Test: instance GHC.Base.Monad (TinyApp.Interactive.Test.InteractiveM s)
+ TinyApp.Interactive.Test: pressKey :: HasCallStack => Key -> InteractiveM s ()
+ TinyApp.Interactive.Test: pressKey' :: HasCallStack => Key -> [Modifier] -> InteractiveM s ()
+ TinyApp.Interactive.Test: pressKeys :: HasCallStack => [Key] -> InteractiveM s ()
+ TinyApp.Interactive.Test: runTestsFor :: Sandbox s -> [TestCase s] -> IO ()
+ TinyApp.Interactive.Test: sendEvent :: HasCallStack => Event -> InteractiveM s ()
Files
- src/TinyApp/Interactive/Test.hs +173/−0
- tinyapp.cabal +6/−5
+ src/TinyApp/Interactive/Test.hs view
@@ -0,0 +1,173 @@+module TinyApp.Interactive.Test+ ( TestCase,+ InteractiveM,+ (~>),+ runTestsFor,+ expectRenderEq,+ expectRenderIs,+ expectStateEq,+ expectStateIs,+ expectContinue,+ expectExit,+ pressKey,+ pressKeys,+ pressKey',+ inputString,+ sendEvent,+ )+where++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, MTL.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 <-+ MTL.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 $ MTL.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}
tinyapp.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: tinyapp-version: 0.2.0.0+version: 0.2.1.0 synopsis: Library to build tiny apps in Haskell description: Library to build tiny apps in Haskell such a REPLs or text-based applications that reacts to keystrokes homepage: https://github.com/bcardiff/haskell-tinyapp@@ -22,13 +22,14 @@ exposed-modules: TinyApp.Repl TinyApp.Interactive+ TinyApp.Interactive.Test -- other-modules: -- other-extensions: build-depends:- base >=4.17.2.1 && <5.0,- brick >= 2.4 && < 2.5,- vty >= 6.2 && < 6.3,- mtl >= 2.2.2 && < 2.3,+ base >= 4.16 && <5.0,+ brick >= 1.4 && < 2.6,+ vty >= 5.37 && < 6.3,+ mtl >= 2.2.2 && < 2.4, hs-source-dirs: src default-language: GHC2021 default-extensions: