Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] How to unit test Brick apps #447

Closed
2 tasks
avh4 opened this issue Mar 4, 2023 · 3 comments
Closed
2 tasks

[WIP] How to unit test Brick apps #447

avh4 opened this issue Mar 4, 2023 · 3 comments

Comments

@avh4
Copy link
Contributor

avh4 commented Mar 4, 2023

I had been looking for some info on how to do this and didn't find it, so I figured I'd share my progress here.

This is a function that can start a Brick app using the Graphics.Vty.Output.Mock mock terminal, simulate the given list of Vty.Events, and then return the final output of the mock renderer. (Note, the mock vty output is a bit weird; the reference of the weird characters it outputs can be seen in it's source code.)

the test function (click to expand)

runApp :: Ord n => Brick.App s e n -> Vty.DisplayRegion -> s -> List Vty.Event -> IO Text

import qualified Brick
import Brick.BChan (newBChan, writeBChan)
import Control.Applicative ((<$>))
import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar, tryTakeMVar)
import Control.Concurrent.STM (newTChanIO)
import Control.Monad (mapM_)
import Data.IORef (newIORef, readIORef, writeIORef)
import qualified Data.String.UTF8 as UTF8
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import GHC.Err (undefined)
import Graphics.Vty (Vty (Vty))
import qualified Graphics.Vty as Vty
import qualified Graphics.Vty.Output.Mock as VtyMock

data RunAppInternalEvent
  = RunAppHalt

runApp :: Ord n => Brick.App s e n -> Vty.DisplayRegion -> s -> List Vty.Event -> IO Text
runApp app displayRegion initialAppState events = do
  -- Create concurrent variables and channels
  pictureRef <- newIORef Vty.emptyPicture
  inputConfigRef <- newIORef undefined
  shutdownRef <- newIORef False
  eventQueue <- newEmptyMVar
  appEventQueue <- newBChan 1
  inputEventQueue <- newTChanIO

  -- Create the mock vty
  (mockData, output) <- VtyMock.mockTerminal displayRegion
  let input =
        Vty.Input
          { Vty._eventChannel = inputEventQueue,
            Vty.shutdownInput = return (),
            Vty.restoreInputState = return (),
            Vty._configRef = inputConfigRef,
            Vty._inputDebug = Nothing
          }
  let vty =
        Vty
          { Vty.update = writeIORef pictureRef,
            Vty.nextEvent = takeMVar eventQueue,
            Vty.nextEventNonblocking = tryTakeMVar eventQueue,
            Vty.inputIface = input,
            Vty.outputIface = output,
            Vty.refresh = return (),
            Vty.shutdown = writeIORef shutdownRef True,
            Vty.isShutdown = readIORef shutdownRef
          }

  -- Start a thread that will send the events
  -- and ultimately send a halt event
  forkIO $ do
    mapM_ (putMVar eventQueue) events
    writeBChan appEventQueue RunAppHalt

  -- Create and start the app
  let wrappedApp =
        Brick.App
          { Brick.appDraw = Brick.appDraw app,
            Brick.appChooseCursor = Brick.appChooseCursor app,
            Brick.appHandleEvent = \case
              Brick.VtyEvent e -> Brick.appHandleEvent app $ Brick.VtyEvent e
              Brick.AppEvent RunAppHalt -> Brick.halt
              Brick.MouseDown n b ms l -> Brick.appHandleEvent app $ Brick.MouseDown n b ms l
              Brick.MouseUp n b l -> Brick.appHandleEvent app $ Brick.MouseUp n b l,
            Brick.appStartEvent = Brick.appStartEvent app,
            Brick.appAttrMap = Brick.appAttrMap app
          }
  _finalState <-
    Brick.customMain
      vty
      (return vty)
      (Just appEventQueue)
      wrappedApp
      initialAppState

  -- Render the final picutre
  picture <- readIORef pictureRef
  displayContext <- Vty.mkDisplayContext output output displayRegion
  Vty.outputPicture displayContext picture
  Text.decodeUtf8 . UTF8.toRep <$> readIORef mockData

I'm hoping to eventually clean this up and contribute it to brick itself if there's interest, but there are a few missing pieces still to do:

  • have an actual vty Text renderer (see Mock render Widgets #405), as the Mock renderer isn't really meant for this use and has an output format that is a bit weird and incomplete for the use of testing apps from an end-user perspective
  • allow the list of events to contain any Brick Event, or app event, and not be limited to only Vty Events.
@locallycompact
Copy link
Contributor

Was there any update here since this issue was raised?

@avh4
Copy link
Contributor Author

avh4 commented Oct 19, 2023

I can't speak for the repo maintainer, but for me personally, I've been able to get by with just the test helper function that I posted in the collapsed details block in the original post, and haven't had enough need to try to generalize or improve it. But anyone can feel free to use that code I posted for any purpose.

@jtdaugherty
Copy link
Owner

I can't report any changes, either. I was really just in a spectator mode on this ticket in case @avh4 came up with something promising. :) It sounds like maybe we've discussed all we can at this point, though, so I'll close this. Please re-open if there's more to discuss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants