#!/usr/bin/env runhaskell {- shelltestrunner - a handy tool for testing command-line programs. Run a given program through "shell" tests specifed by one or more test files, each of which specifies: command-line arguments, input, expected output, expected stderr output, and expected exit code. This was extracted from the hledger project, and inspired by the tests in John Wiegley's ledger project. This uses test-framework's test runner. Output order is currently a bit mixed up. Any command-line options are passed through to the test-framework runner, but they must not contain spaces, so use eg -tpattern not -t pattern. You may be able to get a big speedup by running tests in parallel: try -j8. Usage: $ ghc --make -threaded ./shelltestrunner.hs $ ./shelltestrunner [testrunneropts] executable testfile1 [testfile2 ...] Test file format: @ -opt1 -opt2 arg1 arg2 <<< 0 or more lines of input >>> 0 or more lines of expected output >>>2 0 or more lines of expected error output <<>> @ Lines whose first non-whitespace character is ; are ignored, mostly. The first line is the command line, to be appended to the executable name. All remaining fields are optional; when omitted they are assumed to be "", "", "", and 0 respectively. (c) Simon Michael 2009, released under GNU GPLv3 -} module Main where import Control.Monad (liftM,when) import Data.List (partition) import Data.Maybe (fromMaybe) import qualified Test.HUnit (Test) import System.Environment (getArgs,withArgs) import System.Exit (ExitCode(..)) import System.IO (hGetContents, hPutStr, stderr) import System.Process (runInteractiveCommand, waitForProcess) 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 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 <- getArgs let (opts,args') = partition ((=="-").take 1) args args'' = if null args' && ("--help" `elem` opts) then [""] else args' -- temp (exe:files) = args'' shelltests <- mapM parseShellTest files withArgs opts $ defaultMain $ concatMap (hUnitTestToTests.shellTestToHUnitTest exe) shelltests 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 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} commandline,input,expectedoutput,expectederror,delimiter,line :: Parser String commandline = line input = string "<<<\n" >> (liftM unlines) (line `manyTill` (lookAhead delimiter)) expectedoutput = try $ string ">>>" >> optional (char '1') >> char '\n' >> (liftM unlines) (line `manyTill` (lookAhead delimiter)) expectederror = string ">>>2" >> (liftM $ unlines.tail) (line `manyTill` (lookAhead delimiter)) -- why tail ? delimiter = choice [try $ string "<<<", try $ string ">>>", (eof >> return "")] line = do l <- anyChar `manyTill` newline if take 1 (strip l) == ";" then line else return l expectedexitcode :: Parser ExitCode expectedexitcode = do string "<<<" c <- liftM (toExitCode.read) line string ">>>" return c 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 hPutStr ih i' out <- hGetContents oh err <- hGetContents eh 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:\n%s" f e 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")