sandwich-0.3.1.0: src/Test/Sandwich/Formatters/Socket/Commands.hs
{-# LANGUAGE RankNTypes #-}
module Test.Sandwich.Formatters.Socket.Commands (
handleCommand
) where
import Control.Concurrent.STM
import Control.Monad.Logger
import qualified Data.ByteString.Char8 as BS8
import Data.Foldable (toList)
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.String.Interpolate
import Data.Time
import Test.Sandwich.Formatters.Common.Count
import Test.Sandwich.Formatters.Common.Util
import Test.Sandwich.RunTree
import Test.Sandwich.Types.RunTree
import Test.Sandwich.Types.Spec
import Text.Read (readMaybe)
-- | Handle a single command and return the response text.
-- The response does NOT include the trailing ".\n" terminator; the caller adds that.
handleCommand :: [RunNode BaseContext] -> UTCTime -> String -> IO String
handleCommand rts now cmd = case words cmd of
["help"] -> return helpText
["status"] -> cmdStatus rts
["active"] -> cmdActive rts now
["failures"] -> cmdFailures rts
["pending"] -> cmdPending rts
["tree"] -> cmdTree rts
["node", idStr] -> case readMaybe idStr of
Just nid -> cmdNode rts nid
Nothing -> return [i|Error: invalid node id "#{idStr}"|]
["logs", idStr] -> case readMaybe idStr of
Just nid -> cmdLogs rts nid
Nothing -> return [i|Error: invalid node id "#{idStr}"|]
[] -> return ""
(c:_) -> return [i|Unknown command: #{c}\nType "help" for available commands.|]
helpText :: String
helpText = unlines
[ "Available commands:"
, " help - Show this help"
, " status - Summary counts: total, running, succeeded, failed, pending, not started"
, " active - List currently running nodes"
, " failures - List failed nodes with failure reason"
, " pending - List pending nodes"
, " tree - Full tree with indented status"
, " node <id> - Detail for a specific node"
, " logs <id> - Show logs for a specific node"
, " stream-logs - Stream all logs live (disconnect to stop)"
, " stream-events - Stream node lifecycle events (started/done) live"
, " stream-rts-stats - Stream GHC RTS memory stats every 1s (needs +RTS -T)"
]
-- | Snapshot the tree atomically
snapshot :: [RunNode BaseContext] -> IO [RunNodeFixed BaseContext]
snapshot rts = atomically $ mapM fixRunTree rts
-- * Commands
cmdStatus :: [RunNode BaseContext] -> IO String
cmdStatus rts = do
fixed <- snapshot rts
let total = countWhere isItBlock fixed
running = countWhere isRunningItBlock fixed
succeeded = countWhere isSuccessItBlock fixed
failed = countWhere isFailedItBlock fixed
pend = countWhere isPendingItBlock fixed
notStarted = countWhere isNotStartedItBlock fixed
return $ unlines
[ [i|total: #{total}|]
, [i|running: #{running}|]
, [i|succeeded: #{succeeded}|]
, [i|failed: #{failed}|]
, [i|pending: #{pend}|]
, [i|not started: #{notStarted}|]
]
cmdActive :: [RunNode BaseContext] -> UTCTime -> IO String
cmdActive rts now = do
fixed <- snapshot rts
let nodes = concatMap (extractValues getActiveInfo) fixed
activeNodes = [x | Just x <- nodes]
if null activeNodes
then return "No nodes currently running."
else return $ unlines [formatActive n | n <- activeNodes]
where
getActiveInfo :: RunNodeWithStatus ctx Status (Seq LogEntry) Bool -> Maybe (String, Int, NominalDiffTime)
getActiveInfo node = case runTreeStatus (runNodeCommon node) of
Running {statusStartTime} ->
let c = runNodeCommon node
in Just (runTreeLabel c, runTreeId c, diffUTCTime now statusStartTime)
_ -> Nothing
formatActive (label, nid, elapsed) =
let elapsedStr = formatNominalDiffTime elapsed
in [i| [#{nid}] #{label} (#{elapsedStr})|]
cmdFailures :: [RunNode BaseContext] -> IO String
cmdFailures rts = do
fixed <- snapshot rts
let nodes = concatMap (extractValues getFailureInfo) fixed
failedNodes = [x | Just x <- nodes]
if null failedNodes
then return "No failures."
else return $ unlines [formatFailure n | n <- failedNodes]
where
getFailureInfo :: RunNodeWithStatus ctx Status (Seq LogEntry) Bool -> Maybe (String, Int, FailureReason)
getFailureInfo node = case runTreeStatus (runNodeCommon node) of
Done {statusResult = Failure (Pending {})} -> Nothing
Done {statusResult = Failure reason} ->
let c = runNodeCommon node
in Just (runTreeLabel c, runTreeId c, reason)
_ -> Nothing
formatFailure (label, nid, reason) =
[i| [#{nid}] #{label}: #{showFailureReason reason}|]
cmdPending :: [RunNode BaseContext] -> IO String
cmdPending rts = do
fixed <- snapshot rts
let nodes = concatMap (extractValues getPendingInfo) fixed
pendingNodes = [x | Just x <- nodes]
if null pendingNodes
then return "No pending nodes."
else return $ unlines [formatPendingNode n | n <- pendingNodes]
where
getPendingInfo :: RunNodeWithStatus ctx Status (Seq LogEntry) Bool -> Maybe (String, Int, Maybe String)
getPendingInfo node = case runTreeStatus (runNodeCommon node) of
Done {statusResult = Failure (Pending {failurePendingMessage})} ->
let c = runNodeCommon node
in Just (runTreeLabel c, runTreeId c, failurePendingMessage)
_ -> Nothing
formatPendingNode (label, nid, msg) = case msg of
Just m -> [i| [#{nid}] #{label}: #{m}|]
Nothing -> [i| [#{nid}] #{label}|]
cmdTree :: [RunNode BaseContext] -> IO String
cmdTree rts = do
fixed <- snapshot rts
return $ unlines $ concatMap (renderTree 0) fixed
renderTree :: Int -> RunNodeWithStatus context Status (Seq LogEntry) Bool -> [String]
renderTree depth node =
let c = runNodeCommon node
indent = replicate (depth * 2) ' '
statusStr = showStatusBrief (runTreeStatus c)
label = runTreeLabel c
nid = runTreeId c
line = [i|#{indent}[#{statusStr}] [#{nid}] #{label}|]
children = case node of
RunNodeIt {} -> []
RunNodeIntroduce {runNodeChildrenAugmented} -> concatMap (renderTree (depth + 1)) runNodeChildrenAugmented
RunNodeIntroduceWith {runNodeChildrenAugmented} -> concatMap (renderTree (depth + 1)) runNodeChildrenAugmented
_ -> concatMap (renderTree (depth + 1)) (runNodeChildren node)
in line : children
cmdNode :: [RunNode BaseContext] -> Int -> IO String
cmdNode rts nid = do
fixed <- snapshot rts
let allCommons = concatMap (extractValues (runNodeCommon)) fixed
match = [c | c <- allCommons, runTreeId c == nid]
case match of
[] -> return [i|Error: no node with id #{nid}|]
(c:_) -> return $ unlines
[ [i|id: #{runTreeId c}|]
, [i|label: #{runTreeLabel c}|]
, [i|status: #{showStatusDetail (runTreeStatus c)}|]
, [i|folder: #{maybe "(none)" id (runTreeFolder c)}|]
]
cmdLogs :: [RunNode BaseContext] -> Int -> IO String
cmdLogs rts nid = do
fixed <- snapshot rts
let allNodes = concatMap (extractValues (\n -> (runNodeCommon n))) fixed
match = [c | c <- allNodes, runTreeId c == nid]
case match of
[] -> return [i|Error: no node with id #{nid}|]
(c:_) -> do
let logs = runTreeLogs c
if Seq.null logs
then return "(no logs)"
else return $ unlines [showLogEntry e | e <- toList logs]
-- * Formatting helpers
showStatusBrief :: Status -> String
showStatusBrief NotStarted = "NOT STARTED"
showStatusBrief (Running {}) = "RUNNING"
showStatusBrief (Done {statusResult = Success}) = "OK"
showStatusBrief (Done {statusResult = Failure (Pending {})}) = "PENDING"
showStatusBrief (Done {statusResult = Failure _}) = "FAIL"
showStatusBrief (Done {statusResult = DryRun}) = "DRY RUN"
showStatusBrief (Done {statusResult = Cancelled}) = "CANCELLED"
showStatusDetail :: Status -> String
showStatusDetail NotStarted = "not started"
showStatusDetail (Running {statusStartTime}) = [i|running (started #{show statusStartTime})|]
showStatusDetail (Done {statusStartTime, statusEndTime, statusResult}) =
let elapsed = formatNominalDiffTime (diffUTCTime statusEndTime statusStartTime)
in [i|#{showResultBrief statusResult} (#{elapsed})|]
showResultBrief :: Result -> String
showResultBrief Success = "succeeded"
showResultBrief (Failure (Pending {})) = "pending"
showResultBrief (Failure _) = "failed"
showResultBrief DryRun = "dry run"
showResultBrief Cancelled = "cancelled"
showFailureReason :: FailureReason -> String
showFailureReason (Reason {failureReason}) = failureReason
showFailureReason (ExpectedButGot {failureValue1, failureValue2}) =
[i|Expected #{show failureValue1} but got #{show failureValue2}|]
showFailureReason (DidNotExpectButGot {failureValue1}) =
[i|Did not expect #{show failureValue1}|]
showFailureReason (GotException {failureMessage, failureException}) =
case failureMessage of
Just msg -> [i|#{msg}: #{show failureException}|]
Nothing -> show failureException
showFailureReason (Pending {failurePendingMessage}) =
maybe "pending" id failurePendingMessage
showFailureReason (GetContextException {failureException}) =
[i|Context exception: #{show failureException}|]
showFailureReason (GotAsyncException {failureMessage, failureAsyncException}) =
case failureMessage of
Just msg -> [i|#{msg}: #{show failureAsyncException}|]
Nothing -> show failureAsyncException
showFailureReason (ChildrenFailed {failureNumChildren}) =
[i|#{failureNumChildren} children failed|]
showFailureReason (RawImage {failureFallback}) = failureFallback
showLogEntry :: LogEntry -> String
showLogEntry (LogEntry {logEntryTime, logEntryLevel, logEntryStr}) =
let levelStr :: String
levelStr = case logEntryLevel of
LevelDebug -> "DEBUG"
LevelInfo -> "INFO"
LevelWarn -> "WARN"
LevelError -> "ERROR"
LevelOther t -> show t
msgStr = BS8.unpack logEntryStr
in [i|#{show logEntryTime} [#{levelStr}] #{msgStr}|]