packages feed

genvalidity-mergeless-0.0.0.0: test/Data/MergelessSpec.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Data.MergelessSpec
    ( spec
    ) where

import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Data.Time
import qualified Data.UUID.Typed as Typed
import GHC.Generics (Generic)
import System.Random

import Control.Monad.State

import Test.Hspec
import Test.QuickCheck
import Test.Validity
import Test.Validity.Aeson

import Data.GenValidity.Mergeless ()
import Data.GenValidity.UUID.Typed ()
import Data.Mergeless
import Data.UUID.Typed

{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}

spec :: Spec
spec = do
    eqSpecOnValid @(Store Double Double)
    ordSpecOnValid @(Store Double Double)
    genValiditySpec @(Store Double Double)
    jsonSpecOnValid @(Store Double Double)
    eqSpecOnValid @(StoreItem Double Double)
    ordSpecOnValid @(StoreItem Double Double)
    genValiditySpec @(StoreItem Double Double)
    jsonSpecOnValid @(StoreItem Double Double)
    eqSpecOnValid @(Added Double)
    ordSpecOnValid @(Added Double)
    genValiditySpec @(Added Double)
    jsonSpecOnValid @(Added Double)
    eqSpecOnValid @(Synced Double Double)
    ordSpecOnValid @(Synced Double Double)
    genValiditySpec @(Synced Double Double)
    jsonSpecOnValid @(Synced Double Double)
    eqSpecOnValid @(SyncRequest Double Double)
    ordSpecOnValid @(SyncRequest Double Double)
    genValiditySpec @(SyncRequest Double Double)
    jsonSpecOnValid @(SyncRequest Double Double)
    eqSpecOnValid @(SyncResponse Double Double)
    ordSpecOnValid @(SyncResponse Double Double)
    genValiditySpec @(SyncResponse Double Double)
    jsonSpecOnValid @(SyncResponse Double Double)
    eqSpecOnValid @(CentralItem Double)
    ordSpecOnValid @(CentralItem Double)
    genValiditySpec @(CentralItem Double)
    jsonSpecOnValid @(CentralItem Double)
    eqSpecOnValid @(CentralStore Double Double)
    ordSpecOnValid @(CentralStore Double Double)
    genValiditySpec @(CentralStore Double Double)
    jsonSpecOnValid @(CentralStore Double Double)
    describe "makeSyncRequest" $
        it "produces valid sync requests" $
        producesValidsOnValids (makeSyncRequest @Double @Double)
    describe "mergeSyncResponse" $
        it "produces valid sync stores" $
        producesValidsOnValids2 (mergeSyncResponse @Double @Double)
    describe "processSyncWith" $ do
        it
            "makes no change if the sync request reflects the same local state with an empty sync response" $
            forAllValid $ \synct ->
                forAllValid $ \sis -> do
                    let cs = CentralStore sis
                    let (sr, cs') =
                            evalD $
                            processSyncWith @(UUID Double) @Double genD synct cs $
                            SyncRequest S.empty (M.keysSet sis) S.empty
                    cs' `shouldBe` cs
                    sr `shouldBe` SyncResponse S.empty S.empty S.empty
        it "deletes the deleted items" $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \sreq -> do
                        let (_, cs') =
                                evalD $
                                processSyncWith
                                    @(UUID Double)
                                    @Double
                                    genD
                                    synct
                                    cs
                                    sreq
                        syncRequestUndeletedItems sreq `shouldSatisfy`
                            (not .
                             any
                                 (`S.member` (M.keysSet $ centralStoreItems cs')))
        it "returns the items that were added in the sync response" $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \sreq -> do
                        let (sresp, _) =
                                evalD $
                                processSyncWith
                                    @(UUID Double)
                                    @Double
                                    genD
                                    synct
                                    cs
                                    sreq
                        S.map syncedValue (syncResponseAddedItems sresp) `shouldBe`
                            S.map addedValue (syncRequestAddedItems sreq)
        it "returns the single added item" $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \ai -> do
                        let (sresp, _) =
                                evalD $
                                processSyncWith
                                    @(UUID Double)
                                    @Double
                                    genD
                                    synct
                                    cs
                                    SyncRequest
                                    { syncRequestAddedItems = S.singleton ai
                                    , syncRequestSyncedItems = S.empty
                                    , syncRequestUndeletedItems = S.empty
                                    }
                        S.map syncedValue (syncResponseAddedItems sresp) `shouldBe`
                            S.singleton (addedValue ai)
        it "adds the items that were added" $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \sreq -> do
                        let (_, cs') =
                                evalD $
                                processSyncWith
                                    @(UUID Double)
                                    @Double
                                    genD
                                    synct
                                    cs
                                    sreq
                        S.map addedValue (syncRequestAddedItems sreq) `shouldSatisfy`
                            all
                                (`elem` (M.elems $
                                         M.map centralValue $
                                         centralStoreItems cs'))
        it
            "returns the single remotely added item if the sync request is empty and the central store has one item" $
            forAllValid $ \synct ->
                forAllValid $ \(uuid, ci) -> do
                    let (sresp, _) =
                            evalD $
                            processSyncWith
                                @(UUID Double)
                                @Double
                                genD
                                synct
                                (CentralStore $ M.singleton uuid ci)
                                SyncRequest
                                { syncRequestAddedItems = S.empty
                                , syncRequestSyncedItems = S.empty
                                , syncRequestUndeletedItems = S.empty
                                }
                    S.map syncedValue (syncResponseNewRemoteItems sresp) `shouldBe`
                        S.singleton (centralValue ci)
        it
            "returns all remotely added items when no items are locally added or deleted" $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \sis -> do
                        let (sresp, _) =
                                evalD $
                                processSyncWith
                                    @(UUID Double)
                                    @Double
                                    genD
                                    synct
                                    cs
                                    SyncRequest
                                    { syncRequestAddedItems = S.empty
                                    , syncRequestSyncedItems = sis
                                    , syncRequestUndeletedItems = S.empty
                                    }
                        S.map syncedUuid (syncResponseNewRemoteItems sresp) `shouldBe`
                            S.difference (M.keysSet $ centralStoreItems cs) sis
        it "returns all remotely added items when no items are locally deleted " $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \sis ->
                        forAllValid $ \ais -> do
                            let (sresp, _) =
                                    evalD $
                                    processSyncWith
                                        @(UUID Double)
                                        @Double
                                        genD
                                        synct
                                        cs
                                        SyncRequest
                                        { syncRequestAddedItems = ais
                                        , syncRequestSyncedItems = sis
                                        , syncRequestUndeletedItems = S.empty
                                        }
                            S.map syncedUuid (syncResponseNewRemoteItems sresp) `shouldBe`
                                S.difference
                                    (M.keysSet $ centralStoreItems cs)
                                    sis
        it
            "returns all remotely added items that weren't deleted when no items are locally added " $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \sis ->
                        forAllValid $ \dis -> do
                            let (sresp, _) =
                                    evalD $
                                    processSyncWith
                                        @(UUID Double)
                                        @Double
                                        genD
                                        synct
                                        cs
                                        SyncRequest
                                        { syncRequestAddedItems = S.empty
                                        , syncRequestSyncedItems = sis
                                        , syncRequestUndeletedItems = dis
                                        }
                            S.map syncedUuid (syncResponseNewRemoteItems sresp) `shouldBe`
                                S.difference
                                    (S.difference
                                         (M.keysSet $ centralStoreItems cs)
                                         dis)
                                    sis
        it "returns all remotely added items that weren't deleted" $
            forAllValid $ \synct ->
                forAllValid $ \cs ->
                    forAllValid $ \sreq -> do
                        let (sresp, _) =
                                evalD $
                                processSyncWith
                                    @(UUID Double)
                                    @Double
                                    genD
                                    synct
                                    cs
                                    sreq
                        S.map syncedUuid (syncResponseNewRemoteItems sresp) `shouldBe`
                            S.difference
                                (S.difference
                                     (M.keysSet $ centralStoreItems cs)
                                     (syncRequestUndeletedItems sreq))
                                (syncRequestSyncedItems sreq)
        it
            "successfully syncs two clients using a central store when using incrementing words" $
            forAllValid $ \store1 ->
                forAllValid $ \(synct1, synct2, synct3) -> do
                    let (s1, s2) =
                            evalI $ do
                                let central = CentralStore M.empty
                                let store2 = Store S.empty
                                let sreq1 = makeSyncRequest @Word @Double store1
                                (sresp1, central') <-
                                    processSyncWith genI synct1 central sreq1
                                let store1' = mergeSyncResponse store1 sresp1
                                let sreq2 = makeSyncRequest store2
                                (sresp2, central'') <-
                                    processSyncWith genI synct2 central' sreq2
                                let store2' = mergeSyncResponse store2 sresp2
                                let sreq3 = makeSyncRequest store1'
                                (sresp3, _) <-
                                    processSyncWith genI synct3 central'' sreq3
                                let store1'' = mergeSyncResponse store1' sresp3
                                pure (store1'', store2')
                    s1 `shouldBe` s2
        it
            "successfully syncs two clients using a central store when using deterministic UUIDs" $
            forAllValid $ \store1 ->
                forAllValid $ \(synct1, synct2, synct3) -> do
                    let (s1, s2) =
                            evalD $ do
                                let central = CentralStore M.empty
                                let store2 = Store S.empty
                                let sreq1 =
                                        makeSyncRequest
                                            @(UUID Double)
                                            @Double
                                            store1
                                (sresp1, central') <-
                                    processSyncWith genD synct1 central sreq1
                                let store1' = mergeSyncResponse store1 sresp1
                                let sreq2 = makeSyncRequest store2
                                (sresp2, central'') <-
                                    processSyncWith genD synct2 central' sreq2
                                let store2' = mergeSyncResponse store2 sresp2
                                let sreq3 = makeSyncRequest store1'
                                (sresp3, _) <-
                                    processSyncWith genD synct3 central'' sreq3
                                let store1'' = mergeSyncResponse store1' sresp3
                                pure (store1'', store2')
                    s1 `shouldBe` s2
        it
            "produces valid results when building up a central store from nothing using incrementing words" $
            forAllValid $ \tups ->
                shouldBeValid $
                evalI $ do
                    let initCentralStore = CentralStore M.empty
                    let go cs (store, synct) = do
                            let sreq = makeSyncRequest @Word @Double store
                            (_, central') <- processSyncWith genI synct cs sreq
                            pure central'
                    foldM
                        go
                        initCentralStore
                        (tups :: [(Store Word Double, UTCTime)])
        it
            "produces valid results when building up a central store from nothing using deterministic UUIDs" $
            forAllValid $ \tups ->
                shouldBeValid $
                evalD $ do
                    let initCentralStore = CentralStore M.empty
                    let go cs (store, synct) = do
                            let sreq =
                                    makeSyncRequest @(UUID Double) @Double store
                            (_, central') <- processSyncWith genD synct cs sreq
                            pure central'
                    foldM
                        go
                        initCentralStore
                        (tups :: [(Store (UUID Double) Double, UTCTime)])
        -- This property does not hold.
        xit "produces valid results when using incrementing words" $
            producesValidsOnValids3 $ \synct cs sr ->
                evalI $ processSyncWith @Word @Double genI synct cs sr
        it "produces valid results when using determinisitic UUIDs" $
            producesValidsOnValids3 $ \synct cs sr ->
                evalD $ processSyncWith @(UUID Double) @Double genD synct cs sr
        it "makes syncing idempotent with incrementing words" $
            forAllValid $ \synct1 ->
                forAll (genValid `suchThat` (>= synct1)) $ \synct2 ->
                    forAllValid $ \central1 ->
                        forAllValid $ \local1 -> do
                            let d1 = 0
                            let sreq1 = makeSyncRequest @Word @Double local1
                            let ((sresp1, central2), d2) =
                                    runI
                                        (processSyncWith
                                             genI
                                             synct1
                                             central1
                                             sreq1)
                                        d1
                            let local2 = mergeSyncResponse local1 sresp1
                            let sreq2 = makeSyncRequest local2
                            let ((sresp2, central3), _) =
                                    runI
                                        (processSyncWith
                                             genI
                                             synct2
                                             central2
                                             sreq2)
                                        d2
                            let local3 = mergeSyncResponse local2 sresp2
                            local2 `shouldBe` local3
                            central2 `shouldBe` central3
        it "makes syncing idempotent with deterministic UUIDs" $
            forAllValid $ \synct1 ->
                forAll (genValid `suchThat` (>= synct1)) $ \synct2 ->
                    forAllValid $ \central1 ->
                        forAllValid $ \local1 -> do
                            let d1 = mkStdGen 42
                            let sreq1 =
                                    makeSyncRequest
                                        @(UUID Double)
                                        @Double
                                        local1
                            let ((sresp1, central2), d2) =
                                    runD
                                        (processSyncWith
                                             genD
                                             synct1
                                             central1
                                             sreq1)
                                        d1
                            let local2 = mergeSyncResponse local1 sresp1
                            let sreq2 = makeSyncRequest local2
                            let ((sresp2, central3), _) =
                                    runD
                                        (processSyncWith
                                             genD
                                             synct2
                                             central2
                                             sreq2)
                                        d2
                            let local3 = mergeSyncResponse local2 sresp2
                            local2 `shouldBe` local3
                            central2 `shouldBe` central3
        it "makes syncing idempotent with random UUIDs" $
            forAllValid $ \synct1 ->
                forAll (genValid `suchThat` (>= synct1)) $ \synct2 ->
                    forAllValid $ \central1 ->
                        forAllValid $ \local1 -> do
                            let sreq1 =
                                    makeSyncRequest
                                        @(UUID Double)
                                        @Double
                                        local1
                            (sresp1, central2) <-
                                processSyncWith
                                    nextRandomUUID
                                    synct1
                                    central1
                                    sreq1
                            let local2 = mergeSyncResponse local1 sresp1
                            let sreq2 = makeSyncRequest local2
                            (sresp2, central3) <-
                                processSyncWith
                                    nextRandomUUID
                                    synct2
                                    central2
                                    sreq2
                            let local3 = mergeSyncResponse local2 sresp2
                            local2 `shouldBe` local3
                            central2 `shouldBe` central3
    describe "processSync" $
        it "makes syncing idempotent when using random UUIDs" $
        forAllValid $ \central1 ->
            forAllValid $ \local1 -> do
                let sreq1 = makeSyncRequest @(UUID Double) @Double local1
                (sresp1, central2) <- processSync nextRandomUUID central1 sreq1
                let local2 = mergeSyncResponse local1 sresp1
                let sreq2 = makeSyncRequest local2
                (sresp2, central3) <- processSync nextRandomUUID central2 sreq2
                let local3 = mergeSyncResponse local2 sresp2
                local2 `shouldBe` local3
                central2 `shouldBe` central3

newtype D a = D
    { unD :: State StdGen a
    } deriving (Generic, Functor, Applicative, Monad, MonadState StdGen)

evalD :: D a -> a
evalD d = fst $ runD d $ mkStdGen 42

runD :: D a -> StdGen -> (a, StdGen)
runD = runState . unD

genD :: D (Typed.UUID a)
genD = do
    r <- get
    let (u, r') = random r
    put r'
    pure u

newtype I a = I
    { unI :: State Word a
    } deriving (Generic, Functor, Applicative, Monad, MonadState Word)

evalI :: I a -> a
evalI i = fst $ runI i 0

runI :: I a -> Word -> (a, Word)
runI = runState . unI

genI :: I Word
genI = do
    i <- get
    modify succ
    pure i