packages feed

falsify-0.1.0: test/TestSuite/Prop/Generator/Selective.hs

module TestSuite.Prop.Generator.Selective (tests) where

import Control.Monad
import Control.Selective
import Data.Default
import Data.Word
import Test.Tasty
import Test.Tasty.Falsify

import qualified Test.Falsify.Generator as Gen
import qualified Test.Falsify.Predicate as P

tests :: TestTree
tests = testGroup "TestSuite.Prop.Generator.Selective" [
      testGroup "pair" [
          testProperty                   "ifM"        $ prop_pair ifM
        , testPropertyWith expectFailure "ifS"        $ prop_pair ifS
        , testProperty                   "ifThenElse" $ prop_pair_ifThenElse
        ]
    ]
  where
    expectFailure :: TestOptions
    expectFailure = def {
          expectFailure    = ExpectFailure
        , overrideNumTests = Just 10_000
        }

{-------------------------------------------------------------------------------
  Either

  We only only primitive generators here (avoiding generators like
  'Test.Falsify.Reexported.Generator.Simple.bool') to avoid getting distracted
  by specific implementation details of derived generators.
-------------------------------------------------------------------------------}

-- If we use monadic bind, the seed for the Right value is reused when
-- when we shrink it to Left: they are not independent.
--
-- Here this is still somewhat reasonable, but in general this means we
-- will reuse a seed reduced in one context in a completely different
-- context, which may not make any sense at all.
propEither ::
     (Word64, Either Word64 Word64)
  -> (Word64, Either Word64 Word64)
  -> Bool
propEither _            (_, Left 0) = True -- Can always shrink to 'Minimal'
propEither (_, Right x) (_, Left y) = x == y
propEither _            _           = True

genPair ::
    (forall a. Gen Bool -> Gen a -> Gen a -> Gen a)
  -> Gen (Word64, Either Word64 Word64)
genPair if_ =
    (,) <$> Gen.prim
        <*> if_ ((== 0) <$> Gen.prim)
                (Left   <$> Gen.exhaustive 100)
                (Right  <$> Gen.exhaustive 100)

prop_pair :: (forall a. Gen Bool -> Gen a -> Gen a -> Gen a) -> Property ()
prop_pair if_ =
    testShrinkingOfGen (P.relatedBy ("propEither", propEither)) $
      genPair if_

prop_pair_ifThenElse :: Property ()
prop_pair_ifThenElse =
    testShrinking (P.relatedBy ("stayRight", stayRight)) $ do
      pair <- gen $ genPair ifBoth
      when (prop pair) $ testFailed pair
  where
    prop :: (Word64, Either Word64 Word64) -> Bool
    prop (x, Right y) = x < 10 || y > x
    prop (x, Left  y) = x <  1 || y < x

    -- Since we are generating the left value before the right value, if we
    -- /start/ with a right value, we will then shrink the left value first even
    -- though it is not used: indeed, this /must/ always succeed precisely
    -- /because/ that left value is not used. At that point we can no longer
    -- reduce the Right to a Left, because @Left 0@ is not a counterexample.
    stayRight ::
         (Word64, Either Word64 Word64)
      -> (Word64, Either Word64 Word64)
      -> Bool
    stayRight _            (_, Left 0) = True -- Can always shrink to 'Minimal'
    stayRight (_, Right _) (_, Left _) = False
    stayRight _            _           = True

{-------------------------------------------------------------------------------
  Generic auxiliary
-------------------------------------------------------------------------------}

ifM :: Gen Bool -> Gen a -> Gen a -> Gen a
ifM cond t f = cond `Gen.bindWithoutShortcut` \b -> if b then t else f

ifBoth :: Gen Bool -> Gen a -> Gen a -> Gen a
ifBoth cond t f =
    t `Gen.bindWithoutShortcut` \x ->
    f `Gen.bindWithoutShortcut` \y ->
    cond `Gen.bindWithoutShortcut` \b  ->
    return $ if b then x else y