{-# LANGUAGE RankNTypes #-}

-- |
-- Module:      Test.Lawful.QuickCheck
-- Description: QuickCheck support for lawful-classes
-- Copyright:   (c) 2023, Nicolas Trangez
-- License:     Apache-2.0
-- Maintainer:  ikke@nicolast.be
-- Stability:   alpha
--
-- Support code to check @lawful-classes@ laws using QuickCheck and,
-- optionally, Tasty.
module Test.Lawful.QuickCheck
  ( -- * Tasty integration
    testLaws,
    testLawsWith,

    -- * Utilities
    forAll,
    forAllShow,

    -- * Plumbing
    toProperty,
  )
where

import Control.Monad.Trans.Class (MonadTrans, lift)
import Test.Lawful.Types (Law, Laws)
import Test.QuickCheck (Gen, Property, discard)
import qualified Test.QuickCheck as Q
import Test.QuickCheck.Monadic (PropertyM (MkPropertyM), assert, monadicIO, pick)
import Test.Tasty (TestName, TestTree, testGroup)
import Test.Tasty.QuickCheck (testProperty)

-- | Given a way to evaluate an @m a@ into a base 'Monad', turn a 'Law' into a 'Property'.
toProperty :: (forall a. m a -> PropertyM IO a) -> Law m -> Property
toProperty :: forall (m :: * -> *).
(forall a. m a -> PropertyM IO a) -> Law m -> Property
toProperty forall a. m a -> PropertyM IO a
run Law m
law = forall a. Testable a => PropertyM IO a -> Property
monadicIO forall a b. (a -> b) -> a -> b
$ forall b a. b -> (a -> b) -> Maybe a -> b
maybe forall a. a
discard forall (m :: * -> *). Monad m => Bool -> PropertyM m ()
assert forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< forall a. m a -> PropertyM IO a
run Law m
law

-- | Given 'Laws' for @m@ and a way to evaluate an @m a@ in @'PropertyM' IO@,
-- create a @tasty@ 'TestTree'.
testLaws :: TestName -> (forall a. m a -> PropertyM IO a) -> Laws m -> TestTree
testLaws :: forall (m :: * -> *).
TestName -> (forall a. m a -> PropertyM IO a) -> Laws m -> TestTree
testLaws = forall (m :: * -> *).
(Property -> Property)
-> TestName
-> (forall a. m a -> PropertyM IO a)
-> Laws m
-> TestTree
testLawsWith forall a. a -> a
id

-- | Given 'Laws' for @m@ and a way to evaluate an @m a@ in @'PropertyT' IO@,
-- create a @tasty@ 'TestTree', modifying all created 'Property's with the
-- given function.
--
-- As an example, 'Test.QuickCheck.once' could be used to run every test only
-- once, e.g., because 'm' is not a transformer so there's no way to generate
-- multiple test exemplars using some generator, except for the trivial
-- constant generator.
--
-- @since 0.1.1.0
testLawsWith :: (Property -> Property) -> TestName -> (forall a. m a -> PropertyM IO a) -> Laws m -> TestTree
testLawsWith :: forall (m :: * -> *).
(Property -> Property)
-> TestName
-> (forall a. m a -> PropertyM IO a)
-> Laws m
-> TestTree
testLawsWith Property -> Property
fn TestName
name forall a. m a -> PropertyM IO a
run Laws m
laws = TestName -> [TestTree] -> TestTree
testGroup TestName
name [forall a. Testable a => TestName -> a -> TestTree
testProperty TestName
n (Property -> Property
fn forall a b. (a -> b) -> a -> b
$ forall (m :: * -> *).
(forall a. m a -> PropertyM IO a) -> Law m -> Property
toProperty forall a. m a -> PropertyM IO a
run Law m
l) | (TestName
n, Law m
l) <- Laws m
laws]

-- | Lifted version of 'pick'.
--
-- This can be used to easily create generators for laws which need them.
--
-- __Note__: like 'pick', values generated by 'forAll' do not shrink.
forAll :: (MonadTrans t, Monad m, Show a) => Gen a -> t (PropertyM m) a
forAll :: forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m, Show a) =>
Gen a -> t (PropertyM m) a
forAll = forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (m :: * -> *) a. (Monad m, Show a) => Gen a -> PropertyM m a
pick

-- | Like 'forAll', but for types without a 'Show' instance (or, for which
-- another stringification functions but 'show' should be used).
--
-- Like 'Test.QuickCheck.forAllShow', but in a monadic context.
--
-- This can be used to earily create generators for laws which need them.
--
-- __Note__: like 'forAll', values generated by 'forAllShow' do not shrink.
forAllShow :: (MonadTrans t, Monad m) => (a -> String) -> Gen a -> t (PropertyM m) a
forAllShow :: forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
(a -> TestName) -> Gen a -> t (PropertyM m) a
forAllShow a -> TestName
shw = forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
(MonadTrans t, Monad m) =>
m a -> t m a
lift forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall {m :: * -> *}. Functor m => Gen a -> PropertyM m a
pickShow
  where
    pickShow :: Gen a -> PropertyM m a
pickShow Gen a
gen = forall (m :: * -> *) a.
((a -> Gen (m Property)) -> Gen (m Property)) -> PropertyM m a
MkPropertyM forall a b. (a -> b) -> a -> b
$ \a -> Gen (m Property)
k -> do
      a
a <- Gen a
gen
      m Property
mp <- a -> Gen (m Property)
k a
a
      forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$
        forall prop a.
Testable prop =>
Gen a -> (a -> TestName) -> (a -> prop) -> Property
Q.forAllShow (forall (m :: * -> *) a. Monad m => a -> m a
return a
a) a -> TestName
shw forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. a -> b -> a
const forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m Property
mp