packages feed

dunai-test-0.11.0: src/FRP/Dunai/QuickCheck.hs

{-# LANGUAGE CPP                 #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Copyright  : (c) Ivan Perez, 2017-2023
-- License    : BSD3
-- Maintainer : ivan.perez@keera.co.uk
--
-- QuickCheck generators for input streams.
--
-- Random stream generation can be customized usin three parameters:
--
-- - The distribution for the random time deltas ('Distribution').
-- - The maximum and minimum bounds for the time deltas ('Range').
-- - The maximum stream length ('Length').
--
-- The main function to generate streams is 'generateStream'. The specific time
-- deltas can be customized further using 'generateStreamWith'. Some helper
-- functions are provided to facilitate testing.
module FRP.Dunai.QuickCheck
    (
      -- * Random stream generation
      generateStream
    , generateStreamWith

      -- ** Parameters used to generate random input streams
    , Distribution(..)
    , Range
    , Length

      -- ** Helpers for common cases
    , uniDistStream
    , uniDistStreamMaxDT
    , fixedDelayStream
    , fixedDelayStreamWith
    )
  where

-- External imports
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative (pure, (<$>))
#endif

import Data.Random.Normal  (normal')
import Test.QuickCheck     (Arbitrary, arbitrary, getPositive)
import Test.QuickCheck.Gen (Gen (MkGen), choose, suchThat)

-- Internal imports
import FRP.Dunai.Stream (DTime, SignalSampleStream, groupDeltas)

-- * Random stream generation

-- | Generate random stream.
generateStream :: Arbitrary a
               => Distribution
               -> Range
               -> Length
               -> Gen (SignalSampleStream a)
generateStream = generateStreamWith (\_ _ -> arbitrary)

-- | Generate random stream, parameterized by the value generator.
generateStreamWith :: (Int -> DTime -> Gen a)
                   -> Distribution
                   -> Range
                   -> Length
                   -> Gen (SignalSampleStream a)
generateStreamWith arb DistConstant range len =
  generateConstantStream arb =<< generateStreamLenDT range len
generateStreamWith arb dist (m, n) len = do
    ds <- generateDeltas len
    let l = length ds
    let f n = arb n (ds !! (n - 1))
    xs <- vectorOfWith l f

    x <- arb 0 0
    return $ groupDeltas (x:xs) ds

  where

    deltaF :: Gen DTime
    deltaF = case dist of
               DistRandom -> generateDelta m n
               DistNormal (avg, stddev) -> generateDSNormal avg stddev m n
               _ -> error "dunai-test: generateStreamWith"

    generateDeltas :: Length -> Gen [DTime]
    generateDeltas Nothing              = do l <- arbitrary
                                             vectorOfWith l (\_ -> deltaF)
    generateDeltas (Just (Left l))      = vectorOfWith l (\_ -> deltaF)
    generateDeltas (Just (Right maxds)) = timeStampsUntilWith deltaF maxds

-- | Generate arbitrary stream with fixed length and constant delta.
generateConstantStream :: (Int -> DTime -> Gen a)
                       -> (DTime, Int)
                       -> Gen (SignalSampleStream a)
generateConstantStream arb (x, length) = do
    ys <- vectorOfWith length (`arb` x)
    return $ groupDeltas ys ds
  where
    ds = repeat x

-- | Generate arbitrary stream
generateStreamLenDT :: (Maybe DTime, Maybe DTime)
                    -> Maybe (Either Int DTime)
                    -> Gen (DTime, Int)
generateStreamLenDT range len = do
  x <- uncurry generateDelta range
  l <- case len of
         Nothing         -> getPositive <$> arbitrary
         Just (Left l)   -> pure l
         Just (Right ds) -> pure (floor (ds / x))
  return (x, l)

-- ** Time delta generation

-- | Generate one random delta, possibly within a range.
generateDelta :: Maybe DTime -> Maybe DTime -> Gen DTime
generateDelta (Just x) (Just y) = choose (x, y)
generateDelta (Just x) Nothing  = (x +) . getPositive <$> arbitrary
generateDelta Nothing  (Just y) = choose (2.2251e-308, y)
generateDelta Nothing  Nothing  = getPositive <$> arbitrary

-- | Generate a random delta following a normal distribution, and possibly
-- within a given range.
generateDSNormal :: DTime -> DTime -> Maybe DTime -> Maybe DTime -> Gen DTime
generateDSNormal avg stddev m n = suchThat gen (\x -> mx x && mn x)
  where
    gen = MkGen (\r _ -> fst $ normal' (avg, stddev) r)
    mn  = maybe (const True) (<=) m
    mx  = maybe (const True) (>=) n

-- | Generate random samples up until a max time, with a given time delta
-- generation function.
timeStampsUntilWith :: Gen DTime -> DTime -> Gen [DTime]
timeStampsUntilWith arb = timeStampsUntilWith' arb []
  where
    -- Generate random samples up until a max time, with a given time delta
    -- generation function, and an initial suffix of time deltas.
    timeStampsUntilWith' :: Gen DTime -> [DTime] -> DTime -> Gen [DTime]
    timeStampsUntilWith' arb acc ds
      | ds < 0    = return acc
      | otherwise = do d <- arb
                       let acc' = acc `seq` (d:acc)
                       acc' `seq` timeStampsUntilWith' arb acc' (ds - d)

-- ** Parameters used to generate random input streams

-- | Distributions used for time delta (DT) generation.
data Distribution
  = DistConstant              -- ^ Constant DT for the whole stream.
  | DistNormal (DTime, DTime) -- ^ Variable DT following normal distribution,
                              --   with an average and a standard deviation.
  | DistRandom                -- ^ Completely random (positive) DT.

-- | Upper and lower bounds of time deltas for random DT generation.
type Range = (Maybe DTime, Maybe DTime)

-- | Optional maximum length for a stream, given as a time, or a number of
-- samples.
type Length = Maybe (Either Int DTime)

-- ** Helpers for common cases

-- | Generate a stream of values with uniformly distributed time deltas.
uniDistStream :: Arbitrary a => Gen (SignalSampleStream a)
uniDistStream = generateStream DistRandom (Nothing, Nothing) Nothing

-- | Generate a stream of values with uniformly distributed time deltas, with a
-- max DT.
uniDistStreamMaxDT :: Arbitrary a => DTime -> Gen (SignalSampleStream a)
uniDistStreamMaxDT maxDT =
  generateStream DistRandom (Nothing, Just maxDT) Nothing

-- | Generate a stream of values with a fixed time delta.
fixedDelayStream :: Arbitrary a => DTime -> Gen (SignalSampleStream a)
fixedDelayStream dt = generateStream DistConstant (Just dt, Just dt) Nothing

-- | Generate a stream of values with a fixed time delta.
fixedDelayStreamWith :: Arbitrary a
                     => (DTime -> a)
                     -> DTime
                     -> Gen (SignalSampleStream a)
fixedDelayStreamWith f dt =
    generateStreamWith f' DistConstant (Just dt, Just dt) Nothing
  where
    f' n t = return $ f (fromIntegral n * t)

-- * Extended quickcheck generator

-- | Generates a list of the given length.
vectorOfWith :: Int -> (Int -> Gen a) -> Gen [a]
vectorOfWith k genF = sequence [ genF i | i <- [1..k] ]