quickcheck-quid-0.0.1.1: src/test/Test/QuickCheck/QuidSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Test.QuickCheck.QuidSpec
where
import Control.Arrow
( (&&&) )
import Control.Monad
( replicateM )
import Data.Functor
( (<&>) )
import Data.Maybe
( fromMaybe )
import Data.Numbers.Primes
( primes )
import Data.Ord
( Down (..) )
import Data.Set
( Set )
import Data.Text.Lazy.Builder
( Builder, fromLazyText )
import Fmt
( Buildable (..), indentF, padLeftF, pretty, (+|), (|+) )
import Internal.Test.QuickCheck.Quid
( Quid
, arbitraryQuid
, chooseNatural
, naturalToQuid
, quidToNatural
, shrinkNatural
)
import Internal.Test.QuickCheck.Quid.Combinators.Size
( Size (..) )
import Internal.Test.QuickCheck.Quid.Representations.Decimal
( Decimal (..) )
import Numeric.Natural
( Natural )
import Test.Hspec
( Spec, describe, it, parallel )
import Test.QuickCheck
( Arbitrary (..)
, Fixed (..)
, Gen
, Property
, Testable (..)
, checkCoverage
, conjoin
, counterexample
, cover
, forAllBlind
, label
, liftShrink2
, oneof
, property
, resize
, shrinkMapBy
, withMaxSuccess
, (.&&.)
, (===)
)
import Test.QuickCheck.Classes.Hspec
( testLawsMany )
import Text.Pretty.Simple
( pShow )
import qualified Data.List as L
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Test.QuickCheck.Classes as Laws
spec :: Spec
spec = do
parallel $ describe "Lawfulness of type class instances" $ do
testLawsMany @TestQuid
[ Laws.eqLaws
, Laws.ordLaws
]
testLawsMany @TestQuid
[ Laws.eqLaws
, Laws.ordLaws
, Laws.showLaws
, Laws.showReadLaws
]
parallel $ describe "Uniformity" $ do
it "prop_arbitraryQuid_uniform" $
property prop_arbitraryQuid_uniform
parallel $ describe "Uniqueness" $ do
it "prop_arbitraryQuid_unique" $
property prop_arbitraryQuid_unique
parallel $ describe "Shrinkability" $ do
it "prop_shrinkQuid_lessThan" $
property prop_shrinkQuid_lessThan
it "prop_shrinkQuid_minimalElement" $
property prop_shrinkQuid_minimalElement
it "prop_shrinkQuid_minimalSet" $
property prop_shrinkQuid_minimalSet
it "prop_shrinkQuid_ordered" $
property prop_shrinkQuid_ordered
it "prop_shrinkQuid_unique" $
property prop_shrinkQuid_unique
--------------------------------------------------------------------------------
-- Powers of two
--------------------------------------------------------------------------------
newtype PowerOfTwo = PowerOfTwo {powerOfTwoExponent :: Natural}
deriving (Eq, Ord, Show)
instance Buildable PowerOfTwo where
build (PowerOfTwo e) = "2^" <> build (show e)
evalPowerOfTwo :: PowerOfTwo -> Natural
evalPowerOfTwo = (2 ^) . powerOfTwoExponent
genPowerOfTwo :: (Natural, Natural) -> Gen PowerOfTwo
genPowerOfTwo (lo, hi) = PowerOfTwo <$> chooseNatural (lo, hi)
shrinkPowerOfTwo :: PowerOfTwo -> [PowerOfTwo]
shrinkPowerOfTwo = shrinkMapBy PowerOfTwo powerOfTwoExponent shrinkNatural
--------------------------------------------------------------------------------
-- Prime numbers
--------------------------------------------------------------------------------
newtype PrimeNumber = PrimeNumber {primeNumberIndex :: Natural}
deriving (Eq, Ord, Show)
instance Buildable PrimeNumber where
build = build . show . evalPrimeNumber
evalPrimeNumber :: PrimeNumber -> Natural
evalPrimeNumber = indexToPrime primes . primeNumberIndex
where
indexToPrime (p : ps) i
| i == 0 = p
| otherwise = indexToPrime ps (i - 1)
indexToPrime [] _ =
error "evalPrimeNumber: Unexpected empty list of prime numbers."
genPrimeNumber :: (Natural, Natural) -> Gen PrimeNumber
genPrimeNumber (lo, hi) = PrimeNumber <$> chooseNatural (lo, hi)
shrinkPrimeNumber :: PrimeNumber -> [PrimeNumber]
shrinkPrimeNumber = shrinkMapBy PrimeNumber primeNumberIndex shrinkNatural
--------------------------------------------------------------------------------
-- Partition functions
--------------------------------------------------------------------------------
data PartitionFunction
= Div PowerOfTwo
| Mod PrimeNumber
deriving (Eq, Ord, Show)
instance Buildable PartitionFunction where
build = \case
Div p -> "div " <> padLeftF 5 ' ' p
Mod p -> "mod " <> padLeftF 5 ' ' p
evalPartitionFunction :: PartitionFunction -> (Natural -> Natural)
evalPartitionFunction = \case
Div p -> (`div` evalPowerOfTwo p)
Mod p -> (`mod` evalPrimeNumber p)
--------------------------------------------------------------------------------
-- Partition contexts
--------------------------------------------------------------------------------
data PartitionContext = PartitionContext
{ sizeExponent :: PowerOfTwo
, expectedBucketCount :: Natural
, partitionFunction :: PartitionFunction
}
deriving (Eq, Ord, Show)
instance Buildable PartitionContext where
build c = mconcat
[ "(size = "
, padLeftF 5 ' ' (sizeExponent c)
, ", expected bucket count = "
, padLeftF 3 ' ' (show $ expectedBucketCount c)
, ", partition function = "
, build (partitionFunction c)
, ")"
]
--------------------------------------------------------------------------------
-- Div partitions
--------------------------------------------------------------------------------
data DivPartition = DivPartition
{ divArgument :: PowerOfTwo
, scaleFactor :: PowerOfTwo
}
deriving (Eq, Ord, Show)
evalDivPartition :: DivPartition -> PartitionContext
evalDivPartition DivPartition {divArgument, scaleFactor} =
PartitionContext
{ sizeExponent = PowerOfTwo
$ powerOfTwoExponent divArgument
+ powerOfTwoExponent scaleFactor
, expectedBucketCount = evalPowerOfTwo scaleFactor
, partitionFunction = Div divArgument
}
genDivPartition :: Gen DivPartition
genDivPartition = do
divArgument <- oneof (genPowerOfTwo <$> [(0, 1), (2, 256)])
scaleFactor <- oneof (genPowerOfTwo <$> [(0, 1), (2, 8)])
pure DivPartition {divArgument, scaleFactor}
shrinkDivPartition :: DivPartition -> [DivPartition]
shrinkDivPartition = shrinkMapBy unTuple toTuple $
liftShrink2 shrinkPowerOfTwo shrinkPowerOfTwo
where
unTuple (c, s) = (DivPartition c s)
toTuple (DivPartition c s) = (c, s)
--------------------------------------------------------------------------------
-- Mod partitions
--------------------------------------------------------------------------------
data ModPartition = ModPartition
{ modArgument :: PrimeNumber
, scaleFactor :: PowerOfTwo
}
deriving (Eq, Ord, Show)
evalModPartition :: ModPartition -> PartitionContext
evalModPartition ModPartition {modArgument, scaleFactor} =
PartitionContext
{ sizeExponent = PowerOfTwo
$ primeNumberIndex modArgument
+ powerOfTwoExponent scaleFactor
+ 8
, expectedBucketCount = evalPrimeNumber modArgument
, partitionFunction = Mod modArgument
}
genModPartition :: Gen ModPartition
genModPartition = do
modArgument <- oneof (genPrimeNumber <$> [(0, 1), (2, 32)])
scaleFactor <- oneof (genPowerOfTwo <$> [(0, 1), (2, 256)])
pure ModPartition {modArgument, scaleFactor}
shrinkModPartition :: ModPartition -> [ModPartition]
shrinkModPartition = shrinkMapBy unTuple toTuple $
liftShrink2 shrinkPrimeNumber shrinkPowerOfTwo
where
unTuple (m, s) = (ModPartition m s)
toTuple (ModPartition m s) = (m, s)
--------------------------------------------------------------------------------
-- Partitions
--------------------------------------------------------------------------------
data Partition
= DivPartitionOf DivPartition
| ModPartitionOf ModPartition
deriving (Eq, Ord, Show)
instance Arbitrary Partition where
arbitrary = genPartition
shrink = shrinkPartition
evalPartition :: Partition -> PartitionContext
evalPartition = \case
DivPartitionOf p -> evalDivPartition p
ModPartitionOf p -> evalModPartition p
genPartition :: Gen Partition
genPartition = oneof
[ DivPartitionOf <$> genDivPartition
, ModPartitionOf <$> genModPartition
]
shrinkPartition :: Partition -> [Partition]
shrinkPartition = \case
DivPartitionOf p -> DivPartitionOf <$> shrinkDivPartition p
ModPartitionOf p -> ModPartitionOf <$> shrinkModPartition p
--------------------------------------------------------------------------------
-- Uniformity
--------------------------------------------------------------------------------
prop_arbitraryQuid_uniform :: Partition -> Property
prop_arbitraryQuid_uniform p =
label (pretty partitionContext) $
forAllBlind arbitraryValues prop
where
partitionContext :: PartitionContext
partitionContext@PartitionContext
{ sizeExponent
, expectedBucketCount
, partitionFunction
} = evalPartition p
valueToBucket :: Quid -> Natural
valueToBucket = (evalPartitionFunction partitionFunction) . quidToNatural
arbitraryValue :: Gen Quid
arbitraryValue =
resize (fromIntegral (powerOfTwoExponent sizeExponent)) arbitraryQuid
arbitraryValues :: Gen [Quid]
arbitraryValues =
replicateM (fromIntegral arbitraryValueCount) arbitraryValue
arbitraryValueCount :: Natural
arbitraryValueCount = unFrequency expectedFrequency * expectedBucketCount
expectedFrequency :: Frequency
expectedFrequency = Frequency 1024
minimumPermittedFrequency :: Frequency
minimumPermittedFrequency = expectedFrequency <&> ((* 3) . (`div` 4))
maximumPermittedFrequency :: Frequency
maximumPermittedFrequency = expectedFrequency <&> ((* 5) . (`div` 4))
prop :: [Quid] -> Property
prop values = reports $ checks $ property True
where
reports
= report sizeExponent
"size exponent"
. report arbitraryValueCount
"arbitrary value count"
. report expectedBucketCount
"expected bucket count"
. report occupiedBucketCount
"occupied bucket count"
. report expectedFrequency
"expected frequency"
. report minimumObservedFrequency
"minimum observed frequency"
. report minimumPermittedFrequency
"minimum permitted frequency"
. report maximumObservedFrequency
"maximum observed frequency"
. report maximumPermittedFrequency
"maximum permitted frequency"
checks
= check
(occupiedBucketCount == expectedBucketCount)
"occupiedBucketCount == expectedBucketCount"
. check
(minimumObservedFrequency >= minimumPermittedFrequency)
"minimumObservedFrequency >= minimumPermittedFrequency"
. check
(maximumObservedFrequency <= maximumPermittedFrequency)
"maximumObservedFrequency <= maximumPermittedFrequency"
occupiedBuckets :: [Natural]
occupiedBuckets = valueToBucket <$> values
occupiedBucketFrequencies :: [(Natural, Frequency)]
occupiedBucketFrequencies = frequencies occupiedBuckets
occupiedBucketCount :: Natural
occupiedBucketCount = fromIntegral $ length occupiedBucketFrequencies
minimumObservedFrequency :: Frequency
minimumObservedFrequency =
snd $ lastNote note occupiedBucketFrequencies
where
note = "minimumObservedFrequency: unexpected empty list"
maximumObservedFrequency :: Frequency
maximumObservedFrequency =
snd $ headNote note occupiedBucketFrequencies
where
note = "maximumObservedFrequency: unexpected empty list"
--------------------------------------------------------------------------------
-- Uniqueness
--------------------------------------------------------------------------------
prop_arbitraryQuid_unique :: Property
prop_arbitraryQuid_unique =
withMaxSuccess 1 $
forAllBlind arbitraryFixedSizeQuids $ \uids ->
Set.size (Set.fromList uids) === L.length uids
where
arbitraryFixedSizeQuids :: Gen [TestQuid]
arbitraryFixedSizeQuids = fmap (unSize . getFixed) <$>
replicateM 1_000_000 (arbitrary @(Fixed (Size 256 TestQuid)))
--------------------------------------------------------------------------------
-- Shrinkability
--------------------------------------------------------------------------------
prop_shrinkQuid_lessThan :: Size 256 TestQuid -> Property
prop_shrinkQuid_lessThan (Size q) =
property $ all (< q) (shrink q)
prop_shrinkQuid_minimalElement :: TestQuid -> Property
prop_shrinkQuid_minimalElement q =
checkCoverage $
cover 10 (q /= minimalQuid) "q /= minimalQuid" $
case shrink q of
s : _ -> s === minimalQuid
_ -> q === minimalQuid
where
minimalQuid = TestQuid 0
prop_shrinkQuid_minimalSet :: [Size 256 TestQuid] -> Property
prop_shrinkQuid_minimalSet qs =
label (show $ bucket expectedSize) $
counterexample (show expectedSize) $
counterexample (show minimalSet) $
conjoin
[ Set.toList minimalSet `L.isPrefixOf` allQuids
, Set.size minimalSet == expectedSize
]
where
allQuids :: [TestQuid]
allQuids = TestQuid . naturalToQuid <$> [0 ..]
bucket :: Int -> (Int, Int)
bucket size = (lo, hi)
where
lo = size `div` 10 * 10
hi = lo + 9
expectedSize :: Int
expectedSize = L.length qs
minimalSet :: Set TestQuid
minimalSet = Set.map unSize $ fromMaybe
(error "Cannot shrink to minimal set")
(shrinkWhile ((>= expectedSize) . Set.size) shrink (Set.fromList qs))
prop_shrinkQuid_ordered :: Size 256 TestQuid -> Property
prop_shrinkQuid_ordered (Size q) =
L.sort shrunkValues === shrunkValues
where
shrunkValues = shrink q
prop_shrinkQuid_unique :: Size 256 TestQuid -> Property
prop_shrinkQuid_unique (Size q) =
Set.size (Set.fromList shrunkValues) === L.length shrunkValues
where
shrunkValues = shrink q
--------------------------------------------------------------------------------
-- Shrinking
--------------------------------------------------------------------------------
shrinkWhile :: (a -> Bool) -> (a -> [a]) -> a -> Maybe a
shrinkWhile condition shrinkFn = loop
where
loop a
| condition a =
case L.find condition (shrinkFn a) of
Nothing -> Just a
Just a' -> loop a'
| otherwise =
Nothing
--------------------------------------------------------------------------------
-- Frequencies
--------------------------------------------------------------------------------
type Frequency = FrequencyOf Natural
newtype FrequencyOf a = Frequency {unFrequency :: a}
deriving (Eq, Functor, Ord, Show)
instance Semigroup Frequency where
Frequency f1 <> Frequency f2 = Frequency (f1 + f2)
instance Monoid Frequency where
mempty = Frequency 1
frequencies :: (Foldable f, Ord k) => f k -> [(k, Frequency)]
frequencies
= L.sortOn ((Down . snd) &&& fst)
. Map.toList
. L.foldr (flip (Map.insertWith (<>)) mempty) Map.empty
--------------------------------------------------------------------------------
-- Reporting
--------------------------------------------------------------------------------
-- | Adds a named variable to the counterexample output of a property.
--
-- On failure, uses pretty-printing to show the contents of the variable.
--
report :: (Show a, Testable prop) => a -> String -> prop -> Property
report a name = counterexample $
"" +| name |+ ":\n" +| indentF 4 (pShowBuilder a) |+ ""
where
pShowBuilder :: Show a => a -> Builder
pShowBuilder = fromLazyText . pShow
--------------------------------------------------------------------------------
-- Verification
--------------------------------------------------------------------------------
-- | Adds a named condition to a property.
--
-- On failure, reports the name of the condition that failed.
--
check :: Bool -> String -> Property -> Property
check condition conditionTitle =
(.&&.) (counterexample counterexampleText $ property condition)
where
counterexampleText = "Condition violated: " <> conditionTitle
--------------------------------------------------------------------------------
-- Utilities
--------------------------------------------------------------------------------
headNote :: String -> [a] -> a
headNote note = g
where
g [] = error note
g (a : _) = a
lastNote :: String -> [a] -> a
lastNote note = g
where
g [] = error note
g [a] = a
g (_ : as) = g as
--------------------------------------------------------------------------------
-- Test types
--------------------------------------------------------------------------------
newtype TestQuid = TestQuid Quid
deriving (Read, Show) via (Decimal Quid)
deriving Arbitrary via Quid
deriving stock (Eq, Ord)