packages feed

tasty-autocollect-0.1.0.0: src/Test/Tasty/AutoCollect/Config.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}

module Test.Tasty.AutoCollect.Config (
  AutoCollectConfig (..),
  AutoCollectGroupType (..),
  defaultConfig,
  parseConfig,
) where

import Data.Text (Text)
import qualified Data.Text as Text

{----- Configuration -----}

-- | Configuration for generating the Main module, specified as a block comment.
data AutoCollectConfig = AutoCollectConfig
  { cfgSuiteName :: Maybe Text
  -- ^ The name of the entire test suite
  , cfgGroupType :: AutoCollectGroupType
  -- ^ How tests should be grouped (defaults to "modules")
  , cfgStripSuffix :: Text
  -- ^ The suffix to strip from a test, e.g. @strip_suffix = Test@ will relabel
  -- a module @Foo.BarTest@ to @Foo.Bar@.
  , cfgIngredients :: [Text]
  -- ^ A comma-separated list of extra tasty ingredients to include
  , cfgIngredientsOverride :: Bool
  -- ^ If true, 'cfgIngredients' overrides the default tasty ingredients;
  -- otherwise, they're prepended to the list of default ingredients (defaults to false)
  }
  deriving (Show, Eq)

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)

defaultConfig :: AutoCollectConfig
defaultConfig =
  AutoCollectConfig
    { cfgSuiteName = Nothing
    , cfgGroupType = AutoCollectGroupModules
    , cfgIngredients = []
    , cfgIngredientsOverride = False
    , cfgStripSuffix = ""
    }

parseConfig :: Text -> Either Text AutoCollectConfig
parseConfig = fmap resolve . mapM parseLine . filter (not . isIgnoredLine) . Text.lines
  where
    isIgnoredLine s = Text.null (Text.strip s) || ("#" `Text.isPrefixOf` s)

    parseLine :: Text -> Either Text (AutoCollectConfig -> AutoCollectConfig)
    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
        "suite_name" ->
          pure $ \cfg -> cfg{cfgSuiteName = Just v}
        "group_type" -> do
          groupType <- parseGroupType v
          pure $ \cfg -> cfg{cfgGroupType = groupType}
        "strip_suffix" ->
          pure $ \cfg -> cfg{cfgStripSuffix = v}
        "ingredients" -> do
          let ingredients = map Text.strip . Text.splitOn "," $ v
          pure $ \cfg -> cfg{cfgIngredients = ingredients}
        "ingredients_override" -> do
          override <- parseBool v
          pure $ \cfg -> cfg{cfgIngredientsOverride = override}
        _ -> Left $ "Invalid configuration key: " <> Text.pack (show k)

    resolve fs = compose fs defaultConfig

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)

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)

{----- Utilities -----}

-- | [f, g, h] => (h . g . f)
compose :: [a -> a] -> a -> a
compose fs = foldr (\f acc -> acc . f) id fs