packages feed

antigen 0.2.0.0 → 0.3.0.0

raw patch · 5 files changed

+204/−44 lines, 5 filesdep ~QuickCheckdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: QuickCheck, base

API changes (from Hackage documentation)

- Test.AntiGen: fickleBool :: Bool -> AntiGen Bool
- Test.AntiGen: fickleNum :: (Eq a, Num a, Arbitrary a) => a -> AntiGen a
- Test.AntiGen: fickleTry :: (Eq a, Arbitrary a) => a -> AntiGen a
- Test.AntiGen: fickleTryGen :: Eq a => a -> Gen a -> AntiGen a
+ Test.AntiGen: antiDistinctPair :: (Num a, Arbitrary a, Eq a) => AntiGen (a, a)
+ Test.AntiGen: antiJust :: a -> AntiGen (Maybe a)
+ Test.AntiGen: antiNonEmpty :: AntiGen a -> AntiGen [a]
+ Test.AntiGen: faultyBool :: Bool -> AntiGen Bool
+ Test.AntiGen: faultyNum :: (Eq a, Num a, Arbitrary a) => a -> AntiGen a
+ Test.AntiGen: faultyTry :: (Eq a, Arbitrary a) => a -> AntiGen a
+ Test.AntiGen: faultyTryGen :: Eq a => a -> AntiGen a -> AntiGen a

Files

CHANGELOG.md view
@@ -1,6 +1,11 @@ # Revision history for antigen -## 0.1.3.0+## 0.3.0.0++* Rename `fickle*` to `faulty*`+* Add `antiJust`, `antiNonEmpty`, `antiDistinctPair`++## 0.2.0.0  * Rename `antiNum` and `antiBool` to `fickleNum` and `fickleBool` * Rename `antiTry` and `antiTryGen` to `fickleTry` and `fickleTryGen`
+ README.md view
@@ -0,0 +1,39 @@+# AntiGen++AntiGen lets you write `QuickCheck` generators that can also be negated to generate negative examples. It can be used as a drop-in replacement for `Gen`.++## Example++```haskell+-- Returns an integer `n` (such that 0 <= n <= 5) and a string of length `n` consisting only of characters 'a'+antiGenLengthString :: AntiGen (Int, String)+antiGenLengthString = do+  -- Use (|!) to provide both a positive and a negative generator+  l <- choose (0, 5) |! choose (6, 10)+  s <-+    pure (replicate l 'a') |! do+      NonNegative l' <- suchThat arbitrary $ \(NonNegative x) -> x /= l+      pure $ replicate l' 'b'+  pure (l, s)+```++To generate a positive example, use `runAntiGen`+```+ghci> generate (runAntiGen antiGenLengthString)+(1, "a")+```++To generate a negative example, use `zapAntiGen`+```+ghci> generate (zapAntiGen 1 antiGenLengthString)+(6, "aaaaaa") -- length is too long+ghci> generate (zapAntiGen 1 antiGenLengthString)+(2, "bbbb") -- length of the string does not match up with the integer+```++Notice that there is exactly one mistake in the example above. +The first argument of `zapAntiGen` can be used to specify how many negations the generator should introduce.+```+ghci> generate $ zapAntiGen 2 antiGenLengthString+(10,"b") -- both values are wrong+```
antigen.cabal view
@@ -1,12 +1,14 @@ cabal-version:      3.0 name:               antigen-version:            0.2.0.0-synopsis:           Negatable QuickCheck generators +version:            0.3.0.0+synopsis:           Fault injection for QuickCheck description:-  AntiGen is a library that helps with generating negative examples from a -  QuickCheck generator. The `AntiGen` monad is designed to be similar to the-  `Gen` monad, so that migrating the generators would be as frictionless as -  possible.+  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. license:            MIT license-file:       LICENSE author:             IOG Ledger Team@@ -14,7 +16,8 @@ copyright:          2026 Input Output Global Inc (IOG) category:           Testing build-type:         Simple-extra-doc-files:    CHANGELOG.md+extra-doc-files:    CHANGELOG.md, README.md+tested-with:        GHC == 9.10.3  source-repository head   type: git@@ -45,7 +48,7 @@     hs-source-dirs:   test     main-is:          Main.hs     build-depends:-      base ^>=4.20.2.0,+      base,       antigen,       hspec,       QuickCheck,
src/Test/AntiGen.hs view
@@ -17,16 +17,19 @@   zapAntiGen,    -- * AntiGen combinators-  fickleNum,-  fickleBool,-  fickleTry,-  fickleTryGen,+  faultyNum,+  faultyBool,+  faultyTry,+  faultyTryGen,   antiChoose,   antiChooseBounded,   antiPositive,   antiNonPositive,   antiNegative,   antiNonNegative,+  antiJust,+  antiNonEmpty,+  antiDistinctPair, ) where  import Control.Monad (join)@@ -34,28 +37,26 @@ import Test.AntiGen.Internal import Test.QuickCheck (   Arbitrary (..),-  Gen,   Negative (..),   NonNegative (..),   NonPositive (..),   NonZero (..),   Positive (..),  )-import Test.QuickCheck.GenT (MonadGen (..), frequency, suchThat)+import Test.QuickCheck.GenT (MonadGen (..), elements, frequency, listOf1, suchThat) --- | Returns the provided number. If negated, returns a value that is not equal--- to the provided number.-fickleNum :: (Eq a, Num a, Arbitrary a) => a -> AntiGen a-fickleNum n = pure n |! ((n +) . getNonZero <$> arbitrary)+-- | 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) --- | Returns the provided `Bool`. If negated, returns the negation of that--- `Bool`.-fickleBool :: Bool -> AntiGen Bool-fickleBool b = pure b |! pure (not b)+-- | Returns the provided `Bool`.+-- Negative: returns the negation of that `Bool`.+faultyBool :: Bool -> AntiGen Bool+faultyBool b = pure b |! pure (not b) --- | In the positive case generates a value from the first range. In the--- negative case generates a value from the second range excluding the first--- range.+-- | Generates a value from the first range.+-- Negative: Generates a value from the second range excluding the first range. -- -- Note: The second range must not be a subset of the first range! antiChoose :: (Integral a, Random a) => (a, a) -> (a, a) -> AntiGen a@@ -75,39 +76,76 @@     rngLo = (boundLo, pred lo)     rngHi = (succ hi, boundHi) --- | Generates a value from the range. If negated, returns a random value--- outside the range between `minBound` and `maxBound`.+-- | 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 unless negated, in which case it generates an--- arbitrary value that is different from the provided value. It uses `suchThat`,--- so using it on small types might end up discarding many values.-fickleTry :: (Eq a, Arbitrary a) => a -> AntiGen a-fickleTry a = fickleTryGen a arbitrary+-- | Returns the provided value+-- Negative: Generates an arbitrary value that is different from the provided+-- value.+--+-- Warning: It uses `suchThat`, so using it on small types might end up+-- discarding many values.+faultyTry :: (Eq a, Arbitrary a) => a -> AntiGen a+faultyTry a = faultyTryGen a $ liftGen arbitrary --- | Returns the provided value unless negated, in which case it uses the--- generator to generate a random value that is different from the provided--- value. It uses `suchThat`, so using it on small types might end up +-- | Returns the provided value+-- Negative: Use the generator to generate a random value that is different+-- from the provided value.+--+-- Warning: It uses `suchThat`, so using it on small types might end up -- discarding many values.-fickleTryGen :: Eq a => a -> Gen a -> AntiGen a-fickleTryGen a gen = pure a |! (gen `suchThat` (/= a))+faultyTryGen :: Eq a => a -> AntiGen a -> AntiGen a+faultyTryGen a gen = pure a ||! (gen `suchThat` (/= a)) --- | Negatable generator for positive numbers+-- | Returns a positive number+-- Negative: Returns a non-positive number antiPositive :: (Num a, Ord a, Arbitrary a) => AntiGen a antiPositive = (getPositive <$> arbitrary) |! (getNonPositive <$> arbitrary) --- | Negatable generator for non-positive numbers+-- | Returns a non-positive number+-- Negative: Returns a positive number antiNonPositive :: (Num a, Ord a, Arbitrary a) => AntiGen a antiNonPositive = (getNonPositive <$> arbitrary) |! (getPositive <$> arbitrary) --- | Negatable generator for negative numbers+-- | Returns a negative number+-- Negative: Returns a non-negative number antiNegative :: (Num a, Ord a, Arbitrary a) => AntiGen a antiNegative = (getNegative <$> arbitrary) |! (getNonNegative <$> arbitrary) --- | Negatable generator for non-negative numbers+-- | 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 []++-- | 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))+  )+    |! ( do+           x <- arbitrary+           return (x, x)+       )  -- | Create an `AntiGen` from a positive and a negative `AntiGen` generator (||!) :: AntiGen a -> AntiGen a -> AntiGen a
test/Main.hs view
@@ -5,7 +5,20 @@  import Control.Monad (replicateM) import Data.Data (Proxy (..))-import Test.AntiGen (AntiGen, runAntiGen, zapAntiGen, (|!))+import Test.AntiGen (+  AntiGen,+  antiNegative,+  antiNonNegative,+  antiNonPositive,+  antiPositive,+  faultyBool,+  faultyNum,+  faultyTry,+  runAntiGen,+  zapAntiGen,+  (|!),+  (||!),+ ) import Test.AntiGen.Internal (countDecisionPoints, evalToPartial) import Test.Hspec (Spec, describe, hspec, shouldBe) import Test.Hspec.QuickCheck (prop)@@ -28,9 +41,10 @@   vector,   (.&&.),   (.||.),+  (=/=),   (===),  )-import Test.QuickCheck.GenT (MonadGen (..), oneof)+import Test.QuickCheck.GenT (MonadGen (..), listOf1, oneof)  antiGenPositive :: AntiGen Int antiGenPositive = (getPositive @Int <$> arbitrary) |! (getNonPositive <$> arbitrary)@@ -145,6 +159,57 @@             )           ] +utilsSpec :: Spec+utilsSpec =+  describe "utils" $ do+    describe "faultyNum" $ do+      prop "positive" $ \(n :: Int) -> do+        res <- runAntiGen $ faultyNum n+        pure $ res === n+      prop "negative" $ \(n :: Int) -> do+        res <- zapAntiGen 1 $ faultyNum n+        pure $ res =/= n+    describe "faultyBool" $ do+      prop "positive" $ \b -> do+        res <- runAntiGen $ faultyBool b+        pure $ res === b+      prop "negative" $ \b -> do+        res <- zapAntiGen 1 $ faultyBool b+        pure $ res =/= b+    describe "faultyTry" $ do+      describe "String" $ do+        prop "positive" $ \(s :: String) -> do+          res <- runAntiGen $ faultyTry s+          pure $ res === s+        prop "negative" $ \(s :: String) -> do+          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)+    describe "antiNegative" $ do+      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)+    describe "antiNonNegative" $ do+      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 []+        pure $+          counterexample "is empty" (not $ null res)+            .&&. counterexample "non-positive" (null $ filter (<= 0) res)+      prop "negative" $ do+        res <- zapAntiGen 1 $ listOf1 (antiPositive @Int) ||! pure []+        pure $+          exactlyOne+            [ ("null", null res)+            , ("nonpositive", length (filter (<= 0) res) == 1)+            ]+ main :: IO () main = hspec $ do   describe "AntiGen" $ do@@ -176,3 +241,13 @@         val :: [Bool] <- zapAntiGen 1 . resize sz . sized $ \s ->           liftGen (vector $ 2 * s) |! liftGen (vector s)         pure $ length val === sz+      prop "nested `resize` works correctly" $ do+        x <- resize 30 $ do+          a <- getSize+          b <- scale (+ 1) $ do+            c <- getSize+            d <- scale (+ 1) getSize+            pure [c, d]+          pure $ a : b+        pure $ x === [30, 31, 32]+    utilsSpec