Skip to content

[WIP] How to unit test Brick apps #447

Closed
@avh4

Description

@avh4

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions