{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Test.Framework.CmdlineOptions (
CmdlineOptions(..), defaultCmdlineOptions, parseTestArgs, helpString,
testConfigFromCmdlineOptions
) where
import Test.Framework.TestReporter
import Test.Framework.TestTypes
import Test.Framework.History
import Test.Framework.Utils
#ifndef MIN_VERSION_base
#define MIN_VERSION_base(a,b,c) 1
#endif
#if !MIN_VERSION_base(4,6,0)
import Prelude hiding ( catch )
#endif
import Control.Exception
import Data.Char (toLower)
import Data.Maybe
import System.IO
import System.Environment hiding (getEnv)
import System.Directory
import System.Console.GetOpt
import qualified Text.Regex as R
#ifndef mingw32_HOST_OS
import System.Posix.Terminal
import System.Posix.IO (stdOutput)
import System.Posix.Env
#endif
#ifdef __GLASGOW_HASKELL__
import GHC.Conc ( numCapabilities )
#endif
import qualified Data.ByteString as BS
import Control.Monad
data CmdlineOptions = CmdlineOptions {
opts_quiet :: Bool
, opts_filter :: TestFilter
, opts_help :: Bool
, opts_negated :: [String]
, opts_threads :: Maybe Int
, opts_shuffle :: Bool
, opts_machineOutput :: Bool
, opts_machineOutputXml :: Maybe FilePath
, opts_useColors :: Maybe Bool
, opts_outputFile :: Maybe FilePath
, opts_listTests :: Bool
, opts_split :: Bool
, opts_historyFile :: Maybe FilePath
, opts_failFast :: Bool
, opts_sortByPrevTime :: Bool
, opts_maxPrevTimeMs :: Maybe Milliseconds
, opts_maxCurTimeMs :: Maybe Milliseconds
, opts_prevFactor :: Maybe Double
, opts_timeoutIsSuccess :: Bool
, opts_repeat :: Int
}
defaultCmdlineOptions :: CmdlineOptions
defaultCmdlineOptions = CmdlineOptions {
opts_quiet = False
, opts_filter = const True
, opts_help = False
, opts_negated = []
, opts_threads = Nothing
, opts_shuffle = False
, opts_machineOutput = False
, opts_machineOutputXml = Nothing
, opts_useColors = Nothing
, opts_outputFile = Nothing
, opts_listTests = False
, opts_split = False
, opts_historyFile = Nothing
, opts_failFast = False
, opts_sortByPrevTime = False
, opts_maxPrevTimeMs = Nothing
, opts_maxCurTimeMs = Nothing
, opts_prevFactor = Nothing
, opts_timeoutIsSuccess = False
, opts_repeat = 1
}
processorCount :: Int
#ifdef __GLASGOW_HASKELL__
processorCount = numCapabilities
#else
processorCount = 1
#endif
optionDescriptions :: [OptDescr (CmdlineOptions -> Either String CmdlineOptions)]
optionDescriptions =
[ Option ['q'] ["quiet"]
(NoArg (\o -> Right $ o { opts_quiet = True }))
"Only display errors."
, Option ['n'] ["not"]
(ReqArg (\s o -> Right $ o { opts_negated = s : (opts_negated o) }) "PATTERN")
"Tests to exclude."
, Option ['l'] ["list"]
(NoArg (\o -> Right $ o { opts_listTests = True }))
"List all matching tests."
, Option ['j'] ["threads"]
(OptArg (\ms o -> parseThreads ms >>= \i -> Right $ o { opts_threads = Just i }) "N")
("Run N tests in parallel, default N=" ++ show processorCount ++ ".")
, Option [] ["shuffle"]
(ReqArg (\s o -> parseBool s >>= \b -> Right $ o { opts_shuffle = b }) "BOOL")
"Shuffle test order. Default: false"
, Option ['o'] ["output-file"]
(ReqArg (\s o -> Right $ o { opts_outputFile = Just s }) "FILE")
"Name of output file."
, Option [] ["json"]
(NoArg (\o -> Right $ o { opts_machineOutput = True }))
"Output results in machine-readable JSON format (incremental)."
, Option [] ["xml"]
(ReqArg (\s o -> Right $ o { opts_machineOutputXml = Just s }) "FILE")
"Output results in junit-style XML format."
, Option [] ["split"]
(NoArg (\o -> Right $ o { opts_split = True }))
"Splits results in separate files to avoid file locking (requires -o/--output-file)."
, Option [] ["colors"]
(ReqArg (\s o -> parseBool s >>= \b -> Right $ o { opts_useColors = Just b }) "BOOL")
"Use colors or not."
, Option [] ["history"]
(ReqArg (\s o -> Right $ o { opts_historyFile = Just s }) "FILE")
"Path to the history file. Default: ./.HTF/<ProgramName>.history"
, Option [] ["fail-fast"]
(NoArg (\o -> Right $ o { opts_failFast = True }))
"Fail and abort test run as soon as the first test fails."
, Option [] ["sort-by-prev-time"]
(NoArg (\o -> Right $ o { opts_sortByPrevTime = True }))
"Sort tests ascending by their execution of the previous test run (if available). Default: false"
, Option [] ["max-prev-ms"]
(ReqArg (\s o -> parseRead "--max-prev-ms" s >>= \(ms::Int) -> Right $ o { opts_maxPrevTimeMs = Just ms }) "MILLISECONDS")
"Do not try to execute tests that had a execution time greater than MILLISECONDS in a previous test run."
, Option [] ["max-cur-ms"]
(ReqArg (\s o -> parseRead "--max-cur-ms" s >>= \(ms::Int) ->
Right $ o { opts_maxCurTimeMs = Just ms }) "MILLISECONDS")
"Abort a test that runs more than MILLISECONDS."
, Option [] ["prev-factor"]
(ReqArg (\s o -> parseRead "--prev-factor" s >>= \(ms::Double) ->
Right $ o { opts_prevFactor = Just ms }) "DOUBLE")
"Abort a test that runs more than DOUBLE times slower than in a previous run."
, Option [] ["timeout-is-success"]
(NoArg (\o -> Right $ o { opts_timeoutIsSuccess = True }))
"Do not regard a test timeout as an error."
, Option [] ["repeat"]
(ReqArg (\s o -> parseRead "--repeat" s >>= \(i::Int) ->
Right $ o { opts_repeat = i}) "NUMBER")
"Execute the tests selected on the command line NUMBER times."
, Option ['h'] ["help"]
(NoArg (\o -> Right $ o { opts_help = True }))
"Display this message."
]
where
parseThreads Nothing = Right processorCount
parseThreads (Just s) =
case readM s of
Just i -> Right i
Nothing -> Left ("invalid number of threads: " ++ s)
parseBool s =
Right $
if map toLower s `elem` ["1", "true", "yes", "on"] then True else False
parseRead opt s =
case readM s of
Just i -> Right i
Nothing -> Left ("invalid value for option " ++ opt ++ ": " ++ s)
parseTestArgs :: [String] -> Either String CmdlineOptions
parseTestArgs args =
case getOpt Permute optionDescriptions args of
(optTrans, tests, []) ->
do opts <- foldM (\o f -> f o) defaultCmdlineOptions optTrans
when (opts_shuffle opts && opts_sortByPrevTime opts) $
Left ("Options --shuffle=true and --sort-by-prev-time are in conflict. " ++
"Can only use one of both.\n\n" ++
usageInfo usageHeader optionDescriptions)
case (opts_outputFile opts, opts_split opts) of
(Nothing, True) -> Left ("Option --split requires -o or --output-file\n\n" ++
usageInfo usageHeader optionDescriptions)
_ -> let posStrs = tests
negStrs = opts_negated opts
pos = map mkRegex posStrs
neg = map mkRegex negStrs
pred (FlatTest _ path _ _) =
let flat = flatName path
in if (any (\s -> s `matches` flat) neg)
then False
else null pos || any (\s -> s `matches` flat) pos
in Right (opts { opts_filter = pred })
(_,_,errs) ->
Left (concat errs ++ usageInfo usageHeader optionDescriptions)
where
matches r s = isJust $ R.matchRegex r s
mkRegex s = R.mkRegexWithOpts s True False
usageHeader :: String
usageHeader = ("USAGE: COMMAND [OPTION ...] PATTERN ...\n\n" ++
" where PATTERN is a posix regular expression matching\n" ++
" the names of the tests to run.\n")
helpString :: String
helpString = usageInfo usageHeader optionDescriptions
testConfigFromCmdlineOptions :: CmdlineOptions -> IO TestConfig
testConfigFromCmdlineOptions opts =
do (output, colors) <-
case (opts_outputFile opts, opts_split opts) of
(Just fname, True) -> return (TestOutputSplitted fname, False)
_ -> do (outputHandle, closeOutput, mOutputFd) <- openOutputFile
colors <- checkColors mOutputFd
return (TestOutputHandle outputHandle closeOutput, colors)
let threads = opts_threads opts
reporters = defaultTestReporters (isParallelFromBool $ isJust threads)
(if opts_machineOutput opts then JsonOutput else NoJsonOutput)
(if isJust (opts_machineOutputXml opts) then XmlOutput else NoXmlOutput)
historyFile <- getHistoryFile
history <- getHistory historyFile
return $ TestConfig { tc_quiet = opts_quiet opts
, tc_threads = threads
, tc_shuffle = opts_shuffle opts
, tc_output = output
, tc_outputXml = opts_machineOutputXml opts
, tc_reporters = reporters
, tc_filter = opts_filter opts `mergeFilters` (historicFilter history)
, tc_useColors = colors
, tc_historyFile = historyFile
, tc_history = history
, tc_sortByPrevTime = opts_sortByPrevTime opts
, tc_failFast = opts_failFast opts
, tc_maxSingleTestTime = opts_maxCurTimeMs opts
, tc_prevFactor = opts_prevFactor opts
, tc_timeoutIsSuccess = opts_timeoutIsSuccess opts
, tc_repeat = opts_repeat opts
}
where
#ifdef mingw32_HOST_OS
openOutputFile =
case opts_outputFile opts of
Nothing -> return (stdout, False, Nothing)
Just fname ->
do f <- openFile fname WriteMode
return (f, True, Nothing)
checkColors mOutputFd =
case opts_useColors opts of
Just b -> return b
Nothing -> return False
#else
openOutputFile =
case opts_outputFile opts of
Nothing -> return (stdout, False, Just stdOutput)
Just fname ->
do f <- openFile fname WriteMode
return (f, True, Nothing)
checkColors mOutputFd =
case opts_useColors opts of
Just b -> return b
Nothing ->
do mterm <- getEnv "TERM"
case mterm of
Nothing -> return False
Just s | map toLower s == "dumb" -> return False
_ -> do mx <- getEnv "HTF_NO_COLORS"
case mx of
Just s | map toLower s `elem` ["", "1", "y", "yes", "true"] -> return False
_ -> case mOutputFd of
Just fd -> queryTerminal fd
_ -> return False
#endif
getHistoryFile =
case opts_historyFile opts of
Just fp -> return fp
Nothing ->
do progName <- getProgName
let x = if progName == "<interactive>" then "interactive" else progName
curDir <- getCurrentDirectory
let dir = curDir </> ".HTF"
createDirectoryIfMissing False dir
return $ dir </> (x ++ ".history")
getHistory fp =
do b <- doesFileExist fp
if not b
then return emptyTestHistory
else do bs <- BS.readFile fp
case deserializeTestHistory bs of
Right history -> return history
Left err ->
do hPutStrLn stderr ("Error deserializing content of HTF history file " ++ fp ++ ": " ++ err)
return emptyTestHistory
`catch` (\(e::IOException) ->
do hPutStrLn stderr ("Error reading HTF history file " ++ fp ++ ": " ++ show e)
return emptyTestHistory)
mergeFilters f1 f2 t =
f1 t && f2 t
historicFilter history t =
case opts_maxPrevTimeMs opts of
Nothing -> True
Just ms ->
case max (fmap htr_timeMs (findHistoricTestResult (historyKey t) history))
(fmap htr_timeMs (findHistoricSuccessfulTestResult (historyKey t) history))
of
Nothing -> True
Just t -> t <= ms