packages feed

property-matchers-0.7.0.0: test/PropertyMatchersSpec.hs

{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleContexts #-}
module PropertyMatchersSpec(spec) where

import Control.Applicative
import Control.Exception
import Control.Lens
import GHC.Generics (Generic)

import qualified Prettyprinter as PP
import qualified Prettyprinter.Render.Text as PP

import qualified PropertyMatchers as P
import PropertyMatchers (pattern (:=>), (?))

import Test.Hspec
import Test.SmallCheck hiding (over)
import Test.SmallCheck.Drivers
import qualified Test.SmallCheck.Series as SC
import Debug.RecoverRTTI (anythingToString)
import System.IO.Unsafe (unsafePerformIO)
import Data.Text (Text)
import qualified Data.Text.Lazy as TL
import qualified Prettyprinter.Render.String as PP

data AndOrTree = And AndOrTree AndOrTree | Or AndOrTree AndOrTree | Fail String | Error String | Succeed
  deriving stock (Generic, Show)
instance Monad m => SC.Serial m AndOrTree where
andsAndOrs (And x y) = P.and (andsAndOrs x) (andsAndOrs y)
andsAndOrs (Or x y) = P.or (andsAndOrs x) (andsAndOrs y)
andsAndOrs (Fail str) = P.fail (PP.pretty str) ()
andsAndOrs (Error str) = error str
andsAndOrs Succeed = P.succeed ()

propertyFails :: P.Prop Text -> P.Prop String -> P.Prop (IO a)
propertyFails expectedProp actualProp =
  P.throws @P.PropertyException $ \(P.PropertyFailed _ (PP.renderStrict . PP.layoutCompact -> expected) (anythingToString -> actual)) ->
    expectedProp expected `P.and` actualProp actual

check :: Testable IO a => Depth -> a -> IO ()
check d e = smallCheckM d e >>= \case
  Nothing -> return ()
  Just failure -> error (show failure)

pattern PropertyFailed :: Text -> String -> P.PropertyException
pattern PropertyFailed msg actual <- P.PropertyFailed _ (PP.renderStrict . PP.layoutCompact -> msg) (either (PP.renderString . PP.layoutCompact) anythingToString -> actual)
_PropertyFailed :: Fold P.PropertyException (Text, String)
_PropertyFailed = to
  (\case
    PropertyFailed msg actual -> Just (msg, actual)
    _ -> Nothing) . folded

spec :: Spec
spec = do
  it "succeed" $ do
    P.succeed ()
    P.succeed 1
    P.succeed (\_ -> 1)
    P.succeed (error "not thrown")

  it "fail" $ do
    try (P.fail "test string" 42) >>= \case
      Left
        (PropertyFailed "test string" "42") -> return ()
      _ -> error "expected correct PropertyFailed"

  describe "and/or" $ do
    let runWithMsg t = over _Left displayException <$> try @SomeException (andsAndOrs t)
    let runWithoutMsg t = over _Left (\_ -> ()) <$> try @SomeException (andsAndOrs t)
    it "and idempotency" $ do
      check 3 $ forAll $ \x -> unsafePerformIO $
        (==) <$> runWithMsg x <*> runWithMsg (And x x)
    it "or idempotency" $ do
      check 3 $ forAll $ \x -> unsafePerformIO $
        (==) <$> runWithMsg x <*> runWithMsg (Or x x)
    it "or associativity" $ do
      check 2 $ forAll $ \x y z ->
        unsafePerformIO $ (==) <$> runWithMsg (Or x (Or y z)) <*> runWithMsg (Or (Or x y) z)
    it "and associativity" $ do
      check 2 $ forAll $ \x y z ->
        unsafePerformIO $ (==) <$> runWithMsg (And x (And y z)) <*> runWithMsg (And (And x y) z)
    it "or commutativity" $ do
      check 2 $ forAll $ \x y -> unsafePerformIO $
        (==) <$> runWithoutMsg (Or x y) <*> runWithoutMsg (Or y x)
    it "and commutativity" $ do
      check 2 $ forAll $ \x y -> unsafePerformIO $
        (==) <$> runWithoutMsg (And x y) <*> runWithoutMsg (And y x)
    it "distributivity" $ do
      check 2 $ forAll $ \x y z -> unsafePerformIO $
        (==) <$> runWithMsg (And x (Or y z)) <*> runWithMsg (Or (And x y) (And x z))

  it "fails" $ do
    P.fail "test string" 42
      & P.throws @SomeException P.succeed
    P.fail "test string" 42
      & P.throws @P.PropertyException P.succeed

  it "equals" $ do
    1 & P.equals 1
    try (1 & P.equals 2) >>= \case
      Left (PropertyFailed "2" "1") -> return ()
      r -> error $ "unexpected: " <> show r

  it "match" $ do
    Left 1 & P.match _Left ? P.equals 1
    try (Left 1 & P.match _Right ? P.equals 1) >>= \case
      Left (PropertyFailed "fold to match" "Left 1") -> return ()
      r -> error $ "unexpected: " <> show r

  describe "branch" $ do
    it "singleton" $ do
      Left 2 & P.branch
        [ P.match _Left :=> P.equals 2
        ]
      try @SomeException
        (Right 2 &
          P.branch
            [ P.match _Left :=> P.equals 2 ]
        ) >>=
          P.match _Left P.succeed
    it "commits" $ do
      try @SomeException
        (Left 1 &
          P.branch
            [ P.match _Left :=> P.equals 2
            , const P.succeed :=> P.succeed
            ]
        ) >>= P.match _Left P.succeed
    it "disjunction" $ do
      Left 1 &
        P.branch
          [ P.match _Right :=> P.equals 2
          , P.match _Left :=> P.succeed
          ]

  describe "allOf1" $ do
    it "singleton" $ do
      Left 2 & P.allOf1 _Left ? P.equals 2
    it "empty" $ propertyFails (P.equals "non-empty for fold") P.succeed $
      Left 2 & P.allOf1 _Right ? P.equals 2