{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} module Test.DocTest.Internal.Options where import Prelude () import Prelude.Compat import Control.DeepSeq (NFData) import Data.List.Compat import GHC.Generics (Generic) import Text.Read (readMaybe) import qualified Paths_doctest_parallel import Data.Version (showVersion) #if __GLASGOW_HASKELL__ < 900 import Config as GHC #else import GHC.Settings.Config as GHC #endif import Test.DocTest.Internal.Location (Located (Located), Location) import Test.DocTest.Internal.Interpreter (ghc) import Test.DocTest.Internal.Logging (LogLevel(..)) import qualified Test.DocTest.Internal.Logging as Logging usage :: String usage = unlines [ "Usage:" , " doctest [ options ]... []..." , " doctest --help" , " doctest --version" , " doctest --info" , "" , "Options:" , " -jN number of threads to use" , " --log-level=LEVEL one of: debug, verbose, info, warning, error. Default: info." , "† --implicit-module-import import module before testing it (default)" , "† --randomize-order randomize order in which tests are run" , "† --seed=N use a specific seed to randomize test order" , "† --preserve-it preserve the `it` variable between examples" , " --nix account for Nix build environments (default)" , " --quiet set log level to `Error`, shorthand for `--log-level=error`" , " --verbose set log level to `Verbose`, shorthand for `--log-level=verbose`" , " --debug set log level to `Debug`, shorthand for `--log-level=debug`" , " --help display this help and exit" , " --version output version information and exit" , " --info output machine-readable version information and exit" , "" , "Supported inverted options:" , " --no-nix" , "† --no-implicit-module-import" , "† --no-randomize-order (default)" , "† --no-preserve-it (default)" , "" , "Options marked with a dagger (†) can also be used to set module level options, using" , "an ANN pragma like this:" , "" , " {-# ANN module \"doctest-parallel: --no-randomize-order\" #-} " , "" ] version :: String version = showVersion Paths_doctest_parallel.version ghcVersion :: String ghcVersion = GHC.cProjectVersion versionInfo :: String versionInfo = unlines [ "doctest version " ++ version , "using version " ++ ghcVersion ++ " of the GHC API" , "using " ++ ghc ] info :: String info = "[ " ++ (intercalate "\n, " . map show $ [ ("version", version) , ("ghc_version", ghcVersion) , ("ghc", ghc) ]) ++ "\n]\n" data Result a = ResultStderr String | ResultStdout String | Result a deriving (Eq, Show, Functor) type Warning = String type ModuleName = String data Config = Config { cfgLogLevel :: LogLevel -- ^ Verbosity level. , cfgModules :: [ModuleName] -- ^ Module names to test. An empty list means "test all modules". , cfgThreads :: Maybe Int -- ^ Number of threads to use. Defaults to autodetection based on the number -- of cores. , cfgModuleConfig :: ModuleConfig -- ^ Options specific to modules , cfgNix :: Bool -- ^ Detect Nix build environment and try to make GHC aware of the local package -- being tested. } deriving (Show, Eq, Generic, NFData) data ModuleConfig = ModuleConfig { cfgPreserveIt :: Bool -- ^ Preserve the @it@ variable between examples (default: @False@) , cfgRandomizeOrder :: Bool -- ^ Randomize the order in which test cases in a module are run (default: @False@) , cfgSeed :: Maybe Int -- ^ Initialize random number generator used to randomize test cases when -- 'cfgRandomizeOrder' is set. If set to 'Nothing', a random seed is picked -- from a system RNG source on startup. , cfgImplicitModuleImport :: Bool -- ^ Import a module before testing it. Can be disabled to enabled to test -- non-exposed modules. } deriving (Show, Eq, Generic, NFData) defaultModuleConfig :: ModuleConfig defaultModuleConfig = ModuleConfig { cfgPreserveIt = False , cfgRandomizeOrder = False , cfgSeed = Nothing , cfgImplicitModuleImport = True } defaultConfig :: Config defaultConfig = Config { cfgModules = [] , cfgThreads = Nothing , cfgLogLevel = Info , cfgModuleConfig = defaultModuleConfig , cfgNix = True } parseLocatedModuleOptions :: ModuleName -> ModuleConfig -> [Located String] -> Either (Location, String) ModuleConfig parseLocatedModuleOptions _modName modConfig [] = Right modConfig parseLocatedModuleOptions modName modConfig0 (Located loc o:os) = case parseModuleOption modConfig0 o of Nothing -> Left (loc, o) Just modConfig1 -> parseLocatedModuleOptions modName modConfig1 os parseModuleOption :: ModuleConfig -> String -> Maybe ModuleConfig parseModuleOption config arg = case arg of "--randomize-order" -> Just config{cfgRandomizeOrder=True} "--no-randomize-order" -> Just config{cfgRandomizeOrder=False} "--preserve-it" -> Just config{cfgPreserveIt=True} "--no-preserve-it" -> Just config{cfgPreserveIt=False} "--implicit-module-import" -> Just config{cfgImplicitModuleImport=True} "--no-implicit-module-import" -> Just config{cfgImplicitModuleImport=False} ('-':_) | Just n <- parseSeed arg -> Just config{cfgSeed=Just n} _ -> Nothing parseOptions :: [String] -> Result Config parseOptions = go defaultConfig where go config [] = Result config go config (arg:args) = case arg of "--help" -> ResultStdout usage "--info" -> ResultStdout info "--version" -> ResultStdout versionInfo "--quiet" -> go config{cfgLogLevel=Error} args "--verbose" -> go config{cfgLogLevel=Verbose} args "--debug" -> go config{cfgLogLevel=Debug} args "--nix" -> go config{cfgNix=True} args "--no-nix" -> go config{cfgNix=False} args ('-':_) | Just n <- parseThreads arg -> go config{cfgThreads=Just n} args ('-':_) | Just l <- parseLogLevel arg -> go config{cfgLogLevel=l} args ('-':_) -- Module specific configuration options | Just modCfg <- parseModuleOption (cfgModuleConfig config) arg -> go config{cfgModuleConfig=modCfg} args ('-':_) -> ResultStderr ("Unknown command line argument: " <> arg) mod_ -> go config{cfgModules=mod_ : cfgModules config} args -- | Parse seed argument -- -- >>> parseSeed "--seed=6" -- Just 6 -- >>> parseSeed "--seeeed=6" -- Nothing -- parseSeed :: String -> Maybe Int parseSeed arg = readMaybe =<< parseSpecificFlag arg "seed" -- | Parse seed argument -- -- >>> parseLogLevel "--log-level=Debug" -- Just Debug -- >>> parseLogLevel "--log-level=debug" -- Just Debug -- >>> parseSeed "---log-level=debug" -- Nothing parseLogLevel :: String -> Maybe LogLevel parseLogLevel arg = Logging.parseLogLevel =<< parseSpecificFlag arg "log-level" -- | Parse number of threads argument -- -- >>> parseThreads "-j6" -- Just 6 -- >>> parseThreads "-j-2" -- Nothing -- >>> parseThreads "-jA" -- Nothing -- parseThreads :: String -> Maybe Int parseThreads ('-':'j':n0) = do n1 <- readMaybe n0 if n1 > 0 then Just n1 else Nothing parseThreads _ = Nothing -- | Parse a specific flag with a value, or return 'Nothing' -- -- >>> parseSpecificFlag "--foo" "foo" -- Nothing -- >>> parseSpecificFlag "--foo=" "foo" -- Nothing -- >>> parseSpecificFlag "--foo=5" "foo" -- Just "5" -- >>> parseSpecificFlag "--foo=5" "bar" -- Nothing parseSpecificFlag :: String -> String -> Maybe String parseSpecificFlag arg flag = do case parseFlag arg of ('-':'-':f, value) | f == flag -> value _ -> Nothing -- | Parse a flag into its flag and argument component. -- -- Example: -- -- >>> parseFlag "--optghc=foo" -- ("--optghc",Just "foo") -- >>> parseFlag "--optghc=" -- ("--optghc",Nothing) -- >>> parseFlag "--fast" -- ("--fast",Nothing) parseFlag :: String -> (String, Maybe String) parseFlag arg = case break (== '=') arg of (flag, ['=']) -> (flag, Nothing) (flag, '=':opt) -> (flag, Just opt) (flag, _) -> (flag, Nothing)