module Test.Tasty.CmdLine
( optionParser
, suiteOptions
, suiteOptionParser
, parseOptions
, defaultMainWithIngredients
) where
import Control.Arrow
import Control.Monad
import Data.Maybe
import Data.Proxy
import Data.Typeable (typeRep)
import Options.Applicative
import Options.Applicative.Common (evalParser)
import qualified Options.Applicative.Types as Applicative (Option(..))
import Options.Applicative.Types (Parser(..), OptProperties(..))
import Prelude
import System.Exit
import System.IO
#if !MIN_VERSION_base(4,11,0)
import Data.Monoid
import Data.Foldable (foldMap)
#endif
import Test.Tasty.Core
import Test.Tasty.Runners.Utils
import Test.Tasty.Ingredients
import Test.Tasty.Options
import Test.Tasty.Options.Env
import Test.Tasty.Runners.Reducers
optionParser :: [OptionDescription] -> ([String], Parser OptionSet)
optionParser = second getApp . foldMap toSet where
toSet :: OptionDescription -> ([String], Ap Parser OptionSet)
toSet (Option p) = second
(\parser -> Ap $ (singleOption <$> parser) <|> pure mempty)
(finalizeCLParser p optionCLParser)
finalizeCLParser :: forall proxy v . IsOption v
=> proxy v -> Parser v -> ([String], Parser v)
finalizeCLParser _ p = (warnings, setCLParserShowDefaultValue mbDef p)
where
mbDef :: Maybe String
mbDef = showDefaultValue (defaultValue :: v)
warnings :: [String]
warnings = catMaybes [multipleOptPsWarning, badDefaultWarning]
multipleOptPsWarning :: Maybe String
multipleOptPsWarning
| numOptPs p > 1
= Just $ unlines
[ prov
, "optionCLParser defines multiple options. Consider only defining"
, "a single option here, as defining multiple options does not play"
, "well with how tasty displays default values."
]
| otherwise
= Nothing
badDefaultWarning :: Maybe String
badDefaultWarning
| isJust (evalParser p)
= Just $ unlines
[ prov
, "Using default values (e.g., with Options.Applicative.value) in"
, "optionCLParser is prohibited, as it interferes with tasty's ability"
, "to read environment variable options properly. Moreover, assigning"
, "default values is unnecessary, as their functionality is subsumed"
, "by the defaultValue method of IsOption."
]
| otherwise
= Nothing
prov :: String
prov = "WARNING (in the IsOption instance for "
++ show (typeRep (Proxy :: Proxy v)) ++ "):"
setCLParserShowDefaultValue :: Maybe String -> Parser a -> Parser a
setCLParserShowDefaultValue mbDef = go
where
go :: Parser a -> Parser a
go (OptP o) = OptP o{Applicative.optProps =
modifyDefault (Applicative.optProps o)}
go p@NilP{} = p
go (MultP p1 p2) = MultP (go p1) (go p2)
go (AltP p1 p2) = AltP (go p1) (go p2)
go (BindP p1 p2) = BindP (go p1) (fmap go p2)
modifyDefault :: OptProperties -> OptProperties
modifyDefault op = op{propShowDefault = mbDef}
numOptPs :: Parser a -> Int
numOptPs OptP{} = 1
numOptPs NilP{} = 0
numOptPs (MultP p1 p2) = numOptPs p1 + numOptPs p2
numOptPs (AltP p1 p2) = numOptPs p1 + numOptPs p2
numOptPs (BindP p1 _p2) = numOptPs p1
suiteOptionParser :: [Ingredient] -> TestTree -> ([String], Parser OptionSet)
suiteOptionParser ins tree = optionParser $ suiteOptions ins tree
parseOptions :: [Ingredient] -> TestTree -> IO OptionSet
parseOptions ins tree = do
let (warnings, parser) = suiteOptionParser ins tree
mapM_ (hPutStrLn stderr) warnings
cmdlineOpts <- execParser $
info (helper <*> parser)
( fullDesc <>
header "Mmm... tasty test suite"
)
envOpts <- suiteEnvOptions ins tree
return $ envOpts <> cmdlineOpts
defaultMainWithIngredients :: [Ingredient] -> TestTree -> IO ()
defaultMainWithIngredients ins testTree = do
installSignalHandlers
opts <- parseOptions ins testTree
case tryIngredients ins opts testTree of
Nothing -> do
hPutStrLn stderr
"No ingredients agreed to run. Something is wrong either with your ingredient set or the options."
exitFailure
Just act -> do
ok <- act
if ok then exitSuccess else exitFailure