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 +6/−1
- README.md +39/−0
- antigen.cabal +11/−8
- src/Test/AntiGen.hs +71/−33
- test/Main.hs +77/−2
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