tasty-autocollect-0.4.0: test/Test/Tasty/AutoCollect/ConfigTest.hs
{- AUTOCOLLECT.TEST -}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Test.Tasty.AutoCollect.ConfigTest (
-- $AUTOCOLLECT.TEST.export$
) where
import Control.Monad (forM_)
import Data.Bifunctor (first)
import Data.Char (isSpace)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import System.Directory (createDirectoryIfMissing)
import System.FilePath (takeDirectory, (</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Predicates
import Test.Predicates.HUnit
import Test.Predicates.QuickCheck
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import Test.Tasty.AutoCollect.Config
import TestUtils.QuickCheck
{----- Configuration syntax -----}
test =
testCase "parseConfig ignores comments" $
parseConfig "# this is a comment" @?~ right anything
test =
testCase "parseConfig ignores empty lines" $
parseConfig "\n\n\n" @?~ right anything
test_prop "parseConfig errors on ill-formed lines" =
forAll (invalidLine `suchThat` (not . isIgnored)) $ \line ->
parseConfig line `satisfies` left (startsWith "Invalid configuration line:")
where
isIgnored line = Text.all isSpace line || ("#" `Text.isPrefixOf` line)
linePart = do
ConfigPiece s <- arbitrary
spaces <- arbitrary
pure $ wrapSpaces spaces s
invalidLine =
oneof
[ -- no '=' at all
linePart
, -- '... ='
(<> "=") <$> linePart
, -- '= ...'
("=" <>) <$> linePart
, -- multiple '=' signs
do
Positive n <- arbitrary
Text.intercalate "=" <$> vectorOf (2 + n) linePart
]
test_prop :: ConfigPiece -> Spaces -> Spaces -> Property
test_prop "parseConfig strips whitespace" (ConfigPiece v) kspaces vspaces =
let k' = wrapSpaces kspaces k
v' = wrapSpaces vspaces v
in parseConfig (k' <> "=" <> v') === parseConfig (k <> "=" <> v)
where
k = "suite_name"
{----- Configuration options -----}
test =
testCase "parseConfig parses import" $
parseConfig "import = foo.conf, ../bar/baz.conf"
@?~ right (cfgImports `with` just (eq ["foo.conf", "../bar/baz.conf"]))
test_prop :: ConfigPiece -> Property
test_prop "parseConfig parses suite_name" (ConfigPiece v) =
parseConfig ("suite_name = " <> v)
`satisfies` right (cfgSuiteName `with` just (just (eq v)))
test_prop :: Property
test_prop "parseConfig parses group_type" =
forAll (elements groupTypeOptions) $ \(groupTypeName, groupType) ->
parseConfig ("group_type = " <> groupTypeName)
`satisfies` right (cfgGroupType `with` just (eq groupType))
test_prop :: ConfigPiece -> Property
test_prop "parseConfig errors on invalid group_type" (ConfigPiece v) =
v `notElem` map fst groupTypeOptions ==>
parseConfig ("group_type = " <> v) `satisfies` left (startsWith "Invalid group_type:")
groupTypeOptions :: [(Text, AutoCollectGroupType)]
groupTypeOptions =
[ ("flat", AutoCollectGroupFlat)
, ("modules", AutoCollectGroupModules)
, ("tree", AutoCollectGroupTree)
]
test_prop :: ConfigPiece -> Property
test_prop "parseConfig parses strip_suffix" (ConfigPiece v) =
parseConfig ("strip_suffix = " <> v)
`satisfies` right (cfgStripSuffix `with` just (eq v))
test_prop :: NonEmptyList HsIdentifier -> Property
test_prop "parseConfig parses ingredients" (NonEmpty (map getHsIdentifier -> ingredients)) =
parseConfig ("ingredients = " <> Text.intercalate "," ingredients)
`satisfies` right (cfgIngredients `with` just (eq ingredients))
test_prop :: NonEmptyList (HsIdentifier, Spaces) -> Property
test_prop "parseConfig strips whitespace when parsing ingredients" (NonEmpty (map (first getHsIdentifier) -> identifiers)) =
let ingredientsVal = Text.intercalate "," . map (\(s, spaces) -> wrapSpaces spaces s) $ identifiers
ingredients = map fst identifiers
in parseConfig ("ingredients = " <> ingredientsVal)
`satisfies` right (cfgIngredients `with` just (eq ingredients))
test_prop :: BoolOption -> Property
test_prop "parseConfig parses ingredients_override (case insensitive)" option =
parseConfig ("ingredients_override = " <> getText option)
`satisfies` right (cfgIngredientsOverride `with` just (eq (getBool option)))
test_prop :: ConfigPiece -> Property
test_prop "parseConfig errors on invalid ingredients_override" (ConfigPiece v) =
Text.toLower v `notElem` ["true", "false"] ==>
parseConfig ("ingredients_override = " <> v) `satisfies` left (startsWith "Invalid bool:")
test_prop :: ConfigPiece -> ConfigPiece -> Property
test_prop "parseConfig errors on unknown keys" (ConfigPiece k) (ConfigPiece v) =
(k `notElem` validKeys) && not ("#" `Text.isPrefixOf` k) ==>
parseConfig (k <> " = " <> v) `satisfies` left (startsWith "Invalid configuration key:")
where
validKeys =
[ "suite_name"
, "group_type"
, "ingredients"
, "ingredients_override"
, "strip_suffix"
]
{----- Configuration resolution -----}
test =
testCase "resolveConfig imports config recursively" $
withSystemTempDirectory "tasty-autocollect-resolveConfig" $ \tmpdir -> do
let
files =
[
( "foo/autocollect.conf"
,
[ "import = ../base/autocollect.conf"
, "suite_name = foo"
]
)
,
( "base/autocollect.conf"
,
[ "suite_name = base"
, "ingredients = baseIngredients"
]
)
]
forM_ files $ \(fpRel, fileLines) -> do
let fp = tmpdir </> fpRel
createDirectoryIfMissing True (takeDirectory fp)
Text.writeFile fp (Text.unlines fileLines)
cfg <- resolveConfig (tmpdir </> "Main.hs") mempty{cfgImports = Just ["foo/autocollect.conf"]}
cfgSuiteName cfg @?= Just "foo"
cfgIngredients cfg @?= ["baseIngredients"]
{----- Helpers -----}
-- | A Text suitable for use as a key or value in the configuration.
--
-- Specifically, will be a non-empty string that does not contain '=',
-- newlines, or trailing/leading spaces.
newtype ConfigPiece = ConfigPiece {getConfigPiece :: Text}
deriving (Show)
instance Arbitrary ConfigPiece where
arbitrary =
fmap ConfigPiece $
(`suchThat` not . Text.null) $
Text.strip . Text.filter (`notElem` ['=', '\n']) . getPrintableText <$> arbitrary
data Spaces = Spaces {before :: Int, after :: Int}
deriving (Show)
wrapSpaces :: Spaces -> Text -> Text
wrapSpaces Spaces{..} s =
Text.replicate before " " <> s <> Text.replicate after " "
instance Arbitrary Spaces where
arbitrary = do
Positive before <- arbitraryNumSpaces
Positive after <- arbitraryNumSpaces
pure $ Spaces before after
where
arbitraryNumSpaces =
frequency
[ (10, pure $ Positive 0)
, (1, arbitrary)
]
-- | An arbitrary Haskell identifier
newtype HsIdentifier = HsIdentifier {getHsIdentifier :: Text}
deriving (Show)
instance Arbitrary HsIdentifier where
arbitrary = do
modules <-
frequency
[ (5, pure [])
, (1, listOf (identStartingWith large))
]
ident <- identStartingWith small
pure $ HsIdentifier $ Text.intercalate "." $ modules ++ [ident]
where
small = ['a' .. 'z']
large = ['A' .. 'Z']
digit = ['0' .. '9']
identStartingWith start = do
c <- elements start
cs <- listOf $ elements (small ++ large ++ digit)
pure $ Text.pack (c : cs)
data BoolOption = BoolOption {getBool :: Bool, getText :: Text}
deriving (Show)
instance Arbitrary BoolOption where
arbitrary = do
b <- arbitrary
s <- genMixedCase (Text.pack $ show b)
pure $ BoolOption b s