packages feed

falsify-0.4.0: src/Test/Falsify/Internal/Shrinking.hs

module Test.Falsify.Internal.Shrinking (
    -- * Shrinking
    shrinkFrom
    -- * With full history
  , ShrinkExplanation(..)
  , ShrinkHistory(..)
  , IsValidShrink(..)
  , isValidShrink
  , shrinkHistory
  , shrinkOutcome
  ) where

import Data.Bifunctor
import Data.Either
import Data.List.NonEmpty (NonEmpty((:|)))

import Test.Falsify.Context (Context(Context))
import Test.Falsify.Internal.Generator
import Test.Falsify.SampleTree (SampleTree(..))

import qualified Test.Falsify.Context as Context

{-------------------------------------------------------------------------------
  Explanation
-------------------------------------------------------------------------------}

-- | Shrink explanation
--
-- @p@ is the type of \"positive\" elements that satisfied the predicate (i.e.,
-- valid shrinks), and @n@ is the type of \"negative\" which didn't.
data ShrinkExplanation p n = ShrinkExplanation {
      -- | The value we started, before shrinking
      initial :: p

      -- | The full shrink history
    , history :: ShrinkHistory p n
    }
  deriving (Show)

-- | Shrink explanation
data ShrinkHistory p n =
    -- | We successfully executed a single shrink step
    ShrunkTo p (ShrinkHistory p n)

    -- | We could no shrink any further
    --
    -- We record
    --
    -- * All rejected next steps
    -- * The outcome of the repeated 'Context.Final' step
    --
    -- Recording the rejected next steps is occasionally useful when trying to
    -- figure out why a value didn't shrink any further (what did it try to
    -- shrink to?).
  | ShrinkingDone [n] p

    -- | We stopped shrinking early
    --
    -- This is used when the number of shrink steps is limited.
    -- We record the outcome of the repeated 'Context.Final' step.
  | ShrinkingStopped p
  deriving (Show)

-- | Simplify the shrink explanation to keep only the shrink history
shrinkHistory :: ShrinkExplanation p n -> NonEmpty p
shrinkHistory = \(ShrinkExplanation unshrunk shrunk) ->
    unshrunk :| go shrunk
  where
    go :: ShrinkHistory p n -> [p]
    go (ShrunkTo x xs)       = x : go xs
    go (ShrinkingDone _ns p) = [p]
    go (ShrinkingStopped  p) = [p]

-- | The final shrunk value, as well as all rejected /next/ shrunk steps
--
-- The list of rejected next steps is
--
-- * @Nothing@ if shrinking was terminated early (e.g. 'Context.maxShrinks')
-- * @Just []@ if the final value truly is minimal (typically, it is only
--   minimal wrt to a particular properly, but not the minimal value that a
--   generator can produce).
shrinkOutcome :: forall p n. ShrinkExplanation p n -> (p, Maybe [n])
shrinkOutcome = \ShrinkExplanation{history} ->
    go history
  where
    go :: ShrinkHistory p n -> (p, Maybe [n])
    go (ShrunkTo _p h)      = go h
    go (ShrinkingDone ns p) = (p, Just ns)
    go (ShrinkingStopped p) = (p, Nothing)

{-------------------------------------------------------------------------------
  Mapping
-------------------------------------------------------------------------------}

instance Functor (ShrinkExplanation p) where
  fmap = second

instance Functor (ShrinkHistory p) where
  fmap = second

instance Bifunctor ShrinkExplanation where
  bimap f g ShrinkExplanation{initial, history} = ShrinkExplanation{
        initial = f initial
      , history = bimap f g history
      }

instance Bifunctor ShrinkHistory where
  bimap f g = \case
      ShrunkTo         p h -> ShrunkTo                 (f p) (bimap f g h)
      ShrinkingDone ns p   -> ShrinkingDone (map g ns) (f p)
      ShrinkingStopped p   -> ShrinkingStopped         (f p)

{-------------------------------------------------------------------------------
  Shrinking
-------------------------------------------------------------------------------}

-- | Does a given shrunk value represent a valid shrink step?
data IsValidShrink p n =
    ValidShrink p
  | InvalidShrink n
  deriving stock (Show)

isValidShrink :: IsValidShrink p n -> Either n p
isValidShrink (ValidShrink p)   = Right p
isValidShrink (InvalidShrink n) = Left n

-- | Find smallest value that the generator can produce and still satisfies
-- the predicate.
--
-- Returns the full shrink history.
--
-- To avoid boolean blindness, we use different types for values that satisfy
-- the property and values that do not.
--
-- This is lazy in the shrink history.
shrinkFrom :: forall p n.
     Context.Static
     -- ^ Static context
     --
     -- Passed as-is to the property as part of the t'Context'.
     -- Used to extract shrinking parameters.
  -> Context.Iteration
     -- ^ Iteration context
     --
     -- Passsed as-is to the property as part of the t'Context'.
     -- Not used otherwise.
  -> (Context -> Gen (IsValidShrink p n))
     -- ^ The property we're shrinking
  -> SampleTree
     -- ^ The sample tree we started with
  -> (p, [SampleTree])
     -- ^ The initial result of the generator
  -> ShrinkExplanation p n
shrinkFrom static iteration prop = \st (p, shrunk) ->
    ShrinkExplanation p $ go 0 st shrunk
  where
    go :: Word -> SampleTree -> [SampleTree] -> ShrinkHistory p n
    go i = \st shrunk ->
        -- NOTE: 'partitionEithers' is lazy enough:
        --
        -- > head . fst $ partitionEithers [Left True, undefined] == True
        let candidates :: [(SampleTree, p, [SampleTree])]
            rejected   :: [n]
            (candidates, rejected) = partitionEithers $ map consider shrunk

         in case candidates of
              [] ->
                ShrinkingDone rejected $ runFinal i st
              (st', p, shrunk'):_ | canContinue ->
                ShrunkTo p $ go (succ i) st' shrunk'
              _otherwise ->
                ShrinkingStopped $ runFinal i st
      where
        ctx :: Context
        ctx = Context static iteration $ Context.Shrinking i

        canContinue :: Bool
        canContinue =
             maybe
               True
               (\limit -> i < limit)
               (Context.maxShrinks static)

        consider :: SampleTree -> Either (SampleTree, p, [SampleTree]) n
        consider st =
            case isValid of
              ValidShrink p   -> Left (st, p, shrunk')
              InvalidShrink n -> Right n
          where
            isValid :: IsValidShrink p n
            shrunk' :: [SampleTree]
            (isValid, shrunk') = runGen (prop ctx) st

    -- Run the property one final time
    --
    -- Precondition: must be run on a sample tree for which we know the
    -- property fails.
    runFinal :: Word -> SampleTree -> p
    runFinal i st =
        case fst $ runGen (prop ctx) st of
          ValidShrink   p -> p
          InvalidShrink _ -> error "runFinal: precondition violated"
      where
        ctx :: Context
        ctx = Context static iteration $ Context.Final i