#!/usr/bin/env runhaskell
{-
shelltestrunner - a handy tool for testing command-line programs.
See shelltestrunner.cabal.
(c) Simon Michael 2009, released under GNU GPLv3
-}
module Main
where
--import Control.Concurrent (forkIO)
import Control.Monad (liftM,when)
import Data.List (partition)
import Data.Maybe (fromMaybe,fromJust)
import qualified Test.HUnit (Test)
import System.Console.ParseArgs hiding (args)
import System.Environment (withArgs)
import System.Exit (ExitCode(..),exitWith)
import System.IO (hGetContents, hPutStr, stderr)
import System.Process (runInteractiveCommand, waitForProcess)
-- import System.Process (createProcess, shell, CreateProcess(..), StdStream(..))
import Test.Framework (defaultMain)
import Test.Framework.Providers.HUnit (hUnitTestToTests)
import Test.HUnit hiding (Test)
import Text.ParserCombinators.Parsec
import Text.Printf (printf)
import Debug.Trace
strace :: Show a => a -> a
strace a = trace (show a) a
version :: String
version = "0.3" -- sync with .cabal
data ArgId = HelpFlag
| VersionFlag
| ExecutableArg
deriving (Ord, Eq, Show)
argspec :: [Arg ArgId]
argspec = [
Arg {argIndex = HelpFlag,
argName = Just "help",
argAbbr = Just 'h',
argData = Nothing,
argDesc = "show help"
}
,Arg {argIndex = VersionFlag,
argName = Just "version",
argAbbr = Just 'V',
argData = Nothing,
argDesc = "show version"
}
,Arg {argIndex = ExecutableArg,
argName = Nothing,
argAbbr = Nothing,
argData = argDataRequired "executable" ArgtypeString,
argDesc = "executable program or shell command to test, should accept stdin"
}
]
data ShellTest = ShellTest {
filename :: String
,command :: String
,stdin :: Maybe String
,stdoutExpected :: Maybe String
,stderrExpected :: Maybe String
,exitCodeExpected :: Maybe ExitCode
} deriving (Show)
main :: IO ()
main = do
args <- parseArgsIO ArgsInterspersed argspec
-- parseargs issue: if there's no exe, exits at first gotArg
when (gotArg args VersionFlag) printVersion
when (gotArg args HelpFlag) $ printHelp args
let (unprocessedopts, testfiles) = partition ((=="-").take 1) $ argsRest args
exe = fromJust $ getArgString args ExecutableArg
shelltests <- mapM parseShellTest testfiles
withArgs unprocessedopts $ defaultMain $ concatMap (hUnitTestToTests.shellTestToHUnitTest exe) shelltests
printVersion :: IO ()
printVersion = putStrLn version >> exitWith ExitSuccess
printHelp :: Args ArgId -> IO ()
printHelp args = putStrLn (argsUsage args) >> exitWith ExitSuccess
shellTestToHUnitTest :: FilePath -> ShellTest -> Test.HUnit.Test
shellTestToHUnitTest exe t = filename t ~: do {r <- runShellTest exe t; assertBool "" r}
parseShellTest :: FilePath -> IO ShellTest
parseShellTest = liftM (either (error.show) id) . parseFromFile shelltest
shelltest :: Parser ShellTest
shelltest = do
st <- getParserState
let f = sourceName $ statePos st
many commentline
c <- commandline
i <- optionMaybe input
o <- optionMaybe expectedoutput
e <- optionMaybe expectederror
x <- optionMaybe expectedexitcode
return ShellTest{filename=f,command=c,stdin=i,stdoutExpected=o,stderrExpected=e,exitCodeExpected=x}
line,commentline,commandline,delimiter,input,expectedoutput,expectederror :: Parser String
line = anyChar `manyTill` newline
commentline = char ';' >> anyChar `manyTill` newline
-- noncommentline = do
-- l <- line
-- if take 1 (strip l) == ";" then line else return l
commandline = line
delimiter = choice [try $ string "<<<", try $ string ">>>", (eof >> return "")]
input = string "<<<\n" >> (liftM unlines) (line `manyTill` (lookAhead delimiter))
expectedoutput = try $ string ">>>" >> optional (char '1') >> newline >> (liftM unlines) (line `manyTill` (lookAhead delimiter))
expectederror = try $ string ">>>2\n" >> (liftM unlines) (line `manyTill` (lookAhead delimiter))
expectedexitcode :: Parser ExitCode
expectedexitcode = string ">>>=" >> (liftM (toExitCode.read.unlines) (line `manyTill` eof))
runShellTest :: FilePath -> ShellTest -> IO Bool
runShellTest exe ShellTest{
filename=_,command=c,stdin=i,stdoutExpected=o,stderrExpected=e,exitCodeExpected=x} = do
let cmd = unwords [exe,c]
(i',o',e',x') = (fromMaybe "" i, fromMaybe "" o, fromMaybe "" e, fromMaybe ExitSuccess x)
-- printf "%s .. " f; hFlush stdout
(ih,oh,eh,ph) <- runInteractiveCommand cmd
-- forkIO $ hPutStr ih i' -- separate thread in case cmd does not read stdin
-- (Just ih,Just oh,Just eh,ph) <- createProcess $ (shell cmd){std_in=CreatePipe,std_out=CreatePipe,std_err=CreatePipe}
hPutStr ih i'
out <- hGetContents oh
err <- hGetContents eh
-- on a mac, this hangs if cmd does not read stdin (http://hackage.haskell.org/trac/ghc/ticket/3369)
exit <- waitForProcess ph
let (outputok, errorok, exitok) = (out==o', err==e', exit==x')
if outputok && errorok && exitok
then do
-- putStrLn "ok"
return True
else do
-- hPutStr stderr $ printf "FAIL\n"
when (not outputok) $ printExpectedActual "stdout" o' out
when (not errorok) $ printExpectedActual "stderr" e' err
when (not exitok) $ printExpectedActual "exit code" (show (fromExitCode x')++"\n") (show (fromExitCode exit)++"\n")
return False
printExpectedActual :: String -> String -> String -> IO ()
printExpectedActual f e a = hPutStr stderr $ printf "**Expected %s:\n%s**Got %s:\n%s" f e f a
toExitCode :: Int -> ExitCode
toExitCode 0 = ExitSuccess
toExitCode n = ExitFailure n
fromExitCode :: ExitCode -> Int
fromExitCode ExitSuccess = 0
fromExitCode (ExitFailure n) = n
strip,lstrip,rstrip,dropws :: String -> String
strip = lstrip . rstrip
lstrip = dropws
rstrip = reverse . dropws . reverse
dropws = dropWhile (`elem` " \t")