packages feed

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)