packages feed

mockcat-0.6.0.0: test/Support/ParamSpecNormalize.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE PackageImports #-}
-- | Test-only normalization utilities migrated from former
-- Test.MockCat.Internal.ParamSpec.Normalize. These are not part of the
-- public API and may change freely. Copied here because only the
-- property tests rely on the canonical form & merging behaviour.
module Support.ParamSpecNormalize
  ( NormalParamSpec(..)
  , normalise
  , normaliseMany
  , normaliseManyInt
  , combine
  , toParamSpec
  , acceptsParamSpec
  , acceptsNormal
  ) where

import qualified Data.HashSet as HS
import Data.Hashable (Hashable)
import Support.ParamSpec
import Data.List (sortOn)

-- | Canonical internal form (test-only).
data NormalParamSpec a where
  NExact      :: a -> NormalParamSpec a
  NEnum       :: [a] -> NormalParamSpec a
  NRangeInt   :: Int -> Int -> NormalParamSpec Int
  NUnion      :: [NormalParamSpec a] -> NormalParamSpec a
  NAny        :: NormalParamSpec a
  NOpaquePred :: (a -> Bool) -> String -> NormalParamSpec a

instance Show a => Show (NormalParamSpec a) where
  show (NExact a) = "NExact " ++ show a
  show (NEnum xs) = "NEnum " ++ show xs
  show (NRangeInt l h) = "NRangeInt " ++ show (l,h)
  show (NUnion xs) = "NUnion " ++ show (map show xs)
  show NAny = "NAny"
  show (NOpaquePred _ l) = "NOpaquePred <fn> " ++ show l

instance Eq a => Eq (NormalParamSpec a) where
  NExact a == NExact b = a == b
  NEnum xs == NEnum ys = xs == ys
  NRangeInt a b == NRangeInt c d = a == c && b == d
  NUnion xs == NUnion ys = xs == ys
  NAny == NAny = True
  NOpaquePred _ la == NOpaquePred _ lb = la == lb
  _ == _ = False

normalise :: Hashable a => ParamSpec a -> NormalParamSpec a
normalise ps = case ps of
  PSExact a      -> NExact a
  PSAny          -> NAny
  PSRangeInt l h -> range l h
  PSPredicate f l -> NOpaquePred f l
  PSEnum xs      -> enumNorm xs
 where
  range l h
    | l == h    = NExact l
    | l < h     = NRangeInt l h
    | otherwise = NRangeInt h l
  enumNorm [] = NEnum []
  enumNorm xs =
    let uniq = dedupStable xs
    in case uniq of
         []       -> NEnum []
         [x]      -> NExact x
         (x:y:rs) -> NEnum (x:y:rs)

normaliseMany :: Hashable a => [ParamSpec a] -> NormalParamSpec a
normaliseMany [] = NEnum []
normaliseMany [p] = normalise p
normaliseMany ps = combine (map normalise ps)

combine :: [NormalParamSpec a] -> NormalParamSpec a
combine specs
  | any isAny specs = NAny
  | otherwise = case flatten specs of
      []  -> NEnum []
      [x] -> x
      xs  -> NUnion xs
  where
    isAny NAny = True; isAny _ = False
    flatten [] = []
    flatten (NUnion xs:ys) = flatten xs ++ flatten ys
    flatten (x:xs) = x : flatten xs

normaliseManyInt :: [ParamSpec Int] -> NormalParamSpec Int
normaliseManyInt ps = finalize merged others
  where
    ns = map normalise ps
    flat = flatten ns
    (ranges, others) = partitionInt flat
    merged = mergeRanges ranges

    flatten [] = []
    flatten (NUnion xs:ys) = flatten xs ++ flatten ys
    flatten (x:xs) = x:flatten xs

    partitionInt [] = ([],[])
    partitionInt (n:ns') = case n of
      NRangeInt l h -> let (rs,os) = partitionInt ns' in ( (l,h):rs, os)
      NExact v      -> let (rs,os) = partitionInt ns' in ( (v,v):rs, os)
      _             -> let (rs,os) = partitionInt ns' in ( rs, n:os)

    mergeRanges [] = []
    mergeRanges rs = go (sortOn fst (map norm rs))
      where
        norm (l,h) = if l<=h then (l,h) else (h,l)
        go [] = []
        go [x] = [x]
        go ((l1,h1):(l2,h2):rest)
          | h1 + 1 < l2 = (l1,h1) : go ((l2,h2):rest)
          | otherwise   = go ((l1, max h1 h2):rest)

    finalize [] [] = NEnum []
    finalize [(l,h)] [] | l==h = NExact l
    finalize [(l,h)] []        = NRangeInt l h
    finalize rs os =
      let rangeNodes = map (\(l,h) -> if l==h then NExact l else NRangeInt l h) rs
          parts = rangeNodes ++ os
      in case parts of
           []  -> NEnum []
           [x] -> x
           xs  -> NUnion xs

toParamSpec :: NormalParamSpec a -> ParamSpec a
toParamSpec n = case n of
  NExact a        -> PSExact a
  NEnum xs        -> PSEnum xs
  NUnion _        -> PSAny
  NAny            -> PSAny
  NOpaquePred f l -> PSPredicate f l
  NRangeInt l h   -> PSRangeInt l h

acceptsParamSpec :: Eq a => ParamSpec a -> a -> Bool
acceptsParamSpec ps x = case ps of
  PSExact a        -> x == a
  PSEnum as        -> any (== x) as
  PSRangeInt l h   -> let (lo,hi) = if l <= h then (l,h) else (h,l) in lo <= x && x <= hi
  PSAny            -> True
  PSPredicate f _  -> f x

acceptsNormal :: Eq a => NormalParamSpec a -> a -> Bool
acceptsNormal n x = case n of
  NExact a        -> x == a
  NEnum as        -> any (== x) as
  NUnion as       -> any (\s -> acceptsNormal s x) as
  NAny            -> True
  NOpaquePred f _ -> f x
  NRangeInt l h   -> l <= x && x <= h

dedupStable :: Hashable a => [a] -> [a]
dedupStable = go HS.empty
  where
    go _ [] = []
    go seen (z:zs)
      | HS.member z seen = go seen zs
      | otherwise        = z : go (HS.insert z seen) zs