changeset-0.2.1: test/Main.hs
{-# OPTIONS_GHC -Wno-orphans #-}
module Main (main) where
-- base
import Control.Monad (replicateM_)
import Data.Char (toUpper)
import Data.Function ((&))
import Data.List.NonEmpty (NonEmpty (..))
import Data.Monoid (Last (Last), Product (..), Sum (..))
import Data.Proxy (Proxy (..))
import GHC.Generics (Generic)
import Prelude hiding (Foldable (..))
-- transformers
import Control.Monad.Trans.Reader (ReaderT (..), ask)
-- tasty
import Test.Tasty
-- tasty-hunit
import Test.Tasty.HUnit (testCase, (@?=))
-- falsify
import Test.Falsify.GenDefault (GenDefault (..), ViaGeneric (..))
import Test.Falsify.GenDefault.Std (Std)
import Test.Falsify.Generator (Gen)
import qualified Test.Falsify.Generator as Gen
import Test.Falsify.Predicate ((.$))
import qualified Test.Falsify.Predicate as P
import Test.Falsify.Range (between)
import Test.Tasty.Falsify (assert, gen, testProperty)
-- containers
import qualified Data.Map as M
-- changeset
import Control.Monad.Changeset.Class
import Control.Monad.Trans.Changeset
import Data.Monoid.RightAction (RightAction (..), RightTorsor (..), rEndo, set)
import Data.Monoid.RightAction.Coproduct (inL, inR, (:+:))
import Data.Monoid.RightAction.Generic (actRightGGeneric, actRightGeneric, differenceRightGGeneric, differenceRightGenericChanges)
type M = Changeset Int (Changes Count)
type MT = Changeset Torsor (Changes TorsorChange)
main :: IO ()
main =
defaultMain $
testGroup
"changeset"
[ testGroup
"Changeset"
[ testGroup
"commutative monoids"
[ testGroup
"Order of change and current matters"
[ testCase "change, current" $
evalChangeset (changeSingle Increment >> current) 0 @?= (1 :: Int)
, testCase "current, change" $
let action = flip evalChangeset 0 $ do
n <- current
changeSingle Increment
pure n
in action @?= (0 :: Int)
]
, testGroup
"execChangeset"
[ testCase "pure doesn't change state" $ execChangeset (pure () :: M ()) 0 @?= 0
, testCase "change changes state" $ execChangeset (changeSingle Increment :: M ()) 0 @?= 1
]
]
, testGroup
"noncommutative monoids"
[ testGroup
"Changes"
[ testCase "change is monoid homomorphism" $ do
execChangeset (changeSingle (Cons True) >> changeSingle (Cons False)) [] @?= execChangeset (change (singleChange (Cons True) <> singleChange (Cons False))) ([] :: [Bool])
execChangeset (changeSingle (Cons True) >> changeSingle (Cons False)) [] @?= execChangeset (change (addChange (Cons False) (singleChange (Cons True)))) ([] :: [Bool])
execChangeset (changeSingle (Cons True) >> changeSingle (Cons False)) [] @?= execChangeset (change (changes [Cons True, Cons False])) ([] :: [Bool])
execChangeset (changeSingle (Cons True) >> changeSingle (Cons False)) [] @?= ([False, True] :: [Bool])
, testCase "execChangeset is monoid homomorphism" $
execChangeset (changeSingle (Cons True) >> changeSingle (Cons False)) [] @?= (([] :: [Bool]) & execChangeset (changeSingle (Cons True)) & execChangeset (changeSingle (Cons False)))
]
]
]
, testGroup
"RightAction"
[ testCase "Changes is lawful monoid action" $ do
[] `actRight` singleChange (Cons True) `actRight` singleChange (Cons False) @?= ([] :: [Bool]) `actRight` singleChange (Cons True) <> singleChange (Cons False)
, testGroup "Last" $
rightActionMonoidLaws @Int @(Last Int) genInt genLastInt
, -- RightTorsor (Last s) s does not satisfy differenceRight . actRight = id:
-- when the action sets the value to the same state, differenceRight returns mempty.
testGroup "Sum" $
rightActionMonoidLaws genSumInt genSumInt
, testGroup "Product" $
rightActionMonoidLaws (genProduct genNonZeroRational) (genProduct genNonZeroRational)
, testGroup "()" $
rightActionMonoidLaws @Int @() genInt (pure ())
, -- RightTorsor () s cannot satisfy actRight sOrig (differenceRight sOrig sActed) = sActed
-- when sOrig /= sActed, since () carries no information.
testGroup "Maybe" $
rightActionMonoidLaws @Int @(Maybe (Last Int)) genInt (genMaybe genLastInt)
, testGroup "Tuple" $
rightActionMonoidLaws @(Int, Int) @(Last Int, Last Int)
((,) <$> genInt <*> genInt)
((,) <$> genLastInt <*> genLastInt)
]
, testGroup
"RightTorsor"
[ testGroup "Sum" $
allLaws genSumInt genSumInt
, testGroup "Product" $
allLaws (genProduct genNonZeroRational) (genProduct genNonZeroRational)
]
, testGroup
"MonadChangeset"
[ testCase "ReaderT lifts changeset operations" $
let action = flip execChangeset (0 :: Int) $ flip runReaderT (100 :: Int) $ do
env <- ask
replicateM_ env $ changeSingle Increment
in action @?= 100
]
, testGroup "SetTo" $
rightActionSemigroupLaw @Int @(SetTo Int) genInt genSetTo
: rightTorsorLaws genInt genSetTo
, testGroup
"Changes"
[ testGroup "Count" $
rightActionMonoidLaws @Int @(Changes Count) genInt (genChanges genCount)
, testGroup "ListChange" $
rightActionMonoidLaws @[Int] @(Changes (ListChange Int))
(genSmallList genInt)
(genChanges genListChangeInt)
]
, testGroup
"MaybeChange"
[ testGroup "RightAction" $
rightActionMonoidLaws @(Maybe Int) @(MaybeChange Int)
genMaybeInt
(genMaybeChange genInt)
-- RightTorsor (MaybeChange a) (Maybe a) does not satisfy differenceRight . actRight = id:
-- when the action sets the value to the same state, differenceRight returns mempty.
]
, testGroup
"FmapChange"
[ testGroup "RightAction" $
rightActionMonoidLaws @(Maybe Int) @(FmapChange Maybe (Last Int))
genMaybeInt
(genFmapChange genLastInt)
]
, testGroup
"diff and update"
[ testProperty "diff yields differenceRight from current" $ do
s0 <- gen genTorsor
s1 <- gen genTorsor
assert $
P.eq
.$ ("snd (getChangeset (diff s1) s0)", snd (getChangeset (diff s1 :: MT (Changes TorsorChange)) s0))
.$ ("differenceRight s0 s1", differenceRight s0 s1)
, testProperty "update sets the state" $ do
s0 <- gen genTorsor
s1 <- gen genTorsor
assert $
P.eq
.$ ("execChangeset (update s1) s0", execChangeset (update s1 :: MT ()) s0)
.$ ("s1", s1)
, testProperty "change after update to same state is mempty" $ do
s <- gen genTorsor
assert $
P.expect mempty
.$ ("getChange (update s) s", getChange (update s :: MT ()) s)
]
, testGroup
"Coproduct"
[ testCase ":+: is monoid morphism" $
(0 :: Int) `actRight` (inL (Last (Just 1)) <> inL (Last (Just 2)) :: Last Int :+: Last Int) @?= 0 `actRight` (inL (Last (Just (1 :: Int)) <> Last (Just 2)) :: Last Int :+: Last Int)
, testGroup "RightAction" $
rightActionMonoidLaws @Int @(Last Int :+: Last Int)
genInt
(Gen.choose (inL <$> genLastInt) (inR <$> genLastInt))
]
, testGroup
"FilterableChange"
[ testCase "Can change a map with FilterableChange" $
M.fromList [(0 :: Int, "hello"), (1, "world")] `actRight` FilterableChange (justChange $ ImapChange (\i -> if i == 2 then rEndo toUpper else mempty))
@?= M.fromList [(0 :: Int, "heLlo"), (1, "woRld")]
]
, testGroup
"FilterableChanges"
[ testCase "Can change a map depending on content" $
M.fromList [(0 :: Int, "hello"), (1, "world"), (2, "!")]
`actRight` FilterableChanges
( \case
"hello" -> FilterablePositionChange (set "hi")
"!" -> FilterablePositionDelete
_ -> mempty
)
@?= M.fromList [(0 :: Int, "hi"), (1, "world")]
]
, testGroup
"AlignChanges"
[ testCase "Can change a map with another map" $
M.fromList
[ (0 :: Int, "hello")
, (1, "world")
, (2, "!")
]
`actRight` AlignChanges
( M.fromList
[ (0 :: Int, SetAlignPosition "hi")
, (1, ChangeAlignPosition $ FmapChange @[] $ rEndo toUpper)
, (2, DeleteAlignPosition)
, (3, SetAlignPosition "...")
]
)
@?= M.fromList [(0 :: Int, "hi"), (1, "WORLD"), (3, "...")]
, testGroup "RightAction" $
rightActionMonoidLaws @(M.Map Int (Sum Int)) @(AlignChanges (M.Map Int) (Sum Int) (Sum Int))
genMapIntInt
(genAlignChangesMap (genDefault (Proxy @Std)))
, testGroup
"RightTorsor"
[ -- The differenceRight . actRight law does not hold for AlignChanges:
-- ChangeAlignPosition w on an unchanged entry round-trips to ChangeAlignPosition mempty ≠ w.
-- Only actRight . differenceRight (law 2) holds.
testProperty "actRight . differenceRight" $ do
sOrig <- gen genMapIntInt
sActed <- gen genMapIntInt
assert $
P.eq
.$ ("sOrig `actRight` differenceRight sOrig sActed", sOrig `actRight` differenceRight @(AlignChanges (M.Map Int) (Sum Int) (Sum Int)) sOrig sActed)
.$ ("sActed", sActed)
]
]
, testGroup
"Generic"
[ testGroup
"actRightGeneric"
[ testCase "single change left" $ 0 `actRight` IntActionCounts Increment mempty @?= (1 :: Int)
, testCase "single change product" $ 0 `actRight` IntActionCounts Increment (singleChange Increment) @?= (2 :: Int)
, testCase "single change right" $ 0 `actRight` IntActionLast (set 100) @?= (100 :: Int)
, testCase "single change recursive" $ 0 `actRight` IntActionRec (IntActionLast (set 100)) (IntActionCounts Increment mempty) @?= (101 :: Int)
, testCase "changes" $
(0 :: Int)
`actRight` changes
[ IntActionCounts Increment mempty
, IntActionLast (set 10)
, IntActionCounts Increment (changes [Increment, Increment])
, IntActionRec (IntActionRec (IntActionCounts Increment mempty) (IntActionCounts Increment mempty)) (IntActionCounts Increment (changes [Increment, Increment]))
]
@?= 18
]
, testGroup
"actRightGGeneric"
[ testCase "Wrapper changes wrapper" $ Bar 0 `actRight` BarChange Increment @?= Bar 1
, testCase "Sum changes product" $ Foo 0 0 [] `actRight` changes [FooChangeInt Increment, FooChangeList (Cons ()), FooChangeInt2 Increment] @?= Foo 1 1 [()]
, testCase "Product changes product" $ Foo 0 0 [] `actRight` FooChange2 Increment Nothing (Cons ()) @?= Foo 1 0 [()]
]
, testGroup
"differenceRightGGeneric"
[ testGroup "Wrapper" $ torsorTestSuite (TorsorWrapper 9) (TorsorWrapper 1) (TorsorWrapper 10)
, testGroup
"Sum"
[ testGroup "All fields change" $ torsorTestSuite (changes [TorsorChangeSum 5, TorsorChangeProduct 6, TorsorChangeSum2 7]) (Torsor 2 3 4) (Torsor 7 18 11)
, testGroup "One field changes" $ torsorTestSuite (singleChange (TorsorChangeProduct 6)) (Torsor 2 3 4) (Torsor 2 18 4)
, testGroup "No fields change" $ torsorTestSuite (mempty :: Changes TorsorChange) (Torsor 2 3 4) (Torsor 2 3 4)
, testGroup "Property laws" $
changesLaws @Torsor @TorsorChange
(flip actRight)
differenceRight
genTorsor
genTorsorChange
]
, testGroup
"Product"
( torsorTestSuite (TorsorChange2 5 6 7) (Torsor 2 3 4) (Torsor 7 18 11)
<> rightTorsorLaws @Torsor @TorsorChange2
genTorsor
genTorsorChange2
)
, testGroup
"Newtype"
( torsorTestSuite (TorsorWrapper 9) (TorsorWrapper 1) (TorsorWrapper 10)
<> rightTorsorLaws @TorsorWrapper @TorsorWrapper
genTorsorWrapper
genTorsorWrapper
)
]
]
]
data IntAction
= IntActionCounts Count (Changes Count)
| IntActionLast (Last Int)
| IntActionRec IntAction IntAction
deriving stock (Generic)
instance RightAction IntAction Int where
actRight = actRightGeneric
newtype Bar = Bar Int
deriving stock (Generic, Eq, Show)
newtype BarChange = BarChange Count
deriving stock (Generic)
instance RightAction BarChange Bar where
actRight = actRightGGeneric
data Foo = Foo Int Int [()]
deriving stock (Generic, Eq, Show)
data FooChange = FooChangeInt Count | FooChangeInt2 Count | FooChangeList (ListChange ())
deriving stock (Generic)
instance RightAction FooChange Foo where
actRight = actRightGGeneric
data FooChange2 = FooChange2 Count (Maybe Count) (ListChange ())
deriving stock (Generic)
instance RightAction FooChange2 Foo where
actRight = actRightGGeneric
newtype TorsorWrapper = TorsorWrapper (Sum Integer)
deriving stock (Generic, Eq, Show)
instance RightAction TorsorWrapper TorsorWrapper where
actRight = actRightGGeneric
instance RightTorsor TorsorWrapper TorsorWrapper where
differenceRight = differenceRightGGeneric
data Torsor = Torsor (Sum Integer) (Product Rational) (Sum Rational)
deriving stock (Generic, Eq, Show)
data TorsorChange = TorsorChangeSum (Sum Integer) | TorsorChangeProduct (Product Rational) | TorsorChangeSum2 (Sum Rational)
deriving stock (Generic, Eq, Show)
instance RightAction TorsorChange Torsor where
actRight = actRightGGeneric
instance RightTorsor (Changes TorsorChange) Torsor where
differenceRight = differenceRightGenericChanges
data TorsorChange2 = TorsorChange2 (Sum Integer) (Product Rational) (Sum Rational)
deriving stock (Generic, Eq, Show)
instance RightAction TorsorChange2 Torsor where
actRight = actRightGGeneric
instance RightTorsor TorsorChange2 Torsor where
differenceRight = differenceRightGGeneric
-- * GenDefault instances via ViaGeneric Std
deriving via ViaGeneric Std Count instance GenDefault Std Count
deriving via ViaGeneric Std (Sum Int) instance GenDefault Std (Sum Int)
deriving via ViaGeneric Std (Last Int) instance GenDefault Std (Last Int)
deriving via ViaGeneric Std (SetTo Int) instance GenDefault Std (SetTo Int)
deriving via ViaGeneric Std (ListChange Int) instance GenDefault Std (ListChange Int)
deriving via ViaGeneric Std (AlignPositionChange (Sum Int) (Sum Int)) instance GenDefault Std (AlignPositionChange (Sum Int) (Sum Int))
torsorTestSuite :: forall w s. (RightAction w s, RightTorsor w s, Eq w, Show w, Eq s, Show s) => w -> s -> s -> [TestTree]
torsorTestSuite w sOrig sActed =
[ testCase "act" $ sOrig `actRight` w @?= sActed
, testCase "difference" $ sOrig `differenceRight` sActed @?= w
, testCase "law 1" $ sOrig `differenceRight` (sOrig `actRight` w :: s) @?= w
, testCase "law 2" $ sOrig `actRight` (sOrig `differenceRight` sActed :: w) @?= sActed
]
-- * Reusable law tests
-- | Tests for 'RightTorsor' laws that don't require 'Monoid' on the change type.
rightTorsorLaws :: forall s w. (Eq s, Show s, Eq w, Show w, RightAction w s, RightTorsor w s) => Gen s -> Gen w -> [TestTree]
rightTorsorLaws genS genW =
[ rightTorsorLaw1 genS genW
, rightTorsorLaw2 genS genW
]
rightTorsorLaw1 :: forall s w. (Eq s, Show s, Eq w, Show w, RightAction w s, RightTorsor w s) => Gen s -> Gen w -> TestTree
rightTorsorLaw1 genS genW = testProperty "differenceRight . actRight" $ do
s <- gen genS
w <- gen genW
assert $
P.eq
.$ ("differenceRight s (s `actRight` w)", differenceRight s (s `actRight` w))
.$ ("w", w)
rightTorsorLaw2 :: forall s w proxy. (Eq s, Show s, RightAction w s, RightTorsor w s) => Gen s -> proxy w -> TestTree
rightTorsorLaw2 genS _ = testProperty "actRight . differenceRight" $ do
sOrig <- gen genS
sActed <- gen genS
assert $
P.eq
.$ ("sOrig `actRight` differenceRight sOrig sActed", sOrig `actRight` differenceRight @w sOrig sActed)
.$ ("sActed", sActed)
-- | Tests for 'RightAction' laws that require 'Monoid' on the change type.
rightActionMonoidLaws :: forall s w. (Eq s, Show s, Eq w, Show w, Monoid w, RightAction w s) => Gen s -> Gen w -> [TestTree]
rightActionMonoidLaws genS genW = [rightActionSemigroupLaw genS genW, rightActionMemptyLaw genS genW]
rightActionSemigroupLaw :: forall s w. (Eq s, Show s, Eq w, Show w, Semigroup w, RightAction w s) => Gen s -> Gen w -> TestTree
rightActionSemigroupLaw genS genW =
testProperty "actRight semigroup" $ do
s <- gen genS
m1 <- gen genW
m2 <- gen genW
assert $
P.eq
.$ ("(s `actRight` m1) `actRight` m2", (s `actRight` m1) `actRight` m2)
.$ ("s `actRight` (m1 <> m2)", s `actRight` (m1 <> m2))
rightActionMemptyLaw :: forall s w proxy. (Eq s, Show s, Eq w, Show w, Monoid w, RightAction w s) => Gen s -> proxy w -> TestTree
rightActionMemptyLaw genS _ = testProperty "actRight mempty" $ do
s <- gen genS
assert $
P.eq
.$ ("s `actRight` mempty", s `actRight` (mempty @w))
.$ ("s", s)
-- | Additional 'RightTorsor' law that requires 'Monoid': @differenceRight s s = mempty@.
rightTorsorMemptyLaw :: forall s w. (Eq s, Show s, Eq w, Show w, Monoid w, RightTorsor w s) => Gen s -> Gen w -> [TestTree]
rightTorsorMemptyLaw genS _genW =
[ testProperty "differenceRight self = mempty" $ do
s <- gen genS
assert $
P.expect (mempty @w)
.$ ("differenceRight s s", differenceRight @w s s)
]
-- | All 'RightAction' and 'RightTorsor' laws for a 'Monoid' change type.
allLaws :: (Eq s, Show s, Eq w, Show w, Monoid w, RightAction w s, RightTorsor w s) => Gen s -> Gen w -> [TestTree]
allLaws genS genW = rightActionMonoidLaws genS genW <> (rightTorsorLaws genS genW <> rightTorsorMemptyLaw genS genW)
{- | All laws for the common pattern where @RightAction w s@ and @RightTorsor (Changes w) s@.
Note: The @differenceRight . actRight@ test checks that the round-tripped diff has the same /effect/
rather than being structurally equal, since 'Changes' is a free monoid and multiple representations
can encode the same action.
-}
changesLaws ::
forall s w.
(Eq s, Show s, Eq w, Show w) =>
(Changes w -> s -> s) ->
(s -> s -> Changes w) ->
Gen s ->
Gen w ->
[TestTree]
changesLaws act diffFn genS genW =
[ testProperty "differenceRight . actRight (effect)" $ do
s <- gen genS
w <- gen genChangesW
-- The diff of the round-trip must produce the same effect, not necessarily the same Changes list
assert $
P.eq
.$ ("act (differenceRight s (act w s)) s", act (diffFn s (act w s)) s)
.$ ("act w s", act w s)
, testProperty "actRight . differenceRight" $ do
sOrig <- gen genS
sActed <- gen genS
assert $
P.eq
.$ ("act (differenceRight sOrig sActed) sOrig", act (diffFn sOrig sActed) sOrig)
.$ ("sActed", sActed)
, testProperty "differenceRight self = mempty" $ do
s <- gen genS
assert $
P.expect mempty
.$ ("differenceRight s s", diffFn s s)
]
where
genChangesW :: Gen (Changes w)
genChangesW = genChanges genW
-- * Generators
genInt :: Gen Int
genInt = Gen.int (between (0, 100))
genSetTo :: Gen (SetTo Int)
genSetTo = genDefault (Proxy @Std)
-- | Generate a non-zero rational (for Product torsor, avoiding division by zero).
genNonZeroRational :: Gen Rational
genNonZeroRational = do
n <- Gen.int (between (1, 100))
d <- Gen.int (between (1, 100))
sign <- Gen.bool True
pure $ (if sign then 1 else -1) * fromIntegral n / fromIntegral d
genSumInt :: Gen (Sum Int)
genSumInt = genDefault (Proxy @Std)
genProduct :: Gen a -> Gen (Product a)
genProduct = fmap Product
genLastInt :: Gen (Last Int)
genLastInt = genDefault (Proxy @Std)
genMaybe :: Gen a -> Gen (Maybe a)
genMaybe = Gen.shrinkToNothing
genChanges :: Gen w -> Gen (Changes w)
genChanges genW = changes <$> Gen.list (between (0, 5)) genW
genListChangeInt :: Gen (ListChange Int)
genListChangeInt = genDefault (Proxy @Std)
genCount :: Gen Count
genCount = genDefault (Proxy @Std)
genMaybeChange :: Gen a -> Gen (MaybeChange a)
genMaybeChange genA = Gen.choose (pure mempty) (Gen.choose (setJust <$> genA) (pure setNothing))
genFmapChange :: Gen w -> Gen (FmapChange f w)
genFmapChange = fmap FmapChange
genMaybeInt :: Gen (Maybe Int)
genMaybeInt = genDefault (Proxy @Std)
genSmallList :: Gen a -> Gen [a]
genSmallList = Gen.list (between (0, 5))
genAlignChangesMap :: Gen (AlignPositionChange w s) -> Gen (AlignChanges (M.Map Int) w s)
genAlignChangesMap genAPC = do
kvs <- Gen.list (between (0, 5)) $ do
k <- Gen.int (between (0, 10))
v <- genAPC
pure (k, v)
pure $ AlignChanges $ M.fromList kvs
genSmallMap :: (Ord k) => Gen k -> Gen v -> Gen (M.Map k v)
genSmallMap genK genV = do
kvs <- Gen.list (between (0, 5)) ((,) <$> genK <*> genV)
pure $ M.fromList kvs
genMapIntInt :: Gen (M.Map Int (Sum Int))
genMapIntInt = genSmallMap (Gen.int (between (0, 10))) genSumInt
genSumInteger :: Gen (Sum Integer)
genSumInteger = Sum . fromIntegral <$> Gen.int (between (-100, 100))
genProductRational :: Gen (Product Rational)
genProductRational = do
n <- Gen.int (between (1, 100))
d <- Gen.int (between (1, 100))
sign <- Gen.bool True
pure $ Product $ (if sign then 1 else -1) * fromIntegral n / fromIntegral d
genSumRational :: Gen (Sum Rational)
genSumRational = do
n <- Gen.int (between (-100, 100))
d <- Gen.int (between (1, 100))
pure $ Sum $ fromIntegral n / fromIntegral d
genTorsorWrapper :: Gen TorsorWrapper
genTorsorWrapper = TorsorWrapper <$> genSumInteger
genTorsor :: Gen Torsor
genTorsor = Torsor <$> genSumInteger <*> genProductRational <*> genSumRational
genTorsorChange :: Gen TorsorChange
genTorsorChange =
Gen.oneof
( (TorsorChangeSum <$> genSumInteger)
:| [ TorsorChangeProduct <$> genProductRational
, TorsorChangeSum2 <$> genSumRational
]
)
genTorsorChange2 :: Gen TorsorChange2
genTorsorChange2 = TorsorChange2 <$> genSumInteger <*> genProductRational <*> genSumRational