main-tester-0.2.0.1: src/Test/Main.hs
{-# LANGUAGE RecordWildCards #-}
module Test.Main
( -- * Utilities for testing your main function
captureProcessResult
, ProcessResult(..)
, withStdin
, withEnv
-- ** Re-export from System.Environment
, withArgs
-- ** Re-export from System.Exit
, ExitCode(..)
) where
import qualified Control.Exception as E
import Control.Monad (mapM, mapM_)
import qualified Data.ByteString as B
import GHC.IO.Handle (hDuplicate, hDuplicateTo)
import System.Directory (removeFile, getTemporaryDirectory)
import System.Environment (withArgs, setEnv, unsetEnv, lookupEnv)
import System.Exit (ExitCode(ExitSuccess, ExitFailure))
import System.IO
( Handle
, SeekMode(AbsoluteSeek)
, stdin
, stderr
, stdout
, hClose
, hFlush
, hSetBinaryMode
, hGetBuffering
, hSetBuffering
, hGetEncoding
, hSetEncoding
, hSeek
, openBinaryTempFile
, withBinaryFile
, IOMode(ReadMode)
)
import Test.Main.Internal (ProcessResult(..))
-- |
-- Capture stdout, stderr, and exit code of the given IO action.
--
-- >>> let main = putStr "hello"
-- >>> captureProcessResult main
-- ProcessResult {prStdout = "hello", prStderr = "", prExitCode = ExitSuccess, prException = Nothing}
--
-- If the IO action exit with error message, the exit code of result is 'ExitFailure'.
--
-- >>> import System.IO
-- >>> import System.Exit
-- >>> let main = hPutStr stderr "OMG!" >> exitWith (ExitFailure 1)
-- >>> captureProcessResult main
-- ProcessResult {prStdout = "", prStderr = "OMG!", prExitCode = ExitFailure 1, prException = Nothing}
--
-- @since 0.2.0.0
-- Since v0.2.0.0, this function catches @SomeException@, not only @ExitCode@
-- to prevent it from losing output when an exception other than @ExitCode@ is thrown.
-- To get the thrown error, use @prException@.
--
-- Note: 'prStderr' doesn't contain the error message of the thrown exception.
-- See the example below.
--
-- >>> import Control.Exception
-- >>> let main = ioError $ userError "OMG!"
-- >>> captureProcessResult main
-- ProcessResult {prStdout = "", prStderr = "", prExitCode = ExitFailure 1, prException = Just user error (OMG!)}
captureProcessResult :: IO () -> IO ProcessResult
captureProcessResult action = do
tDir <- getTemporaryDirectory
withBinaryTmpFile tDir "test-stdout" $ \(_oPath, oHd) ->
withBinaryTmpFile tDir "test-stderr" $ \(_ePath, eHd) ->
redirectingHandle stdout oHd $
redirectingHandle stderr eHd $ do
(prExitCode, prException) <- captureExitCodeAndException action
prStdout <- readFromHead oHd stdout
prStderr <- readFromHead eHd stderr
return ProcessResult {..}
where
readFromHead tmpH stdH = do
hFlush stdH
hSeek tmpH AbsoluteSeek 0
B.hGetContents tmpH
captureExitCodeAndException act =
(act >> return (ExitSuccess, Nothing))
`E.catches` [E.Handler forExitCode, E.Handler forSomeException]
forExitCode :: ExitCode -> IO (ExitCode, Maybe E.SomeException)
forExitCode eCode = return (eCode, Nothing)
forSomeException :: E.SomeException -> IO (ExitCode, Maybe E.SomeException)
forSomeException ex = return (ExitFailure 1, Just ex)
withBinaryTmpFile :: FilePath -> String -> ((FilePath, Handle) -> IO a) -> IO a
withBinaryTmpFile parent name =
E.bracket
(openBinaryTempFile parent name)
(\(path, hd) -> do
hClose hd
removeFile path `E.catch` doNothing
)
where
doNothing :: IOError-> IO ()
doNothing _ = return ()
redirectingHandle :: Handle -> Handle -> IO r -> IO r
redirectingHandle from to action = do
saveEnc <- hGetEncoding from
saveBuf <- hGetBuffering from
let redirect = do
save <- hDuplicate from
hDuplicateTo to from
setEnc to
return save
restore save = do
hDuplicateTo save from
hClose save
setEnc from
hSetBuffering from saveBuf
setEnc h =
maybe (hSetBinaryMode h True) (hSetEncoding h) saveEnc
E.bracket redirect restore (const action)
-- |
-- Pass the ByteString to stdin of the given IO action.
--
-- >>> import Data.ByteString.Char8 ()
-- >>> :set -XOverloadedStrings
-- >>> let main = putStrLn . reverse =<< getLine
-- >>> withStdin "abcde" main
-- edcba
withStdin :: B.ByteString -> IO a -> IO a
withStdin bs action =
E.bracket
prepareInputFile
removeFile
(\inPath ->
withBinaryFile inPath ReadMode $ \tmpHd ->
redirectingHandle stdin tmpHd action
)
where
prepareInputFile = do
tDir <- getTemporaryDirectory
E.bracket
(openBinaryTempFile tDir "test-stdin")
(\(_path, hd) -> hClose hd)
(\(path, hd) -> B.hPut hd bs >> return path)
-- |
-- Run the given IO action with the specified environment variables set.
-- The environment variables are specified as pairs of @ENV_VAR_NAME@ and @ENV_VAR_VALUE@.
-- If @ENV_VAR_VALUE@ is @Nothing@, the @ENV_VAR_NAME@ is unset with 'unsetEnv'.
--
-- >>> import System.Environment
-- >>> setEnv "ENV_VAR_TO_UNSET" "value_to_unset"
-- >>> setEnv "ENV_VAR_TO_OVERWRITE" "value_to_overwrite"
-- >>> let main = (print =<< lookupEnv "ENV_VAR_TO_UNSET") >> (print =<< lookupEnv "ENV_VAR_TO_OVERWRITE")
-- >>> withEnv [("ENV_VAR_TO_UNSET", Nothing), ("ENV_VAR_TO_OVERWRITE" , Just "new_value")] main
-- Nothing
-- Just "new_value"
-- >>> main
-- Just "value_to_unset"
-- Just "value_to_overwrite"
withEnv :: [(String, Maybe String)] -> IO a -> IO a
withEnv newVarVals action =
E.bracket
(saveReplaces newVarVals)
replaces
(const action)
where
saveReplaces = mapM $ \varVal@(var, _val) ->
((,) <$> pure var <*> lookupEnv var) <* replace varVal
replaces = mapM_ replace
replace (var, Just val) = setEnv var val
replace (var, Nothing) = unsetEnv var