packages feed

morley-1.4.0: src/Michelson/Test/Util.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

-- | Testing utility functions used by testing framework itself or
-- intended to be used by test writers.

module Michelson.Test.Util
  ( leftToShowPanic
  , leftToPrettyPanic
  , failedTest
  , succeededTest
  , eitherIsLeft
  , eitherIsRight
  , total
  , meanTimeUpperBoundProp
  , meanTimeUpperBoundPropNF
  , genEither
  , genTuple2
  , runGen
  , roundtripTree

  -- * Re-exports
  --
  -- | These functions from "Time" are re-exported here to make it convenient to call
  -- 'meanTimeUpperBoundProp' and 'meanTimeUpperBoundPropNF'.
  , mcs, ms, sec, minute

  -- * Deprecated
  , failedProp
  , succeededProp
  , qcIsLeft
  , qcIsRight
  , roundtripTest
  ) where

import Criterion (Benchmarkable, benchmarkWith', nf, whnf)
import Criterion.Main (defaultConfig)
import Criterion.Types (SampleAnalysis(anMean), Verbosity(Quiet), reportAnalysis, verbosity)
import Data.Typeable (typeRep)
import Fmt (Buildable, pretty)
import Hedgehog
  (Gen, MonadGen, MonadTest, Property, annotate, eval, evalIO, failure, forAll, property, success,
  tripping, withTests)
import qualified Hedgehog.Gen as Gen
import Hedgehog.Internal.Gen (runGenT)
import qualified Hedgehog.Internal.Seed as Seed
import Hedgehog.Internal.Tree (TreeT(runTreeT), nodeValue)
import qualified Hedgehog.Range as Range
import Statistics.Types (Estimate(estPoint))
import Test.QuickCheck (Arbitrary)
import qualified Test.QuickCheck.Property as QC
import Test.Tasty (TestTree)
import Test.Tasty.Hedgehog (testProperty)
import qualified Test.Tasty.QuickCheck as TQC
import Text.Printf (printf)
import Time
  (KnownDivRat, KnownUnitName, Microsecond, Millisecond, Minute, Nanosecond, Picosecond, RatioNat,
  Second, Time, mcs, minute, ms, ns, sec, timeout, toUnit, unTime, unitNameVal)

leftToShowPanic :: (Show e, HasCallStack) => Either e a -> a
leftToShowPanic = either (error . show) id

leftToPrettyPanic :: (Buildable e, HasCallStack) => Either e a -> a
leftToPrettyPanic = either (error . pretty) id

----------------------------------------------------------------------------
-- Property
----------------------------------------------------------------------------

-- | A 'QC.Property' that always fails with given message.
failedProp :: Text -> QC.Property
failedProp r = QC.property $ QC.failed { QC.reason = toString r }
{-# DEPRECATED failedProp "Use 'failedtest' instead." #-}

-- | A 'QC.Property' that always succeeds.
succeededProp :: QC.Property
succeededProp = QC.property True
{-# DEPRECATED succeededProp "Use 'succeededTest' instead." #-}

-- | The 'QC.Property' holds on `Left a`.
qcIsLeft :: Show b => Either a b -> QC.Property
qcIsLeft = \case
  Left _ -> succeededProp
  Right x -> failedProp $ "expected Left, got Right (" <> show x <> ")"
{-# DEPRECATED qcIsLeft "Use 'eitherIsLeft' instead." #-}

-- | The 'QC.Property' holds on `Right b`.
qcIsRight :: Show a => Either a b -> QC.Property
qcIsRight = \case
  Right _ -> succeededProp
  Left x -> failedProp $ "expected Right, got Left (" <> show x <> ")"
{-# DEPRECATED qcIsRight "Use 'eitherIsRight' instead." #-}

-- | A 'Property' that always fails with given message.
failedTest :: (HasCallStack, MonadTest m) => Text -> m ()
failedTest r = withFrozenCallStack $ annotate (toString r) >> failure

-- | A 'Property' that always succeeds.
succeededTest :: MonadTest m => m ()
succeededTest = success

-- | The 'Property' holds on `Left a`.
eitherIsLeft :: (Show b, MonadTest m, HasCallStack) => Either a b -> m ()
eitherIsLeft = \case
  Left _ -> succeededTest
  Right x -> withFrozenCallStack $ failedTest $ "expected Left, got Right (" <> show x <> ")"

-- | The 'Property' holds on `Right b`.
eitherIsRight :: (Show a, MonadTest m, HasCallStack) => Either a b -> m ()
eitherIsRight = \case
  Right _ -> succeededTest
  Left x -> withFrozenCallStack $ failedTest $ "expected Right, got Left (" <> show x <> ")"

-- | Checks that a value is total, i.e., doesn't crash when evaluated,
-- by reducing it to its normal form.
--
-- Equivalent to QuickCheck's @total@.
total :: (MonadTest m, NFData a, HasCallStack) => a -> m a
total a = (withFrozenCallStack $ eval $ rnf a) $> a

-- | Benchmarks the given function and checks that the mean time to evaluate to weak head
-- normal form is under the given amount of time.
--
-- This test fails if the benchmark takes longer than 30 seconds to run.
meanTimeUpperBoundProp
  :: (KnownDivRat unit Second, KnownUnitName unit, HasCallStack)
  => Time unit -> (a -> b) -> a -> Property
meanTimeUpperBoundProp upperBound run arg =
  withFrozenCallStack $
    checkReport upperBound $ whnf run arg

-- | Benchmarks the given function and checks that the mean time to evaluate to
-- normal form is under the given amount of time.
--
-- This test aborts and fails if the benchmark takes longer than 120 seconds to run.
meanTimeUpperBoundPropNF
  :: (KnownDivRat unit Second, KnownUnitName unit, HasCallStack, NFData b)
  => Time unit -> (a -> b) -> a -> Property
meanTimeUpperBoundPropNF upperBound run arg =
  withFrozenCallStack $
    checkReport upperBound $ nf run arg

checkReport
  :: (KnownDivRat unit Second, KnownUnitName unit)
  => HasCallStack => Time unit -> Benchmarkable -> Property
checkReport upperBound benchmarkable =
  withTests 1 $ property $
    evalIO runBench >>= \case
      Nothing -> failedTest "Expected benchmark to complete within 120 seconds."
      Just report ->
        let mean = sec . realToFrac @Double @RatioNat . estPoint . anMean $ reportAnalysis report
        in  if mean < toUnit @Second upperBound
              then succeededTest
              else failedTest $
                "Expected mean estimate to be under "
                <> show upperBound
                <> ", but was "
                <> display mean
  where
    runBench = timeout (minute 2) $
      benchmarkWith' (defaultConfig { verbosity = Quiet }) benchmarkable

    display :: Time Second -> Text
    display n = case n of
      (toUnit @Minute -> x) | x > minute 1 -> format x
      (toUnit @Second -> x) | x > sec 1 -> format x
      (toUnit @Millisecond -> x) | x > ms 1 -> format x
      (toUnit @Microsecond -> x) | x > mcs 1 -> format x
      (toUnit @Nanosecond -> x) | x > ns 1 -> format x
      _ -> format (toUnit @Picosecond n)

    format :: forall unit. KnownUnitName unit => Time unit -> Text
    format n =
      toText @String $ printf "%.4f%s"
        (realToFrac @RatioNat @Double $ unTime n)
        (unitNameVal @unit)

----------------------------------------------------------------------------
-- Generator
----------------------------------------------------------------------------

-- | Randomly selects one of the two generators.
genEither :: MonadGen m => m a -> m b -> m (Either a b)
genEither genA genB = Gen.choice [ Left <$> genA, Right <$> genB ]

-- | Generates an @a@ and a @b@ and wraps them in a tuple.
genTuple2 :: MonadGen m => m a -> m b -> m (a, b)
genTuple2 = liftA2 (,)

-- | Run the given generator deterministically, by fixing its size and seed.
runGen :: HasCallStack => Range.Size -> Word64 -> Gen a -> a
runGen size seed genT =
  let tree = runGenT size (Seed.from seed) genT
      node = fromMaybe discardedErr $ runIdentity $ runMaybeT $ runTreeT tree
      discardedErr = error $
        "Generator could not produce a value for size "
        <> show size <> " and seed " <> show seed
  in  nodeValue node



----------------------------------------------------------------------------
-- Roundtrip
----------------------------------------------------------------------------

-- | This 'TestTree' contains a property based test for conversion from
-- some @x@ to some @y@ and back to @x@ (it should successfully return
-- the initial @x@).
roundtripTest
  :: forall x y err.
     ( Show x
     , Show err
     , Typeable x
     , Arbitrary x
     , Eq x
     , Eq err
     )
  => (x -> y)
  -> (y -> Either err x)
  -> TestTree
roundtripTest xToY yToX = TQC.testProperty typeName check
  where
    typeName = show $ typeRep (Proxy @x)
    check :: x -> QC.Property
    check x = yToX (xToY x) QC.=== Right x
{-# DEPRECATED roundtripTest "Use 'roundtripTree' instead." #-}

-- | This 'TestTree' contains a property based test for conversion from
-- some @x@ to some @y@ and back to @x@ (it should successfully return
-- the initial @x@).
roundtripTree
  :: forall x y err.
     ( Show x
     , Show y
     , Show err
     , Typeable x
     , Eq x
     , Eq err
     )
  => Gen x
  -> (x -> y)
  -> (y -> Either err x)
  -> TestTree
roundtripTree genX xToY yToX = testProperty typeNameX prop
  where
    typeNameX = show $ typeRep (Proxy @x)
    prop :: Property
    prop = property $ do
      x <- forAll genX
      tripping x xToY yToX