packages feed

dvv-0.1.2.1: test/Spec.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-orphans #-}

import Algebra.PartialOrd (PartialOrd (..))
import Control.Monad (foldM)
import Data.DVV
import qualified Data.HashMap.Strict as Map
import Data.Hashable (Hashable)
import Test.Hspec hiding (context)
import Test.QuickCheck

-- Helper to make tests more readable
type ID = String

type Value = String

main :: IO ()
main = hspec $ do
  describe "DVV Operations" $ do
    it "should yield the correct size, values, and context for an empty DVV" $ do
      let d = EmptyDVV :: DVV ID Value
      size d `shouldBe` 0
      values d `shouldBe` []
      context d `shouldBe` mkVersionVector Map.empty

    it "should yield the correct size, values, and context for a singleton DVV" $ do
      let d = SingletonDVV "A" "v1" :: DVV ID Value
      size d `shouldBe` 1
      values d `shouldBe` ["v1"]
      context d `shouldBe` mkVersionVector (Map.fromList [("A", 1)])

    it "should handle events correctly (update)" $ do
      -- A starts with v1
      let d0 = SingletonDVV "A" "v1" :: DVV ID Value

      -- A updates with v2, knowing about d0
      let ctx0 = context d0
      let d1 = event d0 (Just ctx0) "A" "v2"

      size d1 `shouldBe` 1
      values d1 `shouldBe` ["v2"]
      context d1 `shouldBe` mkVersionVector (Map.fromList [("A", 2)])

      -- A updates again with v3
      let ctx1 = context d1
      let d2 = event d1 (Just ctx1) "A" "v3"

      size d2 `shouldBe` 1
      values d2 `shouldBe` ["v3"]
      context d2 `shouldBe` mkVersionVector (Map.fromList [("A", 3)])

    it "should handle concurrent updates (siblings)" $ do
      -- A starts with v1
      let d0 = SingletonDVV "A" "v1" :: DVV ID Value

      -- B also starts with v2 (concurrent to A:v1)
      let dOther = SingletonDVV "B" "v2" :: DVV ID Value

      -- Sync them
      let dSync = sync d0 dOther
      size dSync `shouldBe` 2
      values dSync `shouldContain` ["v1"]
      values dSync `shouldContain` ["v2"]
      context dSync `shouldBe` mkVersionVector (Map.fromList [("A", 1), ("B", 1)])

    it "should resolve siblings with event superseding" $ do
      -- Start with synced state having {A:1, v1} and {B:1, v2}
      let d0 = SingletonDVV "A" "v1" :: DVV ID Value
          dOther = SingletonDVV "B" "v2"
          dSync = sync d0 dOther

      -- A updates. A knows about the whole context of dSync.
      let ctx = context dSync
          dFinal = event dSync (Just ctx) "A" "v3"

      size dFinal `shouldBe` 1
      values dFinal `shouldBe` ["v3"]
      -- Dot should be A:2. Context should be {A: 2, B: 1}
      context dFinal `shouldBe` mkVersionVector (Map.fromList [("A", 2), ("B", 1)])

    it "should keep concurrent updates if context is missing information" $ do
      -- B makes an update v1
      let dB = SingletonDVV "B" "v1" :: DVV ID Value

      -- A makes an update v2, but A DOES NOT KNOW about B.
      let dA = SingletonDVV "A" "v2"

      -- Sync them. Should have both.
      let dSync = sync dA dB

      -- A updates again, using only A's context.
      let ctxA = context dA
          dFinal = event dSync (Just ctxA) "A" "v3"

      -- v2 (A:1) is superseded. v1 (B:1) is NOT.
      size dFinal `shouldBe` 2
      values dFinal `shouldContain` ["v1"]
      values dFinal `shouldContain` ["v3"]
      context dFinal `shouldBe` mkVersionVector (Map.fromList [("A", 2), ("B", 1)])

    it "should handle multiple siblings from different actors" $ do
      let dA = SingletonDVV "A" "v1" :: DVV ID Value
          dB = SingletonDVV "B" "v2"
          dC = SingletonDVV "C" "v3"

          dAB = sync dA dB
          dABC = sync dAB dC

      size dABC `shouldBe` 3
      context dABC
        `shouldBe` mkVersionVector (Map.fromList [("A", 1), ("B", 1), ("C", 1)])

    it "should prune old history" $ do
      -- Create a DVV with history {A: 10, B: 5}
      let d0 = SingletonDVV "A" "v1" :: DVV ID Value
          d10 = foldl (\d i -> event d (Just $ context d) "A" (show @Int i)) d0 [2 .. 10]
          dB = SingletonDVV "B" "vB"
          dB5 = foldl (\d i -> event d (Just $ context d) "B" (show @Int i)) dB [2 .. 5]

          dSync = sync d10 dB5

      context dSync `shouldBe` mkVersionVector (Map.fromList [("A", 10), ("B", 5)])

      -- To test prune properly, we need history that IS NOT supported by a value.
      -- Make A supersede B:5 first (so B:5 is removed from values), but B:5 remains in history.
      let dVoidB = event dSync (Just $ context dSync) "A" "superseding-everything"

      -- Now A:11. B:5 is superseded (removed from values).
      -- Values: {A:11}. Vector: {A:11, B:5} (merged context).
      context dVoidB `shouldBe` mkVersionVector (Map.fromList [("A", 11), ("B", 5)])

      let dPruned = prune 8 dVoidB
      -- Now B:5 < 8, so it should be removed from vector.
      -- A:11 >= 8, keeps A:11.
      context dPruned `shouldBe` mkVersionVector (Map.fromList [("A", 11)])

    it "should reconcile siblings into a single value with new dot" $ do
      let dA = SingletonDVV "A" "v1" :: DVV ID Value
          dB = SingletonDVV "B" "v2"
          dSync = sync dA dB

      size dSync `shouldBe` 2

      -- Reconcile with "concatenation"
      let dReconciled = reconcile (\a b -> a ++ b) "C" dSync

      size dReconciled `shouldBe` 1
      let val = head (values dReconciled)
      val `shouldSatisfy` (\v -> v == "v1v2" || v == "v2v1")

      -- Context should now include C:1
      context dReconciled
        `shouldBe` mkVersionVector (Map.fromList [("A", 1), ("B", 1), ("C", 1)])

    it "should use Last-Write-Wins (lww) to pick a winner" $ do
      let dA = SingletonDVV "A" "apple" :: DVV ID Value
          dB = SingletonDVV "B" "banana"
          dSync = sync dA dB

      -- lww with string comparison (lexicographical)
      let dLWW = lww compare "A" dSync

      size dLWW `shouldBe` 1
      values dLWW `shouldBe` ["banana"] -- banana > apple

      -- Reconcile advances the dot for the actor performing the reconcile (A).
      -- Previous A was 1. New dot is A:2.
      -- B remains at 1 (as history).
      context dLWW `shouldBe` mkVersionVector (Map.fromList [("A", 2), ("B", 1)])

  describe "DVV Show Instance" $ do
    it "should show EmptyDVV as Erlang empty DVVSet" $ do
      show (EmptyDVV :: DVV ID Value) `shouldBe` "([],[])"

    it "should show SingletonDVV as Erlang DVVSet with one entry" $ do
      let d = SingletonDVV "A" "v1" :: DVV ID Value
      -- ( [("A", 1, ["v1"])], [] )
      show d `shouldBe` "([(\"A\",1,[\"v1\"])],[])"

    it "should show concurrent values grouped by actor" $ do
      let dA = SingletonDVV "A" "v1" :: DVV ID Value
          dB = SingletonDVV "B" "v2"
          d0 = sync dA dB
      -- Two concurrent values: A:1:v1 and B:1:v2
      show d0 `shouldBe` "([(\"A\",1,[\"v1\"]),(\"B\",1,[\"v2\"])],[])"

    it "should show causal history and concurrent values correctly" $ do
      let dA = SingletonDVV "A" "v1" :: DVV ID Value
          ctxA = context dA
          dB = event dA (Just ctxA) "B" "v2"
      -- A:1 is history, B:1 is the current value
      -- Format: [("A",1,[]), ("B",1,["v2"])]
      show dB `shouldBe` "([(\"A\",1,[]),(\"B\",1,[\"v2\"])],[])"

    it
      "should correctly handle the example scenario from the Erlang documentation example"
      $ do
        -- Image Step 1: C1 PUT v1 ~ {} -> State A (A, 1, [v1])
        let s0 = EmptyDVV :: DVV ID Value
            s1 = event s0 Nothing "A" "v1"
            s1' = SingletonDVV "A" "v1"
        s1 `shouldBe` s1'
        show s1 `shouldBe` "([(\"A\",1,[\"v1\"])],[])"

        -- Image Step 2: C2 PUT v2 ~ {} -> State A (A, 2, [v2, v1])
        -- C2's PUT is concurrent with C1's (context {})
        let emptyCtx = mkVersionVector Map.empty
            s2 = event s1 (Just emptyCtx) "A" "v2"
        show s2 `shouldBe` "([(\"A\",2,[\"v2\",\"v1\"])],[])"

        -- Image Step 3: C1 GET and then PUT v3 ~ (A, 1) -> State A (A, 3, [v3, v2])
        -- C1 provides context (A, 1), so v1 is superseded but v2 is not.
        let ctxA1 = mkVersionVector (Map.singleton "A" 1)
            s3 = event s2 (Just ctxA1) "A" "v3"
        show s3 `shouldBe` "([(\"A\",3,[\"v3\",\"v2\"])],[])"

  describe "DVV QuickCheck Properties" $ do
    it "sync is idempotent: sync d d == d (semantically)" $
      property $ \(d :: DVV ID Value) ->
        let synced = sync d d
         in (values synced, context synced) `shouldBe` (values d, context d)

    it "sync is commutative: sync d1 d2 == sync d2 d1" $
      property $ \(d1 :: DVV ID Value) (d2 :: DVV ID Value) ->
        sync d1 d2 `shouldBe` sync d2 d1

    it "sync is associative: sync (sync d1 d2) d3 == sync d1 (sync d2 d3)" $
      property $ \(d1 :: DVV ID Value) (d2 :: DVV ID Value) (d3 :: DVV ID Value) ->
        sync (sync d1 d2) d3 `shouldBe` sync d1 (sync d2 d3)

    it "sync with EmptyDVV is identity: sync EmptyDVV d == d" $
      property $ \(d :: DVV ID Value) ->
        sync EmptyDVV d `shouldBe` d

    it "context is monotonic after sync: context d1 <= context (sync d1 d2)" $
      property $ \(d1 :: DVV ID Value) (d2 :: DVV ID Value) ->
        let ctx1 = context d1
            ctxSync = context (sync d1 d2)
         in all
              (\(k, v) -> Map.lookup k (getVersionVectorCounts ctxSync) >= Just v)
              (Map.toList (getVersionVectorCounts ctx1))

    it "event increases context counter for the actor" $
      property $ \(d :: DVV ID Value) (actor :: ID) (val :: Value) ->
        let ctx = context d
            d' = event d (Just ctx) actor val
            ctx' = context d'
            oldCount = Map.findWithDefault 0 actor (getVersionVectorCounts ctx)
            newCount = Map.findWithDefault 0 actor (getVersionVectorCounts ctx')
         in newCount `shouldBe` (oldCount + 1)

    it "event with full context produces exactly one value" $
      property $ \(d :: DVV ID Value) (actor :: ID) (val :: Value) ->
        let ctx = context d
            d' = event d (Just ctx) actor val
         in size d' `shouldBe` 1

    it "values are preserved or superseded after sync" $
      property $ \(d1 :: DVV ID Value) (d2 :: DVV ID Value) ->
        let synced = sync d1 d2
         in size synced <= size d1 + size d2

    it "prune removes only old history, not active values" $
      property $ \(d :: DVV ID Value) (threshold :: Count) ->
        let pruned = prune threshold d
         in values pruned `shouldBe` values d

    it "reconcile reduces siblings to one value" $
      property $ \(d :: DVV ID Value) (actor :: ID) ->
        let reconciled = reconcile (++) actor d
         in size reconciled <= 1

    it "lww reduces siblings to one value" $
      property $ \(d :: DVV ID Value) (actor :: ID) ->
        let resolved = lww compare actor d
         in size resolved <= 1

    it "context contains all actor IDs from values" $
      property $ \(d :: DVV ID Value) ->
        case d of
          EmptyDVV -> True
          SingletonDVV actor _ -> Map.member actor (getVersionVectorCounts (context d))
          DVV _ vals ->
            let ctx = getVersionVectorCounts (context d)
                actors = [actor | Dot actor _ <- Map.keys vals]
             in all (`Map.member` ctx) actors

    describe "PartialOrd and Ord Properties" $ do
      it "Dot PartialOrd is only defined for same actor" $ do
        let d1 = Dot "A" 10 :: Dot ID
            d2 = Dot "B" 10 :: Dot ID
            d3 = Dot "A" 11 :: Dot ID
        comparable d1 d2 `shouldBe` False
        leq d1 d3 `shouldBe` True
        leq d3 d1 `shouldBe` False

      it "DVV PartialOrd follows causality (happensBefore)" $ do
        let d1 = SingletonDVV "A" "v1" :: DVV ID Value
            d2 = event d1 (Just $ context d1) "A" "v2"
            d3 = SingletonDVV "B" "v3" :: DVV ID Value
        leq d1 d2 `shouldBe` True
        leq d2 d1 `shouldBe` False
        comparable d1 d3 `shouldBe` False -- incomparable
      it "DVV Ord is consistent with PartialOrd" $
        property $ \(d1 :: DVV ID Value) (d2 :: DVV ID Value) ->
          if leq d1 d2
            then if d1 == d2 then compare d1 d2 == EQ else compare d1 d2 == LT
            else
              if leq d2 d1
                then compare d1 d2 == GT
                else compare d1 d2 /= EQ

      it "Replicates Erlang less_test scenarios" $ do
        -- A  = update(new_list(v1),[a]),
        let a = SingletonDVV "a" "v1" :: DVV ID Value
        let ctxA = context a

        -- B  = update(new_list(join(A),[v2]), a),
        -- Effectively: event on A (to get context) with "a" -> "v2"
        let b = event a (Just ctxA) "a" "v2"

        -- B2 = update(new_list(join(A),[v2]), b),
        -- Event on A, actor "b"
        let b2 = event a (Just ctxA) "b" "v2"

        -- B3 = update(new_list(join(A),[v2]), z),
        -- Event on A, actor "z"
        let b3 = event a (Just ctxA) "z" "v2"

        -- C  = update(new_list(join(B),[v3]), A, c),
        let ctxB = context b
        let c = event b (Just ctxB) "c" "v3"

        -- D  = update(new_list(join(C),[v4]), B2, d),
        -- Base is C's context. Update with actor "d". Context B2.
        -- C has {a:2, c:1}. B2 has {a:1, b:1}.
        -- C does not know b:1.
        -- B2 knows b:1.
        -- New DVV will merge contexts: {a:2, b:1, c:1}.
        -- Value v4 (d:1).
        -- Supersedes values in C and B2?
        -- C vals: {a:2, c:1} (if dots kept). v3.
        -- B2 vals: {b:1} v2.
        -- New val: v4 (d:1).
        -- Contexts merged.
        -- Check if v3 (c:1) is superseded by {a:2, b:1, c:1} + d:1?
        -- c:1 is in context. Yes.
        -- v2 (b:1) is in context. Yes.
        -- So D should have only v4.

        -- We construct D by simulating this "update from C base with B2 context":
        -- We assume we start with C (as it provides the base history 'join(C)').
        -- But we add B2's context.
        let ctxB2 = context b2
        let d = event c (Just ctxB2) "d" "v4"
        -- event merges ctxB2 into C's history.
        -- currentMax for "d" in C is 0. Next 1.
        -- Result DVV history: union(C, B2) = {a:2, b:1, c:1, d:1}.
        -- Values: v4.

        -- Assertions
        -- less(A, B) -> A <= B and A != B.
        leq a b `shouldBe` True
        a /= b `shouldBe` True

        -- less(A, C)
        leq a c `shouldBe` True
        a /= c `shouldBe` True

        -- less(B, C)
        leq b c `shouldBe` True
        b /= c `shouldBe` True

        -- less(B, D)
        leq b d `shouldBe` True
        b /= d `shouldBe` True

        -- less(B2, D)
        leq b2 d `shouldBe` True
        b2 /= d `shouldBe` True

        -- less(A, D)
        leq a d `shouldBe` True
        a /= d `shouldBe` True

        -- not less(B2, C) -> B2 not <= C
        leq b2 c `shouldBe` False

        -- not less(B, B2)
        leq b b2 `shouldBe` False

        -- not less(B2, B)
        leq b2 b `shouldBe` False

        -- not less(A, A) (strict) -> leq is True, but Eq.
        -- Erlang less is strict.
        leq a a `shouldBe` True

        -- not less(C, C)
        leq c c `shouldBe` True

        -- not less(D, B2)
        leq d b2 `shouldBe` False

        -- not less(B3, D)
        -- B3 {a:1, z:1}. D {a:2, b:1, c:1, d:1}.
        -- D does NOT know z:1.
        -- So B3 not <= D.
        leq b3 d `shouldBe` False

-- QuickCheck Arbitrary instances

instance
  (Arbitrary actorID, Hashable actorID, Arbitrary value) =>
  Arbitrary (DVV actorID value)
  where
  arbitrary = sized $ \n -> do
    if n <= 0
      then pure EmptyDVV
      else
        frequency
          [ (1, pure EmptyDVV)
          , (3, SingletonDVV <$> arbitrary <*> arbitrary)
          , (2, arbitraryDVV)
          ]
   where
    arbitraryDVV = do
      numActors <- chooseInt (1, 5)
      actors <- vectorOf numActors arbitrary
      -- Build up a DVV through events
      let base = EmptyDVV
      foldM addEvent base actors

    addEvent d actor = do
      val <- arbitrary
      let ctx = context d
      pure $ event d (Just ctx) actor val

  shrink EmptyDVV = []
  shrink (SingletonDVV _ _) = [EmptyDVV]
  shrink (DVV _ vals)
    | Map.null vals = [EmptyDVV]
    | otherwise =
        EmptyDVV : [SingletonDVV actor val | (Dot actor _, val) <- Map.toList vals]