packages feed

quickspec-2.2: src/QuickSpec/Internal/Testing/QuickCheck.hs

-- Testing conjectures using QuickCheck.
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE FlexibleContexts, FlexibleInstances, RecordWildCards, MultiParamTypeClasses, GeneralizedNewtypeDeriving #-}
module QuickSpec.Internal.Testing.QuickCheck where

import QuickSpec.Internal.Testing
import QuickSpec.Internal.Pruning
import QuickSpec.Internal.Prop
import Test.QuickCheck
import Test.QuickCheck.Gen
import Test.QuickCheck.Random
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
import Data.List
import System.Random hiding (uniform)
import QuickSpec.Internal.Terminal
import Data.Lens.Light

data Config =
  Config {
    cfg_num_tests :: Int,
    cfg_max_test_size :: Int,
    cfg_fixed_seed :: Maybe QCGen}
  deriving Show

lens_num_tests = lens cfg_num_tests (\x y -> y { cfg_num_tests = x })
lens_max_test_size = lens cfg_max_test_size (\x y -> y { cfg_max_test_size = x })
lens_fixed_seed = lens cfg_fixed_seed (\x y -> y { cfg_fixed_seed = x })

data Environment testcase term result =
  Environment {
    env_config :: Config,
    env_tests :: [testcase],
    env_eval :: testcase -> term -> Maybe result }

newtype Tester testcase term result m a =
  Tester (ReaderT (Environment testcase term result) m a)
  deriving (Functor, Applicative, Monad, MonadIO, MonadTerminal, MonadPruner term' res')

instance MonadTrans (Tester testcase term result) where
  lift = Tester . lift

run ::
  Config -> Gen testcase -> (testcase -> term -> Maybe result) ->
  Tester testcase term result m a -> Gen (m a)
run config@Config{..} gen eval (Tester x) = do
  seed <- maybe arbitrary return cfg_fixed_seed
  let
    seeds = unfoldr (Just . split) seed
    n = fromIntegral (ceiling (fromIntegral cfg_num_tests * (1 - zeroProportion)))
    zeroes = cfg_num_tests - n
    k = max 1 cfg_max_test_size
    bias = 3
    -- Run this proportion of tests of size 0.
    zeroProportion = 0.01
    -- Bias tests towards smaller sizes.
    -- We do this by distributing the cube of the size uniformly.
    sizes =
      replicate zeroes 0 ++
      (reverse $ map (k -) $
       map (truncate . (** (1/fromInteger bias)) . fromIntegral) $
       uniform (toInteger n) (toInteger k^bias))
    tests = zipWith (unGen gen) seeds sizes
  return $ runReaderT x
    Environment {
      env_config = config,
      env_tests = tests,
      env_eval = eval }

-- uniform n k: generate a list of n integers which are distributed evenly between 0 and k-1.
uniform :: Integer -> Integer -> [Integer]
uniform n k =
  -- n `div` k: divide evenly as far as possible.
  concat [replicate (fromIntegral (n `div` k)) i | i <- [0..k-1]] ++
  -- The leftovers get distributed at equal intervals.
  [i * k `div` leftovers | i <- [0..leftovers-1]]
  where
    leftovers = n `mod` k

instance (Monad m, Eq result) => MonadTester testcase term (Tester testcase term result m) where
  test prop =
    Tester $ do
      env@Environment{..} <- ask
      return $! foldr testAnd TestPassed (map (quickCheckTest env prop) env_tests)
  retest testcase prop =
    Tester $ do
      env@Environment{..} <- ask
      return $! quickCheckTest env prop testcase

quickCheckTest :: Eq result =>
  Environment testcase term result -> Prop term -> testcase -> TestResult testcase
quickCheckTest Environment{env_config = Config{..}, ..} (lhs :=>: rhs) testcase =
  foldr testAnd (testEq rhs) (map testEq lhs)
  where
    testEq (t :=: u) =
      case (env_eval testcase t, env_eval testcase u) of
        (Just t, Just u)
          | t == u -> TestPassed
          | otherwise -> TestFailed testcase
        _ -> Untestable