packages feed

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