-- | The test driver configuration options module. -- -- Anything that can be passed as an argument to the test driver -- definition exists as a field in the 'Config' type. module Test.Tasty.Discover.Internal.Config ( -- * Configuration Options Config (..) , GlobPattern , SkipTest (..) , OnPlatform (..) , checkPlatform , onLinux , onDarwin , onWindows , onUnix -- * Configuration Parser , parseConfig -- * Configuration Defaults , defaultConfig ) where import Data.Maybe (isJust) import GHC.Generics (Generic) import System.Console.GetOpt (ArgDescr (NoArg, ReqArg), ArgOrder (Permute), OptDescr (Option), getOpt') import System.FilePath (()) import System.Info (os) import Test.Tasty.Options (IsOption (..), safeRead) -- | A tasty ingredient. type Ingredient = String -- | A glob pattern. type GlobPattern = String -- | Newtype wrapper for skip test option. -- -- This option type integrates with Tasty's option system to control whether -- tests should be skipped. When set to @SkipTest True@, tests will show as -- @[SKIPPED]@ in yellow in the test output and won't actually execute. -- -- Used internally by the 'skip' function and 'Flavored' type to implement -- test skipping functionality. newtype SkipTest = SkipTest Bool deriving stock (Show, Eq, Generic) instance IsOption SkipTest where defaultValue = SkipTest False parseValue = fmap SkipTest . safeRead optionName = return "skip-test" optionHelp = return "Skip test execution (useful for debugging test discovery)" -- | Newtype wrapper for platform-specific test filtering. -- -- This option type allows tests to be conditionally executed based on platform -- criteria. The wrapped function takes a platform string and returns whether -- the test should run on that platform. -- -- Platform values correspond to System.Info.os: -- - "linux" for Linux systems -- - "darwin" for macOS -- - "mingw32" for Windows (GHC compiled) -- - "unix" matches both "linux" and "darwin" -- -- Example usage: -- @ -- -- Only run on Linux -- onLinux :: OnPlatform -- onLinux = OnPlatform (== "linux") -- -- -- Run on Unix-like systems -- onUnix :: OnPlatform -- onUnix = OnPlatform (\p -> p `elem` ["linux", "darwin"]) -- @ newtype OnPlatform = OnPlatform (String -> Bool) instance IsOption OnPlatform where defaultValue = OnPlatform (const True) -- Run on all platforms by default parseValue s = case s of "linux" -> Just $ OnPlatform (== "linux") "darwin" -> Just $ OnPlatform (== "darwin") "mingw32" -> Just $ OnPlatform (== "mingw32") -- Windows with GHC "windows" -> Just $ OnPlatform (== "mingw32") -- Alias for mingw32 "unix" -> Just $ OnPlatform (\p -> p `elem` ["linux", "darwin"]) _ -> Nothing optionName = return "on-platform" optionHelp = return "Run test only on specified platform (linux|darwin|mingw32|windows|unix)" -- | Check if the current platform matches the OnPlatform criteria checkPlatform :: OnPlatform -> Bool checkPlatform (OnPlatform f) = f os -- | Helper function: only run on Linux onLinux :: OnPlatform onLinux = OnPlatform (== "linux") -- | Helper function: only run on macOS onDarwin :: OnPlatform onDarwin = OnPlatform (== "darwin") -- | Helper function: only run on Windows (mingw32) onWindows :: OnPlatform onWindows = OnPlatform (== "mingw32") -- | Helper function: only run on Unix-like systems (Linux or macOS) onUnix :: OnPlatform onUnix = OnPlatform (\p -> p `elem` ["linux", "darwin"]) -- | The discovery and runner configuration. data Config = Config { modules :: Maybe GlobPattern -- ^ Glob pattern for matching modules during test discovery. , moduleSuffix :: Maybe String -- ^ <<>>: Module suffix. , searchDir :: FilePath -- ^ Directory where to look for tests. , generatedModuleName :: Maybe String -- ^ Name of the generated main module. , ignores :: Maybe GlobPattern -- ^ Glob pattern for ignoring modules during test discovery. , ignoredModules :: [FilePath] -- ^ <<>>: Ignored modules by full name. , tastyIngredients :: [Ingredient] -- ^ Tasty ingredients to use. , tastyOptions :: [String] -- ^ Options passed to tasty , inPlace :: Bool -- ^ Whether the source file should be modified in-place. , noModuleSuffix :: Bool -- ^ <<>>: suffix and look in all modules. , debug :: Bool -- ^ Debug the generated module. , treeDisplay :: Bool -- ^ Tree display for the test results table. , noMain :: Bool -- ^ Whether to generate main function. } deriving stock (Show, Generic) -- | The default configuration defaultConfig :: FilePath -> Config defaultConfig theSearchDir = Config Nothing Nothing theSearchDir Nothing Nothing [] [] [] False False False False False -- | Deprecation message for old `--[no-]module-suffix` option. moduleSuffixDeprecationMessage :: String moduleSuffixDeprecationMessage = error $ concat [ "\n\n" , "----------------------------------------------------------\n" , "DEPRECATION NOTICE: `--[no-]module-suffix` is deprecated.\n" , "The default behaviour now discovers all test module suffixes.\n" , "Please use the `--modules=''` option to specify.\n" , "----------------------------------------------------------\n" ] -- | Deprecation message for old `--ignore-module` option. ignoreModuleDeprecationMessage :: String ignoreModuleDeprecationMessage = error $ concat [ "\n\n" , "----------------------------------------------------------\n" , "DEPRECATION NOTICE: `--ignore-module` is deprecated.\n" , "Please use the `--ignores=''` option instead.\n" , "----------------------------------------------------------\n" ] -- | Configuration options parser. parseConfig :: FilePath -> String -> [String] -> Either String Config parseConfig srcDir prog args = case getOpt' Permute (options srcDir) args of (opts, rest, rest', []) -> let config = foldl (flip id) (defaultConfig srcDir) { tastyOptions = rest ++ rest' } opts in if noModuleSuffix config || isJust (moduleSuffix config) then error moduleSuffixDeprecationMessage else if not $ null (ignoredModules config) then error ignoreModuleDeprecationMessage else Right config (_, _, _, err:_) -> formatError err where formatError err = Left (prog ++ ": " ++ err) -- | All configuration options. options :: FilePath -> [OptDescr (Config -> Config)] options srcDir = [ Option [] ["modules"] (ReqArg (\s c -> c {modules = Just s}) "GLOB-PATTERN") "Specify desired modules with a glob pattern (white-list)" , Option [] ["module-suffix"] (ReqArg (\s c -> c {moduleSuffix = Just s}) "SUFFIX") "<<>>: Specify desired test module suffix" , Option [] ["search-dir"] (ReqArg (\s c -> c {searchDir = srcDir s}) "DIR") "Directory where to look for tests relative to the directory of src. By default, this is the directory of src." , Option [] ["generated-module"] (ReqArg (\s c -> c {generatedModuleName = Just s}) "MODULE") "Qualified generated module name" , Option [] ["ignores"] (ReqArg (\s c -> c {ignores = Just s}) "GLOB-PATTERN") "Specify desired modules to ignore with a glob pattern (black-list)" , Option [] ["ignore-module"] (ReqArg (\s c -> c {ignoredModules = s : ignoredModules c}) "FILE") "<<>>: Ignore a test module" , Option [] ["ingredient"] (ReqArg (\s c -> c {tastyIngredients = s : tastyIngredients c}) "INGREDIENT") "Qualified tasty ingredient name" , Option [] ["in-place"] (NoArg $ \c -> c {inPlace = True}) "Whether the source file should be modified in-place" , Option [] ["no-module-suffix"] (NoArg $ \c -> c {noModuleSuffix = True}) "<<>>: Ignore test module suffix and import them all" , Option [] ["debug"] (NoArg $ \c -> c {debug = True}) "Debug output of generated test module" , Option [] ["tree-display"] (NoArg $ \c -> c {treeDisplay = True}) "Display test output hierarchically" , Option [] ["no-main"] (NoArg $ \c -> c {noMain = True}) "Do not generate a main function" ]