packages feed

hegel-0.1.0: test/Hegel/IntegrationSpec.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}

module Hegel.IntegrationSpec (spec) where

import Control.Exception (SomeException, displayException, try)
import Control.Monad (when)
import Data.List (isInfixOf)
import Test.Hspec

import Hegel

captureFailureText :: IO () -> IO String
captureFailureText action = do
  result <- try action :: IO (Either SomeException ())
  case result of
    Left e -> pure (displayException e)
    Right () -> expectationFailure "expected property test to fail" >> pure ""

withTestCases :: Int -> Settings -> Settings
withTestCases n stgs = stgs { settingsTestCases = n }

spec :: Spec
spec = do
  describe "E2E: simple passing test" $ do
    it "integers are self-equal" $ do
      runHegelTest (withTestCases 10) $ \tc -> do
        n <- draw tc (integers def)
        when (n /= n) $
          error "integer not equal to itself"

  describe "E2E: simple failing test" $ do
    it "catches HegelTestFailure" $ do
      let action = runHegelTest (withTestCases 10) $ \tc -> do
            _ <- draw tc (integers def { rangeMin = Just 0, rangeMax = Just 100 })
            error "deliberate failure"
      action `shouldThrow` anyException

    it "reports the source location for Hegel.assert failures" $ do
      let expectedFile = __FILE__
      let expectedLine = show ((__LINE__ + 3) :: Int)
      msg <- captureFailureText $
        runHegelTest (withTestCases 10) $ \_tc ->
          assert False "deliberate failure with location"
      msg `shouldSatisfy` isInfixOf expectedFile
      msg `shouldSatisfy` isInfixOf (":" ++ expectedLine ++ ":")

    it "reports only the smallest failing example for a stable origin" $ do
      msg <- captureFailureText $
        runHegelTest
          ( \stgs ->
              stgs
                { settingsTestCases = 200
                , settingsDerandomize = True
                , settingsDatabase = Disabled
                }
          )
          $ \tc -> do
            xs <-
              draw
                tc
                ( lists
                    (integers def { rangeMin = Just 0, rangeMax = Just 0 })
                    def
                      { rangeMin = Just 0
                      , rangeMax = Just 12
                      }
                )
            assert (length xs < 10) ("expected list shorter than 10, got xs = " ++ show xs)
      msg `shouldSatisfy` isInfixOf "expected list shorter than 10, got xs = [0,0,0,0,0,0,0,0,0,0]"
      msg `shouldSatisfy` not . isInfixOf "got xs = [0,0,0,0,0,0,0,0,0,0,0]"

  describe "E2E: assume" $ do
    it "assume True does not reject" $ do
      runHegelTest (withTestCases 10) $ \tc -> do
        n <- draw tc (integers def)
        assume tc True
        when (n /= n) $
          error "unreachable"

    it "assume False rejects the test case" $ do
      -- All test cases are rejected, but the test should still complete
      -- (it just won't find any valid cases, which is fine for low counts)
      runHegelTest (withTestCases 5) $ \tc -> do
        n <- draw tc (integers def { rangeMin = Just 0, rangeMax = Just 100 })
        assume tc (n > 50)
        -- If we get here, n > 50 holds
        pure ()

  describe "E2E: draw multiple types" $ do
    it "draws booleans, integers, floats, text, and binary" $ do
      runHegelTest (withTestCases 10) $ \tc -> do
        _ <- draw tc booleans
        _ <- draw tc (integers def { rangeMin = Just 0, rangeMax = Just 100 })
        _ <-
          draw
            tc
            ( floats def
                { floatBounds =
                    RangeOpts
                      { rangeMin = Just 0.0,
                        rangeMax = Just 1.0
                      }
                }
            )
        _ <- draw tc (text def { rangeMax = Just 50 })
        _ <- draw tc (binary def { rangeMax = Just 50 })
        pure ()

  describe "E2E: note and target" $ do
    it "note records a message without failure" $ do
      runHegelTest (withTestCases 5) $ \tc -> do
        n <- draw tc (integers def)
        note tc ("Generated value: " ++ show n)
        pure ()

    it "target guides the search" $ do
      runHegelTest (withTestCases 10) $ \tc -> do
        n <- draw tc (integers def { rangeMin = Just 0, rangeMax = Just 1000 })
        target tc (fromIntegral n) "maximize n"
        pure ()