packages feed

genvalidity-mergeful 0.1.0.0 → 0.2.0.0

raw patch · 10 files changed

+890/−569 lines, 10 filesdep +criteriondep +genvalidity-criteriondep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: criterion, genvalidity-criterion

Dependency ranges changed: base

API changes (from Hackage documentation)

- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked i, GHC.Classes.Ord i, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.ClientStore i a)
- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked i, GHC.Classes.Ord i, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.ServerStore i a)
- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked i, GHC.Classes.Ord i, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.SyncRequest i a)
- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked i, GHC.Classes.Ord i, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.SyncResponse i a)
- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid i, GHC.Show.Show i, GHC.Classes.Ord i, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.ClientStore i a)
- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid i, GHC.Show.Show i, GHC.Classes.Ord i, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.ServerStore i a)
- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid i, GHC.Show.Show i, GHC.Classes.Ord i, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.SyncRequest i a)
- Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid i, GHC.Show.Show i, GHC.Classes.Ord i, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.SyncResponse i a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked ci, Data.GenValidity.GenUnchecked si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked ci, Data.GenValidity.GenUnchecked si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked ci, Data.GenValidity.GenUnchecked si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenUnchecked si, GHC.Classes.Ord si, Data.GenValidity.GenUnchecked a) => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.ServerStore si a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid ci, Data.GenValidity.GenValid si, GHC.Show.Show ci, GHC.Show.Show si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid ci, Data.GenValidity.GenValid si, GHC.Show.Show ci, GHC.Show.Show si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid ci, Data.GenValidity.GenValid si, GHC.Show.Show ci, GHC.Show.Show si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.GenValidity.Mergeful.Collection: instance (Data.GenValidity.GenValid si, GHC.Show.Show si, GHC.Classes.Ord si, Data.GenValidity.GenValid a) => Data.GenValidity.GenValid (Data.Mergeful.Collection.ServerStore si a)
+ Data.GenValidity.Mergeful.Collection: instance Data.GenValidity.GenUnchecked si => Data.GenValidity.GenUnchecked (Data.Mergeful.Collection.ClientAddition si)
+ Data.GenValidity.Mergeful.Collection: instance Data.GenValidity.GenValid si => Data.GenValidity.GenValid (Data.Mergeful.Collection.ClientAddition si)
+ Data.GenValidity.Mergeful.Item: instance Data.GenValidity.GenUnchecked a => Data.GenValidity.GenUnchecked (Data.Mergeful.Item.ItemMergeResult a)
+ Data.GenValidity.Mergeful.Item: instance Data.GenValidity.GenValid a => Data.GenValidity.GenValid (Data.Mergeful.Item.ItemMergeResult a)
+ Data.GenValidity.Mergeful.Value: instance Data.GenValidity.GenUnchecked a => Data.GenValidity.GenUnchecked (Data.Mergeful.Value.ValueMergeResult a)
+ Data.GenValidity.Mergeful.Value: instance Data.GenValidity.GenValid a => Data.GenValidity.GenValid (Data.Mergeful.Value.ValueMergeResult a)

Files

+ bench/Bench.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Criterion.Main as Criterion+import Data.GenValidity.Criterion+import Data.GenValidity.Mergeful ()+import Data.Mergeful.Collection+import Data.Mergeful.Item+import Data.Mergeful.Value++main :: IO ()+main =+  Criterion.defaultMain+    [ bgroup "Utils" [genValidBench @ServerTime, genValidBench @(Timed Bool)],+      bgroup+        "Value"+        [ genValidBench @ChangedFlag,+          genValidBench @(ValueMergeResult Bool),+          genValidBench @(ClientValue Bool),+          genValidBench @(ValueSyncRequest Bool),+          genValidBench @(ValueSyncResponse Bool),+          genValidBench @(ServerValue Bool)+        ],+      bgroup+        "Item"+        [ genValidBench @(ItemMergeResult Bool),+          genValidBench @(ClientItem Bool),+          genValidBench @(ItemSyncRequest Bool),+          genValidBench @(ItemSyncResponse Bool),+          genValidBench @(ServerItem Bool)+        ],+      bgroup+        "Collection"+        [ genValidBench @ClientId,+          genValidBench @(ClientStore ClientId Int Bool),+          genValidBench @(SyncRequest ClientId Int Bool),+          genValidBench @(ClientAddition Int),+          genValidBench @(SyncResponse ClientId Int Bool),+          genValidBench @(ServerStore Int Bool)+        ]+    ]
genvalidity-mergeful.cabal view
@@ -1,19 +1,19 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: 86196b99586836b920b517c4273efac7cc705762e7ff8c445e911cfdafe35620+-- hash: cab74b20dcf0eadc6956a8ce9d4325af4494785285a9b5a43aa8fda713de2bb1  name:           genvalidity-mergeful-version:        0.1.0.0+version:        0.2.0.0 description:    Please see the README on GitHub at <https://github.com/NorfairKing/mergeful#readme> homepage:       https://github.com/NorfairKing/mergeful#readme bug-reports:    https://github.com/NorfairKing/mergeful/issues author:         Tom Sydney Kerckhove maintainer:     syd@cs-syd.eu-copyright:      Copyright: (c) 2019 Tom Sydney Kerckhove+copyright:      Copyright: (c) 2019-2020 Tom Sydney Kerckhove license:        MIT license-file:   LICENSE build-type:     Simple@@ -38,7 +38,7 @@       src   build-depends:       QuickCheck-    , base >=4.7 && <5+    , base <5     , containers     , genvalidity     , genvalidity-containers@@ -60,7 +60,7 @@   ghc-options: -threaded -rtsopts -with-rtsopts=-N   build-depends:       QuickCheck-    , base >=4.7 && <5+    , base <5     , containers     , genvalidity-hspec     , genvalidity-hspec-aeson@@ -73,4 +73,20 @@     , random     , time     , uuid+  default-language: Haskell2010++benchmark mergeful-bench+  type: exitcode-stdio-1.0+  main-is: Bench.hs+  other-modules:+      Paths_genvalidity_mergeful+  hs-source-dirs:+      bench+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base <5+    , criterion+    , genvalidity-criterion+    , genvalidity-mergeful+    , mergeful   default-language: Haskell2010
src/Data/GenValidity/Mergeful/Collection.hs view
@@ -1,35 +1,36 @@ {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-}  module Data.GenValidity.Mergeful.Collection where +import Data.GenValidity+import Data.GenValidity.Containers ()+import Data.GenValidity.Mergeful.Item ()+import Data.GenValidity.Time () import Data.Map (Map) import qualified Data.Map as M+import Data.Mergeful import Data.Set (Set) import qualified Data.Set as S- import Data.Traversable--import Data.GenValidity-import Data.GenValidity.Containers ()-import Data.GenValidity.Time ()- import Test.QuickCheck -import Data.GenValidity.Mergeful.Item ()--import Data.Mergeful- instance GenUnchecked ClientId  instance GenValid ClientId -instance (GenUnchecked i, Ord i, GenUnchecked a) => GenUnchecked (ClientStore i a)+instance+  (GenUnchecked ci, GenUnchecked si, Ord ci, Ord si, GenUnchecked a) =>+  GenUnchecked (ClientStore ci si a) -instance (GenValid i, Show i, Ord i, GenValid a) => GenValid (ClientStore i a) where+instance+  (GenValid ci, GenValid si, Show ci, Show si, Ord ci, Ord si, GenValid a) =>+  GenValid (ClientStore ci si a)+  where   genValid =     (`suchThat` isValid) $ do-      identifiers <- scale (* 3) genValid+      identifiers <- genValid       (s1, s2) <- splitSet identifiers       (s3, s4) <- splitSet s1       clientStoreAddedItems <- genValid@@ -39,18 +40,23 @@       pure ClientStore {..}   shrinkValid = shrinkValidStructurally -instance (GenUnchecked i, Ord i, GenUnchecked a) => GenUnchecked (ServerStore i a)+instance (GenUnchecked si, Ord si, GenUnchecked a) => GenUnchecked (ServerStore si a) -instance (GenValid i, Show i, Ord i, GenValid a) => GenValid (ServerStore i a) where+instance (GenValid si, Show si, Ord si, GenValid a) => GenValid (ServerStore si a) where   genValid = genValidStructurallyWithoutExtraChecking   shrinkValid = shrinkValidStructurallyWithoutExtraFiltering -instance (GenUnchecked i, Ord i, GenUnchecked a) => GenUnchecked (SyncRequest i a)+instance+  (GenUnchecked ci, GenUnchecked si, Ord ci, Ord si, GenUnchecked a) =>+  GenUnchecked (SyncRequest ci si a) -instance (GenValid i, Show i, Ord i, GenValid a) => GenValid (SyncRequest i a) where+instance+  (GenValid ci, GenValid si, Show ci, Show si, Ord ci, Ord si, GenValid a) =>+  GenValid (SyncRequest ci si a)+  where   genValid =     (`suchThat` isValid) $ do-      identifiers <- scale (* 3) genValid+      identifiers <- genValid       (s1, s2) <- splitSet identifiers       (s3, s4) <- splitSet s1       syncRequestNewItems <- genValid@@ -60,12 +66,23 @@       pure SyncRequest {..}   shrinkValid = shrinkValidStructurally -instance (GenUnchecked i, Ord i, GenUnchecked a) => GenUnchecked (SyncResponse i a)+instance GenUnchecked si => GenUnchecked (ClientAddition si) -instance (GenValid i, Show i, Ord i, GenValid a) => GenValid (SyncResponse i a) where+instance GenValid si => GenValid (ClientAddition si) where+  genValid = genValidStructurallyWithoutExtraChecking+  shrinkValid = shrinkValidStructurallyWithoutExtraFiltering++instance+  (GenUnchecked ci, GenUnchecked si, Ord ci, Ord si, GenUnchecked a) =>+  GenUnchecked (SyncResponse ci si a)++instance+  (GenValid ci, GenValid si, Show ci, Show si, Ord ci, Ord si, GenValid a) =>+  GenValid (SyncResponse ci si a)+  where   genValid =     (`suchThat` isValid) $ do-      identifiers <- scale (* 10) genValid+      identifiers <- scale (* 2) genValid       (s01, s02) <- splitSet identifiers       (s03, s04) <- splitSet s01       (s05, s06) <- splitSet s02@@ -75,10 +92,11 @@       (s13, s14) <- splitSet s06       (s15, s16) <- splitSet s07       syncResponseClientAdded <--        do m <- genValid :: Gen (Map ClientId ())-           if S.null s08-             then pure M.empty-             else forM m $ \() -> (,) <$> elements (S.toList s08) <*> genValid+        do+          m <- genValid :: Gen (Map ci ())+          if S.null s08+            then pure M.empty+            else forM m $ \() -> ClientAddition <$> elements (S.toList s08) <*> genValid       syncResponseClientChanged <- mapWithIds s09       let syncResponseClientDeleted = s10       syncResponseServerAdded <- mapWithIds s11
src/Data/GenValidity/Mergeful/Item.hs view
@@ -3,10 +3,14 @@ module Data.GenValidity.Mergeful.Item where  import Data.GenValidity-+import Data.GenValidity.Mergeful.Timed () import Data.Mergeful.Item -import Data.GenValidity.Mergeful.Timed ()+instance GenUnchecked a => GenUnchecked (ItemMergeResult a)++instance GenValid a => GenValid (ItemMergeResult a) where+  genValid = genValidStructurallyWithoutExtraChecking+  shrinkValid = shrinkValidStructurallyWithoutExtraFiltering  instance GenUnchecked a => GenUnchecked (ClientItem a) 
src/Data/GenValidity/Mergeful/Timed.hs view
@@ -3,8 +3,8 @@ module Data.GenValidity.Mergeful.Timed where  import Data.GenValidity- import Data.Mergeful.Timed+import Test.QuickCheck  instance GenUnchecked a => GenUnchecked (Timed a) @@ -15,5 +15,10 @@ instance GenUnchecked ServerTime  instance GenValid ServerTime where-  genValid = genValidStructurallyWithoutExtraChecking+  genValid = ServerTime <$> arbitrary++  -- Use the quickcheck generator to produce Word64s+  -- This will hide the failures around maxBound for Word64+  -- but that's fine in this case.+  -- See also the comment for the 'ServerTime' constructor.   shrinkValid = shrinkValidStructurallyWithoutExtraFiltering
src/Data/GenValidity/Mergeful/Value.hs view
@@ -3,14 +3,20 @@ module Data.GenValidity.Mergeful.Value where  import Data.GenValidity--import Data.Mergeful.Value- import Data.GenValidity.Mergeful.Timed ()+import Data.Mergeful.Value  instance GenUnchecked ChangedFlag -instance GenValid ChangedFlag+instance GenValid ChangedFlag where+  genValid = genValidStructurallyWithoutExtraChecking+  shrinkValid = shrinkValidStructurallyWithoutExtraFiltering++instance GenUnchecked a => GenUnchecked (ValueMergeResult a)++instance GenValid a => GenValid (ValueMergeResult a) where+  genValid = genValidStructurallyWithoutExtraChecking+  shrinkValid = shrinkValidStructurallyWithoutExtraFiltering  instance GenUnchecked a => GenUnchecked (ClientValue a) 
test/Data/Mergeful/CollectionSpec.hs view
@@ -2,295 +2,247 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}  module Data.Mergeful.CollectionSpec-  ( spec-  ) where+  ( spec,+  )+where +import Control.Monad.State import Data.Functor.Identity+import Data.GenValidity.Mergeful ()+import Data.GenValidity.Mergeful.Item ()+import Data.GenValidity.UUID () import Data.List import qualified Data.Map as M+import Data.Mergeful import qualified Data.Set as S import Data.UUID import GHC.Generics (Generic) import System.Random-import Text.Show.Pretty--import Control.Monad.State- import Test.Hspec+import Test.Hspec.QuickCheck import Test.QuickCheck import Test.Validity import Test.Validity.Aeson--import Data.GenValidity.UUID ()--import Data.Mergeful-import Data.Mergeful.Timed--import Data.GenValidity.Mergeful ()-import Data.GenValidity.Mergeful.Item ()+import Text.Show.Pretty  {-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}  spec :: Spec-spec = do+spec = modifyMaxShrinks (min 0) $ do   genValidSpec @ClientId   jsonSpecOnValid @ClientId-  genValidSpec @(ClientStore Int Int)-  jsonSpecOnValid @(ClientStore Int Int)+  genValidSpec @(ClientStore ClientId Int Int)+  jsonSpecOnValid @(ClientStore ClientId Int Int)   genValidSpec @(ServerStore Int Int)   jsonSpecOnValid @(ServerStore Int Int)-  genValidSpec @(SyncRequest Int Int)-  jsonSpecOnValid @(SyncRequest Int Int)-  genValidSpec @(SyncResponse Int Int)-  jsonSpecOnValid @(SyncResponse Int Int)-  describe "initialClientStore" $ it "is valid" $ shouldBeValid $ initialClientStore @Int @Int+  genValidSpec @(SyncRequest ClientId Int Int)+  jsonSpecOnValid @(SyncRequest ClientId Int Int)+  genValidSpec @(ClientAddition Int)+  jsonSpecOnValid @(ClientAddition Int)+  genValidSpec @(SyncResponse ClientId Int Int)+  jsonSpecOnValid @(SyncResponse ClientId Int Int)+  describe "initialClientStore"+    $ it "is valid"+    $ shouldBeValid+    $ initialClientStore @ClientId @Int @Int   describe "addItemToClientStore" $ do-    it "produces valid stores" $ producesValidsOnValids2 (addItemToClientStore @Int @Int)-    it "makes the client store one bigger" $-      forAllValid $ \cs ->+    it "produces valid stores" $ producesValidsOnValids2 (addItemToClientStore @ClientId @Int @Int)+    it "makes the client store one bigger"+      $ forAllValid+      $ \cs ->         forAllValid $ \a ->-          clientStoreSize @Int @Int (addItemToClientStore a cs) `shouldBe` (clientStoreSize cs + 1)-    it "ensures that the added item is in fact in the result" $-      forAllValid $ \cs ->+          clientStoreSize @ClientId @Int @Int (addItemToClientStore a cs)+            `shouldBe` (clientStoreSize cs + 1)+    it "ensures that the added item is in fact in the result"+      $ forAllValid+      $ \cs ->         forAllValid $ \a ->-          let cs' = addItemToClientStore @Int @Int a cs+          let cs' = addItemToClientStore @ClientId @Int @Int a cs            in a `elem` clientStoreAddedItems cs'   describe "markItemDeletedInClientStore" $ do-    it "produces valid stores" $ producesValidsOnValids2 (markItemDeletedInClientStore @Int @Int)+    it "produces valid stores" $+      producesValidsOnValids2 (markItemDeletedInClientStore @ClientId @Int @Int)     it "makes the client store one smaller" $       let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0        in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->             forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->-              (clientStoreSize @Int @Int (markItemDeletedInClientStore i cs) + 1) `shouldBe`-              clientStoreSize cs+              (clientStoreSize @ClientId @Int @Int (markItemDeletedInClientStore i cs) + 1)+                `shouldBe` clientStoreSize cs     it "ensures that the added item is in fact not in the result" $       let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0        in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->             forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->-              let cs' = markItemDeletedInClientStore @Int @Int i cs+              let cs' = markItemDeletedInClientStore @ClientId @Int @Int i cs                in M.lookup (Right i) (clientStoreItems cs') `shouldBe` Nothing   describe "changeItemInClientStore" $ do-    it "produces valid stores" $ producesValidsOnValids3 (changeItemInClientStore @Int @Int)+    it "produces valid stores" $+      producesValidsOnValids3 (changeItemInClientStore @ClientId @Int @Int)     it "doesn't change the size of the client store" $       let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0        in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->             forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->               forAllValid $ \a ->-                clientStoreSize @Int @Int (changeItemInClientStore i a cs) `shouldBe`-                clientStoreSize cs+                clientStoreSize @ClientId @Int @Int (changeItemInClientStore i a cs)+                  `shouldBe` clientStoreSize cs     it "ensures that the changed item is in fact in the result" $       let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0        in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->             forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->               forAllValid $ \a ->-                let cs' = changeItemInClientStore @Int @Int i a cs+                let cs' = changeItemInClientStore @ClientId @Int @Int i a cs                  in M.lookup (Right i) (clientStoreItems cs') `shouldBe` Just a   describe "deleteItemFromClientStore" $ do-    it "produces valid stores" $ producesValidsOnValids2 (deleteItemFromClientStore @Int @Int)+    it "produces valid stores" $+      producesValidsOnValids2 (deleteItemFromClientStore @ClientId @Int @Int)     it "makes the client store one smaller" $       let p cs = S.size (clientStoreClientIdSet cs) > 0        in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->             forAll (elements $ S.toList $ clientStoreClientIdSet cs) $ \cid ->-              (clientStoreSize @Int @Int (deleteItemFromClientStore cid cs) + 1) `shouldBe`-              clientStoreSize cs+              (clientStoreSize @ClientId @Int @Int (deleteItemFromClientStore cid cs) + 1)+                `shouldBe` clientStoreSize cs   describe "initialServerStore" $ it "is valid" $ shouldBeValid $ initialServerStore @Int @Int-  describe "initialSyncRequest" $ it "is valid" $ shouldBeValid $ initialSyncRequest @Int @Int-  describe "emptySyncResponse" $ it "is valid" $ shouldBeValid $ emptySyncResponse @Int @Int-  describe "makeSyncRequest" $-    it "produces valid requests" $ producesValidsOnValids (makeSyncRequest @Int @Int)-  describe "mergeAddedItems" $-    it "produces valid results" $ producesValidsOnValids2 (mergeAddedItems @Int @Int)-  describe "mergeSyncedButChangedItems" $-    it "produces valid results" $ producesValidsOnValids2 (mergeSyncedButChangedItems @Int @Int)-  describe "mergeAddedItems" $-    it "produces valid results" $ producesValidsOnValids2 (mergeAddedItems @Int @Int)-  describe "mergeSyncedButChangedItems" $-    it "produces valid results" $ producesValidsOnValids2 (mergeSyncedButChangedItems @Int @Int)-  describe "mergeDeletedItems" $-    it "produces valid results" $ producesValidsOnValids2 (mergeDeletedItems @Int @Int)-  describe "mergeSyncResponseIgnoreProblems" $-    it "produces valid requests" $-    forAllValid $ \store ->-      forAllValid $ \response ->-        let res = mergeSyncResponseIgnoreProblems @Int @Int store response-         in case prettyValidate res of-              Right _ -> pure ()-              Left err ->-                expectationFailure $-                unlines-                  [ "Store:"-                  , ppShow store-                  , "Response:"-                  , ppShow response-                  , "Invalid result:"-                  , ppShow res-                  , "error:"-                  , err-                  ]-  describe "processServerSync" $-    it "produces valid tuples of a response and a store" $-    producesValidsOnValids2-      (\store request -> evalD $ processServerSync genD (store :: ServerStore UUID Int) request)-  describe "Syncing with mergeSyncResponseIgnoreProblems" $ do-    mergeFunctionSpec @Int mergeSyncResponseIgnoreProblems-    it "does not lose data after a conflict occurs" $-      forAllValid $ \uuid ->-        forAllValid $ \time1 ->-          forAllValid $ \i1 ->-            forAllValid $ \i2 ->-              forAllValid $ \i3 ->-                evalDM $ do-                  let sstore1 =-                        ServerStore-                          {serverStoreItems = M.singleton (uuid :: UUID) (Timed (i1 :: Int) time1)}-                      -- The server has an item-                      -- The first client has synced it, and modified it.-                  let cAstore1 =-                        initialClientStore-                          {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i2 time1)}-                      -- The second client has synced it too, and modified it too.-                  let cBstore1 =-                        initialClientStore-                          {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)}-                      -- Client A makes sync request 1-                  let req1 = makeSyncRequest cAstore1-                      -- The server processes sync request 1-                  (resp1, sstore2) <- processServerSync genD sstore1 req1-                  let time2 = incrementServerTime time1-                      -- The server updates the item accordingly-                  lift $ do-                    resp1 `shouldBe`-                      (emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2})-                    sstore2 `shouldBe`-                      (ServerStore {serverStoreItems = M.singleton uuid (Timed i2 time2)})-                      -- Client A merges the response-                  let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp1-                  lift $-                    cAstore2 `shouldBe`-                    (initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed i2 time2)})-                      -- Client B makes sync request 2-                  let req2 = makeSyncRequest cBstore1-                      -- The server processes sync request 2-                  (resp2, sstore3) <- processServerSync genD sstore2 req2-                      -- The server reports a conflict and does not change its store-                  lift $ do-                    resp2 `shouldBe`-                      (emptySyncResponse {syncResponseConflicts = M.singleton uuid (Timed i2 time2)})-                    sstore3 `shouldBe` sstore2-                      -- Client B merges the response-                  let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp2-                      -- Client does not update, but keeps its conflict-                  lift $-                    cBstore2 `shouldBe`-                    (initialClientStore-                       {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)})-                      -- Client A and Client B now *do not* have the same store+  describe "initialSyncRequest"+    $ it "is valid"+    $ shouldBeValid+    $ initialSyncRequest @ClientId @Int @Int+  describe "emptySyncResponse"+    $ it "is valid"+    $ shouldBeValid+    $ emptySyncResponse @ClientId @Int @Int+  describe "makeSyncRequest"+    $ it "produces valid requests"+    $ producesValidsOnValids (makeSyncRequest @ClientId @Int @Int)+  describe "mergeAddedItems"+    $ it "produces valid results"+    $ producesValidsOnValids2 (mergeAddedItems @ClientId @Int @Int)+  describe "mergeSyncedButChangedItems"+    $ it "produces valid results"+    $ producesValidsOnValids2 (mergeSyncedButChangedItems @Int @Int)+  describe "mergeDeletedItems"+    $ it "produces valid results"+    $ producesValidsOnValids2 (mergeDeletedItems @Int @Int)+  xdescribe "There must not be id conflicts" $ do+    describe "mergeSyncResponseFromServer"+      $ it "produces valid requests"+      $ forAllValid+      $ \store ->+        forAllValid $ \response ->+          let res = mergeSyncResponseFromServer @ClientId @Int @Int store response+           in case prettyValidate res of+                Right _ -> pure ()+                Left err ->+                  expectationFailure $+                    unlines+                      [ "Store:",+                        ppShow store,+                        "Response:",+                        ppShow response,+                        "Invalid result:",+                        ppShow res,+                        "error:",+                        err+                      ]+    describe "mergeSyncResponseFromClient"+      $ it "produces valid requests"+      $ forAllValid+      $ \store ->+        forAllValid $ \response ->+          let res = mergeSyncResponseFromClient @ClientId @Int @Int store response+           in case prettyValidate res of+                Right _ -> pure ()+                Left err ->+                  expectationFailure $+                    unlines+                      [ "Store:",+                        ppShow store,+                        "Response:",+                        ppShow response,+                        "Invalid result:",+                        ppShow res,+                        "error:",+                        err+                      ]+    describe "mergeSyncResponseUsingCRDT"+      $ it "produces valid requests"+      $ forAllValid+      $ \store ->+        forAllValid $ \response ->+          let res = mergeSyncResponseUsingCRDT @ClientId @Int @Int max store response+           in case prettyValidate res of+                Right _ -> pure ()+                Left err ->+                  expectationFailure $+                    unlines+                      [ "Store:",+                        ppShow store,+                        "Response:",+                        ppShow response,+                        "Invalid result:",+                        ppShow res,+                        "error:",+                        err+                      ]+  describe "processServerSync"+    $ it "produces valid tuples of a response and a store"+    $ producesValidsOnValids2+      ( \store request ->+          evalD $ processServerSync @ClientId @UUID genD (store :: ServerStore UUID Int) request+      )+  describe "Syncing with mergeSyncResponseFromClient" $ do+    let strat = mergeFromClientStrategy+    helperFunctionsSpec strat+    mergeFunctionSpec @Int strat+    noDataLossSpec @Int strat+    xdescribe "does not hold" $ do+      emptyResponseSpec @Int strat+      noDivergenceSpec @Int strat   describe "Syncing with mergeSyncResponseFromServer" $ do-    mergeFunctionSpec @Int mergeSyncResponseFromServer-    describe "mergeSyncResponseFromServer" $-      it "only differs from mergeSyncResponseIgnoreProblems on conflicts" $-      forAllValid $ \cstore ->-        forAllValid $ \sresp@SyncResponse {..} -> do-          let cstoreA = mergeSyncResponseFromServer (cstore :: ClientStore UUID Int) sresp-              cstoreB = mergeSyncResponseIgnoreProblems cstore sresp-          if cstoreA == cstoreB-            then pure ()-            else unless-                   (or-                      [ not (M.null syncResponseConflicts)-                      , not (M.null syncResponseConflictsClientDeleted)-                      , not (S.null syncResponseConflictsServerDeleted)-                      ]) $-                 expectationFailure $-                 unlines-                   [ "There was a difference between mergeSyncResponseFromServer and mergeSyncResponseIgnoreProblems that was somehow unrelated to the conflicts:"-                   , "syncResponseConflicts:"-                   , ppShow syncResponseConflicts-                   , "syncResponseConflictsClientDeleted:"-                   , ppShow syncResponseConflictsClientDeleted-                   , "syncResponseConflictsServerDeleted:"-                   , ppShow syncResponseConflictsServerDeleted-                   , "client store after mergeSyncResponseFromServer:"-                   , ppShow cstoreA-                   , "client store after mergeSyncResponseIgnoreProblems:"-                   , ppShow cstoreB-                   ]-    it "does not diverge after a conflict occurs" $-      forAllValid $ \uuid ->-        forAllValid $ \time1 ->-          forAllValid $ \i1 ->-            forAllValid $ \i2 ->-              forAllValid $ \i3 ->-                evalDM $ do-                  let sstore1 =-                        ServerStore-                          {serverStoreItems = M.singleton (uuid :: UUID) (Timed (i1 :: Int) time1)}-                      -- The server has an item-                      -- The first client has synced it, and modified it.-                  let cAstore1 =-                        initialClientStore-                          {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i2 time1)}-                      -- The second client has synced it too, and modified it too.-                  let cBstore1 =-                        initialClientStore-                          {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)}-                      -- Client A makes sync request 1-                  let req1 = makeSyncRequest cAstore1-                      -- The server processes sync request 1-                  (resp1, sstore2) <- processServerSync genD sstore1 req1-                  let time2 = incrementServerTime time1-                      -- The server updates the item accordingly-                  lift $ do-                    resp1 `shouldBe`-                      (emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2})-                    sstore2 `shouldBe`-                      (ServerStore {serverStoreItems = M.singleton uuid (Timed i2 time2)})-                      -- Client A merges the response-                  let cAstore2 = mergeSyncResponseFromServer cAstore1 resp1-                  lift $-                    cAstore2 `shouldBe`-                    (initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed i2 time2)})-                      -- Client B makes sync request 2-                  let req2 = makeSyncRequest cBstore1-                      -- The server processes sync request 2-                  (resp2, sstore3) <- processServerSync genD sstore2 req2-                      -- The server reports a conflict and does not change its store-                  lift $ do-                    resp2 `shouldBe`-                      (emptySyncResponse {syncResponseConflicts = M.singleton uuid (Timed i2 time2)})-                    sstore3 `shouldBe` sstore2-                      -- Client B merges the response-                  let cBstore2 = mergeSyncResponseFromServer cBstore1 resp2-                      -- Client does not update, but keeps its conflict-                  lift $ do-                    cBstore2 `shouldBe`-                      (initialClientStore-                         {clientStoreSyncedItems = M.singleton uuid (Timed i2 time2)})-                        -- Client A and Client B now have the same store-                    cBstore2 `shouldBe` cAstore2-  describe "Syncing with mergeSyncResponseUsingStrategy with a GCounter" $-    mergeFunctionSpec @Int $-    mergeSyncResponseUsingStrategy-      ItemMergeStrategy-        { itemMergeStrategyMergeChangeConflict =-            \clientItem (Timed serverItem t) -> Timed (max clientItem serverItem) t-        , itemMergeStrategyMergeClientDeletedConflict = \serverItem -> Just serverItem-        , itemMergeStrategyMergeServerDeletedConflict = \_ -> Nothing-        }+    let strat = mergeFromServerStrategy+    helperFunctionsSpec strat+    mergeFunctionSpec @Int strat+    noDivergenceSpec @Int strat+    emptyResponseSpec @Int strat+    noDifferentExceptForConflicts @Int strat mergeFromClientStrategy+    xdescribe "does not hold" $ noDataLossSpec @Int strat+  describe "Syncing with mergeSyncResponseUsingStrategy with a GCounter" $ do+    let strat = mergeUsingCRDTStrategy max+    helperFunctionsSpec strat+    mergeFunctionSpec strat+    noDataLossSpec strat+    noDivergenceSpec strat+    emptyResponseSpec strat+    noDifferentExceptForConflicts strat mergeFromClientStrategy+    noDifferentExceptForConflicts strat mergeFromServerStrategy +helperFunctionsSpec :: ItemMergeStrategy Int -> Spec+helperFunctionsSpec ItemMergeStrategy {..} = do+  describe "mergeSyncedButChangedConflicts"+    $ it "produces valid results"+    $ producesValidsOnValids2 (mergeSyncedButChangedConflicts @Int @Int itemMergeStrategyMergeChangeConflict)+  describe "mergeClientDeletedConflicts"+    $ it "produces valid results"+    $ producesValidsOnValids (mergeClientDeletedConflicts @Int @Int itemMergeStrategyMergeClientDeletedConflict)+  describe "mergeServerDeletedConflicts"+    $ it "produces valid results"+    $ producesValidsOnValids (mergeServerDeletedConflicts @Int @Int itemMergeStrategyMergeServerDeletedConflict)+ mergeFunctionSpec ::-     forall a. (Show a, Ord a, GenValid a)-  => (forall i. Ord i =>-                  ClientStore i a -> SyncResponse i a -> ClientStore i a)-  -> Spec-mergeFunctionSpec mergeFunc = do-  describe "Single client" $-    describe "Multi-item" $ do-      it "succesfully downloads everything from the server for an empty client" $-        forAllValid $ \sstore1 ->+  forall a.+  (Show a, Ord a, GenValid a) =>+  ItemMergeStrategy a ->+  Spec+mergeFunctionSpec strat = do+  let mergeFunc = mergeSyncResponseUsingStrategy strat+  describe "Single client"+    $ describe "Multi-item"+    $ do+      it "succesfully downloads everything from the server for an empty client"+        $ forAllValid+        $ \sstore1 ->           evalDM $ do             let cstore1 = initialClientStore             let req = makeSyncRequest cstore1@@ -299,8 +251,9 @@             lift $ do               sstore2 `shouldBe` sstore1               clientStoreSyncedItems cstore2 `shouldBe` serverStoreItems sstore2-      it "succesfully uploads everything to the server for an empty server" $-        forAllValid $ \items ->+      it "succesfully uploads everything to the server for an empty server"+        $ forAllValid+        $ \items ->           evalDM $ do             let cstore1 = initialClientStore {clientStoreAddedItems = items}             let sstore1 = initialServerStore@@ -308,11 +261,12 @@             (resp, sstore2) <- processServerSync genD sstore1 req             let cstore2 = mergeFunc cstore1 resp             lift $ do-              sort (M.elems (M.map timedValue (clientStoreSyncedItems cstore2))) `shouldBe`-                sort (M.elems items)+              sort (M.elems (M.map timedValue (clientStoreSyncedItems cstore2)))+                `shouldBe` sort (M.elems items)               clientStoreSyncedItems cstore2 `shouldBe` serverStoreItems sstore2-      it "is idempotent with one client" $-        forAllValid $ \cstore1 ->+      it "is idempotent with one client"+        $ forAllValid+        $ \cstore1 ->           forAllValid $ \sstore1 ->             evalDM $ do               let req1 = makeSyncRequest cstore1@@ -326,262 +280,467 @@                 sstore2 `shouldBe` sstore3   describe "Multiple clients" $ do     describe "Single-item" $ do-      it "successfully syncs an addition accross to a second client" $-        forAllValid $ \i ->+      it "successfully syncs an addition accross to a second client"+        $ forAllValid+        $ \i ->           evalDM $ do             let cAstore1 = initialClientStore {clientStoreAddedItems = M.singleton (ClientId 0) i}-              -- Client B is empty+            -- Client B is empty             let cBstore1 = initialClientStore-              -- The server is empty+            -- The server is empty             let sstore1 = initialServerStore-              -- Client A makes sync request 1+            -- Client A makes sync request 1             let req1 = makeSyncRequest cAstore1-              -- The server processes sync request 1+            -- The server processes sync request 1             (resp1, sstore2) <- processServerSync genD sstore1 req1             let addedItems = syncResponseClientAdded resp1             case M.toList addedItems of-              [(ClientId 0, (uuid, st))] -> do+              [(ClientId 0, ClientAddition uuid st)] -> do                 let time = initialServerTime                 lift $ st `shouldBe` time                 let items = M.singleton uuid (Timed i st)                 lift $ sstore2 `shouldBe` (ServerStore {serverStoreItems = items})-                  -- Client A merges the response+                -- Client A merges the response                 let cAstore2 = mergeFunc cAstore1 resp1                 lift $ cAstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})-                  -- Client B makes sync request 2+                -- Client B makes sync request 2                 let req2 = makeSyncRequest cBstore1-                  -- The server processes sync request 2+                -- The server processes sync request 2                 (resp2, sstore3) <- processServerSync genD sstore2 req2                 lift $ do                   resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})                   sstore3 `shouldBe` sstore2-                  -- Client B merges the response+                -- Client B merges the response                 let cBstore2 = mergeFunc cBstore1 resp2                 lift $ cBstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})-                  -- Client A and Client B now have the same store+                -- Client A and Client B now have the same store                 lift $ cAstore2 `shouldBe` cBstore2               _ -> lift $ expectationFailure "Should have found exactly one added item."-      it "successfully syncs a modification accross to a second client" $-        forAllValid $ \uuid ->+      it "successfully syncs a modification accross to a second client"+        $ forAllValid+        $ \uuid ->           forAllValid $ \i ->             forAllValid $ \j ->               forAllValid $ \time1 ->                 evalDM $ do                   let cAstore1 =                         initialClientStore-                          {clientStoreSyncedItems = M.singleton uuid (Timed i time1)}-                    -- Client B had synced that same item, but has since modified it+                          { clientStoreSyncedItems = M.singleton uuid (Timed i time1)+                          }+                  -- Client B had synced that same item, but has since modified it                   let cBstore1 =                         initialClientStore-                          {clientStoreSyncedButChangedItems = M.singleton uuid (Timed j time1)}-                    -- The server is has the item that both clients had before+                          { clientStoreSyncedButChangedItems = M.singleton uuid (Timed j time1)+                          }+                  -- The server is has the item that both clients had before                   let sstore1 = ServerStore {serverStoreItems = M.singleton uuid (Timed i time1)}-                    -- Client B makes sync request 1+                  -- Client B makes sync request 1                   let req1 = makeSyncRequest cBstore1-                    -- The server processes sync request 1+                  -- The server processes sync request 1                   (resp1, sstore2) <- processServerSync genD sstore1 req1                   let time2 = incrementServerTime time1                   lift $ do-                    resp1 `shouldBe`-                      emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2}-                    sstore2 `shouldBe`-                      ServerStore {serverStoreItems = M.singleton uuid (Timed j time2)}-                    -- Client B merges the response+                    resp1+                      `shouldBe` emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2}+                    sstore2+                      `shouldBe` ServerStore {serverStoreItems = M.singleton uuid (Timed j time2)}+                  -- Client B merges the response                   let cBstore2 = mergeFunc cBstore1 resp1                   lift $-                    cBstore2 `shouldBe`-                    initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed j time2)}-                    -- Client A makes sync request 2+                    cBstore2+                      `shouldBe` initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed j time2)}+                  -- Client A makes sync request 2                   let req2 = makeSyncRequest cAstore1-                    -- The server processes sync request 2+                  -- The server processes sync request 2                   (resp2, sstore3) <- processServerSync genD sstore2 req2                   lift $ do-                    resp2 `shouldBe`-                      emptySyncResponse-                        {syncResponseServerChanged = M.singleton uuid (Timed j time2)}+                    resp2+                      `shouldBe` emptySyncResponse+                        { syncResponseServerChanged = M.singleton uuid (Timed j time2)+                        }                     sstore3 `shouldBe` sstore2-                    -- Client A merges the response+                  -- Client A merges the response                   let cAstore2 = mergeFunc cAstore1 resp2                   lift $-                    cAstore2 `shouldBe`-                    initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed j time2)}-                    -- Client A and Client B now have the same store+                    cAstore2+                      `shouldBe` initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed j time2)}+                  -- Client A and Client B now have the same store                   lift $ cAstore2 `shouldBe` cBstore2-      it "succesfully syncs a deletion across to a second client" $-        forAllValid $ \uuid ->+      it "succesfully syncs a deletion across to a second client"+        $ forAllValid+        $ \uuid ->           forAllValid $ \time1 ->             forAllValid $ \i ->               evalDM $ do                 let cAstore1 =                       initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed i time1)}-                  -- Client A has a synced item.-                  -- Client B had synced that same item, but has since deleted it.+                -- Client A has a synced item.+                -- Client B had synced that same item, but has since deleted it.                 let cBstore1 = initialClientStore {clientStoreDeletedItems = M.singleton uuid time1}-                  -- The server still has the undeleted item+                -- The server still has the undeleted item                 let sstore1 = ServerStore {serverStoreItems = M.singleton uuid (Timed i time1)}-                  -- Client B makes sync request 1+                -- Client B makes sync request 1                 let req1 = makeSyncRequest cBstore1-                  -- The server processes sync request 1+                -- The server processes sync request 1                 (resp1, sstore2) <- processServerSync genD sstore1 req1                 lift $ do                   resp1 `shouldBe` emptySyncResponse {syncResponseClientDeleted = S.singleton uuid}                   sstore2 `shouldBe` initialServerStore-                  -- Client B merges the response+                -- Client B merges the response                 let cBstore2 = mergeFunc cBstore1 resp1                 lift $ cBstore2 `shouldBe` initialClientStore-                  -- Client A makes sync request 2+                -- Client A makes sync request 2                 let req2 = makeSyncRequest cAstore1-                  -- The server processes sync request 2+                -- The server processes sync request 2                 (resp2, sstore3) <- processServerSync genD sstore2 req2                 lift $ do                   resp2 `shouldBe` emptySyncResponse {syncResponseServerDeleted = S.singleton uuid}                   sstore3 `shouldBe` sstore2-                  -- Client A merges the response+                -- Client A merges the response                 let cAstore2 = mergeFunc cAstore1 resp2                 lift $ cAstore2 `shouldBe` initialClientStore-                  -- Client A and Client B now have the same store+                -- Client A and Client B now have the same store                 lift $ cAstore2 `shouldBe` cBstore2-      it "does not run into a conflict if two clients both try to sync a deletion" $-        forAllValid $ \uuid ->+      it "does not run into a conflict if two clients both try to sync a deletion"+        $ forAllValid+        $ \uuid ->           forAllValid $ \time1 ->             forAllValid $ \i ->               evalDM $ do                 let cAstore1 = initialClientStore {clientStoreDeletedItems = M.singleton uuid time1}-                  -- Both client a and client b delete an item.+                -- Both client a and client b delete an item.                 let cBstore1 = initialClientStore {clientStoreDeletedItems = M.singleton uuid time1}-                  -- The server still has the undeleted item+                -- The server still has the undeleted item                 let sstore1 = ServerStore {serverStoreItems = M.singleton uuid (Timed i time1)}-                  -- Client A makes sync request 1+                -- Client A makes sync request 1                 let req1 = makeSyncRequest cAstore1-                  -- The server processes sync request 1+                -- The server processes sync request 1                 (resp1, sstore2) <- processServerSync genD sstore1 req1                 lift $ do-                  resp1 `shouldBe`-                    (emptySyncResponse {syncResponseClientDeleted = S.singleton uuid})+                  resp1+                    `shouldBe` (emptySyncResponse {syncResponseClientDeleted = S.singleton uuid})                   sstore2 `shouldBe` (ServerStore {serverStoreItems = M.empty})-                  -- Client A merges the response+                -- Client A merges the response                 let cAstore2 = mergeFunc cAstore1 resp1                 lift $ cAstore2 `shouldBe` initialClientStore-                  -- Client B makes sync request 2+                -- Client B makes sync request 2                 let req2 = makeSyncRequest cBstore1-                  -- The server processes sync request 2+                -- The server processes sync request 2                 (resp2, sstore3) <- processServerSync genD sstore2 req2                 lift $ do-                  resp2 `shouldBe`-                    (emptySyncResponse {syncResponseClientDeleted = S.singleton uuid})+                  resp2+                    `shouldBe` (emptySyncResponse {syncResponseClientDeleted = S.singleton uuid})                   sstore3 `shouldBe` sstore2-                  -- Client B merges the response+                -- Client B merges the response                 let cBstore2 = mergeFunc cBstore1 resp2                 lift $ do                   cBstore2 `shouldBe` initialClientStore-                    -- Client A and Client B now have the same store+                  -- Client A and Client B now have the same store                   cAstore2 `shouldBe` cBstore2     describe "Multiple items" $ do-      it "successfully syncs additions accross to a second client" $-        forAllValid $ \is ->+      it "successfully syncs additions accross to a second client"+        $ forAllValid+        $ \is ->           evalDM $ do             let cAstore1 = initialClientStore {clientStoreAddedItems = is}-              -- Client B is empty+            -- Client B is empty             let cBstore1 = initialClientStore-              -- The server is empty+            -- The server is empty             let sstore1 = initialServerStore-              -- Client A makes sync request 1+            -- Client A makes sync request 1             let req1 = makeSyncRequest cAstore1-              -- The server processes sync request 1+            -- The server processes sync request 1             (resp1, sstore2) <- processServerSync genD sstore1 req1             let (rest, items) = mergeAddedItems is (syncResponseClientAdded resp1)             lift $ do               rest `shouldBe` M.empty               sstore2 `shouldBe` (ServerStore {serverStoreItems = items})-              -- Client A merges the response+            -- Client A merges the response             let cAstore2 = mergeFunc cAstore1 resp1             lift $ cAstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})-              -- Client B makes sync request 2+            -- Client B makes sync request 2             let req2 = makeSyncRequest cBstore1-              -- The server processes sync request 2+            -- The server processes sync request 2             (resp2, sstore3) <- processServerSync genD sstore2 req2             lift $ do               resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})               sstore3 `shouldBe` sstore2-              -- Client B merges the response+            -- Client B merges the response             let cBstore2 = mergeFunc cBstore1 resp2             lift $ cBstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})-              -- Client A and Client B now have the same store+            -- Client A and Client B now have the same store             lift $ cAstore2 `shouldBe` cBstore2-      it "succesfully syncs deletions across to a second client" $-        forAllValid $ \items ->+      it "succesfully syncs deletions across to a second client"+        $ forAllValid+        $ \items ->           forAllValid $ \time1 ->             evalDM $ do               let syncedItems = M.map (\i -> Timed i time1) items                   itemTimes = M.map (const time1) items                   itemIds = M.keysSet items               let cAstore1 = initialClientStore {clientStoreSyncedItems = syncedItems}-                -- Client A has synced items-                -- Client B had synced the same items, but has since deleted them.+              -- Client A has synced items+              -- Client B had synced the same items, but has since deleted them.               let cBstore1 = initialClientStore {clientStoreDeletedItems = itemTimes}-                -- The server still has the undeleted item+              -- The server still has the undeleted item               let sstore1 = ServerStore {serverStoreItems = syncedItems}-                -- Client B makes sync request 1+              -- Client B makes sync request 1               let req1 = makeSyncRequest cBstore1-                -- The server processes sync request 1+              -- The server processes sync request 1               (resp1, sstore2) <- processServerSync genD sstore1 req1               lift $ do                 resp1 `shouldBe` emptySyncResponse {syncResponseClientDeleted = itemIds}                 sstore2 `shouldBe` initialServerStore-                -- Client B merges the response+              -- Client B merges the response               let cBstore2 = mergeFunc cBstore1 resp1               lift $ cBstore2 `shouldBe` initialClientStore-                -- Client A makes sync request 2+              -- Client A makes sync request 2               let req2 = makeSyncRequest cAstore1-                -- The server processes sync request 2+              -- The server processes sync request 2               (resp2, sstore3) <- processServerSync genD sstore2 req2               lift $ do                 resp2 `shouldBe` emptySyncResponse {syncResponseServerDeleted = itemIds}                 sstore3 `shouldBe` sstore2-                -- Client A merges the response+              -- Client A merges the response               let cAstore2 = mergeFunc cAstore1 resp2               lift $ cAstore2 `shouldBe` initialClientStore-                -- Client A and Client B now have the same store+              -- Client A and Client B now have the same store               lift $ cAstore2 `shouldBe` cBstore2-      it "does not run into a conflict if two clients both try to sync a deletion" $-        forAllValid $ \items ->+      it "does not run into a conflict if two clients both try to sync a deletion"+        $ forAllValid+        $ \items ->           forAllValid $ \time1 ->             evalDM $ do               let cAstore1 =                     initialClientStore {clientStoreDeletedItems = M.map (const time1) items}-                -- Both client a and client b delete their items.+              -- Both client a and client b delete their items.               let cBstore1 =                     initialClientStore {clientStoreDeletedItems = M.map (const time1) items}-                -- The server still has the undeleted items+              -- The server still has the undeleted items               let sstore1 = ServerStore {serverStoreItems = M.map (\i -> Timed i time1) items}-                -- Client A makes sync request 1+              -- Client A makes sync request 1               let req1 = makeSyncRequest cAstore1-                -- The server processes sync request 1+              -- The server processes sync request 1               (resp1, sstore2) <- processServerSync genD sstore1 req1               lift $ do                 resp1 `shouldBe` (emptySyncResponse {syncResponseClientDeleted = M.keysSet items})                 sstore2 `shouldBe` (ServerStore {serverStoreItems = M.empty}) -- TODO will probably need some sort of tombstoning.-                -- Client A merges the response+                      -- Client A merges the response               let cAstore2 = mergeFunc cAstore1 resp1               lift $ cAstore2 `shouldBe` initialClientStore-                -- Client B makes sync request 2+              -- Client B makes sync request 2               let req2 = makeSyncRequest cBstore1-                -- The server processes sync request 2+              -- The server processes sync request 2               (resp2, sstore3) <- processServerSync genD sstore2 req2               lift $ do                 resp2 `shouldBe` (emptySyncResponse {syncResponseClientDeleted = M.keysSet items})                 sstore3 `shouldBe` sstore2-                -- Client B merges the response+              -- Client B merges the response               let cBstore2 = mergeFunc cBstore1 resp2               lift $ do                 cBstore2 `shouldBe` initialClientStore-                  -- Client A and Client B now have the same store+                -- Client A and Client B now have the same store                 cAstore2 `shouldBe` cBstore2 -newtype D m a =-  D-    { unD :: StateT StdGen m a-    }+noDataLossSpec ::+  forall a.+  (Show a, Ord a, GenValid a) =>+  ItemMergeStrategy a ->+  Spec+noDataLossSpec strat = do+  let mergeFunc = mergeSyncResponseUsingStrategy strat+  it "does not lose data after a conflict occurs"+    $ forAllValid+    $ \uuid ->+      forAllValid $ \time1 ->+        forAllValid $ \i1 ->+          forAllValid $ \i2 ->+            forAllValid $ \i3 ->+              evalDM $ do+                let sstore1 = ServerStore {serverStoreItems = M.singleton uuid (Timed i1 time1)}+                -- The server has an item+                -- The first client has synced it, and modified it.+                let cAstore1 =+                      initialClientStore+                        { clientStoreSyncedButChangedItems = M.singleton uuid (Timed i2 time1)+                        }+                -- The second client has synced it too, and modified it too.+                let cBstore1 =+                      initialClientStore+                        { clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)+                        }+                -- Client A makes sync request 1+                let req1 = makeSyncRequest @ClientId @UUID cAstore1+                -- The server processes sync request 1+                (resp1, sstore2) <- processServerSync genD sstore1 req1+                let time2 = incrementServerTime time1+                -- The server updates the item accordingly+                lift $ do+                  resp1+                    `shouldBe` (emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2})+                  sstore2+                    `shouldBe` (ServerStore {serverStoreItems = M.singleton uuid (Timed i2 time2)})+                -- Client A merges the response+                let cAstore2 = mergeFunc cAstore1 resp1+                lift $+                  cAstore2+                    `shouldBe` (initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed i2 time2)})+                -- Client B makes sync request 2+                let req2 = makeSyncRequest cBstore1+                -- The server processes sync request 2+                (resp2, sstore3) <- processServerSync genD sstore2 req2+                -- The server reports a conflict and does not change its store+                lift $ do+                  resp2+                    `shouldBe` (emptySyncResponse {syncResponseConflicts = M.singleton uuid (Timed i2 time2)})+                  sstore3 `shouldBe` sstore2+                -- Client B merges the response+                let cBstore2 = mergeSyncResponseFromClient @ClientId @UUID cBstore1 resp2+                -- Client does not update, but keeps its conflict+                -- Client A and Client B now *do not* have the same store+                lift $+                  cBstore2+                    `shouldBe` ( initialClientStore+                                   { clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)+                                   }+                               )++noDivergenceSpec ::+  forall a.+  (Show a, Ord a, GenValid a) =>+  ItemMergeStrategy a ->+  Spec+noDivergenceSpec strat = do+  let mergeFunc = mergeSyncResponseUsingStrategy strat+  it "does not diverge after a conflict occurs"+    $ forAllValid+    $ \uuid ->+      forAllValid $ \time1 ->+        forAllValid $ \iS ->+          forAllValid $ \iA ->+            forAllValid $ \iB ->+              evalDM $ do+                let sstore1 = ServerStore {serverStoreItems = M.singleton uuid (Timed iS time1)}+                -- The server has an item+                -- The first client has synced it, and modified it.+                let cAstore1 =+                      initialClientStore+                        { clientStoreSyncedButChangedItems = M.singleton uuid (Timed iA time1)+                        }+                -- The second client has synced it too, and modified it too.+                let cBstore1 =+                      initialClientStore+                        { clientStoreSyncedButChangedItems = M.singleton uuid (Timed iB time1)+                        }+                -- Client A makes sync request 1+                let req1 = makeSyncRequest @ClientId @UUID cAstore1+                -- The server processes sync request 1+                (resp1, sstore2) <- processServerSync genD sstore1 req1+                let time2 = incrementServerTime time1+                -- The server updates the item accordingly+                lift $ do+                  resp1+                    `shouldBe` (emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2})+                  sstore2+                    `shouldBe` (ServerStore {serverStoreItems = M.singleton uuid (Timed iA time2)})+                -- Client A merges the response+                let cAstore2 = mergeFunc cAstore1 resp1+                -- Client A has the item from the server because there was no conflict.+                lift $+                  cAstore2+                    `shouldBe` initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed iA time2)}+                -- Client B makes sync request 2+                let req2 = makeSyncRequest cBstore1+                -- The server processes sync request 2+                (resp2, sstore3) <- processServerSync genD sstore2 req2+                -- The server reports a conflict and does not change its store+                lift $ do+                  resp2+                    `shouldBe` (emptySyncResponse {syncResponseConflicts = M.singleton uuid (Timed iA time2)})+                  sstore3 `shouldBe` sstore2+                -- Client B merges the response+                let cBstore2 = mergeFunc cBstore1 resp2+                lift $ do+                  let expected = case itemMergeStrategyMergeChangeConflict strat iB iA of+                        KeepLocal -> initialClientStore {clientStoreSyncedButChangedItems = M.singleton uuid (Timed iB time1)}+                        TakeRemote -> initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed iA time2)}+                        Merged im -> initialClientStore {clientStoreSyncedButChangedItems = M.singleton uuid (Timed im time2)}+                  cBstore2+                    `shouldBe` expected+                -- In case of a previous merge, the synced item will still be changed, so we need to sync again with B and then with A+                let req3 = makeSyncRequest cBstore2+                (resp3, sstore4) <- processServerSync genD sstore3 req3+                let cBstore3 = mergeFunc cBstore2 resp3+                let req4 = makeSyncRequest cAstore2+                (resp4, _) <- processServerSync genD sstore4 req4+                let cAstore3 = mergeFunc cAstore2 resp4+                lift $+                  cBstore3 `shouldBe` cAstore3++emptyResponseSpec ::+  forall a.+  (Show a, Eq a, Ord a, GenValid a) =>+  ItemMergeStrategy a ->+  Spec+emptyResponseSpec strat = do+  let mergeFunc = mergeSyncResponseUsingStrategy strat+  it "is returns an empty response on the second sync with no modifications"+    $ forAllValid+    $ \cstore1 ->+      forAllValid $ \sstore1 ->+        evalDM $ do+          let req1 = makeSyncRequest @ClientId @UUID cstore1+          (resp1, sstore2) <- processServerSync genD sstore1 req1+          let cstore2 = mergeFunc cstore1 resp1+              req2 = makeSyncRequest cstore2+          (resp2, _) <- processServerSync genD sstore2 req2+          lift $ resp2 `shouldBe` emptySyncResponse++noDifferentExceptForConflicts ::+  forall a.+  (Show a, Eq a, Ord a, GenValid a) =>+  ItemMergeStrategy a ->+  ItemMergeStrategy a ->+  Spec+noDifferentExceptForConflicts strat1 strat2 = do+  let mergeFunc1 = mergeSyncResponseUsingStrategy strat1+      mergeFunc2 = mergeSyncResponseUsingStrategy strat2+  describe "mergeSyncResponseFromServer"+    $ it "only differs from mergeSyncResponseFromClient on conflicts"+    $ forAllValid+    $ \cstore ->+      forAllValid $ \sresp@SyncResponse {..} -> do+        let cstoreA = mergeFunc1 (cstore :: ClientStore ClientId UUID a) sresp+            cstoreB = mergeFunc2 cstore sresp+        if cstoreA == cstoreB+          then pure ()+          else+            unless+              ( or+                  [ not (M.null syncResponseConflicts),+                    not (M.null syncResponseConflictsClientDeleted),+                    not (S.null syncResponseConflictsServerDeleted)+                  ]+              )+              $ expectationFailure+              $ unlines+                [ "There was a difference between mergeFunc1 and mergeFunc2 that was somehow unrelated to the conflicts:",+                  "syncResponseConflicts:",+                  ppShow syncResponseConflicts,+                  "syncResponseConflictsClientDeleted:",+                  ppShow syncResponseConflictsClientDeleted,+                  "syncResponseConflictsServerDeleted:",+                  ppShow syncResponseConflictsServerDeleted,+                  "client store after mergeFunc1:",+                  ppShow cstoreA,+                  "client store after mergeFunc2:",+                  ppShow cstoreB+                ]++newtype D m a+  = D+      { unD :: StateT StdGen m a+      }   deriving (Generic, Functor, Applicative, Monad, MonadState StdGen, MonadTrans, MonadIO)  evalD :: D Identity a -> a
test/Data/Mergeful/ItemSpec.hs view
@@ -1,19 +1,19 @@+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeApplications #-}  module Data.Mergeful.ItemSpec-  ( spec-  ) where+  ( spec,+  )+where +import Data.GenValidity.Mergeful.Item ()+import Data.Mergeful.Item+import Data.Mergeful.Timed import Test.Hspec import Test.QuickCheck import Test.Validity import Test.Validity.Aeson -import Data.Mergeful.Item-import Data.Mergeful.Timed--import Data.GenValidity.Mergeful.Item ()- {-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}  forAllSubsequent :: Testable prop => ((ServerTime, ServerTime) -> prop) -> Property@@ -31,13 +31,21 @@   jsonSpecOnValid @(ItemSyncRequest Int)   genValidSpec @(ItemSyncResponse Int)   jsonSpecOnValid @(ItemSyncResponse Int)-  describe "makeItemSyncRequest" $-    it "produces valid requests" $ producesValidsOnValids (makeItemSyncRequest @Int)-  describe "mergeItemSyncResponseRaw" $-    it "produces valid client stores" $ producesValidsOnValids2 (mergeItemSyncResponseRaw @Int)-  describe "mergeItemSyncResponseIgnoreProblems" $-    it "produces valid client stores" $-    producesValidsOnValids2 (mergeItemSyncResponseIgnoreProblems @Int)+  describe "makeItemSyncRequest"+    $ it "produces valid requests"+    $ producesValidsOnValids (makeItemSyncRequest @Int)+  describe "mergeItemSyncResponseRaw"+    $ it "produces valid client stores"+    $ producesValidsOnValids2 (mergeItemSyncResponseRaw @Int)+  describe "mergeItemSyncResponseFromServer"+    $ it "produces valid client stores"+    $ producesValidsOnValids2 (mergeItemSyncResponseFromServer @Int)+  describe "mergeItemSyncResponseFromClient"+    $ it "produces valid client stores"+    $ producesValidsOnValids2 (mergeItemSyncResponseFromClient @Int)+  describe "mergeItemSyncResponseUsingCRDT"+    $ it "produces valid client stores"+    $ producesValidsOnValids2 (mergeItemSyncResponseUsingCRDT @Int max)   describe "processServerItemSync" $ do     it "produces valid responses and stores" $ producesValidsOnValids2 (processServerItemSync @Int)     it "makes no changes if the sync request reflects the state of the empty server" $ do@@ -46,8 +54,9 @@       let (resp, store2) = processServerItemSync @Int store1 req       store2 `shouldBe` store1       resp `shouldBe` ItemSyncResponseInSyncEmpty-    it "makes no changes if the sync request reflects the state of the full server" $-      forAllValid $ \i ->+    it "makes no changes if the sync request reflects the state of the full server"+      $ forAllValid+      $ \i ->         forAllValid $ \st -> do           let store1 = ServerFull $ Timed i st               req = ItemSyncRequestKnown st@@ -55,16 +64,18 @@           store2 `shouldBe` store1           resp `shouldBe` ItemSyncResponseInSyncFull     describe "Client changes" $ do-      it "adds the item that the client tells the server to add" $-        forAllValid $ \i -> do+      it "adds the item that the client tells the server to add"+        $ forAllValid+        $ \i -> do           let store1 = ServerEmpty               req = ItemSyncRequestNew i           let (resp, store2) = processServerItemSync @Int store1 req           let time = initialServerTime           store2 `shouldBe` ServerFull (Timed i time)           resp `shouldBe` ItemSyncResponseClientAdded time-      it "changes the item that the client tells the server to change" $-        forAllValid $ \i ->+      it "changes the item that the client tells the server to change"+        $ forAllValid+        $ \i ->           forAllValid $ \j ->             forAllValid $ \st -> do               let store1 = ServerFull (Timed i st)@@ -73,8 +84,9 @@               let time = incrementServerTime st               store2 `shouldBe` ServerFull (Timed j time)               resp `shouldBe` ItemSyncResponseClientChanged time-      it "deletes the item that the client tells the server to delete" $-        forAllValid $ \i ->+      it "deletes the item that the client tells the server to delete"+        $ forAllValid+        $ \i ->           forAllValid $ \st -> do             let store1 = ServerFull (Timed i st)                 req = ItemSyncRequestDeletedLocally st@@ -82,32 +94,36 @@             store2 `shouldBe` ServerEmpty             resp `shouldBe` ItemSyncResponseClientDeleted     describe "Server changes" $ do-      it "tells the client that there is a new item at the server side" $-        forAllValid $ \i ->+      it "tells the client that there is a new item at the server side"+        $ forAllValid+        $ \i ->           forAllValid $ \st -> do             let store1 = ServerFull (Timed i st)                 req = ItemSyncRequestPoll             let (resp, store2) = processServerItemSync @Int store1 req             store2 `shouldBe` store1             resp `shouldBe` ItemSyncResponseServerAdded (Timed i st)-      it "tells the client that there is a modified item at the server side" $-        forAllValid $ \i ->+      it "tells the client that there is a modified item at the server side"+        $ forAllValid+        $ \i ->           forAllSubsequent $ \(st, st') -> do             let store1 = ServerFull (Timed i st')                 req = ItemSyncRequestKnown st             let (resp, store2) = processServerItemSync @Int store1 req             store2 `shouldBe` store1             resp `shouldBe` ItemSyncResponseServerChanged (Timed i st')-      it "tells the client that there is a deleted item at the server side" $-        forAllValid $ \st -> do+      it "tells the client that there is a deleted item at the server side"+        $ forAllValid+        $ \st -> do           let store1 = ServerEmpty               req = ItemSyncRequestKnown st           let (resp, store2) = processServerItemSync @Int store1 req           store2 `shouldBe` store1           resp `shouldBe` ItemSyncResponseServerDeleted     describe "Conflicts" $ do-      it "notices a conflict if the client and server are trying to sync different items" $-        forAllValid $ \i ->+      it "notices a conflict if the client and server are trying to sync different items"+        $ forAllValid+        $ \i ->           forAllValid $ \j ->             forAllSubsequent $ \(st, st') -> do               let store1 = ServerFull (Timed i st')@@ -116,8 +132,9 @@               store2 `shouldBe` store1               resp `shouldBe` ItemSyncResponseConflict (Timed i st')       it-        "notices a server-deleted-conflict if the client has a deleted item and server has a modified item" $-        forAllValid $ \i ->+        "notices a server-deleted-conflict if the client has a deleted item and server has a modified item"+        $ forAllValid+        $ \i ->           forAllSubsequent $ \(st, st') -> do             let store1 = ServerFull (Timed i st')                 req = ItemSyncRequestDeletedLocally st@@ -125,8 +142,9 @@             store2 `shouldBe` store1             resp `shouldBe` ItemSyncResponseConflictClientDeleted (Timed i st')       it-        "notices a server-deleted-conflict if the client has a modified item and server has no item" $-        forAllValid $ \i ->+        "notices a server-deleted-conflict if the client has a modified item and server has no item"+        $ forAllValid+        $ \i ->           forAllValid $ \st -> do             let store1 = ServerEmpty                 req = ItemSyncRequestKnownButChanged (Timed i st)@@ -134,139 +152,184 @@             store2 `shouldBe` store1             resp `shouldBe` ItemSyncResponseConflictServerDeleted   describe "syncing" $ do-    it "it always possible to add an item from scratch" $-      forAllValid $ \i -> do-        let cstore1 = ClientAdded (i :: Int)-        let sstore1 = ServerEmpty-        let req1 = makeItemSyncRequest cstore1-            (resp1, sstore2) = processServerItemSync sstore1 req1-            cstore2 = mergeItemSyncResponseIgnoreProblems cstore1 resp1-        let time = initialServerTime-        resp1 `shouldBe` ItemSyncResponseClientAdded time-        sstore2 `shouldBe` ServerFull (Timed i time)-        cstore2 `shouldBe` ClientItemSynced (Timed i time)-    it "succesfully syncs an addition across to a second client" $-      forAllValid $ \i -> do-        let cAstore1 = ClientAdded i-        -- Client B is empty-        let cBstore1 = ClientEmpty-        -- The server is empty-        let sstore1 = ServerEmpty-        -- Client A makes sync request 1-        let req1 = makeItemSyncRequest cAstore1-        -- The server processes sync request 1-        let (resp1, sstore2) = processServerItemSync @Int sstore1 req1-        let time = initialServerTime-        resp1 `shouldBe` ItemSyncResponseClientAdded time-        sstore2 `shouldBe` ServerFull (Timed i time)-        -- Client A merges the response-        let cAstore2 = mergeItemSyncResponseIgnoreProblems cAstore1 resp1-        cAstore2 `shouldBe` ClientItemSynced (Timed i time)-        -- Client B makes sync request 2-        let req2 = makeItemSyncRequest cBstore1-        -- The server processes sync request 2-        let (resp2, sstore3) = processServerItemSync sstore2 req2-        resp2 `shouldBe` ItemSyncResponseServerAdded (Timed i time)-        sstore3 `shouldBe` ServerFull (Timed i time)-        -- Client B merges the response-        let cBstore2 = mergeItemSyncResponseIgnoreProblems cBstore1 resp2-        cBstore2 `shouldBe` ClientItemSynced (Timed i time)-        -- Client A and Client B now have the same store-        cAstore2 `shouldBe` cBstore2-    it "succesfully syncs a modification across to a second client" $-      forAllValid $ \time1 ->-        forAllValid $ \i ->-          forAllValid $ \j -> do-            let cAstore1 = ClientItemSynced (Timed i time1)-            -- Client B had synced that same item, but has since modified it-            let cBstore1 = ClientItemSyncedButChanged (Timed j time1)-            -- The server is has the item that both clients had before-            let sstore1 = ServerFull (Timed i time1)-            -- Client B makes sync request 1-            let req1 = makeItemSyncRequest cBstore1-            -- The server processes sync request 1-            let (resp1, sstore2) = processServerItemSync @Int sstore1 req1-            let time2 = incrementServerTime time1-            resp1 `shouldBe` ItemSyncResponseClientChanged time2-            sstore2 `shouldBe` ServerFull (Timed j time2)-            -- Client B merges the response-            let cBstore2 = mergeItemSyncResponseIgnoreProblems cBstore1 resp1-            cBstore2 `shouldBe` ClientItemSynced (Timed j time2)-            -- Client A makes sync request 2-            let req2 = makeItemSyncRequest cAstore1-            -- The server processes sync request 2-            let (resp2, sstore3) = processServerItemSync sstore2 req2-            resp2 `shouldBe` ItemSyncResponseServerChanged (Timed j time2)-            sstore3 `shouldBe` ServerFull (Timed j time2)-            -- Client A merges the response-            let cAstore2 = mergeItemSyncResponseIgnoreProblems cAstore1 resp2-            cAstore2 `shouldBe` ClientItemSynced (Timed j time2)-            -- Client A and Client B now have the same store-            cAstore2 `shouldBe` cBstore2-    it "succesfully syncs a deletion across to a second client" $-      forAllValid $ \time1 ->-        forAllValid $ \i -> do+    describe "fromServer" $ do+      syncingSpec @Int mergeFromServer+      emptyResponseSpec @Int mergeFromServer+    describe "fromClient" $ do+      syncingSpec @Int mergeFromClient+      xdescribe "does not hold" $ emptyResponseSpec @Int mergeFromClient+    describe "gadt" $ do+      let merge = mergeUsingCRDT @Int max+      syncingSpec merge+      emptyResponseSpec merge++syncingSpec ::+  forall a.+  (Show a, Eq a, GenValid a) =>+  (ClientItem a -> ItemMergeResult a -> ClientItem a) ->+  Spec+syncingSpec mergeStrategy = do+  let mergeSyncResponse ci = mergeStrategy ci . mergeItemSyncResponseRaw ci+  it "it always possible to add an item from scratch"+    $ forAllValid+    $ \i -> do+      let cstore1 = ClientAdded i+      let sstore1 = ServerEmpty+      let req1 = makeItemSyncRequest cstore1+          (resp1, sstore2) = processServerItemSync sstore1 req1+          cstore2 = mergeSyncResponse cstore1 resp1+      let time = initialServerTime+      resp1 `shouldBe` ItemSyncResponseClientAdded time+      sstore2 `shouldBe` ServerFull (Timed i time)+      cstore2 `shouldBe` ClientItemSynced (Timed i time)+  it "succesfully syncs an addition across to a second client"+    $ forAllValid+    $ \i -> do+      let cAstore1 = ClientAdded i+      -- Client B is empty+      let cBstore1 = ClientEmpty+      -- The server is empty+      let sstore1 = ServerEmpty+      -- Client A makes sync request 1+      let req1 = makeItemSyncRequest cAstore1+      -- The server processes sync request 1+      let (resp1, sstore2) = processServerItemSync sstore1 req1+      let time = initialServerTime+      resp1 `shouldBe` ItemSyncResponseClientAdded time+      sstore2 `shouldBe` ServerFull (Timed i time)+      -- Client A merges the response+      let cAstore2 = mergeSyncResponse cAstore1 resp1+      cAstore2 `shouldBe` ClientItemSynced (Timed i time)+      -- Client B makes sync request 2+      let req2 = makeItemSyncRequest cBstore1+      -- The server processes sync request 2+      let (resp2, sstore3) = processServerItemSync sstore2 req2+      resp2 `shouldBe` ItemSyncResponseServerAdded (Timed i time)+      sstore3 `shouldBe` ServerFull (Timed i time)+      -- Client B merges the response+      let cBstore2 = mergeSyncResponse cBstore1 resp2+      cBstore2 `shouldBe` ClientItemSynced (Timed i time)+      -- Client A and Client B now have the same store+      cAstore2 `shouldBe` cBstore2+  it "succesfully syncs a modification across to a second client"+    $ forAllValid+    $ \time1 ->+      forAllValid $ \i ->+        forAllValid $ \j -> do           let cAstore1 = ClientItemSynced (Timed i time1)-          -- Client B had synced that same item, but has since deleted it-          let cBstore1 = ClientDeleted time1-          -- The server still has the undeleted item+          -- Client B had synced that same item, but has since modified it+          let cBstore1 = ClientItemSyncedButChanged (Timed j time1)+          -- The server is has the item that both clients had before           let sstore1 = ServerFull (Timed i time1)           -- Client B makes sync request 1           let req1 = makeItemSyncRequest cBstore1           -- The server processes sync request 1-          let (resp1, sstore2) = processServerItemSync @Int sstore1 req1-          resp1 `shouldBe` ItemSyncResponseClientDeleted-          sstore2 `shouldBe` ServerEmpty+          let (resp1, sstore2) = processServerItemSync sstore1 req1+          let time2 = incrementServerTime time1+          resp1 `shouldBe` ItemSyncResponseClientChanged time2+          sstore2 `shouldBe` ServerFull (Timed j time2)           -- Client B merges the response-          let cBstore2 = mergeItemSyncResponseIgnoreProblems cBstore1 resp1-          cBstore2 `shouldBe` ClientEmpty+          let cBstore2 = mergeSyncResponse cBstore1 resp1+          cBstore2 `shouldBe` ClientItemSynced (Timed j time2)           -- Client A makes sync request 2           let req2 = makeItemSyncRequest cAstore1           -- The server processes sync request 2           let (resp2, sstore3) = processServerItemSync sstore2 req2-          resp2 `shouldBe` ItemSyncResponseServerDeleted-          sstore3 `shouldBe` ServerEmpty-          -- Client A merges the response-          let cAstore2 = mergeItemSyncResponseIgnoreProblems cAstore1 resp2-          cAstore2 `shouldBe` ClientEmpty-          -- Client A and Client B now have the same store-          cAstore2 `shouldBe` cBstore2-    it "does not run into a conflict if two clients both try to sync a deletion" $-      forAllValid $ \time1 ->-        forAllValid $ \i -> do-          let cAstore1 = ClientDeleted time1-          -- Both client a and client b delete an item.-          let cBstore1 = ClientDeleted time1-          -- The server still has the undeleted item-          let sstore1 = ServerFull (Timed i time1)-          -- Client A makes sync request 1-          let req1 = makeItemSyncRequest cAstore1-          -- The server processes sync request 1-          let (resp1, sstore2) = processServerItemSync @Int sstore1 req1-          resp1 `shouldBe` ItemSyncResponseClientDeleted-          sstore2 `shouldBe` ServerEmpty+          resp2 `shouldBe` ItemSyncResponseServerChanged (Timed j time2)+          sstore3 `shouldBe` ServerFull (Timed j time2)           -- Client A merges the response-          let cAstore2 = mergeItemSyncResponseIgnoreProblems cAstore1 resp1-          cAstore2 `shouldBe` ClientEmpty-          -- Client B makes sync request 2-          let req2 = makeItemSyncRequest cBstore1-          -- The server processes sync request 2-          let (resp2, sstore3) = processServerItemSync sstore2 req2-          resp2 `shouldBe` ItemSyncResponseClientDeleted-          sstore3 `shouldBe` ServerEmpty-          -- Client B merges the response-          let cBstore2 = mergeItemSyncResponseIgnoreProblems cBstore1 resp2-          cBstore2 `shouldBe` ClientEmpty+          let cAstore2 = mergeSyncResponse cAstore1 resp2+          cAstore2 `shouldBe` ClientItemSynced (Timed j time2)           -- Client A and Client B now have the same store           cAstore2 `shouldBe` cBstore2-    it "is idempotent with one client" $-      forAllValid $ \cstore1 ->-        forAllValid $ \sstore1 -> do-          let req1 = makeItemSyncRequest (cstore1 :: ClientItem Int)-              (resp1, sstore2) = processServerItemSync sstore1 req1-              cstore2 = mergeItemSyncResponseIgnoreProblems cstore1 resp1-              req2 = makeItemSyncRequest cstore2-              (resp2, sstore3) = processServerItemSync sstore2 req2-              cstore3 = mergeItemSyncResponseIgnoreProblems cstore2 resp2-          cstore2 `shouldBe` cstore3-          sstore2 `shouldBe` sstore3+  it "succesfully syncs a deletion across to a second client"+    $ forAllValid+    $ \time1 ->+      forAllValid $ \i -> do+        let cAstore1 = ClientItemSynced (Timed i time1)+        -- Client B had synced that same item, but has since deleted it+        let cBstore1 = ClientDeleted time1+        -- The server still has the undeleted item+        let sstore1 = ServerFull (Timed i time1)+        -- Client B makes sync request 1+        let req1 = makeItemSyncRequest cBstore1+        -- The server processes sync request 1+        let (resp1, sstore2) = processServerItemSync sstore1 req1+        resp1 `shouldBe` ItemSyncResponseClientDeleted+        sstore2 `shouldBe` ServerEmpty+        -- Client B merges the response+        let cBstore2 = mergeSyncResponse cBstore1 resp1+        cBstore2 `shouldBe` ClientEmpty+        -- Client A makes sync request 2+        let req2 = makeItemSyncRequest cAstore1+        -- The server processes sync request 2+        let (resp2, sstore3) = processServerItemSync sstore2 req2+        resp2 `shouldBe` ItemSyncResponseServerDeleted+        sstore3 `shouldBe` ServerEmpty+        -- Client A merges the response+        let cAstore2 = mergeSyncResponse cAstore1 resp2+        cAstore2 `shouldBe` ClientEmpty+        -- Client A and Client B now have the same store+        cAstore2 `shouldBe` cBstore2+  it "does not run into a conflict if two clients both try to sync a deletion"+    $ forAllValid+    $ \time1 ->+      forAllValid $ \i -> do+        let cAstore1 = ClientDeleted time1+        -- Both client a and client b delete an item.+        let cBstore1 = ClientDeleted time1+        -- The server still has the undeleted item+        let sstore1 = ServerFull (Timed i time1)+        -- Client A makes sync request 1+        let req1 = makeItemSyncRequest cAstore1+        -- The server processes sync request 1+        let (resp1, sstore2) = processServerItemSync sstore1 req1+        resp1 `shouldBe` ItemSyncResponseClientDeleted+        sstore2 `shouldBe` ServerEmpty+        -- Client A merges the response+        let cAstore2 = mergeSyncResponse cAstore1 resp1+        cAstore2 `shouldBe` ClientEmpty+        -- Client B makes sync request 2+        let req2 = makeItemSyncRequest cBstore1+        -- The server processes sync request 2+        let (resp2, sstore3) = processServerItemSync sstore2 req2+        resp2 `shouldBe` ItemSyncResponseClientDeleted+        sstore3 `shouldBe` ServerEmpty+        -- Client B merges the response+        let cBstore2 = mergeSyncResponse cBstore1 resp2+        cBstore2 `shouldBe` ClientEmpty+        -- Client A and Client B now have the same store+        cAstore2 `shouldBe` cBstore2+  it "is idempotent with one client"+    $ forAllValid+    $ \cstore1 ->+      forAllValid $ \sstore1 -> do+        let req1 = makeItemSyncRequest cstore1+            (resp1, sstore2) = processServerItemSync sstore1 req1+            cstore2 = mergeSyncResponse cstore1 resp1+            req2 = makeItemSyncRequest cstore2+            (resp2, sstore3) = processServerItemSync sstore2 req2+            cstore3 = mergeSyncResponse cstore2 resp2+        cstore3 `shouldBe` cstore2+        sstore3 `shouldBe` sstore2++emptyResponseSpec ::+  forall a.+  (Show a, Eq a, GenValid a) =>+  (ClientItem a -> ItemMergeResult a -> ClientItem a) ->+  Spec+emptyResponseSpec mergeStrategy = do+  let mergeSyncResponse ci = mergeStrategy ci . mergeItemSyncResponseRaw ci+  it "is returns an empty response on the second sync with no modifications"+    $ forAllValid+    $ \cstore1 ->+      forAllValid $ \sstore1 -> do+        let req1 = makeItemSyncRequest cstore1+            (resp1, sstore2) = processServerItemSync sstore1 req1+            cstore2 = mergeSyncResponse cstore1 resp1+            req2 = makeItemSyncRequest cstore2+            (resp2, _) = processServerItemSync sstore2 req2+        case resp2 of+          ItemSyncResponseInSyncEmpty -> pure ()+          ItemSyncResponseInSyncFull -> pure ()+          _ -> expectationFailure $ "Sync response was not empty: " <> show resp2
test/Data/Mergeful/TimedSpec.hs view
@@ -1,16 +1,15 @@ {-# LANGUAGE TypeApplications #-}  module Data.Mergeful.TimedSpec-  ( spec-  ) where+  ( spec,+  )+where +import Data.GenValidity.Mergeful.Item ()+import Data.Mergeful.Timed import Test.Hspec import Test.Validity import Test.Validity.Aeson--import Data.Mergeful.Timed--import Data.GenValidity.Mergeful.Item ()  spec :: Spec spec = do
test/Data/Mergeful/ValueSpec.hs view
@@ -1,19 +1,18 @@ {-# LANGUAGE TypeApplications #-}  module Data.Mergeful.ValueSpec-  ( spec-  ) where+  ( spec,+  )+where +import Data.GenValidity.Mergeful.Value ()+import Data.Mergeful.Timed+import Data.Mergeful.Value import Test.Hspec import Test.QuickCheck import Test.Validity import Test.Validity.Aeson -import Data.Mergeful.Timed-import Data.Mergeful.Value--import Data.GenValidity.Mergeful.Value ()- {-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}  forAllSubsequent :: Testable prop => ((ServerTime, ServerTime) -> prop) -> Property@@ -31,26 +30,30 @@   jsonSpecOnValid @(ValueSyncRequest Int)   genValidSpec @(ValueSyncResponse Int)   jsonSpecOnValid @(ValueSyncResponse Int)-  describe "makeValueSyncRequest" $-    it "produces valid requests" $ producesValidsOnValids (makeValueSyncRequest @Int)-  describe "mergeValueSyncResponseRaw" $-    it "produces valid client stores" $ producesValidsOnValids2 (mergeValueSyncResponseRaw @Int)-  describe "mergeValueSyncResponseIgnoreProblems" $-    it "produces valid client stores" $-    producesValidsOnValids2 (mergeValueSyncResponseIgnoreProblems @Int)+  describe "makeValueSyncRequest"+    $ it "produces valid requests"+    $ producesValidsOnValids (makeValueSyncRequest @Int)+  describe "mergeValueSyncResponseRaw"+    $ it "produces valid client stores"+    $ producesValidsOnValids2 (mergeValueSyncResponseRaw @Int)+  describe "mergeValueSyncResponseIgnoreProblems"+    $ it "produces valid client stores"+    $ producesValidsOnValids2 (mergeValueSyncResponseIgnoreProblems @Int)   describe "processServerValueSync" $ do     it "produces valid responses and stores" $ producesValidsOnValids2 (processServerValueSync @Int)-    it "makes no changes if the sync request reflects the state of the server" $-      forAllValid $ \i ->+    it "makes no changes if the sync request reflects the state of the server"+      $ forAllValid+      $ \i ->         forAllValid $ \st -> do           let store1 = ServerValue $ Timed i st               req = ValueSyncRequestKnown st           let (resp, store2) = processServerValueSync @Int store1 req           store2 `shouldBe` store1           resp `shouldBe` ValueSyncResponseInSync-    describe "Client changes" $-      it "changes the item that the client tells the server to change" $-      forAllValid $ \i ->+    describe "Client changes"+      $ it "changes the item that the client tells the server to change"+      $ forAllValid+      $ \i ->         forAllValid $ \j ->           forAllValid $ \st -> do             let store1 = ServerValue (Timed i st)@@ -59,18 +62,20 @@             let time = incrementServerTime st             store2 `shouldBe` ServerValue (Timed j time)             resp `shouldBe` ValueSyncResponseClientChanged time-    describe "Server changes" $-      it "tells the client that there is a modified item at the server side" $-      forAllValid $ \i ->+    describe "Server changes"+      $ it "tells the client that there is a modified item at the server side"+      $ forAllValid+      $ \i ->         forAllSubsequent $ \(st, st') -> do           let store1 = ServerValue (Timed i st')               req = ValueSyncRequestKnown st           let (resp, store2) = processServerValueSync @Int store1 req           store2 `shouldBe` store1           resp `shouldBe` ValueSyncResponseServerChanged (Timed i st')-    describe "Conflicts" $-      it "notices a conflict if the client and server are trying to sync different items" $-      forAllValid $ \i ->+    describe "Conflicts"+      $ it "notices a conflict if the client and server are trying to sync different items"+      $ forAllValid+      $ \i ->         forAllValid $ \j ->           forAllSubsequent $ \(st, st') -> do             let store1 = ServerValue (Timed i st')@@ -79,8 +84,9 @@             store2 `shouldBe` store1             resp `shouldBe` ValueSyncResponseConflict (Timed i st')   describe "syncing" $ do-    it "succesfully syncs a modification across to a second client" $-      forAllValid $ \time1 ->+    it "succesfully syncs a modification across to a second client"+      $ forAllValid+      $ \time1 ->         forAllValid $ \i ->           forAllValid $ \j -> do             let cAstore1 = ClientValue (Timed i time1) NotChanged@@ -109,8 +115,9 @@             cAstore2 `shouldBe` ClientValue (Timed j time2) NotChanged             -- Client A and Client B now have the same store             cAstore2 `shouldBe` cBstore2-    it "is idempotent with one client" $-      forAllValid $ \cstore1 ->+    it "is idempotent with one client"+      $ forAllValid+      $ \cstore1 ->         forAllValid $ \sstore1 -> do           let req1 = makeValueSyncRequest (cstore1 :: ClientValue Int)               (resp1, sstore2) = processServerValueSync sstore1 req1