tasty-sugar-1.3.0.0: src/internal/Test/Tasty/Sugar/ParamCheck.hs
{-# LANGUAGE LambdaCase #-}
-- | Functions for checking different parameter/value combinations.
module Test.Tasty.Sugar.ParamCheck
(
eachFrom
, getPVals
, singlePVals
, pvalMatch
, removePVals
, pmatchCmp
, pmatchMax
, dirMatches
, inEachNothing
, isCompatible
)
where
import Control.Monad
import Control.Monad.Logic
import Data.Function ( on )
import qualified Data.List as L
import Data.Maybe ( catMaybes, fromJust, isNothing, listToMaybe )
import Data.Bifunctor ( first )
import Data.Maybe ( fromMaybe )
import Test.Tasty.Sugar.Types
-- | Core Logic function to iteratively return elements of a list via
-- backtracking.
eachFrom :: [a] -> Logic a
eachFrom = foldr (mplus . return) mzero
-- | Returns various combinations of parameter value selections
getPVals :: [ParameterPattern] -> Logic [(String, Maybe String)]
getPVals = mapM getPVal
where
getPVal (pn, Nothing) = return (pn, Nothing)
getPVal (pn, Just pv) = do pv' <- eachFrom pv
return (pn, Just pv')
-- | Returns a ParameterPattern admitting only a single value for each parameter,
-- ensuring that the value is compatible with any existing NamedParamMatch. This
-- is useful for callers wishing to handle each combination of parameter values
-- separately.
singlePVals :: [NamedParamMatch] -> [ParameterPattern]
-> Logic [ParameterPattern]
singlePVals sel = eachVal . L.sort
where eachVal [] = return []
eachVal ((pn,Nothing):ps) =
let this = (pn, (:[]) <$> (lookup pn sel >>= getParamVal))
in (this :) <$> eachVal ps
eachVal ((pn,Just pvs):ps) =
do pv <- eachFrom $ case lookup pn sel >>= getParamVal of
Nothing -> L.sort pvs
Just v -> [v]
((pn, Just [pv]) :) <$> eachVal ps
-- | Generate each possible combination of Explicit or non-Explicit
-- (Assumed or NotSpecified) parameter value and the corresponding
-- string with each combination of separators. The string will be
-- used to match against input files.
--
-- Note that valid combinations require that if a parameter is
-- non-Explicit, all following parameters must also be non-Explicit.
--
-- The preset set of parameters are any parameters *already* matched
-- against (usually in the rootName); these parameters may or may not
-- be present in the filename matched from the output of this
-- function, but if they are present, they must have the values
-- specified in the preset (instead of having any of the possible
-- values allowed for that parameter).
--
-- It's also possible that since this returns varying combinations of
-- parameters, that there may be multiple files that will match
-- against these combinations. Therefore, the results also indicate
-- how many of the parameters are used in the associated matching
-- string since the caller will usually select the match with the
-- highest ranking (number of matched parameters) in the filename.
-- [Note that it is not possibly to simply use the length of the
-- @[NamedParamMatch]@ return component since that may contain values
-- from the preset that don't actually occur in the match string.
pvalMatch :: Separators
-> [NamedParamMatch]
-> [(String, Maybe String)]
-> Logic ([NamedParamMatch], Int, String)
pvalMatch seps preset pvals =
let (ppv, _rpv) = L.partition isPreset pvals
isPreset p = fst p `elem` (fmap fst preset)
matchesPreset = all matchPreset ppv
matchPreset (pn,mpv) = maybe False (matchPresetVal mpv) $
lookup pn preset
matchPresetVal mpv pv = case mpv of
Just v -> paramMatchVal v pv
Nothing -> True
genPVStr :: [NamedParamMatch] -> Logic String
genPVStr pvs =
let vstr = fromMaybe "" . getExplicit . snd
sepJoin :: String -> NamedParamMatch -> Logic String
sepJoin r v = if isExplicit (snd v)
then do s <- eachFrom seps
return $ [s] <> vstr v <> r
else return r
in if null seps
then return $ foldr (\v r -> vstr v <> r) "" pvs
else do s <- eachFrom seps
foldM sepJoin [s] pvs
in do guard $ matchesPreset
candidateVals <- pvVals preset pvals
let rset = preset <> removePVals candidateVals preset
orderedRset = fmap from_rset $ fmap fst pvals
from_rset n = let v = maybe NotSpecified id $ L.lookup n rset in (n,v)
pvstr <- genPVStr orderedRset
return (rset, length orderedRset, pvstr)
-- | Generate the various combinations of parameters+values from the possible
-- set specified by the input.
pvVals :: [NamedParamMatch] -> [(String, Maybe String)] -> Logic [NamedParamMatch]
pvVals _ [] = return []
pvVals presets ((pn, mpv):ps) =
do nxt <- pvVals presets ps
let explicit v = return $ (pn, Explicit v) : nxt
notExplicit = let pMatchImpl =
case lookup pn presets of
Nothing -> maybe NotSpecified Assumed
Just presetV -> const presetV
in return $ (pn, pMatchImpl mpv) : nxt
(maybe mzero explicit mpv) `mplus` notExplicit
-- | Removes the second set of named params from the first set, leaving the
-- remainder of the first set that isn't matched in the second set.
removePVals :: [(String, a)] -> [(String, b)] -> [(String, a)]
removePVals main rmv = filter (not . (`elem` (fst <$> rmv)) . fst) main
-- | This provides an Ordering result of comparing two sets of NamedParamMatch.
-- This can be used for sorting or other prioritization of named matches.
pmatchCmp :: [ NamedParamMatch ] -> [ NamedParamMatch ] -> Ordering
pmatchCmp p1 p2 =
let comparisons =
[
-- the one with more Explicit matches is better
compare `on` (length . filter (isExplicit . snd))
-- the one with more parameters (usually the same)
, compare `on` length
-- comparing keys
, compare `on` (L.sort . fmap fst)
]
-- comparing the correlated ParamMatch values
<> map (\k -> compare `on` (lookup k)) (fst <$> p1)
in cascadeCompare comparisons p1 p2
cascadeCompare :: [ a -> a -> Ordering ] -> a -> a -> Ordering
cascadeCompare [] _ _ = EQ
cascadeCompare (o:os) a b = case o a b of
EQ -> cascadeCompare os a b
x -> x
-- | Returns the maximum of two arguments based on comparing the
-- [NamedParamMatch] extracted from each argument (via the passed function).
pmatchMax :: (a -> [NamedParamMatch]) -> a -> a -> a
pmatchMax f a b = case pmatchCmp (f a) (f b) of
LT -> b
_ -> a
-- | Given the root directory and a file in that directory, along with the
-- possible parameters and values, return each valid set of parameter matches
-- from that file, along with the remaining unmatched parameter possibilities.
--
-- The first set of parameters is the total set, and the second set represents
-- those that could be identified in the path subdirs; this is needed to prevent
-- a wildcard ParameterPattern in the second set from matching values explicit to
-- other parameters.
dirMatches :: CandidateFile -> [ParameterPattern] -> [ParameterPattern]
-> Logic ([NamedParamMatch], [ParameterPattern])
dirMatches fname fullParams params = do
let pathPart = candidateSubdirs fname
let findVMatch :: FilePath -> (String, Maybe [String]) -> Maybe String
findVMatch e (pn,pv) =
case pv of
Nothing -> Nothing
Just vs -> if e `elem` vs then Just pn else Nothing
let findPVMatch parms pthPartE found =
listToMaybe (catMaybes (map (findVMatch pthPartE) parms)) : found
let pmatches = foldr (findPVMatch params) [] pathPart
let freeParam = fst <$> L.find (isNothing . snd) params
let freeParts =
let allpvals = concat $ catMaybes (snd <$> fullParams)
in (not . (`elem` allpvals)) <$> pathPart
dmatch <- fmap (fmap Explicit)
. fmap (first fromJust)
. filter (not . isNothing . fst)
<$> ((return (zip pmatches pathPart))
`mplus`
(inEachNothing freeParam $ zip3 pmatches freeParts pathPart))
let drem = removePVals params dmatch
return (dmatch, drem)
-- | Return each substitution of the first argument for each location in the
-- second list that has a Nothing label and a True parameter; leave non-Nothings
-- in the second list unchanged.
inEachNothing :: Maybe a -> [(Maybe a,Bool,b)] -> Logic [(Maybe a,b)]
inEachNothing mark into = do
let canSubst (a,b,_) = b && isNothing a
let spots = filter (\i -> canSubst (into !! i)) $ [0..(length into) - 1]
i <- eachFrom spots
let deBool (a,_,c) = (a,c)
let thrd (_,_,c) = c
return
$ (deBool <$> take i into)
<> [ (mark, thrd (into !! i)) ]
<> (deBool <$> drop (i + 1) into)
-- | isCompatible can be used as a filter predicate to determine if the specified
-- file is compatible with the provided parameters and chosen parameter values.
-- One principle compatibility check is ensuring that there is no *other*
-- parameter value in the filename that conflicts with a chosen parameter value.
isCompatible :: Separators
-> [ParameterPattern]
-> [(String, Maybe String)]
-> CandidateFile
-> Bool
isCompatible seps params pvals fname =
let splitFName n = let (p,r) = break (`elem` seps) n
in p : if null r then [] else splitFName (tail r)
parts = let n' = splitFName $ candidateFile fname
in candidateSubdirs fname <> n'
noConflict _ (_,Nothing) = True
noConflict ps (pn,Just vs) = all (not . isConflict pn vs) ps
isConflict pn vs p = and [ p `elem` vs
, maybe False (Just p /=) $ lookup pn pvals
]
in all (noConflict parts) params