packages feed

tasty-falsify-0.1.0: test/TestSuite/Generator/Function.hs

{-# LANGUAGE OverloadedStrings #-}

module TestSuite.Generator.Function (tests) where

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

import qualified Data.Set as Set

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

{-------------------------------------------------------------------------------
  List of tests
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "TestSuite.Generator.Function" [
      testGroup "BoolToBool" [
          testProperty "notConstant" prop_BoolToBool_notConstant
        , testProperty "constant"    prop_BoolToBool_constant
        ]
    , testProperty                "Word8ToBool"   prop_Word8ToBool
    , testPropertyWith fewerTests "IntegerToBool" prop_IntegerToBool
    , testProperty                "IntToInt"      prop_IntToInt
    , testPropertyWith fewerTests "StringToBool"  prop_StringToBool
    ]
  where
    -- These tests are pretty slow
    fewerTests :: TestOptions
    fewerTests = def {
        overrideNumTests = Just 10
      }

{-------------------------------------------------------------------------------
  Functions @Bool -> Bool@

  TODO: Should we define these in terms of the concrete functions instead?
-------------------------------------------------------------------------------}

prop_BoolToBool_notConstant :: Property ()
prop_BoolToBool_notConstant =
    testMinimum (P.satisfies ("isConstant", isConstant)) $ do
      fn <- gen $ Gen.fun (Gen.bool False)
      let Fn f = fn
      -- "No function Bool -> Bool can be constant"
      unless (f False /= f True) $ testFailed fn
  where
    isConstant :: Fun Bool Bool -> Bool
    isConstant fn = show fn == "{_->False}"

prop_BoolToBool_constant :: Property ()
prop_BoolToBool_constant = do
    testMinimum (P.satisfies ("notConstant", notConstant)) $ do
      fn <- gen $ Gen.fun (Gen.bool False)
      let Fn f = fn
      -- "Every function Bool -> Bool is constant"
      unless (f False == f True) $ testFailed fn
  where
    notConstant :: Fun Bool Bool -> Bool
    notConstant fn = or [
          show fn == "{True->True, _->False}"
        , show fn == "{False->True, _->False}"
        ]

{-------------------------------------------------------------------------------
  Functions @Word8 -> Bool@
-------------------------------------------------------------------------------}

prop_Word8ToBool :: Property ()
prop_Word8ToBool = do
    testMinimum (P.satisfies ("notConstant", notConstant)) $ do
      fn <- gen $ Gen.fun (Gen.bool False)
      -- "Every function Word8 -> Bool is constant"
      unless (isConstant fn) $ testFailed fn
  where
    notConstant :: Fun Word8 Bool -> Bool
    notConstant fn = any aux [0 .. 255]
      where
        aux :: Word8 -> Bool
        aux n = show fn == "{" ++ show n ++ "->True, _->False}"

    isConstant :: Fun Word8 Bool -> Bool
    isConstant (Fn f) =
        (\s -> Set.size s == 1) $
          Set.fromList (map f [minBound .. maxBound])

{-------------------------------------------------------------------------------
  Functions @Integer -> Bool@

  This is the first test where the input domain is infinite.
-------------------------------------------------------------------------------}

prop_IntegerToBool :: Property ()
prop_IntegerToBool =
    testMinimum (P.satisfies ("expected", expected)) $ do
      fn <- gen $ Gen.fun (Gen.bool False)
      let Fn f = fn
      -- "Every fn from Integer to Bool must give the same result for π and φ"
      unless (f 3142 == f 1618) $ testFailed fn
  where
    expected :: Fun Integer Bool -> Bool
    expected fn = or [
          show fn == "{1618->True, _->False}"
        , show fn == "{3142->True, _->False}"
        ]

{-------------------------------------------------------------------------------
  Functions @Int -> Int@
-------------------------------------------------------------------------------}

prop_IntToInt :: Property ()
prop_IntToInt =
    testMinimum (P.satisfies ("expected", expected)) $ do
      fn <- gen $ Gen.fun (Gen.inRange $ Range.inclusive (0, 100))
      let Fn f = fn
      unless (f 0 == 0 && f 1 == 0) $ testFailed fn
  where
    expected :: Fun Int Int -> Bool
    expected fn = or [
          show fn == "{1->1, _->0}"
        , show fn == "{0->1, _->0}"
        ]

{-------------------------------------------------------------------------------
  Functions @String -> Bool@

  This example (as well as 'test_IntToInt_mapFilter') is adapted from
  Koen Claessen's presentation "Shrinking and showing functions"
  at Haskell Symposium 2012 <https://www.youtube.com/watch?v=CH8UQJiv9Q4>.

  TODO: His example uses longer strings, which does work, it's just expensive.
  We can definitely use some performance optimization here.
-------------------------------------------------------------------------------}

prop_StringToBool :: Property ()
prop_StringToBool =
    testMinimum (P.satisfies ("expected", expected)) $ do
      fn <- gen $ Gen.fun (Gen.bool False)
      let Fn p = fn
      unless (p "abc" `implies` p "def") $ testFailed fn
  where
    -- TODO: Actually, the second case doesn't seem to get triggered. Not sure
    -- why; maybe it's just unlikely..?
    expected :: Fun String Bool -> Bool
    expected fn = or [
          show fn == "{\"abc\"->True, _->False}"
        , show fn == "{\"def\"->True, _->False}"
        ]

    implies :: Bool -> Bool -> Bool
    implies False _ = True
    implies True  b = b