falsify-0.4.0: src/Test/Falsify/Internal/Generator/Shrinking.hs
module Test.Falsify.Internal.Generator.Shrinking (
-- * User-specified shrinking
shrinkToOneOf
, firstThen
, shrinkWith
-- * Support for shrink trees
, fromShrinkTree
, toShrinkTree
, toShrinkTreeWithContext
) where
import Prelude hiding (properFraction)
import Data.Word
import qualified Data.Tree as Rose
import Test.Falsify.Internal.Generator
import Test.Falsify.SampleTree (SampleTree(..), Sample(..))
import Test.Falsify.ShrinkTree (ShrinkTree(..))
import qualified Test.Falsify.Context as Context
import qualified Test.Falsify.ShrinkTree as ShrinkTree
{-------------------------------------------------------------------------------
Specialized shrinking behaviour
-------------------------------------------------------------------------------}
-- | Start with @x@, then shrink to one of the @xs@
--
-- Once shrunk, will not shrink again.
--
-- Minimal value is the first shrunk value, if it exists, and the original
-- otherwise.
shrinkToOneOf :: forall a. a -> [a] -> Gen a
shrinkToOneOf x xs =
aux <$> primWith shrinker
where
aux :: Sample -> a
aux (NotShrunk _) = x
aux (Shrunk i) = index i xs
-- When we shrink, we will try a bunch of new sample trees; we must ensure
-- that we can try /any/ of the possible shrunk values.
--
-- We use this to implement 'fromShrinkTree'. Here, we explore a rose tree
-- of possibilities; at every level in the tree, once we make a choice,
-- we should commit to that choice and not consider it over and over again.
-- Thus, once shrunk, we should not shrink any further.
shrinker :: Sample -> [Word64]
shrinker (Shrunk _) = []
shrinker (NotShrunk _) = zipWith const [0..] xs
-- Index the list of possible shrunk values. This is a bit like @(!!)@ from
-- the prelude, but with some edge cases.
--
-- - If the list is empty, we return the unshrunk value.
-- - Otherwise, if the index exceeds the bounds, we return the last element.
--
-- These two special cases can arise in one of two circumstances:
--
-- - When we run the generator against the 'Minimal' tree. This will give us
-- a @Shrunk 0@ value, independent of what the specified shrinking
-- function does, and it is important that we produce the right value.
-- - When the generator is run against a sample tree that was shrunk wrt to
-- a /different/ generator. In this case the value could be anything;
-- we return the final ("least preferred") element, and then rely on
-- later shrinking to replace this with a more preferred element.
index :: Word64 -> [a] -> a
index _ [] = x
index _ [y] = y
index 0 (y:_) = y
index n (_:ys) = index (n - 1) ys
-- | Generator that always produces @x@ as initial value, and shrinks to @y@
firstThen :: forall a. a -> a -> Gen a
firstThen x y = x `shrinkToOneOf` [y]
-- | Shrink with provided shrinker
--
-- This provides compatibility with QuickCheck-style manual shrinking.
--
-- Defined in terms of 'fromShrinkTree'; see discussion there for some
-- notes on performance.
shrinkWith :: forall a. (a -> [a]) -> Gen a -> Gen a
shrinkWith f gen = do
-- It is critical that we do not apply normal shrinking of the 'SampleTree'
-- here (not even to 'Minimal'). If we did, then the resulting shrink tree
-- would change, and we would be unable to iteratively construct a path
-- through the shrink tree.
--
-- Of course, it can still happen that the generator gets reapplied in a
-- different context; we must take this case into account in
-- 'shrinkToOneOf'.
x <- withoutShrinking gen
fromShrinkTree $ ShrinkTree.unfold x f
{-------------------------------------------------------------------------------
Shrink trees
-------------------------------------------------------------------------------}
-- | Construct generator from shrink tree
--
-- This provides compatibility with Hedgehog-style integrated shrinking.
--
-- This is O(n^2) in the number of shrink steps: as this shrinks, the generator
-- is growing a path of indices which locates a particular value in the shrink
-- tree (resulting from unfolding the provided shrinking function). At each
-- step during the shrinking process the shrink tree is re-evaluated and the
-- next value in the tree is located; since this path throws linearly, the
-- overall cost is O(n^2).
--
-- The O(n^2) cost is only incurred on /locating/ the next element to be tested;
-- the property is not reevaluated at already-shrunk values.
fromShrinkTree :: forall a. ShrinkTree a -> Gen a
fromShrinkTree = go . unwrapShrinkTree
where
go :: Rose.Tree a -> Gen a
go (Rose.Node x xs) = do
next <- Nothing `shrinkToOneOf` map Just xs
case next of
Nothing -> return x
Just x' -> go x'
-- | Expose the full shrink tree of a generator
--
-- The generator is passed 'Context.Initial' for the initial step, and then
-- 'Context.Shrinking' with the number of the shrink step after that. The
-- 'Test.Falsify.Context.Final' step is /not/ included.
--
-- This generator does not shrink.
toShrinkTree :: forall a. Gen a -> Gen (ShrinkTree a)
toShrinkTree gen = fmap snd <$> toShrinkTreeWithContext False (const gen)
-- | Generalization of 'toShrinkTree'
toShrinkTreeWithContext :: forall a.
Bool -- ^ Include 'Context.Final' step?
-> (Context.Execution -> Gen a)
-> Gen (ShrinkTree (Context.Execution, a))
toShrinkTreeWithContext includeFinal gen = do
initSeed <- Seed Context.Initial <$> captureLocalTree
return $ WrapShrinkTree $ Rose.unfoldTree aux initSeed
where
aux :: Seed -> ((Context.Execution, a), [Seed])
aux seed@Seed{seedContext, seedSampleTree} =
case runGen (gen seedContext) seedSampleTree of
(a, shrunk) -> ((seedContext, a), nextSeed seed shrunk)
nextSeed :: Seed -> [SampleTree] -> [Seed]
nextSeed Seed{seedContext, seedSampleTree} shrunk =
case seedContext of
Context.Initial ->
case shrunk of
[] | includeFinal -> [Seed (Context.Final 0) seedSampleTree]
_ -> map (Seed $ Context.Shrinking 0) shrunk
Context.Shrinking i ->
case shrunk of
[] | includeFinal -> [Seed (Context.Final $ succ i) seedSampleTree]
_ -> map (Seed $ Context.Shrinking $ succ i) shrunk
Context.Final _ ->
[]
-- | Internal: 'Rose.unfoldTree' seed, for constructing the shrink tree
data Seed = Seed{
seedContext :: Context.Execution
, seedSampleTree :: SampleTree
}