opaleye-0.10.2.1: Test/QuickCheck.hs
{-# LANGUAGE Arrows #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeSynonymInstances #-}
module QuickCheck where
import Prelude hiding (compare, (.), id)
import Connection (Connection, withConnection)
import Opaleye.Test.Arbitrary
import Opaleye.Test.Fields
import Opaleye.Test.TraverseA (traverseA1)
import qualified Opaleye as O
import qualified Opaleye.Join as OJ
import qualified Opaleye.Exists as OE
import qualified Database.PostgreSQL.Simple as PGS
import qualified Control.Arrow as Arrow
import Control.Arrow ((<<<))
import Control.Category (Category, (.), id)
import Control.Monad (when, guard)
import qualified Data.Profunctor.Product.Default as D
import qualified Data.List as List
import qualified Data.MultiSet as MultiSet
import qualified Data.Monoid as Monoid
import qualified Data.Ord as Ord hiding (compare)
import qualified Data.Set as Set
import qualified Data.Maybe as Maybe
import qualified Test.QuickCheck as TQ
import Test.QuickCheck ((===), (.&&.))
arbitraryOrdering :: ArbitraryOrder -> Haskells -> Haskells -> Ord.Ordering
arbitraryOrdering =
Monoid.mconcat
. map (\(direction, index) ->
(case direction of
Asc -> id
Desc -> flip)
-- If the list is empty we have to conjure up an arbitrary
-- value of type Field. We don't know how to order
-- MaybeFields (yet) so we do the same if we hit a
-- MaybeFields.
--
-- Note that this one will compare CInt Int
-- to CBool Bool, but it never gets asked to
-- do so, so we don't care.
(Ord.comparing (\c -> let l = unChoices c
len = length l
in if len > 0 then
case l !! (index `mod` length l) of
Left i -> i
Right _ -> CInt 0
else
CInt 0)))
. unArbitraryOrder
newtype SelectArrDenotation a b =
SelectArrDenotation { unSelectArrDenotation :: PGS.Connection -> a -> IO [b] }
type SelectDenotation = SelectArrDenotation ()
instance Functor (SelectArrDenotation a) where
fmap = onList . fmap
instance Applicative (SelectArrDenotation a) where
pure = Arrow.arr . const
f <*> x = fmap (uncurry ($)) (f Arrow.&&& x)
instance Category SelectArrDenotation where
id = Arrow.arr id
(.) = \(SelectArrDenotation f) (SelectArrDenotation g) ->
SelectArrDenotation (\conn a -> do
bs <- g conn a
concatMapM (f conn) bs)
instance Arrow.Arrow SelectArrDenotation where
arr f = SelectArrDenotation (\_ -> pure . pure . f)
first f = SelectArrDenotation (\conn (a, b) -> do
as' <- unSelectArrDenotation f conn a
pure (fmap (\a' -> (a', b)) as'))
instance Arrow.ArrowChoice SelectArrDenotation where
left f = SelectArrDenotation $ \conn -> \case
Left l -> (fmap . fmap) Left (unSelectArrDenotation f conn l)
Right r -> (pure . pure . pure) r
runSelectArrDenotation :: SelectArrDenotation a b
-> a
-> PGS.Connection
-> IO [b]
runSelectArrDenotation sab a conn = unSelectArrDenotation sab conn a
($$) :: Arrow.Arrow arr => arr a b -> a -> arr () b
($$) f a = f <<< Arrow.arr (const a)
unApply :: (a -> SelectDenotation b) -> SelectArrDenotation a b
unApply f = SelectArrDenotation (\conn a -> unSelectArrDenotation (f a) conn ())
onList :: ([a] -> [b]) -> SelectArrDenotation i a -> SelectArrDenotation i b
onList f = SelectArrDenotation . (fmap . fmap . fmap) f . unSelectArrDenotation
onListK :: (a -> [b]) -> SelectArrDenotation a b
onListK f = SelectArrDenotation (\_ a -> pure (f a))
-- This is taking liberties. Firstly it errors out when two fields
-- are of different types. It should probably return a Maybe or an
-- Either. Secondly, it doesn't detect when lists are the same
-- length and it probably should.
--
-- We don't have the ability to aggregate MaybeFields, at least, not
-- yet. Therefore we just replace them with Nothing.
aggregateDenotation :: [Haskells] -> [Haskells]
aggregateDenotation cs = if null cs
then []
else (pure
. List.foldl1' combine
. map emptyOutChoices
) cs
where combine h1 h2 = Choices (zipWith (curry (\case
(Left l1, Left l2) -> Left $ case (l1, l2) of
(CInt i1, CInt i2) -> CInt (i1 + i2)
(CBool b1, CBool b2) -> CBool (b1 && b2)
(CString s1, CString s2) -> CString (s1 ++ ", " ++ s2)
_ -> error "Impossible"
(Right _, Right _) -> Right Nothing
_ -> error "Impossible")) (unChoices h1) (unChoices h2))
emptyOutChoices c = Choices $ flip map (unChoices c) $ \case
Left l -> Left l
Right _ -> Right Nothing
optionalDenotation :: SelectArrDenotation i a -> SelectArrDenotation i (Maybe a)
optionalDenotation = onList $ \case
[] -> [Nothing]
xs -> map Just xs
lateralDenotation :: (a -> SelectDenotation r) -> SelectArrDenotation a r
lateralDenotation = unApply
optionalRestrictDenotation :: SelectDenotation Haskells
-> SelectArrDenotation (Haskells -> Bool) (Maybe Haskells)
optionalRestrictDenotation hs = optionalDenotation $ proc cond -> do
a <- hs -< ()
onListK guard -< cond a
Arrow.returnA -< a
pureList :: [a] -> SelectDenotation a
pureList = SelectArrDenotation . pure . pure . pure
concatMapM :: Monad m => (a -> m [b]) -> [a] -> m [b]
concatMapM f = fmap concat . mapM f
denotationArrExplicit :: (ah -> af)
-> O.FromFields bf bh
-> O.SelectArr af bf
-> SelectArrDenotation ah bh
denotationArrExplicit toFields qr q =
SelectArrDenotation (\conn h ->
let fs = pure (toFields h)
in O.runSelectExplicit qr conn (q <<< fs))
denotationExplicit :: O.FromFields fields a
-> O.Select fields
-> SelectDenotation a
denotationExplicit = denotationArrExplicit O.toFields
denotation :: O.Select Fields -> SelectDenotation Haskells
denotation = denotationExplicit fromFieldsFields
denotationArr :: O.SelectArr Fields Fields
-> SelectArrDenotation Haskells Haskells
denotationArr = denotationArrExplicit fieldsOfHaskells fromFieldsFields
denotationMaybeFields :: O.Select (O.MaybeFields Fields)
-> SelectDenotation (Maybe Haskells)
denotationMaybeFields =
denotationExplicit (O.fromFieldsMaybeFields fromFieldsFields)
denotationArrMaybeFields
:: O.SelectArr (O.MaybeFields Fields) (O.MaybeFields Fields)
-> SelectArrDenotation (Maybe Haskells) (Maybe Haskells)
denotationArrMaybeFields =
denotationArrExplicit
(O.toFieldsExplicit (O.toFieldsMaybeFields nullspecFields toFieldsFields))
(O.fromFieldsMaybeFields fromFieldsFields)
unSelectDenotations :: SelectDenotation a
-> SelectDenotation b
-> ([a] -> [b] -> IO TQ.Property)
-> Connection
-> IO TQ.Property
unSelectDenotations one two = unSelectArrDenotations one two ()
unSelectArrDenotations :: SelectArrDenotation i a
-> SelectArrDenotation i b
-> i
-> ([a] -> [b] -> IO TQ.Property)
-> Connection
-> IO TQ.Property
unSelectArrDenotations one two i k conn = do
withConnection conn (runSelectArrDenotation one i) >>= \case
Left _ -> discard
Right oner -> withConnection conn (runSelectArrDenotation two i) >>= \case
Left _ -> discard
Right twor -> k oner twor
where discard = do
putStrLn "A denotation failed to run but it was not our fault"
pure (TQ.property TQ.Discard)
-- { Comparing the results
-- compareNoSort is stronger than compare so prefer to use it where
-- possible. If the queries do not compare equal but do compare equal
-- sorted then switch to "compare". That's no big deal.
compareNoSort :: (Ord a, Show a)
=> SelectDenotation a
-> SelectDenotation a
-> Connection
-> IO TQ.Property
compareNoSort one two =
unSelectDenotations one two $ \one' two' -> do
when (one' /= two')
(putStrLn $ if List.sort one' == List.sort two'
then "[but they are equal sorted]"
else "AND THEY'RE NOT EVEN EQUAL SORTED!")
return (one' === two')
compare :: (Show a, Ord a)
=> SelectDenotation a
-> SelectDenotation a
-> Connection
-> IO TQ.Property
compare one two = unSelectDenotations one two $ \one' two' ->
return (List.sort one' === List.sort two')
-- The condition is *not* that denotation of the sort is equal to sort
-- of the denotation, because the ordering may not fully specify the
-- order.
compareSortedBy :: (Show a, Ord a)
=> (a -> a -> Ord.Ordering)
-> SelectDenotation a
-> SelectDenotation a
-> Connection
-> IO TQ.Property
compareSortedBy o one two = unSelectDenotations one two $ \one' two' ->
return ((List.sort one' === List.sort two')
.&&. isSortedBy o one')
type ArbitraryArgument = ArbitraryHaskells
denotationC :: Arrow.Arrow selectArr
=> (SelectDenotation Haskells -> selectArr () b -> t1 -> t2)
-> O.SelectArr Fields Fields
-> selectArr Haskells b
-> ArbitraryHaskells
-> t1
-> t2
denotationC compare_ q d (ArbitraryHaskells h) conn =
compare_ (denotation (q . pure (fieldsOfHaskells h))) (d $$ h) conn
compareDenotation :: O.SelectArr Fields Fields
-> SelectArrDenotation Haskells Haskells
-> ArbitraryArgument
-> Connection
-> IO TQ.Property
compareDenotation = denotationC compare
compareDenotationMaybe :: O.SelectArr (O.MaybeFields Fields) Fields
-> SelectArrDenotation (Maybe Haskells) Haskells
-> ArbitraryMaybeHaskells
-> Connection
-> IO TQ.Property
compareDenotationMaybe q d (ArbitraryMaybeHaskells h) =
compare (denotation (q . pure (fieldsOfMaybeHaskells h))) (d $$ h)
compareDenotationMaybe2 :: O.SelectArr (O.MaybeFields Fields)
(O.MaybeFields Fields)
-> SelectArrDenotation (Maybe Haskells) (Maybe Haskells)
-> ArbitraryMaybeHaskells
-> Connection
-> IO TQ.Property
compareDenotationMaybe2 q d (ArbitraryMaybeHaskells h) =
compare (denotationMaybeFields (q . pure (fieldsOfMaybeHaskells h)))
(d $$ h)
compareDenotationNoSort :: O.SelectArr Fields Fields
-> SelectArrDenotation Haskells Haskells
-> ArbitraryArgument
-> Connection
-> IO TQ.Property
compareDenotationNoSort = denotationC compareNoSort
compareDenotation' :: (O.Select Fields -> O.Select Fields)
-> ([Haskells] -> [Haskells])
-> ArbitrarySelect
-> Connection
-> IO TQ.Property
compareDenotation' f g (ArbitrarySelect q) =
compare (denotation (f q)) (onList g (denotation q))
compareDenotationMaybe' :: (O.Select Fields -> O.Select (O.MaybeFields Fields))
-> (SelectDenotation Haskells -> SelectDenotation (Maybe Haskells))
-> ArbitrarySelect
-> Connection
-> IO TQ.Property
compareDenotationMaybe' f g (ArbitrarySelect q) =
compare (denotationMaybeFields (f q)) (g (denotation q))
compareDenotationNoSort' :: (O.Select Fields -> O.Select Fields)
-> ([Haskells] -> [Haskells])
-> ArbitrarySelect
-> Connection
-> IO TQ.Property
compareDenotationNoSort' f g (ArbitrarySelect q) =
compareNoSort (denotation (f q)) (onList g (denotation q))
-- }
-- { The tests
fields :: ArbitraryHaskells -> ArbitraryArgument -> Connection -> IO TQ.Property
-- ^ The ArbitraryArgument isn't really used
fields (ArbitraryHaskells c) =
compareDenotationNoSort (pure (fieldsOfHaskells c)) (pure c)
compose :: ArbitrarySelectArr
-> ArbitrarySelectArr
-> ArbitraryArgument
-> Connection
-> IO TQ.Property
compose (ArbitrarySelectArr q1) (ArbitrarySelectArr q2) = do
compareDenotation (q1 . q2) (denotationArr q1 . denotationArr q2)
-- We need to test this separately otherwise traverseMaybeFields won't
-- be properly tested. We need that [traverseMaybeFields q <<< q'] is
-- [traverseMaybeFields q] <<< [q] and we only test it when q' is of
-- the form pure h. The 'compose' tests do the rest, therefore we
-- need a version of 'compose' that checks composition of things whose
-- types are 'MaybeFields Fields'.
composeMaybe :: ArbitrarySelectArrMaybe
-> ArbitrarySelectArrMaybe
-> ArbitraryMaybeHaskells
-> Connection
-> IO TQ.Property
composeMaybe (ArbitrarySelectArrMaybe q1) (ArbitrarySelectArrMaybe q2) = do
compareDenotationMaybe2 (q1 . q2) (denotation_ q1 . denotation_ q2)
where denotation_ = denotationArrMaybeFields
identity :: ArbitraryArgument
-> Connection
-> IO TQ.Property
identity = compareDenotation id id
arr :: ArbitraryFunction
-> ArbitraryArgument
-> Connection
-> IO TQ.Property
arr (ArbitraryFunction f) =
compareDenotationNoSort (Arrow.arr f) (Arrow.arr f)
fmap' :: ArbitraryFunction
-> ArbitrarySelectArr
-> ArbitraryArgument
-> Connection
-> IO TQ.Property
fmap' (ArbitraryFunction f) (ArbitrarySelectArr q) =
compareDenotationNoSort (fmap f q)
(fmap f (denotationArr q))
apply :: ArbitrarySelectArr
-> ArbitrarySelectArr
-> ArbitraryArgument
-> Connection
-> IO TQ.Property
apply (ArbitrarySelectArr q1) (ArbitrarySelectArr q2) =
compareDenotation (pair <$> q1 <*> q2)
(pair <$> denotationArr q1 <*> denotationArr q2)
where pair x y = Choices [Right (pure x), Right (pure y)]
-- When combining arbitrary queries with the applicative product <*>
-- the limit of the denotation is not always the denotation of the
-- limit. Without some ordering applied before the limit the returned
-- rows can vary. If an ordering is applied beforehand we can check
-- the invariant that the returned rows always compare smaller than
-- the remainder under the applied ordering.
--
-- Strangely the same caveat doesn't apply to offset.
limit :: ArbitraryPositiveInt
-> ArbitrarySelect
-> ArbitraryOrder
-> Connection
-> IO TQ.Property
limit (ArbitraryPositiveInt l) (ArbitrarySelect q) o = do
let limited = O.limit l (O.orderBy (arbitraryOrder o) q)
unSelectDenotations (denotation limited) (denotation q) $ \limited' unlimited' -> do
let remainder = MultiSet.fromList unlimited'
`MultiSet.difference`
MultiSet.fromList limited'
maxChosen :: Maybe Haskells
maxChosen = maximumBy (arbitraryOrdering o) limited'
minRemain :: Maybe Haskells
minRemain = minimumBy (arbitraryOrdering o) (MultiSet.toList remainder)
cond :: Maybe Bool
cond = lteBy (arbitraryOrdering o) <$> maxChosen <*> minRemain
condBool :: Bool
condBool = Maybe.fromMaybe True cond
return ((length limited' === min l (length unlimited'))
.&&. condBool)
offset :: ArbitraryPositiveInt -> ArbitrarySelect -> Connection
-> IO TQ.Property
offset (ArbitraryPositiveInt n) =
compareDenotationNoSort' (O.offset n) (drop n)
order :: ArbitraryOrder -> ArbitrarySelect -> Connection -> IO TQ.Property
order o (ArbitrarySelect q) =
compareSortedBy (arbitraryOrdering o)
(denotation (O.orderBy (arbitraryOrder o) q))
(denotation q)
distinct :: ArbitrarySelect -> Connection -> IO TQ.Property
distinct =
compareDenotation' (O.distinctExplicit unpackFields distinctFields) nub
-- When we generalise compareDenotation... we can just test
--
-- compareDenotation... conn restrict restrictDenotation
restrict :: ArbitraryArgument -> Connection -> IO TQ.Property
restrict =
compareDenotationNoSort restrictFirstBool restrictFirstBoolDenotation
values :: ArbitraryHaskellsList -> Connection -> IO TQ.Property
values (ArbitraryHaskellsList l) =
compareNoSort (denotation (fmap fieldsList (O.values (fmap O.toFields l))))
(pureList (fmap fieldsList l))
-- We test values entries of length two in values, and values entries
-- of length zero here. Ideally we would find some way to merge them.
valuesEmpty :: [()] -> Connection -> IO TQ.Property
valuesEmpty l =
compareNoSort (denotationExplicit D.def (O.values l))
(pureList l)
aggregate :: ArbitrarySelect -> Connection -> IO TQ.Property
aggregate =
compareDenotationNoSort' (O.aggregateExplicit unpackFields aggregateFields)
aggregateDenotation
label :: String -> ArbitrarySelect -> Connection -> IO TQ.Property
label comment = compareDenotationNoSort' (O.label comment) id
optional :: ArbitrarySelect -> Connection -> IO TQ.Property
optional = compareDenotationMaybe' (OJ.optionalExplicit unpackFields)
optionalDenotation
optionalRestrict :: ArbitrarySelect -> Connection -> IO TQ.Property
optionalRestrict =
compareDenotationMaybe' optionalRestrictF optionalRestrictFDenotation
where optionalRestrictF = f (firstBoolOrTrue (O.sqlBool True))
(O.optionalRestrictExplicit unpackFields)
optionalRestrictFDenotation = f (firstBoolOrTrue True)
optionalRestrictDenotation
f x y z = Arrow.arr (\() -> fst . x) Arrow.>>> (y z)
maybeFieldsToSelect :: ArbitraryMaybeHaskells -> Connection -> IO TQ.Property
maybeFieldsToSelect =
compareDenotationMaybe O.maybeFieldsToSelect
(onListK Maybe.maybeToList)
traverseMaybeFields :: ArbitrarySelectArr
-> ArbitraryMaybeHaskells
-> Connection
-> IO TQ.Property
traverseMaybeFields (ArbitrarySelectArr q) =
compareDenotationMaybe2 (traverse' q)
(traverseA1 (denotationArr q))
where traverse' = O.traverseMaybeFieldsExplicit unpackFields unpackFields
lateral :: ArbitraryKleisli
-> ArbitraryArgument
-> Connection
-> IO TQ.Property
lateral (ArbitraryKleisli f) =
compareDenotation (O.lateral f) (lateralDenotation (denotation . f'))
where f' = f . fieldsOfHaskells
exists :: ArbitrarySelect -> Connection -> IO TQ.Property
exists = compareDenotationNoSort' (existsQ OE.exists) (existsQ existsList)
where existsList l = [not (null l)]
existsQ existsf q = do
exists_ <- existsf q
pure (Choices [Left (CBool exists_)])
{- TODO
* Nullability
* Operators (mathematical, logical, etc.)
-}
-- }
-- { Running the QuickCheck
-- One way that the property tests can fail is because of LIMIT and
-- OFFSET. It seems that a query returning LIMIT or OFFSET does not
-- always return the same result when it is part of a larger query.
-- This happens rarely. We could sort before LIMIT or OFFSET to make
-- it even rarer.
run :: Connection -> IO ()
run conn = do
let prop1 :: TQ.Testable prop
=> (a -> Connection -> IO prop) -> a -> TQ.Property
prop1 = fmap (\p -> TQ.ioProperty (p conn))
prop2 :: TQ.Testable prop
=> (a1 -> a2 -> Connection -> IO prop)
-> a1 -> a2 -> TQ.Property
prop2 = (fmap . fmap) (\p -> TQ.ioProperty (p conn))
prop3 :: TQ.Testable prop
=> (a1 -> a2 -> a3 -> Connection -> IO prop)
-> a1 -> a2 -> a3 -> TQ.Property
prop3 = (fmap . fmap . fmap) (\p -> TQ.ioProperty (p conn))
test1 :: (Show a, TQ.Arbitrary a, TQ.Testable prop)
=> (a -> Connection -> IO prop) -> IO ()
test1 = t . prop1
test2 :: (Show a1, Show a2, TQ.Arbitrary a1, TQ.Arbitrary a2,
TQ.Testable prop)
=> (a1 -> a2 -> Connection -> IO prop) -> IO ()
test2 = t . prop2
test3 :: (Show a1, Show a2, Show a3,
TQ.Arbitrary a1, TQ.Arbitrary a2, TQ.Arbitrary a3,
TQ.Testable prop)
=> (a1 -> a2 -> a3 -> Connection -> IO prop) -> IO ()
test3 = t . prop3
t p = errorIfNotSuccess
=<< TQ.quickCheckWithResult (TQ.stdArgs { TQ.maxSuccess = 1000 }) p
test1 identity
test2 arr
test3 compose
test3 composeMaybe
test2 fields
test3 fmap'
test3 apply
test3 limit
test2 offset
test2 order
test1 distinct
test1 restrict
test1 values
test1 valuesEmpty
test1 aggregate
test2 label
test1 optional
test1 optionalRestrict
test1 maybeFieldsToSelect
test2 traverseMaybeFields
test2 lateral
test1 exists
-- }
-- { Utilities
nub :: Ord a => [a] -> [a]
nub = Set.toList . Set.fromList
errorIfNotSuccess :: TQ.Result -> IO ()
errorIfNotSuccess r = if TQ.isSuccess r
then return ()
else error "Failed"
restrictFirstBoolDenotation :: SelectArrDenotation Haskells Haskells
restrictFirstBoolDenotation = proc hs -> do
onListK guard -< fst (firstBoolOrTrue True hs)
Arrow.returnA -< hs
isSortedBy ::(a -> a -> Ord.Ordering) -> [a] -> Bool
isSortedBy comp xs = all (uncurry (.<=)) (zip xs (tail' xs))
where tail' [] = []
tail' (_:ys) = ys
x .<= y = lteBy comp x y
lteBy :: (a -> a -> Ord.Ordering) -> a -> a -> Bool
lteBy comp x y = comp x y /= Ord.GT
maximumBy :: (a -> a -> Ord.Ordering) -> [a] -> Maybe a
maximumBy _ [] = Nothing
maximumBy c xs@(_:_) = Just (List.maximumBy c xs)
minimumBy :: (a -> a -> Ord.Ordering) -> [a] -> Maybe a
minimumBy = maximumBy . flip
-- }