packages feed

antigen-0.4.0.0: test/Main.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Main (main) where

import Control.Monad (replicateM)
import Data.Data (Proxy (..))
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,
  antiNonPositive,
  antiPositive,
  faultyBool,
  faultyNum,
  faultyTry,
  replicateMNorm,
  runAntiGen,
  zapAntiGen,
  (|!),
  (||!),
 )
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 (..),
  CoArbitrary,
  Gen,
  NonNegative (..),
  NonPositive (..),
  Positive (..),
  Property,
  Testable (..),
  counterexample,
  forAll,
  forAllBlind,
  getSize,
  label,
  scale,
  suchThat,
  vector,
  (.&&.),
  (.||.),
  (=/=),
  (===),
 )
import Test.QuickCheck.GenT (MonadGen (..), listOf1, oneof)
import Type.Reflection (Typeable, typeRep)

antiGenPositive :: AntiGen Int
antiGenPositive = (getPositive @Int <$> arbitrary) |! (getNonPositive <$> arbitrary)

antiGenTuple :: AntiGen (Int, Int)
antiGenTuple = do
  x <- antiGenPositive
  y <- antiGenPositive
  pure (x, y)

antiGenSmall :: AntiGen Int
antiGenSmall = choose (0, 5) |! choose (6, 10)

antiGenLengthStringStatic :: AntiGen (Int, String)
antiGenLengthStringStatic = do
  l <- antiGenSmall
  pure (l, replicate l 'a')

antiGenLengthString :: AntiGen (Int, String)
antiGenLengthString = do
  l <- antiGenSmall
  s <-
    pure (replicate l 'a') |! do
      NonNegative l' <- suchThat arbitrary $ \(NonNegative x) -> x /= l
      pure $ replicate l' 'b'
  pure (l, s)

antiGenEither :: AntiGen (Either Int [Bool])
antiGenEither = do
  oneof
    [ Left <$> antiGenPositive
    , Right <$> do
        l <- antiGenSmall
        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

exactlyOne :: [(String, Bool)] -> Property
exactlyOne [] = counterexample "None of the conditions hold" $ property False
exactlyOne ((lbl, p) : ps) = label lbl (p .&&. noneOf (snd <$> ps)) .||. (not p .&&. exactlyOne ps)

someGen :: (Arbitrary a, CoArbitrary a) => Proxy a -> Gen (Gen a)
someGen p =
  oneof
    [ pure <$> arbitrary
    , do
        x <- scale (`div` 2) $ someGen p
        f <- arbitrary
        pure $ f <$> x
    , do
        x <- scale (`div` 4) $ someGen p
        y <- scale (`div` 4) $ someGen p
        f <- arbitrary
        pure $ f <$> x <*> y
    ]

zapAntiGenSpec :: Spec
zapAntiGenSpec =
  describe "zapAntiGen" $ do
    prop "zapping `antiGenPositive` once generates negative examples" $ do
      x <- zapAntiGen 1 antiGenPositive
      pure $ x <= 0
    prop "zapping `antiGenPositive` zero times generates a positive example" $ do
      x <- zapAntiGen 0 antiGenPositive
      pure $ x > 0
    prop "zapping `antiGenTuple` once results in a single non-positive Int" $ do
      (x, y) <- zapAntiGen 1 antiGenTuple
      pure $
        label "x is non-positive" (x <= 0) .||. label "y is non-positive" (y <= 0)
    prop "zapping `antiGenTuple` twice results in two non-positive Ints" $ do
      (x, y) <- zapAntiGen 2 antiGenTuple
      pure $
        counterexample ("x = " <> show x <> " is positive") (x <= 0)
          .&&. counterexample ("y = " <> show y <> " is positive") (y <= 0)
    prop
      "zapping the length of the string propagates to the string generator"
      . forAll (zapAntiGen 1 antiGenLengthStringStatic)
      $ \(l, s) -> length s === l
    prop
      "zapping `antiGenLengthString` either generates invalid Int or a string of invalid length"
      . forAll (zapAntiGen 1 antiGenLengthString)
      $ \(l, s) ->
        exactlyOne
          [ ("l > 5", l > 5)
          , ("length s /= l", length s /= l)
          ]
    prop
      "zapping `antiGenEither` once gives a nice distribution"
      . forAll (zapAntiGen 1 antiGenEither)
      $ \x ->
        exactlyOne
          [
            ( "Left v <= 0"
            , case x of
                Right _ -> False
                Left v -> v <= 0
            )
          ,
            ( "Right length (filter not v) == 1"
            , case x of
                Left _ -> False
                Right v -> length (filter not v) == 1
            )
          ,
            ( "Right length > 5"
            , case x of
                Left _ -> False
                Right v -> length v > 5
            )
          ]

chooseBoundedIntegralTest :: forall a. Typeable a => Spec
chooseBoundedIntegralTest =
  prop (show (typeRep @a) <> " (0, n)") $ \(Positive (n :: Word64)) -> do
    res <- zapAntiGen 1 (antiChooseBounded (0, n))
    pure $ res `shouldSatisfy` (> n)

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)
            ]
    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
    describe "treeDepth" $ do
      prop "pure has depth of zero" $ do
        pt <- evalToPartial $ pure ()
        pure $ countDecisionPoints pt `shouldBe` 0
      prop "single bind has depth of one, right identity holds" $ do
        let
          m = return =<< antiGenPositive
        pt <- evalToPartial m
        pt' <- evalToPartial antiGenPositive
        pure $ countDecisionPoints pt === countDecisionPoints pt' .&&. countDecisionPoints pt === 1
    zapAntiGenSpec
    describe "runAntiGen" $ do
      prop "runAntiGen . liftGen == id" $
        \(seed :: Int) -> forAllBlind (someGen $ Proxy @Int) $ \g -> do
          let g' = runAntiGen (liftGen g)
          res <- variant seed g
          res' <- variant seed g'
          pure $ res === res'
    describe "MonadGen" $ do
      prop "applying `sized` to a negatable generator preserves negation" $ do
        size <- getSize
        val <- zapAntiGen 1 . sized $ \sz -> pure sz |! pure (-sz)
        pure $ val === -size
      prop "`resize` has an effect when zapping" $ do
        sz <- choose (0, 10)
        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
    withAnnotationSpec
    prettyZapResultSpec