packages feed

sandwich-0.3.1.0: src/Test/Sandwich/Formatters/Socket.hs

-- | The socket formatter creates a Unix domain socket that accepts interactive,
-- line-based commands to query the live test tree state.
--
-- This is a "secondary formatter," i.e. one that can run in the background while
-- a "primary formatter" (such as the TerminalUI or Print formatters) runs in the
-- foreground.
--
-- The socket server stays alive for the entire duration of the test executable,
-- so you can query it even after tests have completed (until 'finalizeFormatter'
-- cleans it up).
--
-- Connect with @socat - UNIX-CONNECT:\<run-root\>/socket.sock@ and type
-- @help@ to see available commands.

module Test.Sandwich.Formatters.Socket (
  defaultSocketFormatter
  , SocketFormatter(..)
  ) where

import Control.Concurrent.STM
import Control.Monad.IO.Class
import Data.IORef
import System.FilePath
import Test.Sandwich.Formatters.Socket.Server
import Test.Sandwich.Interpreters.RunTree.Util (waitForTree)
import Test.Sandwich.ManagedAsync
import Test.Sandwich.Types.ArgParsing
import Test.Sandwich.Types.RunTree
import UnliftIO.Async (Async, cancel)


data SocketFormatter = SocketFormatter {
  socketFormatterPath :: Maybe FilePath
  -- ^ Socket path. Nothing = \<run-root\>/socket.sock
  , socketFormatterServerAsync :: IORef (Maybe (Async ()))
  -- ^ Internal: handle to the running server thread, used for cleanup.
  , socketFormatterLogBroadcast :: TChan (Int, String, LogEntry)
  -- ^ Broadcast channel for streaming logs to connected clients.
  , socketFormatterEventBroadcast :: TChan NodeEvent
  -- ^ Broadcast channel for streaming node lifecycle events to connected clients.
  }

instance Show SocketFormatter where
  show (SocketFormatter {socketFormatterPath}) =
    "SocketFormatter {socketFormatterPath = " <> show socketFormatterPath <> "}"

defaultSocketFormatter :: IO SocketFormatter
defaultSocketFormatter = do
  ref <- newIORef Nothing
  chan <- newBroadcastTChanIO
  eventChan <- newBroadcastTChanIO
  return SocketFormatter {
    socketFormatterPath = Nothing
    , socketFormatterServerAsync = ref
    , socketFormatterLogBroadcast = chan
    , socketFormatterEventBroadcast = eventChan
    }

instance Formatter SocketFormatter where
  formatterName _ = "socket-formatter"
  runFormatter = run
  finalizeFormatter sf _ _ = liftIO $ do
    mAsync <- readIORef (socketFormatterServerAsync sf)
    case mAsync of
      Nothing -> return ()
      Just a -> cancel a

run :: (MonadIO m) => SocketFormatter -> [RunNode BaseContext] -> Maybe (CommandLineOptions ()) -> BaseContext -> m ()
run (SocketFormatter {..}) rts _maybeCommandLineOptions bc = do
  let runId = baseContextRunId bc
  case resolveSocketPath of
    Nothing -> return ()
    Just path -> liftIO $ do
      a <- managedAsync runId "socket-server" (socketServer runId path rts socketFormatterLogBroadcast socketFormatterEventBroadcast)
      writeIORef socketFormatterServerAsync (Just a)
      -- Block until all tests complete
      mapM_ waitForTree rts
  where
    resolveSocketPath = case socketFormatterPath of
      Just p -> Just p
      Nothing -> case baseContextRunRoot bc of
        Just runRoot -> Just (runRoot </> "socket.sock")
        Nothing -> Nothing