tasty-autocollect-0.4.2: src/Test/Tasty/AutoCollect/Config.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ViewPatterns #-}
module Test.Tasty.AutoCollect.Config (
AutoCollectConfig' (..),
AutoCollectConfig,
AutoCollectConfigPartial,
AutoCollectGroupType (..),
parseConfig,
resolveConfig,
) where
import Control.Applicative ((<|>))
import Control.Monad (forM)
import Data.Functor.Identity (Identity)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.IO qualified as Text
import System.FilePath (takeDirectory, (</>))
{----- Configuration -----}
type family Apply f a where
Apply Maybe a = Maybe a
Apply Identity a = a
-- | Configuration for generating the Main module, specified as a block comment.
data AutoCollectConfig' f = AutoCollectConfig
{ cfgImports :: Apply f [FilePath]
-- ^ Files to import
, cfgSuiteName :: Apply f (Maybe Text)
-- ^ The name of the entire test suite
, cfgGroupType :: Apply f AutoCollectGroupType
-- ^ How tests should be grouped (defaults to "modules")
, cfgStripSuffix :: Apply f Text
-- ^ The suffix to strip from a test, e.g. @strip_suffix = Test@ will relabel
-- a module @Foo.BarTest@ to @Foo.Bar@.
, cfgIngredients :: Apply f [Text]
-- ^ A comma-separated list of extra tasty ingredients to include
, cfgIngredientsOverride :: Apply f Bool
-- ^ If true, 'cfgIngredients' overrides the default tasty ingredients;
-- otherwise, they're prepended to the list of default ingredients (defaults to false)
, cfgCustomMain :: Apply f Bool
}
type AutoCollectConfigPartial = AutoCollectConfig' Maybe
deriving instance Show AutoCollectConfigPartial
deriving instance Eq AutoCollectConfigPartial
type AutoCollectConfig = AutoCollectConfig' Identity
deriving instance Show AutoCollectConfig
deriving instance Eq AutoCollectConfig
data AutoCollectGroupType
= -- | All tests will be flattened like
--
-- @
-- test1
-- test2
-- test3
-- @
AutoCollectGroupFlat
| -- | Tests will be grouped by module
--
-- @
-- MyModule.MyTest1
-- test1
-- test2
-- MyModule.MyTest2
-- test3
-- @
AutoCollectGroupModules
| -- | Test modules will be grouped as a tree
--
-- @
-- MyModule
-- MyTest1
-- test1
-- test2
-- MyTest2
-- test3
-- @
AutoCollectGroupTree
deriving (Show, Eq)
-- | Config on RHS overrides config on LHS.
instance Semigroup AutoCollectConfigPartial where
cfg1 <> cfg2 =
AutoCollectConfig
{ cfgImports = cfgImports cfg2 <|> cfgImports cfg1
, cfgSuiteName = cfgSuiteName cfg2 <|> cfgSuiteName cfg1
, cfgGroupType = cfgGroupType cfg2 <|> cfgGroupType cfg1
, cfgIngredients = cfgIngredients cfg2 <|> cfgIngredients cfg1
, cfgIngredientsOverride = cfgIngredientsOverride cfg2 <|> cfgIngredientsOverride cfg1
, cfgStripSuffix = cfgStripSuffix cfg2 <|> cfgStripSuffix cfg1
, cfgCustomMain = cfgCustomMain cfg2 <|> cfgCustomMain cfg1
}
instance Monoid AutoCollectConfigPartial where
mempty =
AutoCollectConfig
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
{----- Parsing -----}
parseConfig :: Text -> Either Text AutoCollectConfigPartial
parseConfig = fmap mconcat . mapM parseLine . filter (not . isIgnoredLine) . Text.lines
where
isIgnoredLine s = Text.null (Text.strip s) || ("#" `Text.isPrefixOf` s)
parseLine :: Text -> Either Text AutoCollectConfigPartial
parseLine s = do
(k, v) <-
case Text.splitOn "=" s of
[Text.strip -> k, Text.strip -> v]
| not (Text.null k)
, not (Text.null v) ->
pure (k, v)
_ -> Left $ "Invalid configuration line: " <> Text.pack (show s)
case k of
"import" ->
pure emptyConfig{cfgImports = Just $ map Text.unpack $ parseCSV v}
"suite_name" ->
pure emptyConfig{cfgSuiteName = Just (Just v)}
"group_type" -> do
groupType <- parseGroupType v
pure emptyConfig{cfgGroupType = Just groupType}
"strip_suffix" ->
pure emptyConfig{cfgStripSuffix = Just v}
"ingredients" ->
pure emptyConfig{cfgIngredients = Just $ parseCSV v}
"ingredients_override" -> do
override <- parseBool v
pure emptyConfig{cfgIngredientsOverride = Just override}
"custom_main" -> do
customMain <- parseBool v
pure emptyConfig{cfgCustomMain = Just customMain}
_ -> Left $ "Invalid configuration key: " <> Text.pack (show k)
emptyConfig = mempty :: AutoCollectConfigPartial
parseGroupType :: Text -> Either Text AutoCollectGroupType
parseGroupType = \case
"flat" -> pure AutoCollectGroupFlat
"modules" -> pure AutoCollectGroupModules
"tree" -> pure AutoCollectGroupTree
ty -> Left $ "Invalid group_type: " <> Text.pack (show ty)
parseCSV :: Text -> [Text]
parseCSV = map Text.strip . Text.splitOn ","
parseBool :: Text -> Either Text Bool
parseBool s =
case Text.toLower s of
"true" -> pure True
"false" -> pure False
_ -> Left $ "Invalid bool: " <> Text.pack (show s)
{----- Resolving -----}
resolveConfig :: FilePath -> AutoCollectConfigPartial -> IO AutoCollectConfig
resolveConfig path0 cfg0 = resolve <$> resolveImports path0 cfg0
where
resolveImports path cfg = do
let imports = fromMaybe [] $ cfgImports cfg
fmap (mergeConfigs cfg) . forM imports $ \imp -> do
let fp = takeDirectory path </> imp
file <- Text.readFile fp
case parseConfig file of
Right cfg' -> resolveImports fp cfg'
Left e -> errorWithoutStackTrace $ "Could not parse imported config (" <> fp <> "): " <> Text.unpack e
mergeConfigs cfg importedCfgs = mconcat importedCfgs <> cfg
resolve :: AutoCollectConfigPartial -> AutoCollectConfig
resolve AutoCollectConfig{..} =
AutoCollectConfig
{ cfgImports = []
, cfgSuiteName = fromMaybe Nothing cfgSuiteName
, cfgGroupType = fromMaybe AutoCollectGroupModules cfgGroupType
, cfgIngredients = fromMaybe [] cfgIngredients
, cfgIngredientsOverride = fromMaybe False cfgIngredientsOverride
, cfgStripSuffix = fromMaybe "" cfgStripSuffix
, cfgCustomMain = fromMaybe False cfgCustomMain
}