stack-3.7.1: tests/integration/lib/StackTest/Repl.hs
{- |
Integration-test helpers & fixtures for testing `stack repl`
-}
module StackTest.Repl
( Repl
, ReplConnection (..)
, nextPrompt
, replCommand
, replGetChar
, replGetLine
, stackRepl
-- * Reexport
, module StackTest
) where
import Control.Exception (SomeException, catch, displayException, finally)
import Control.Monad ((>=>), unless, when)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Reader
import Control.Monad.Trans.State qualified as State
import Data.Maybe (fromMaybe)
import Data.Foldable (toList)
import Data.Sequence as Seq (Seq(Empty), (|>), fromList)
import GHC.Stack (HasCallStack)
import System.Directory (removeFile)
import System.Environment (lookupEnv)
import System.Exit (ExitCode (..), exitFailure)
import System.IO
( BufferMode (NoBuffering, LineBuffering), Handle, IOMode (ReadMode)
, hClose, hGetChar, hGetContents', hGetLine, hPutStrLn, hSetBuffering
, openTempFile
, withFile
)
import System.Process
( CreateProcess (std_err, std_in, std_out)
, StdStream (CreatePipe, UseHandle)
, createProcess, proc, waitForProcess
)
import StackTest
type Repl = ReaderT ReplConnection IO
data ReplConnection = ReplConnection
{ replStdin :: Handle
, replStdout :: Handle
}
replCommand :: String -> Repl ()
replCommand cmd = do
(ReplConnection replStdinHandle _) <- ask
-- echo what we send to the test's stdout
liftIO . putStrLn $ "____> " <> cmd
liftIO $ hPutStrLn replStdinHandle cmd
replGetChar :: Repl Char
replGetChar = asks replStdout >>= liftIO . hGetChar
replGetLine :: Repl String
replGetLine = ask >>= liftIO . hGetLine . replStdout
nextPrompt :: Repl ()
nextPrompt = State.evalStateT poll Seq.Empty where
poll = do
c <- lift (asks replStdout) >>= liftIO . hGetChar
State.modify (|> c)
when (c == '\n') $ do
State.get >>= liftIO . putStr . ("ghci> " ++) . toList
State.put Seq.Empty
buf <- State.get
unless (buf == Seq.fromList "ghci> ")
poll
runRepl
:: HasCallStack
=> FilePath
-> [String]
-> Repl ()
-> IO ExitCode
runRepl cmd args actions = do
(stderrBufPath, stderrBufHandle) <- openTempStderrBufferFile
hSetBuffering stderrBufHandle NoBuffering
logInfo $ "Running: " ++ cmd ++ " " ++ unwords (map showProcessArgDebug args) ++ "\n\
\ with stderr in " ++ stderrBufPath
-- launch the GHCi subprocess, grab its FD handles and process handle
(Just rStdin, Just rStdout, Nothing, ph) <-
createProcess (proc cmd args)
{ std_in = CreatePipe
, std_out = CreatePipe
, std_err = UseHandle stderrBufHandle
}
hSetBuffering rStdin LineBuffering
hSetBuffering rStdout NoBuffering
-- run the test script which is to talk to the GHCi subprocess.
runReaderT actions (ReplConnection rStdin rStdout)
-- the nested actions script may fail in arbitrary ways; handle that here,
-- attaching the subprocess stderr as relevant context
`catch` \(e :: SomeException) -> do
putStrLn "=============================="
putStrLn "EXCEPTION in test: "
putStrLn . quote $ displayException e
putStrLn "------[ stderr of repl ]------"
withFile stderrBufPath ReadMode $ hGetContents' >=> putStr . quote
putStrLn "=============================="
`finally` do
hClose stderrBufHandle
removeFile stderrBufPath
-- once done with the test, signal EOF on stdin for clean termination of ghci
hClose rStdin
-- read out the exit-code
waitForProcess ph
-- | Roll a bicycle, rather than just `import Path.IO (getTempDir, openTempFile)`,
-- because it's a hassle to use anything beyond base & boot libs here.
openTempStderrBufferFile :: IO (FilePath, Handle)
openTempStderrBufferFile = getTempDir >>= (`openTempFile` "err.log") where
getTempDir | isWindows = fromMaybe "" <$> lookupEnv "TEMP"
| otherwise = pure "/tmp"
-- | Testing helper to exercise `stack repl`.
stackRepl :: HasCallStack => [String] -> Repl () -> IO ()
stackRepl args action = do
stackExe' <- stackExe
ec <- runRepl stackExe' ("repl" : "--ghci-options=-ignore-dot-ghci" : args) action
unless (ec == ExitSuccess) $ do
putStrLn $ "repl exited with " <> show ec
exitFailure
quote :: String -> String
quote = unlines . map ("> " <>) . lines