packages feed

antigen 0.3.1.0 → 0.4.0.0

raw patch · 12 files changed

+688/−98 lines, 12 filesdep +containersdep +deepseqdep +filepathdep −mtldep ~QuickCheckdep ~freePVP ok

version bump matches the API change (PVP)

Dependencies added: containers, deepseq, filepath, hspec-golden, text

Dependencies removed: mtl

Dependency ranges changed: QuickCheck, free

API changes (from Hackage documentation)

- Test.AntiGen: tryZapAntiGen :: Int -> AntiGen a -> Gen (Maybe a)
- Test.AntiGen.Internal: tryZapAntiGen :: Int -> AntiGen a -> Gen (Maybe a)
+ Test.AntiGen: (#!) :: AntiGen a -> Text -> AntiGen a
+ Test.AntiGen: ZapResult :: a -> [NonEmpty Text] -> Int -> ZapResult a
+ Test.AntiGen: [zrAnnotation] :: ZapResult a -> [NonEmpty Text]
+ Test.AntiGen: [zrValue] :: ZapResult a -> a
+ Test.AntiGen: [zrZapped] :: ZapResult a -> Int
+ Test.AntiGen: antiSamePair :: (Arbitrary a, Num a, Eq a) => AntiGen (a, a)
+ Test.AntiGen: data ZapResult a
+ Test.AntiGen: faultyNumRange :: (Random a, Eq a) => a -> (a, a) -> AntiGen a
+ Test.AntiGen: infixl 5 #!
+ Test.AntiGen: infixl 6 ||!
+ Test.AntiGen: prettyZapResult :: ZapResult a -> Text
+ Test.AntiGen: replicateMNorm :: Int -> AntiGen a -> AntiGen [a]
+ Test.AntiGen: reweigh :: Float -> AntiGen a -> AntiGen a
+ Test.AntiGen: scaleWeight :: (Float -> Float) -> AntiGen a -> AntiGen a
+ Test.AntiGen: withAnnotation :: Text -> AntiGen a -> AntiGen a
+ Test.AntiGen: zapAntiGenResult :: Int -> AntiGen a -> Gen (ZapResult a)
+ Test.AntiGen.Internal: (#!) :: AntiGen a -> Text -> AntiGen a
+ Test.AntiGen.Internal: ZapResult :: a -> [NonEmpty Text] -> Int -> ZapResult a
+ Test.AntiGen.Internal: [zrAnnotation] :: ZapResult a -> [NonEmpty Text]
+ Test.AntiGen.Internal: [zrValue] :: ZapResult a -> a
+ Test.AntiGen.Internal: [zrZapped] :: ZapResult a -> Int
+ Test.AntiGen.Internal: data ZapResult a
+ Test.AntiGen.Internal: infixl 5 #!
+ Test.AntiGen.Internal: infixl 6 |!
+ Test.AntiGen.Internal: instance GHC.Base.Functor Test.AntiGen.Internal.ZapResult
+ Test.AntiGen.Internal: instance GHC.Base.Semigroup (Test.AntiGen.Internal.ZapResult a)
+ Test.AntiGen.Internal: prettyZapResult :: ZapResult a -> Text
+ Test.AntiGen.Internal: reweigh :: Float -> AntiGen a -> AntiGen a
+ Test.AntiGen.Internal: scaleWeight :: (Float -> Float) -> AntiGen a -> AntiGen a
+ Test.AntiGen.Internal: withAnnotation :: Text -> AntiGen a -> AntiGen a
+ Test.AntiGen.Internal: zapAntiGenResult :: Int -> AntiGen a -> Gen (ZapResult a)
- Test.AntiGen.Internal: zapAt :: Int -> PartialGen a -> Gen (PartialGen a)
+ Test.AntiGen.Internal: zapAt :: Float -> PartialGen a -> Gen (ZapResult (PartialGen a))

Files

+ .golden/mixed_annotations.golden view
@@ -0,0 +1,3 @@+Zapped 3 decision points+Annotations:+  - annotated
+ .golden/multiple_zaps.golden view
@@ -0,0 +1,5 @@+Zapped 3 decision points+Annotations:+  - user.name+  - user.email+  - address.street
+ .golden/no_zaps.golden view
@@ -0,0 +1,1 @@+Zapped 0 decision points
+ .golden/single_zap_nested.golden view
@@ -0,0 +1,3 @@+Zapped 1 decision points+Annotations:+  - root.child.leaf
+ .golden/single_zap_no_annotation.golden view
@@ -0,0 +1,1 @@+Zapped 1 decision points
+ .golden/single_zap_simple.golden view
@@ -0,0 +1,3 @@+Zapped 1 decision points+Annotations:+  - positive
CHANGELOG.md view
@@ -1,5 +1,17 @@ # Revision history for antigen +## 0.4.0.0++* Add `antiSamePair`+* Add `faultyNumRange`+* Add hierarchical annotations with `withAnnotation` and `(#!)`+* Add `ZapResult` with annotation tracking+* Add `zapAntiGenResult` to get full zap metadata+* Add `prettyZapResult` for displaying zap results+* Add weighted decision points with `scaleWeight` and `reweigh`+* Add `replicateMNorm` for normalized list generation+* Remove `tryZapAntiGen` (use `zapAntiGenResult` instead)+ ## 0.3.1.0  * Add `tryZapAntiGen`
antigen.cabal view
@@ -1,11 +1,10 @@ cabal-version:      3.0 name:               antigen-version:            0.3.1.0+version:            0.4.0.0 synopsis:           Fault injection for QuickCheck description:   AntiGen extends QuickCheck to allow injecting random faults into QuickCheck    generators.-  .   It introduces the `AntiGen` monad, a drop-in replacement for `Gen` that allows    developers to define negative generators alongside their standard    positive generators.@@ -17,6 +16,7 @@ category:           Testing build-type:         Simple extra-doc-files:    CHANGELOG.md, README.md+data-files:         .golden/*.golden tested-with:        GHC == 9.10.3  source-repository head@@ -24,19 +24,20 @@   location: https://github.com/input-output-hk/antigen  common warnings-    ghc-options: -Wall+    ghc-options: -Wall -Wunused-packages  library     import:           warnings     exposed-modules:         Test.AntiGen       Test.AntiGen.Internal-    build-depends:    +    build-depends:       base >=4.18 && <5,-      QuickCheck >= 2.15.0 && < 2.18,-      free >= 5.2 && < 5.3,-      mtl >= 2.3.1 && < 2.4,+      containers >= 0.6 && < 0.9,+      QuickCheck >= 2.14 && < 2.19,+      free >= 5.1 && < 5.3,       random >= 1.2 && < 1.4,+      text >= 2.0 && < 2.2,       quickcheck-transformer >= 0.3.1 && < 0.4,     hs-source-dirs:   src     default-language: Haskell2010@@ -47,12 +48,17 @@     type:             exitcode-stdio-1.0     hs-source-dirs:   test     main-is:          Main.hs+    other-modules:    Paths_antigen+    autogen-modules:  Paths_antigen     build-depends:       base,       antigen,+      filepath,       hspec,+      hspec-golden,       QuickCheck,       quickcheck-transformer,+      text,  benchmark bench   import: warnings@@ -64,5 +70,7 @@     antigen,     base,     criterion,+    deepseq,     QuickCheck,     quickcheck-transformer,+    text,
bench/Main.hs view
@@ -1,9 +1,21 @@ {-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Main (main) where -import Criterion.Main (Benchmarkable, bench, defaultMain, nfIO)-import Test.AntiGen.Internal (AntiGen, evalPartial, evalToPartial, zapAt, (|!))+import Control.DeepSeq (deepseq)+import Criterion.Main (Benchmarkable, bench, bgroup, defaultMain, nfIO)+import qualified Data.Text as T+import Test.AntiGen.Internal (+  AntiGen,+  ZapResult (..),+  evalPartial,+  evalToPartial,+  withAnnotation,+  zapAt,+  (|!),+ ) import Test.QuickCheck (Arbitrary (..), generate) import Test.QuickCheck.GenT (MonadGen (..)) @@ -19,16 +31,73 @@           pure $ y : x : xs         [] -> error "Got empty list" -bindListZap :: Int -> Int -> Benchmarkable-bindListZap len i =-  nfIO . generate . variant (12345 :: Int) . fmap evalPartial $-    zapAt i =<< evalToPartial (bindList len)+-- Version with annotations on each decision point+bindListAnnotated :: Int -> AntiGen [Int]+bindListAnnotated 1 = (: []) <$> liftGen arbitrary+bindListAnnotated n+  | n <= 0 = pure []+  | otherwise = do+      rest <- bindListAnnotated (n - 1)+      case rest of+        x : xs -> do+          y <- withAnnotation (T.pack (show n)) $ pure (succ x) |! pure (pred x)+          pure $ y : x : xs+        [] -> error "Got empty list" +-- Only force the value+bindListZapValue :: Int -> Int -> Benchmarkable+bindListZapValue len i =+  nfIO . generate . variant (12345 :: Int) . fmap (evalPartial . zrValue) $+    zapAt (fromIntegral i) =<< evalToPartial (bindList len)++-- Force value, annotation, and zapped count+bindListZapAll :: Int -> Int -> Benchmarkable+bindListZapAll len i =+  nfIO . generate . variant (12345 :: Int) . fmap forceAll $+    zapAt (fromIntegral i) =<< evalToPartial (bindList len)+  where+    forceAll ZapResult {..} =+      zrAnnotation `deepseq` (evalPartial zrValue, zrZapped)++-- Annotated versions+annotatedZapValue :: Int -> Int -> Benchmarkable+annotatedZapValue len i =+  nfIO . generate . variant (12345 :: Int) . fmap (evalPartial . zrValue) $+    zapAt (fromIntegral i) =<< evalToPartial (bindListAnnotated len)++annotatedZapAll :: Int -> Int -> Benchmarkable+annotatedZapAll len i =+  nfIO . generate . variant (12345 :: Int) . fmap forceAll $+    zapAt (fromIntegral i) =<< evalToPartial (bindListAnnotated len)+  where+    forceAll ZapResult {..} =+      zrAnnotation `deepseq` (evalPartial zrValue, zrZapped)+ main :: IO () main =   defaultMain-    [ bench "bindList 10_000 zap at 0" $ bindListZap 10_000 0-    , bench "bindList 10_000 zap at 9_000" $ bindListZap 10_000 9_000-    , bench "bindList 1_000_000 zap at 0" $ bindListZap 1_000_000 0-    , bench "bindList 1_000_000 zap at 900_000" $ bindListZap 1_000_000 900_000+    [ bgroup+        "value only"+        [ bench "10_000 zap at 0" $ bindListZapValue 10_000 0+        , bench "10_000 zap at 9_000" $ bindListZapValue 10_000 9_000+        , bench "1_000_000 zap at 0" $ bindListZapValue 1_000_000 0+        , bench "1_000_000 zap at 900_000" $ bindListZapValue 1_000_000 900_000+        ]+    , bgroup+        "force all"+        [ bench "10_000 zap at 0" $ bindListZapAll 10_000 0+        , bench "10_000 zap at 9_000" $ bindListZapAll 10_000 9_000+        , bench "1_000_000 zap at 0" $ bindListZapAll 1_000_000 0+        , bench "1_000_000 zap at 900_000" $ bindListZapAll 1_000_000 900_000+        ]+    , bgroup+        "annotated value only"+        [ bench "10_000 zap at 0" $ annotatedZapValue 10_000 0+        , bench "10_000 zap at 9_000" $ annotatedZapValue 10_000 9_000+        ]+    , bgroup+        "annotated force all"+        [ bench "10_000 zap at 0" $ annotatedZapAll 10_000 0+        , bench "10_000 zap at 9_000" $ annotatedZapAll 10_000 9_000+        ]     ]
src/Test/AntiGen.hs view
@@ -11,14 +11,24 @@  module Test.AntiGen (   AntiGen,+  ZapResult (..),   (|!),+  (#!),   (||!),+  withAnnotation,   runAntiGen,   zapAntiGen,-  tryZapAntiGen,+  zapAntiGenResult,+  prettyZapResult,+  scaleWeight,+  reweigh, +  -- * Normalized monad combinators+  replicateMNorm,+   -- * AntiGen combinators   faultyNum,+  faultyNumRange,   faultyBool,   faultyTry,   faultyTryGen,@@ -30,12 +40,25 @@   antiNonNegative,   antiJust,   antiNonEmpty,+  antiSamePair,   antiDistinctPair, ) where -import Control.Monad (join)+import Control.Monad (join, replicateM) import System.Random (Random)-import Test.AntiGen.Internal+import Test.AntiGen.Internal (+  AntiGen,+  ZapResult (..),+  prettyZapResult,+  reweigh,+  runAntiGen,+  scaleWeight,+  withAnnotation,+  zapAntiGen,+  zapAntiGenResult,+  (#!),+  (|!),+ ) import Test.QuickCheck (   Arbitrary (..),   Negative (..),@@ -44,19 +67,25 @@   NonZero (..),   Positive (..),  )-import Test.QuickCheck.GenT (MonadGen (..), elements, listOf1, oneof, suchThat)+import Test.QuickCheck.GenT (MonadGen (..), listOf1, oneof, suchThat)  -- | Returns the provided number.+-- -- Negative: returns a value that is not equal to the provided number. faultyNum :: (Eq a, Num a, Arbitrary a) => a -> AntiGen a faultyNum n = pure n |! ((n +) . getNonZero <$> arbitrary) +faultyNumRange :: (Random a, Eq a) => a -> (a, a) -> AntiGen a+faultyNumRange n rng = pure n |! (choose rng `suchThat` (/= n))+ -- | Returns the provided `Bool`.+-- -- Negative: returns the negation of that `Bool`. faultyBool :: Bool -> AntiGen Bool faultyBool b = pure b |! pure (not b)  -- | Generates a value from the first range.+-- -- Negative: Generates a value from the second range excluding the first range. antiChoose :: (Integral a, Random a) => (a, a) -> (a, a) -> AntiGen a antiChoose rng@(lo, hi) (boundLo, boundHi)@@ -70,12 +99,14 @@     rngHi = (succ hi, boundHi)  -- | Generates a value from the range.+-- -- Negative: Returns a random value outside the range between `minBound` and -- `maxBound`. antiChooseBounded :: (Integral a, Random a, Bounded a) => (a, a) -> AntiGen a antiChooseBounded rng = antiChoose rng (minBound, maxBound)  -- | Returns the provided value+-- -- Negative: Generates an arbitrary value that is different from the provided -- value. --@@ -85,6 +116,7 @@ faultyTry a = faultyTryGen a $ liftGen arbitrary  -- | Returns the provided value+-- -- Negative: Use the generator to generate a random value that is different -- from the provided value. --@@ -94,46 +126,62 @@ faultyTryGen a gen = pure a ||! (gen `suchThat` (/= a))  -- | Returns a positive number+-- -- Negative: Returns a non-positive number antiPositive :: (Num a, Ord a, Arbitrary a) => AntiGen a antiPositive = (getPositive <$> arbitrary) |! (getNonPositive <$> arbitrary)  -- | Returns a non-positive number+-- -- Negative: Returns a positive number antiNonPositive :: (Num a, Ord a, Arbitrary a) => AntiGen a antiNonPositive = (getNonPositive <$> arbitrary) |! (getPositive <$> arbitrary)  -- | Returns a negative number+-- -- Negative: Returns a non-negative number antiNegative :: (Num a, Ord a, Arbitrary a) => AntiGen a antiNegative = (getNegative <$> arbitrary) |! (getNonNegative <$> arbitrary)  -- | Returns a non-negative number+-- -- Negative: Returns a negative number antiNonNegative :: (Num a, Ord a, Arbitrary a) => AntiGen a antiNonNegative = (getNonNegative <$> arbitrary) |! (getNegative <$> arbitrary)  -- | Returns `Just x`+-- -- Negative: Returns `Nothing` antiJust :: a -> AntiGen (Maybe a) antiJust x = pure (Just x) ||! pure Nothing  -- | Returns a non-empty list+-- -- Negative: Generate an empty list antiNonEmpty :: AntiGen a -> AntiGen [a] antiNonEmpty x = listOf1 x ||! pure [] +-- | Generate a pair with equal values+--+-- Negative: Generates a pair of distinct values+antiSamePair :: (Arbitrary a, Num a, Eq a) => AntiGen (a, a)+antiSamePair =+  ((\x -> (x, x)) <$> arbitrary)+    |! ( do+           x <- arbitrary+           NonZero s <- arbitrary+           return (x, x + s)+       )+ -- | Generates a pair (x, y) where x /= y.+-- -- Negative: Generates a pair (x, y) where x == y. antiDistinctPair :: (Num a, Arbitrary a, Eq a) => AntiGen (a, a) antiDistinctPair =   ( do       x <- arbitrary-      -- Generate a non-zero offset to guarantee x /= y-      s <- elements [-1, 1]-      a <- arbitrary-      let offset = if a == 0 then 1 else abs a-      return (x, x + (s * offset))+      NonZero s <- arbitrary+      return (x, x + s)   )     |! ( do            x <- arbitrary@@ -143,3 +191,13 @@ -- | Create an `AntiGen` from a positive and a negative `AntiGen` generator (||!) :: AntiGen a -> AntiGen a -> AntiGen a a ||! b = join $ pure a |! pure b++-- | Like 'replicateM', but normalizes the weight of each element by @1\/n@.+--+-- The total weight of the list becomes the average weight of its elements,+-- rather than the sum. This prevents longer lists from having a+-- disproportionately higher chance of being zapped.+replicateMNorm :: Int -> AntiGen a -> AntiGen [a]+replicateMNorm n = replicateM n . scaleWeight (/ fromIntegral n)++infixl 6 ||!
src/Test/AntiGen/Internal.hs view
@@ -1,71 +1,128 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeApplications #-} {-# LANGUAGE UndecidableInstances #-}  module Test.AntiGen.Internal (   AntiGen,+  ZapResult (..),+  prettyZapResult,   (|!),+  (#!),   zapAntiGen,-  tryZapAntiGen,+  zapAntiGenResult,   runAntiGen,   evalToPartial,   evalPartial,   countDecisionPoints,   zapAt,+  withAnnotation,+  scaleWeight,+  reweigh, ) where  import Control.Monad ((<=<)) import Control.Monad.Free.Church (F (..), MonadFree (..))-import Control.Monad.Free.Class (wrapT)-import Control.Monad.State.Strict (MonadState (..), StateT (..), evalStateT, modify')-import Control.Monad.Trans (MonadTrans (..))-import Test.QuickCheck (Gen, getSize)-import Test.QuickCheck.GenT (GenT (..), MonadGen (..), runGenT)+import Data.Foldable (toList)+import Data.List.NonEmpty (NonEmpty)+import qualified Data.List.NonEmpty as NE+import Data.Sequence (Seq (..))+import qualified Data.Sequence as Seq+import Data.Text (Text)+import qualified Data.Text as T+#if MIN_VERSION_QuickCheck(2,18,0)+import System.Random (SplitGen (..))+#else+import System.Random (RandomGen (split))+#endif+import Test.QuickCheck (getSize)+import Test.QuickCheck.Gen (Gen (..))+import Test.QuickCheck.GenT (MonadGen (..))+import Test.QuickCheck.Random (QCGen) +#if !MIN_VERSION_QuickCheck(2,18,0)+splitGen :: RandomGen g => g -> (g, g)+splitGen = split+#endif+ data BiGen next where-  BiGen :: Gen t -> Maybe (Gen t) -> (t -> next) -> BiGen next+  BiGen :: Gen t -> Maybe (Gen t) -> Float -> (t -> next) -> BiGen next+  Annotate :: Text -> AntiGen t -> (t -> next) -> BiGen next+  Reweigh :: (Float -> Float) -> AntiGen t -> (t -> next) -> BiGen next  instance Functor BiGen where-  fmap f (BiGen p n c) = BiGen p n $ f . c+  fmap f (BiGen p n w c) = BiGen p n w $ f . c+  fmap f (Annotate ann inner c) = Annotate ann inner $ f . c+  fmap f (Reweigh g inner c) = Reweigh g inner $ f . c  newtype AntiGen a = AntiGen (F BiGen a)   deriving (Functor, Applicative, Monad, MonadFree BiGen)  mapGen :: (forall x. Gen x -> Gen x) -> AntiGen a -> AntiGen a-mapGen f (AntiGen (F m)) = m pure $ \(BiGen pos neg c) ->-  wrap $ BiGen (f pos) (f <$> neg) c+mapGen f (AntiGen (F m)) = m pure $ \case+  BiGen pos neg w c -> wrap $ BiGen (f pos) (f <$> neg) w c+  Annotate ann inner c -> wrap $ Annotate ann (mapGen f inner) c+  Reweigh g inner c -> wrap $ Reweigh g (mapGen f inner) c  instance MonadGen AntiGen where-  liftGen g = AntiGen $ F $ \p b -> b $ BiGen g Nothing p+  liftGen g = AntiGen $ F $ \p b -> b $ BiGen g Nothing 1 p   variant n = mapGen (variant n)-  sized f = wrap $ BiGen (f <$> getSize) Nothing id+  sized f = wrap $ BiGen (f <$> getSize) Nothing 1 id   resize n m = mapGen (resize n) m   choose = liftGen . choose +mkAntiGen :: Gen a -> Gen a -> AntiGen a+mkAntiGen active alt =+  AntiGen $ F $ \p b -> b $ BiGen (p <$> active) (Just $ p <$> alt) 1 id+ -- | Create a negatable generator by providing a positive and a negative -- generator (|!) :: Gen a -> Gen a -> AntiGen a-pos |! neg = AntiGen $ F $ \p b -> b $ BiGen pos (Just neg) p+(|!) = mkAntiGen +infixl 6 |!++-- | Postfix annotation operator. Annotates an 'AntiGen' with a label that+-- will be included in 'ZapResult' when this generator is zapped.+--+-- @+-- myGen = positive |! negative #! "sign"+-- @+(#!) :: AntiGen a -> Text -> AntiGen a+(#!) = flip withAnnotation++infixl 5 #!++-- | Wrap an AntiGen with an annotation+withAnnotation :: Text -> AntiGen a -> AntiGen a+withAnnotation ann inner = wrap $ Annotate ann inner pure++scaleWeight :: (Float -> Float) -> AntiGen a -> AntiGen a+scaleWeight rw inner = wrap $ Reweigh rw inner pure++reweigh :: Float -> AntiGen a -> AntiGen a+reweigh w = scaleWeight $ const w+ data DecisionPoint next where   DecisionPoint ::     { dpValue :: t-    , dpPositiveGen :: Gen t-    , dpNegativeGen :: Maybe (Gen t)+    , dpActiveGen :: Gen t+    , dpAlternativeGen :: Maybe (Gen t)+    , dpAnnotation :: Seq Text+    , dpWeight :: Float     , dpContinuation :: t -> next     } ->     DecisionPoint next  instance Functor DecisionPoint where-  fmap f (DecisionPoint v p n c) = DecisionPoint v p n $ f . c+  fmap f (DecisionPoint v p n a w c) = DecisionPoint v p n a w $ f . c  continue :: DecisionPoint next -> next continue DecisionPoint {..} = dpContinuation dpValue@@ -74,50 +131,140 @@   deriving (Functor, Applicative, Monad, MonadFree DecisionPoint)  evalToPartial :: AntiGen a -> Gen (PartialGen a)-evalToPartial (AntiGen (F m)) = runGenT $ m pure $ \(BiGen pos mNeg c) -> do-  value <- liftGen pos-  wrapT $ DecisionPoint value pos mNeg c+evalToPartial (AntiGen (F m)) = MkGen $ \qcGen sz ->+  m kp kf Seq.empty id qcGen sz+  where+    kp :: a -> Seq Text -> (Float -> Float) -> QCGen -> Int -> PartialGen a+    kp x _ _ _ _ = pure x +    kf ::+      BiGen (Seq Text -> (Float -> Float) -> QCGen -> Int -> PartialGen a) ->+      Seq Text ->+      (Float -> Float) ->+      QCGen ->+      Int ->+      PartialGen a+    kf (BiGen activeGen altGen w cont) path rw qcGen sz =+      let (qcGenValue, qcGenCont) = splitGen qcGen+          value = unGen activeGen qcGenValue sz+       in wrap $+            DecisionPoint+              { dpValue = value+              , dpActiveGen = activeGen+              , dpAlternativeGen = altGen+              , dpAnnotation = path+              , dpWeight = rw w+              , dpContinuation = \v -> cont v path rw qcGenCont sz+              }+    kf (Annotate ann (AntiGen (F inner)) cont) path rw qcGen sz = do+      let (qcGenInner, qcGenCont) = splitGen qcGen+      t <- inner kp kf (path :|> ann) rw qcGenInner sz+      cont t path rw qcGenCont sz+    kf (Reweigh g (AntiGen (F inner)) cont) path rw qcGen sz = do+      let (qcGenInner, qcGenCont) = splitGen qcGen+      t <- inner kp kf path (rw . g) qcGenInner sz+      cont t path rw qcGenCont sz+ countDecisionPoints :: PartialGen a -> Int countDecisionPoints (PartialGen (F m)) = m (const 0) $ \dp@DecisionPoint {..} ->-  case dpNegativeGen of+  case dpAlternativeGen of     Just _ -> succ $ continue dp     Nothing -> continue dp -zapAt :: Int -> PartialGen a -> Gen (PartialGen a)-zapAt cutoffDepth (PartialGen (F m)) = do-  let-    wrapGenState mm = StateT $ \s -> GenT $ \g sz ->-      let eval (StateT x) =-            let GenT f = x s-             in f g sz-       in wrap $ eval <$> mm-  runGenT . (`evalStateT` cutoffDepth) . m pure $ \dp@DecisionPoint {..} ->-    case dpNegativeGen of-      Just neg -> do-        d <- get-        modify' pred-        if d == 0-          then do-            -- Negate the generator-            value <- lift $ liftGen neg-            wrapGenState $ DecisionPoint value neg Nothing dpContinuation-          else wrapGenState dp-      Nothing -> wrapGenState dp+totalWeight :: PartialGen a -> Float+totalWeight (PartialGen (F m)) = m (const 0) $ \dp@DecisionPoint {..} ->+  case dpAlternativeGen of+    Just _ -> continue dp + dpWeight+    Nothing -> continue dp -zap :: PartialGen a -> Gen (PartialGen a)-zap p-  | let maxDepth = countDecisionPoints p-  , maxDepth > 0 = do-      cutoffDepth <- choose (0, maxDepth - 1)-      zapAt cutoffDepth p-  | otherwise = pure p+data ZapResult a = ZapResult+  { zrValue :: a+  , zrAnnotation :: [NonEmpty Text]+  , zrZapped :: Int+  }+  deriving (Functor) -zapNTimes :: Int -> PartialGen a -> Gen (PartialGen a)-zapNTimes n-  | n <= 0 = pure-  | otherwise = zapNTimes (n - 1) <=< zap+instance Semigroup (ZapResult a) where+  ZapResult v a1 z1 <> ZapResult _ a2 z2 =+    ZapResult v (a1 <> a2) (z1 + z2) +-- | Pretty print the annotation paths from a ZapResult+prettyZapResult :: ZapResult a -> Text+prettyZapResult ZapResult {..} =+  T.unlines $+    [ "Zapped " <> T.pack (show zrZapped) <> " decision points"+    ]+      <> case zrAnnotation of+        [] -> []+        anns -> "Annotations:" : map prettyPath anns+  where+    prettyPath :: NonEmpty Text -> Text+    prettyPath path = "  - " <> T.intercalate "." (NE.toList path)++zapAt :: Float -> PartialGen a -> Gen (ZapResult (PartialGen a))+zapAt cutoffWeight (PartialGen (F m)) = MkGen $ \qcGen sz ->+  m kp (kf qcGen sz) $ Just cutoffWeight+  where+    kp :: a -> Maybe Float -> ZapResult (PartialGen a)+    kp x _ = ZapResult (pure x) mempty 0++    kf ::+      QCGen ->+      Int ->+      DecisionPoint (Maybe Float -> ZapResult (PartialGen a)) ->+      Maybe Float ->+      ZapResult (PartialGen a)+    kf qcGen sz DecisionPoint {..} mn =+      case dpAlternativeGen of+        Just altGen+          | Just n <- mn+          , n < dpWeight ->+              ZapResult+                { zrValue =+                    let newValue = unGen altGen qcGen sz+                     in wrap $+                          DecisionPoint+                            { dpValue = newValue+                            , dpActiveGen = altGen+                            , dpAlternativeGen = Nothing+                            , dpContinuation = \v -> zrValue (dpContinuation v Nothing)+                            , ..+                            }+                , zrAnnotation = toList (NE.nonEmpty (toList dpAnnotation))+                , zrZapped = 1+                }+        _ ->+          -- Preserve tree structure+          let n' = case dpAlternativeGen of+                Just _ -> (\x -> x - dpWeight) <$> mn+                Nothing -> mn+              restResult = dpContinuation dpValue n'+           in ZapResult+                { zrValue =+                    wrap $+                      DecisionPoint+                        { dpContinuation = \v -> zrValue (dpContinuation v n')+                        , ..+                        }+                , zrAnnotation = zrAnnotation restResult+                , zrZapped = zrZapped restResult+                }++zap :: PartialGen a -> Gen (ZapResult (PartialGen a))+zap p =+  let w = totalWeight p+   in if w == 0+        then pure $ ZapResult p [] 0+        else (`zapAt` p) =<< choose (0, w)++zapNTimes :: Int -> PartialGen a -> Gen (ZapResult a)+zapNTimes n x+  | n <= 0 = pure $ ZapResult (evalPartial x) [] 0+  | otherwise = do+      zapResult <- zap x+      rest <- zapNTimes (pred n) $ zrValue zapResult+      pure $ rest <> fmap evalPartial zapResult+ evalPartial :: PartialGen a -> a evalPartial (PartialGen (F m)) = m id continue @@ -127,16 +274,12 @@ -- the `AntiGen` is lower than `n`, then the number of negations will be less -- than `n`. zapAntiGen :: Int -> AntiGen a -> Gen a-zapAntiGen n = fmap evalPartial <$> zapNTimes n <=< evalToPartial+zapAntiGen n = fmap zrValue . zapAntiGenResult n  -- | Create a negative generator from an `AntiGen` by introducing at most--- `n` mistakes. If there are no decision points, it will return `Nothing`.-tryZapAntiGen :: Int -> AntiGen a -> Gen (Maybe a)-tryZapAntiGen n ag = do-  p <- evalToPartial ag-  if countDecisionPoints p > 0-    then Just . evalPartial <$> zapNTimes n p-    else pure Nothing+-- `n` mistakes.+zapAntiGenResult :: Int -> AntiGen a -> Gen (ZapResult a)+zapAntiGenResult n = zapNTimes n <=< evalToPartial  -- | Create a positive generator from the provided `AntiGen`. runAntiGen :: AntiGen a -> Gen a
test/Main.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}@@ -7,9 +8,15 @@  import Control.Monad (replicateM) import Data.Data (Proxy (..))-import Data.Word (Word32, Word64)+import Data.List (sort)+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.Text as T+import Data.Word (Word32, Word64, Word8)+import Paths_antigen (getDataDir)+import System.FilePath ((</>)) import Test.AntiGen (   AntiGen,+  antiChoose,   antiChooseBounded,   antiNegative,   antiNonNegative,@@ -18,13 +25,23 @@   faultyBool,   faultyNum,   faultyTry,+  replicateMNorm,   runAntiGen,   zapAntiGen,   (|!),   (||!),  )-import Test.AntiGen.Internal (countDecisionPoints, evalToPartial)-import Test.Hspec (Spec, describe, hspec, shouldBe, shouldSatisfy)+import Test.AntiGen.Internal (+  ZapResult (..),+  countDecisionPoints,+  evalToPartial,+  prettyZapResult,+  reweigh,+  withAnnotation,+  zapAntiGenResult,+ )+import Test.Hspec (Spec, describe, hspec, it, shouldBe, shouldSatisfy)+import Test.Hspec.Golden (Golden (..)) import Test.Hspec.QuickCheck (prop) import Test.QuickCheck (   Arbitrary (..),@@ -86,6 +103,70 @@         replicateM l $ pure True |! pure False     ] +annotatedPositive :: AntiGen Int+annotatedPositive =+  withAnnotation "must be positive" $+    (getPositive @Int <$> arbitrary) |! (getNonPositive <$> arbitrary)++annotatedTuple :: AntiGen (Int, Int)+annotatedTuple = do+  x <-+    withAnnotation "first positive" $ (getPositive @Int <$> arbitrary) |! (getNonPositive <$> arbitrary)+  y <-+    withAnnotation "second positive" $+      (getPositive @Int <$> arbitrary) |! (getNonPositive <$> arbitrary)+  pure (x, y)++complexAnnotations :: AntiGen (Int, Int)+complexAnnotations =+  withAnnotation "root" $ do+    a <- withAnnotation "a" antiPositive+    b <- withAnnotation "b" antiPositive+    pure (a, b)++-- | Annotated sum type - each branch has its own annotation+annotatedEither :: AntiGen (Either Int Int)+annotatedEither =+  withAnnotation "either" $+    oneof+      [ withAnnotation "left" $ Left <$> antiPositive+      , withAnnotation "right" $ Right <$> antiNegative+      ]++-- | Three levels of nesting+deeplyNested :: AntiGen Int+deeplyNested =+  withAnnotation "level1" $+    withAnnotation "level2" $+      withAnnotation "level3" $+        antiPositive++-- | Mixed annotated and unannotated in sequence+mixedAnnotations :: AntiGen (Int, Int, Int)+mixedAnnotations = do+  a <- withAnnotation "first" antiPositive+  b <- antiPositive -- unannotated+  c <- withAnnotation "third" antiPositive+  pure (a, b, c)++-- | Sibling scopes that don't inherit from each other+siblingScopes :: AntiGen (Int, Int)+siblingScopes = do+  a <- withAnnotation "scopeA" antiPositive+  b <- withAnnotation "scopeB" antiPositive+  pure (a, b)++-- | Annotation wrapping a pure value (no decision points inside)+annotatedPure :: AntiGen Int+annotatedPure = withAnnotation "pure" $ pure 42++-- | Annotation with decision point after the annotated section+annotationThenDecision :: AntiGen (Int, Int)+annotationThenDecision = do+  a <- withAnnotation "annotated" antiPositive+  b <- antiPositive -- should have empty annotation+  pure (a, b)+ noneOf :: [Bool] -> Property noneOf [] = property True noneOf (x : xs) = not x .&&. noneOf xs@@ -196,17 +277,17 @@           res <- zapAntiGen 1 $ faultyTry s           pure $ res =/= s     describe "antiPositive" $ do-      prop "positive" . forAll (runAntiGen $ antiPositive @Int) $ (> 0)-      prop "negative" . forAll (zapAntiGen 1 $ antiPositive @Int) $ (<= 0)+      prop "positive" $ forAll (runAntiGen $ antiPositive @Int) (> 0)+      prop "negative" $ forAll (zapAntiGen 1 $ antiPositive @Int) (<= 0)     describe "antiNegative" $ do-      prop "positive" . forAll (runAntiGen $ antiNegative @Int) $ (< 0)-      prop "negative" . forAll (zapAntiGen 1 $ antiNegative @Int) $ (>= 0)+      prop "positive" $ forAll (runAntiGen $ antiNegative @Int) (< 0)+      prop "negative" $ forAll (zapAntiGen 1 $ antiNegative @Int) (>= 0)     describe "antiNonPositive" $ do-      prop "positive" . forAll (runAntiGen $ antiNonPositive @Int) $ (<= 0)-      prop "negative" . forAll (zapAntiGen 1 $ antiNonPositive @Int) $ (> 0)+      prop "positive" $ forAll (runAntiGen $ antiNonPositive @Int) (<= 0)+      prop "negative" $ forAll (zapAntiGen 1 $ antiNonPositive @Int) (> 0)     describe "antiNonNegative" $ do-      prop "positive" . forAll (runAntiGen $ antiNonNegative @Int) $ (>= 0)-      prop "negative" . forAll (zapAntiGen 1 $ antiNonNegative @Int) $ (< 0)+      prop "positive" $ forAll (runAntiGen $ antiNonNegative @Int) (>= 0)+      prop "negative" $ forAll (zapAntiGen 1 $ antiNonNegative @Int) (< 0)     describe "(||!)" $ do       prop "positive" $ do         res <- runAntiGen $ listOf1 (antiPositive @Int) ||! pure []@@ -220,11 +301,212 @@             [ ("null", null res)             , ("nonpositive", length (filter (<= 0) res) == 1)             ]+    describe "antiChoose" $ do+      prop "positive and negative" $ do+        vals <- sort <$> replicateM 4 (arbitrary @Word8)+        case vals of+          [loB, lo, hi, hiB] -> do+            let g = antiChoose @Word8 (lo, hi) (loB, hiB)+            pos <- runAntiGen g+            neg <- zapAntiGen 1 g+            let+              failMsg =+                unlines+                  [ "loB = " <> show loB+                  , "hiB = " <> show hiB+                  , "lo = " <> show lo+                  , "hi = " <> show hi+                  , "pos = " <> show pos+                  , "neg = " <> show neg+                  ]+              posGood = pos >= lo && pos <= hi+              negGood = neg < lo || (neg >= hi && neg <= hiB)+              trivial = lo == loB && hi == hiB+            pure . counterexample failMsg $+              trivial .||. counterexample "pos failure" posGood .&&. counterexample "neg failure" negGood+          _ -> error "Impossible happened"     describe "chooseBoundedIntegral" $ do       chooseBoundedIntegralTest @Word64       chooseBoundedIntegralTest @Word32       chooseBoundedIntegralTest @Int+    describe "replicateMNorm" $ do+      prop "behaves like replicateM when not zapped" . forAll (choose (0, 1000)) $ \n -> do+        let gen = antiPositive @Int+        fromNorm <- runAntiGen $ replicateMNorm n gen+        fromReplicateM <- runAntiGen $ replicateM n gen+        pure $+          counterexample ("replicateMNorm: " <> show fromNorm) $+            counterexample ("replicateM: " <> show fromReplicateM) $+              length fromNorm === length fromReplicateM+      prop "produces correct length" . forAll (choose (0, 1000)) $ \n -> do+        result <- runAntiGen $ replicateMNorm n (antiPositive @Int)+        pure $ length result === n+      prop "all elements satisfy the generator property when not zapped" $ do+        n <- choose (0, 1000)+        result <- runAntiGen $ replicateMNorm n (antiPositive @Int)+        pure $ all (> 0) result+    describe "reweigh" $ do+      prop "zero weight generator is never zapped" $ do+        let gen = do+              x <- reweigh 0 $ pure "A" |! pure "a"+              y <- pure "B" |! pure "b"+              pure (x, y)+        (x, y) <- zapAntiGen 1 gen+        pure $+          counterexample ("x = " <> x <> ", y = " <> y) $+            x === "A" .&&. y === "b" +withAnnotationSpec :: Spec+withAnnotationSpec =+  describe "withAnnotation" $ do+    prop "behaves like (|!) for runAntiGen (active generator)" $ do+      x <- runAntiGen annotatedPositive+      pure $ x > 0+    prop "behaves like (|!) for zapAntiGen (alternative generator)" $ do+      x <- zapAntiGen 1 annotatedPositive+      pure $ x <= 0+    prop "annotation is captured in ZapResult when zapped" $ do+      result <- zapAntiGenResult 1 annotatedPositive+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          zrAnnotation result === ["must be positive" :| []]+    prop "no annotation in ZapResult when not zapped (n=0)" $ do+      result <- zapAntiGenResult 0 annotatedPositive+      pure $ zrAnnotation result === []+    prop "multiple annotations collected when zapping multiple points" $ do+      result <- zapAntiGenResult 2 annotatedTuple+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          length (zrAnnotation result) === 2+            .&&. ("first positive" :| []) `elem` zrAnnotation result+            .&&. ("second positive" :| []) `elem` zrAnnotation result+    prop "single zap of composed annotated generator gets one annotation" $ do+      result <- zapAntiGenResult 1 annotatedTuple+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          length (zrAnnotation result) === 1+    prop "non-annotated (|!) produces empty annotation list" $ do+      result <- zapAntiGenResult 1 antiGenPositive+      pure $ zrAnnotation result === []+    prop "nested withAnnotation produces hierarchical path" $ do+      let nested =+            withAnnotation "foo" $+              withAnnotation "bar" $+                (getPositive @Int <$> arbitrary) |! (getNonPositive <$> arbitrary)+      result <- zapAntiGenResult 1 nested+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          zrAnnotation result === ["foo" :| ["bar"]]+    prop "withAnnotation on unannotated produces single-element path" $ do+      let annotated = withAnnotation "outer" $ (getPositive @Int <$> arbitrary) |! (getNonPositive <$> arbitrary)+      result <- zapAntiGenResult 1 annotated+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          zrAnnotation result === ["outer" :| []]+    prop "complexAnnotations: zapping both points shows hierarchical paths" $ do+      result <- zapAntiGenResult 2 complexAnnotations+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          length (zrAnnotation result) === 2+            .&&. ("root" :| ["a"]) `elem` zrAnnotation result+            .&&. ("root" :| ["b"]) `elem` zrAnnotation result+    prop "annotatedEither: sum type branches have correct paths" $ do+      result <- zapAntiGenResult 1 annotatedEither+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          (zrAnnotation result === ["either" :| ["left"]])+            .||. (zrAnnotation result === ["either" :| ["right"]])+    prop "deeplyNested: three-level path captured" $ do+      result <- zapAntiGenResult 1 deeplyNested+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          zrAnnotation result === ["level1" :| ["level2", "level3"]]+    prop "mixedAnnotations: only annotated points produce annotations" $ do+      result <- zapAntiGenResult 3 mixedAnnotations+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          -- 3 decision points zapped, but only 2 have annotations+          zrZapped result === 3+            .&&. length (zrAnnotation result) === 2+            .&&. ("first" :| []) `elem` zrAnnotation result+            .&&. ("third" :| []) `elem` zrAnnotation result+    prop "siblingScopes: siblings don't inherit from each other" $ do+      result <- zapAntiGenResult 2 siblingScopes+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          length (zrAnnotation result) === 2+            .&&. ("scopeA" :| []) `elem` zrAnnotation result+            .&&. ("scopeB" :| []) `elem` zrAnnotation result+    prop "annotatedPure: no decision points means no annotations" $ do+      result <- zapAntiGenResult 1 annotatedPure+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          zrAnnotation result === []+            .&&. zrZapped result === 0+    prop "annotationThenDecision: scope ends after annotated section" $ do+      result <- zapAntiGenResult 2 annotationThenDecision+      pure $+        counterexample ("annotations: " <> show (zrAnnotation result)) $+          -- 2 decision points zapped, but only 1 has an annotation+          zrZapped result === 2+            .&&. length (zrAnnotation result) === 1+            .&&. ("annotated" :| []) `elem` zrAnnotation result++-- | Golden test that doesn't create actual files+golden :: String -> String -> IO (Golden String)+golden name actual = do+  dataDir <- getDataDir+  pure $+    Golden+      { output = actual+      , encodePretty = id+      , writeToFile = writeFile+      , readFromFile = readFile+      , goldenFile = dataDir </> ".golden" </> name ++ ".golden"+      , actualFile = Nothing+      , failFirstTime = False+      }++prettyZapResultSpec :: Spec+prettyZapResultSpec =+  describe "prettyZapResult" $ do+    it "no zaps" $+      golden "no_zaps" $+        T.unpack $+          prettyZapResult $+            ZapResult () [] 0+    it "single zap without annotation" $+      golden "single_zap_no_annotation" $+        T.unpack $+          prettyZapResult $+            ZapResult () [] 1+    it "single zap with simple annotation" $+      golden "single_zap_simple" $+        T.unpack $+          prettyZapResult $+            ZapResult () ["positive" :| []] 1+    it "single zap with nested annotation" $+      golden "single_zap_nested" $+        T.unpack $+          prettyZapResult $+            ZapResult () ["root" :| ["child", "leaf"]] 1+    it "multiple zaps with annotations" $+      golden "multiple_zaps" $+        T.unpack $+          prettyZapResult $+            ZapResult+              ()+              [ "user" :| ["name"]+              , "user" :| ["email"]+              , "address" :| ["street"]+              ]+              3+    it "zaps with mixed annotated and unannotated" $+      golden "mixed_annotations" $+        T.unpack $+          prettyZapResult $+            ZapResult () ["annotated" :| []] 3+ main :: IO () main = hspec $ do   describe "AntiGen" $ do@@ -266,3 +548,5 @@           pure $ a : b         pure $ x === [30, 31, 32]     utilsSpec+    withAnnotationSpec+    prettyZapResultSpec