module TestLib (Config(..), mainWith) where
import SimpleGetOpt
import Control.Monad (foldM,when)
import System.Directory ( getDirectoryContents,doesDirectoryExist
, createDirectoryIfMissing,canonicalizePath )
import System.Environment (withArgs)
import System.FilePath((</>),(<.>),splitFileName,splitDirectories,takeFileName
, isRelative, pathSeparator )
import System.Process ( createProcess,CreateProcess(..), StdStream(..)
, proc, waitForProcess, readProcessWithExitCode
)
import System.IO(IOMode(..),withFile,Handle,hSetBuffering,BufferMode(..))
import System.Exit(exitSuccess)
import Test.Framework (defaultMain,Test,testGroup)
import Test.Framework.Providers.HUnit (testCase)
import Test.HUnit (assertFailure)
import qualified Control.Exception as X
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
data Config = Config
{ cfgDefaultBinary :: String
, cfgBinOpts :: String -> [String]
, cfgIsTestCase :: String -> Bool
}
mainWith :: Config -> IO ()
mainWith cfg =
do let optSpec = options cfg
opts <- getOpts optSpec
when (optHelp opts) $
do dumpUsage optSpec
exitSuccess
bin' <- if pathSeparator `elem` optBinary opts
&& isRelative (optBinary opts)
then canonicalizePath (optBinary opts)
else return (optBinary opts)
resultsDir <- canonicalizePath (optResultDir opts)
let opts' = opts { optResultDir = resultsDir, optBinary = bin' }
createDirectoryIfMissing True resultsDir
testFiles <- findTests opts'
withArgs (optOther opts') (defaultMain (generateTests opts' testFiles))
data Options = Options
{ optBinary :: String
, optOther :: [String]
, optHelp :: Bool
, optResultDir :: FilePath
, optTests :: [FilePath]
, optDiffTool :: Maybe String
, optIgnoreExpected :: Bool
, optCfg :: Config
}
options :: Config -> OptSpec Options
options cfg = OptSpec
{ progDefaults = Options { optBinary = cfgDefaultBinary cfg
, optOther = []
, optHelp = False
, optResultDir = "output"
, optTests = []
, optDiffTool = Nothing
, optIgnoreExpected = False
, optCfg = cfg
}
, progOptions =
[ Option "c" ["exe"]
"the binary executable to use"
$ ReqArg "PATH" $ \s o -> Right o { optBinary = s }
, Option "r" ["result-dir"]
"the result directory for test runs"
$ ReqArg "PATH" $ \s o -> Right o { optResultDir = s }
, Option "p" ["diff-prog"]
"use this diffing program on failures"
$ ReqArg "PROG" $ \s o -> Right o { optDiffTool = Just s }
, Option "T" []
"add an argument to pass to the test-runner main"
$ ReqArg "STRING" $ \s o -> Right o { optOther = s : optOther o }
, Option "i" ["ignore-expected"]
"ignore expected failures"
$ NoArg $ \o -> Right o { optIgnoreExpected = True }
, Option "h" ["help"]
"display this message"
$ NoArg $ \o -> Right o { optHelp = True }
]
, progParamDocs =
[ ("FILES/DIRS", "The tests to run.") ]
, progParams = \p o -> Right o { optTests = p : optTests o }
}
generateTests :: Options -> TestFiles -> [Test]
generateTests opts = loop ""
where
loop dir tests = as ++ grouped
where
as = map (generateAssertion opts dir) (Set.toList (files tests))
grouped = [ testGroup path (loop (dir </> path) t)
| (path,t) <- Map.toList (subDirs tests) ]
generateAssertion :: Options -> FilePath -> FilePath -> Test
generateAssertion opts dir file = testCase file runTest
where
resultDir = optResultDir opts </> dir
goldFile = dir </> file <.> "stdout"
knownFailureFile = dir </> file <.> "fails"
resultOut = resultDir </> file <.> "stdout"
runTest =
do createDirectoryIfMissing True resultDir
withFile resultOut WriteMode $ \ hout ->
do hSetBuffering hout NoBuffering
runBinary opts hout dir file
out <- readFile resultOut
expected <- readFile goldFile
mbKnown <- X.try (readFile knownFailureFile)
checkOutput mbKnown expected out
checkOutput mbKnown expected out
| expected == out =
case mbKnown of
Left _ -> return ()
Right _ ->
assertFailure $
"Test completed successfully. Please remove " ++ knownFailureFile
| otherwise =
case mbKnown of
Left (X.SomeException {})
| Just prog <- optDiffTool opts ->
do goldFile' <- canonicalizePath goldFile
assertFailure $ unlines
[ unwords [ prog, goldFile', "\\\n ", resultOut ]
, makeGold resultOut goldFile'
]
| otherwise ->
do goldFile' <- canonicalizePath goldFile
(_,diffOut,_) <-
readProcessWithExitCode "diff" [ goldFile', resultOut ] ""
assertFailure $ unlines [ diffOut, makeGold resultOut goldFile' ]
Right fail_msg
| optIgnoreExpected opts -> return ()
| otherwise -> assertFailure fail_msg
makeGold out gold =
unlines [ "# If output is OK:"
, unwords [ "cp", out, "\\\n ", gold ]
]
runBinary :: Options -> Handle -> FilePath -> String -> IO ()
runBinary opts hout path file =
do let bin = optBinary opts
args = cfgBinOpts (optCfg opts) file
(_, _, _, ph) <- createProcess (proc bin args)
{ cwd = Just path
, std_out = UseHandle hout
, std_in = Inherit
, std_err = UseHandle hout
}
_ <- waitForProcess ph
return ()
data TestFiles = TestFiles
{ subDirs :: Map String TestFiles
, files :: Set String
}
noTests :: TestFiles
noTests = TestFiles { subDirs = Map.empty, files = Set.empty }
joinTests :: TestFiles -> TestFiles -> TestFiles
joinTests ts1 ts2 = TestFiles
{ files = Set.union (files ts1) (files ts2)
, subDirs = Map.unionWith joinTests (subDirs ts1) (subDirs ts2)
}
testFile :: FilePath -> TestFiles
testFile path = foldr addDir baseTest dirs
where
baseTest = noTests { files = Set.singleton file }
(dir,file) = splitFileName path
dirs = splitDirectories dir
addDir d t = TestFiles (Map.singleton d t) Set.empty
findTests :: Options -> IO TestFiles
findTests opts = searchMany noTests (optTests opts)
where
searchMany tests = foldM step tests
step tests path =
do isDir <- doesDirectoryExist path
if isDir
then do fs <- getDirectoryContents path
searchMany tests [ path </> f | f <- fs, not (isDotFile f) ]
else if isTestFile path
then return $! joinTests (testFile path) tests
else return tests
isDotFile path = case path of
'.' : _ -> True
_ -> False
isTestFile f = cfgIsTestCase (optCfg opts) (takeFileName f)