dojang-0.2.1: test/Dojang/Types/Gen.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Dojang.Types.Gen
( arbitraryManifest
, architecture
, ciText
, fileRoute
, fileRoute'
, fileRouteMap
, emptyMonikerNameText
, environment
, environmentPredicate
, environmentVariable
, filePathExpression
, hook
, hookMap
, invalidMonikerNameText
, kernel
, manifest
, manifest'
, monikerMap
, monikerMap'
, monikerName
, monikerNameText
, monikerNameTextHavingInvalidCharacterWithIndex
, monikerNameTextStartingWithNonLetter
, monikerNameWithCIText
, monikerNameError
, operatingSystem
, osPath
, osString
, specificity
) where
import Data.Char (isAlpha, isAlphaNum, isAscii, isControl, toUpper)
import Data.List (nub)
import System.IO.Unsafe (unsafePerformIO)
import Prelude hiding (lookup)
import Data.CaseInsensitive (CI, mk)
import Data.HashMap.Strict (HashMap, fromList, keys, lookup)
import Data.Text (Text, cons, length, pack)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range (Range, constant, constantFrom, singleton)
import System.OsPath (OsPath, encodeFS, joinPath)
import System.OsString (OsString)
import Test.Hspec.Hedgehog (MonadGen)
import Data.Map.Strict qualified as Map
import Dojang.MonadFileSystem (FileType (..))
import Dojang.Types.Environment
( Architecture (..)
, Environment (Environment)
, Kernel (..)
, OperatingSystem (..)
)
import Dojang.Types.EnvironmentPredicate
( EnvironmentPredicate (..)
, normalizePredicate
)
import Dojang.Types.EnvironmentPredicate.Specificity (Specificity (..))
import Dojang.Types.FilePathExpression
( EnvironmentVariable
, FilePathExpression (..)
)
import Dojang.Types.FileRoute qualified as FileRoute
import Dojang.Types.FileRouteMap (FileRouteMap)
import Dojang.Types.Hook (Hook (..), HookMap, HookType (..))
import Dojang.Types.Manifest (Manifest (..))
import Dojang.Types.MonikerMap (MonikerMap)
import Dojang.Types.MonikerName
( MonikerName
, MonikerNameError (..)
, parseMonikerName
)
ciText :: (MonadGen m) => Range Int -> m Char -> m (CI Text)
ciText range char = mk <$> Gen.text range char
monikerNameText :: (MonadGen m) => m Text
monikerNameText = do
firstChar <- Gen.text (constant 1 1) Gen.alpha
rest <- Gen.text (constantFrom 0 0 256) Gen.alphaNum
return $ firstChar <> rest
monikerNameWithCIText :: (MonadGen m) => m (MonikerName, CI Text)
monikerNameWithCIText = do
name <- monikerNameText
let Right monikerName' = parseMonikerName name
return (monikerName', mk name)
monikerName :: (MonadGen m) => m MonikerName
monikerName = fst <$> monikerNameWithCIText
invalidMonikerNameText :: (MonadGen m) => m Text
invalidMonikerNameText =
Gen.choice
[ emptyMonikerNameText
, monikerNameTextStartingWithNonLetter
, fst <$> monikerNameTextHavingInvalidCharacterWithIndex
]
emptyMonikerNameText :: (MonadGen m) => m Text
emptyMonikerNameText = Gen.text (constant 0 0) Gen.unicodeAll
monikerNameTextStartingWithNonLetter :: (MonadGen m) => m Text
monikerNameTextStartingWithNonLetter = do
firstChar <- Gen.text (constant 1 1) Gen.digit
rest <- Gen.text (constantFrom 0 0 256) Gen.unicodeAll
return $ firstChar <> rest
monikerNameTextHavingInvalidCharacterWithIndex :: (MonadGen m) => m (Text, Int)
monikerNameTextHavingInvalidCharacterWithIndex = do
a <- Gen.text (constantFrom 1 1 256) Gen.alpha
b <-
Gen.text (constant 1 1) $
Gen.filterT (\c -> c /= '-' && c /= '_' && not (isAlphaNum c)) Gen.unicodeAll
c <- Gen.text (constantFrom 0 0 256) Gen.alphaNum
return (a <> b <> c, Data.Text.length a)
monikerNameError :: (MonadGen m) => m MonikerNameError
monikerNameError =
Gen.choice
[ return Empty
, return StartingWithNonLetter
, HavingInvalidCharacter <$> Gen.integral (constantFrom 0 0 256)
]
operatingSystem :: (MonadGen m) => m OperatingSystem
operatingSystem =
Gen.choice
[ return Android
, return FreeBSD
, return Linux
, return MacOS
, return NetBSD
, return OpenBSD
, return Windows
, OtherOS <$> ciText (constant 0 256) Gen.unicodeAll
]
architecture :: (MonadGen m) => m Architecture
architecture =
Gen.choice
[ return AArch64
, return X86
, return X86_64
, Etc <$> ciText (constant 0 256) Gen.unicodeAll
]
kernelName :: (MonadGen m) => m (CI Text)
kernelName = ciText (constant 1 50) Gen.unicodeAll
kernelRelease :: (MonadGen m) => m (CI Text)
kernelRelease = ciText (constant 1 50) Gen.unicodeAll
kernel :: (MonadGen m) => m Kernel
kernel = Kernel <$> kernelName <*> kernelRelease
environment :: (MonadGen m) => m Environment
environment = Environment <$> operatingSystem <*> architecture <*> kernel
environmentPredicate :: (MonadGen m) => m EnvironmentPredicate
environmentPredicate = environmentPredicate' 5
environmentPredicate' :: (MonadGen m) => Int -> m EnvironmentPredicate
environmentPredicate' maxDepth =
Gen.choice $
[ return Always
, Moniker <$> monikerName
, OperatingSystem <$> operatingSystem
, Architecture <$> architecture
, KernelName <$> kernelName
, KernelRelease <$> kernelRelease
, KernelReleasePrefix <$> (ciText (constant 0 50) Gen.unicodeAll)
, KernelReleaseSuffix <$> (ciText (constant 0 50) Gen.unicodeAll)
]
++ if maxDepth < 1
then []
else
[ Not <$> environmentPredicate' nextMaxDepth
, And
<$> Gen.nonEmpty
(constant 0 nextMaxDepth)
(environmentPredicate' nextMaxDepth)
, Or
<$> Gen.nonEmpty
(constant 0 nextMaxDepth)
(environmentPredicate' nextMaxDepth)
]
where
nextMaxDepth :: Int
nextMaxDepth = maxDepth - 1
specificity :: (MonadGen m) => m Specificity
specificity =
Specificity
<$> Gen.integral (constantFrom 0 0 8)
<*> Gen.integral (constantFrom 0 0 8)
osChar :: (MonadGen m) => m Char
osChar =
Gen.filterT (not . isControl) $
Gen.frequency [(5, Gen.alphaNum), (3, Gen.latin1), (2, Gen.unicode)]
environmentVariable :: (MonadGen m) => m EnvironmentVariable
environmentVariable =
Gen.frequency
[ (8, typicalEnvVar)
, (2, Gen.text (constantFrom 1 1 256) osChar)
]
where
typicalEnvVar :: (MonadGen m) => m EnvironmentVariable
typicalEnvVar = do
firstChar <- Gen.upper
rest <-
Gen.text
(constantFrom 1 1 50)
$ Gen.frequency [(8, Gen.upper), (1, Gen.digit), (1, return '_')]
return $ cons firstChar rest
filePathExpression :: (MonadGen m) => m FilePathExpression
filePathExpression = filePathExpression' 5
filePathExpression' :: (MonadGen m) => Int -> m FilePathExpression
filePathExpression' maxDepth =
Gen.choice $
[ bareComponent
, root
, substitution
]
++ if maxDepth > 0
then
[ concatenation
, pathSeparator
, substitutionWithDefault
, conditionalSubstitution
]
else []
where
filePathExpr :: (MonadGen m) => m FilePathExpression
filePathExpr = filePathExpression' (maxDepth - 1)
bareComponent :: (MonadGen m) => m FilePathExpression
bareComponent =
BareComponent
<$> Gen.text
(constantFrom 1 1 256)
( Gen.filterT
(`notElem` ['/', '\\', '$', '{', '}', ':', '\r', '\n'])
osChar
)
root :: (MonadGen m) => m FilePathExpression
root = Root . fmap toUpper <$> Gen.maybe Gen.alpha
concatenation :: (MonadGen m) => m FilePathExpression
concatenation =
Gen.filterT
( \case
Concatenation (Concatenation _ _) _ -> False
Concatenation _ (Concatenation _ _) -> False
Concatenation (PathSeparator _ _) _ -> False
Concatenation _ (PathSeparator _ _) -> False
c@(Concatenation (BareComponent _) b) ->
not (startsWithBareComponent b) && not (containsRoot c)
c -> not $ containsRoot c
)
$ Concatenation
<$> filePathExpr
<*> filePathExpr
pathSeparator :: (MonadGen m) => m FilePathExpression
pathSeparator =
PathSeparator
<$> filePathExpr
<*> Gen.filterT
(\case PathSeparator _ _ -> False; e -> not $ containsRoot e)
filePathExpr
envVar :: (MonadGen m) => m EnvironmentVariable
envVar = do
first <- Gen.filterT (\c -> isAscii c && isAlpha c || c == '_') osChar
rest <-
Gen.text (constantFrom 0 0 256) $
Gen.filterT (\c -> isAscii c && isAlphaNum c || c == '_') osChar
return $ first `cons` rest
substitution :: (MonadGen m) => m FilePathExpression
substitution = Substitution <$> envVar
substitutionWithDefault :: (MonadGen m) => m FilePathExpression
substitutionWithDefault =
SubstitutionWithDefault
<$> envVar
<*> Gen.filterT (\case Root _ -> False; _ -> True) filePathExpr
conditionalSubstitution :: (MonadGen m) => m FilePathExpression
conditionalSubstitution =
ConditionalSubstitution
<$> envVar
<*> Gen.filterT (\case Root _ -> False; _ -> True) filePathExpr
startsWithBareComponent :: FilePathExpression -> Bool
startsWithBareComponent (BareComponent _) = True
startsWithBareComponent (Concatenation a _) = startsWithBareComponent a
startsWithBareComponent (PathSeparator a _) = startsWithBareComponent a
startsWithBareComponent _ = False
containsRoot :: FilePathExpression -> Bool
containsRoot (Root _) = True
containsRoot (Concatenation a b) = containsRoot a || containsRoot b
containsRoot (PathSeparator a b) = containsRoot a || containsRoot b
containsRoot (SubstitutionWithDefault _ expr) = containsRoot expr
containsRoot (ConditionalSubstitution _ expr) = containsRoot expr
containsRoot _ = False
monikerMap :: (MonadGen m) => m (HashMap MonikerName EnvironmentPredicate)
monikerMap = monikerMap' (constantFrom 0 0 10)
monikerMap'
:: (MonadGen m) => Range Int -> m (HashMap MonikerName EnvironmentPredicate)
monikerMap' sizeRange = do
keys' <- Gen.list sizeRange monikerName
let cardinality = Prelude.length keys'
values <-
Gen.list (constant cardinality cardinality) $
normalizePredicate
<$> environmentPredicate
return $ fromList $ zip keys' values
fileRoute :: (MonadGen m) => m FileRoute.FileRoute
fileRoute = do
mm <- monikerMap' (constant 0 5)
fileRoute' (constant 0 5) mm environmentPredicate
fileRoute'
:: (MonadGen m)
=> Range Int
-> MonikerMap
-> m EnvironmentPredicate
-> m FileRoute.FileRoute
fileRoute' predicatesNumberRange mm predGen = do
predicates <-
nub . map normalizePredicate <$> Gen.list predicatesNumberRange predGen
let cardinality = Prelude.length predicates
paths <-
Gen.list (constant cardinality cardinality) $ Gen.maybe filePathExpression
fileOrDir <- Gen.element [File, Directory]
return $ FileRoute.fileRoute' (`lookup` mm) (predicates `zip` paths) fileOrDir
osString :: (MonadGen m) => Range Int -> m Char -> m OsString
osString range = fmap (unsafePerformIO . encodeFS) . Gen.string range
osPath :: (MonadGen m) => Range Int -> m OsPath
osPath range =
joinPath
<$> Gen.list
range
( osString
(constant 1 100)
( Gen.filterT
(\c -> c `notElem` "/\\:" && not (isControl c))
Gen.unicode
)
)
fileRouteMap :: (MonadGen m) => Range Int -> MonikerMap -> m FileRouteMap
fileRouteMap range monikers =
Gen.map range $ do
key <- osPath (singleton 1)
value <- representableRoute
return (key, value)
where
monikerPredicates :: [EnvironmentPredicate]
monikerPredicates =
fmap Moniker $ case keys monikers of
[] ->
let Right undefined' = parseMonikerName $ pack "undefined"
in [undefined']
names -> names
representableRoute :: (MonadGen m) => m FileRoute.FileRoute
representableRoute =
fileRoute' (constant 0 5) monikers $ Gen.element monikerPredicates
arbitraryFileRouteMap
:: (MonadGen m) => Range Int -> MonikerMap -> m FileRouteMap
arbitraryFileRouteMap range monikers =
Gen.map range $ do
key <- osPath (singleton 1)
value <- fileRoute' (constant 0 5) monikers environmentPredicate
return (key, value)
ignoreMap :: (MonadGen m) => m (Map.Map OsPath [String])
ignoreMap = Gen.map (constantFrom 0 0 5) $ do
path <- osPath (singleton 1)
patterns <- Gen.list (constantFrom 1 1 5) $ Gen.string (constant 0 20) osChar
pure (path, patterns)
manifest' :: forall m. (MonadGen m) => Range Int -> Range Int -> m Manifest
manifest' monikerMapRange fileRouteMapRange = do
monikers <- monikerMap' monikerMapRange
fileRoutes <- fileRouteMap fileRouteMapRange monikers
ignorePatterns <- ignoreMap
hooks <- hookMap (constant 0 3)
return $ Manifest monikers fileRoutes ignorePatterns hooks
manifest :: (MonadGen m) => m Manifest
manifest = manifest' (constantFrom 0 0 5) (constantFrom 0 0 5)
arbitraryManifest :: (MonadGen m) => m Manifest
arbitraryManifest = do
monikers <- monikerMap' (constantFrom 0 0 5)
fileRoutes <- arbitraryFileRouteMap (constantFrom 0 0 5) monikers
ignorePatterns <- ignoreMap
hooks <- hookMap (constant 0 3)
return $ Manifest monikers fileRoutes ignorePatterns hooks
hook :: (MonadGen m) => m Hook
hook = do
cmd <- osPath (constant 1 3)
argsCount <- Gen.integral (constant 0 5)
args' <- Gen.list (singleton argsCount) $ Gen.text (constant 1 20) Gen.alphaNum
cond <- normalizePredicate <$> environmentPredicate' 2
workDir <- Gen.maybe $ osPath (constant 1 3)
ignoreFail <- Gen.bool
return
Hook
{ command = cmd
, args = args'
, condition = cond
, workingDirectory = workDir
, ignoreFailure = ignoreFail
}
hookMap :: (MonadGen m) => Range Int -> m HookMap
hookMap range = do
-- Generate a list of hook types to include (0 to 4 types)
hookTypes <-
Gen.subsequence [PreApply, PreFirstApply, PostFirstApply, PostApply]
pairs <- mapM (\ht -> (,) ht <$> Gen.list range hook) hookTypes
return $ Map.fromList [(ht, hs) | (ht, hs) <- pairs, not (null hs)]