packages feed

moonlight-delta-0.1.0.0: test/patch/PatchLaws.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ScopedTypeVariables #-}

module PatchLaws
  ( PatchChain (..),
    PatchStaleCase (..),
    patchLaws,
  )
where

import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import DeltaLaws (deltaNormalizeLaws)
import LawManifest
  ( lawManifestCase,
    lawProperty,
  )
import Moonlight.Core (IsLawName (..), constructorLawName)
import Moonlight.Delta.Patch
import Test.QuickCheck
  ( Gen,
    Property,
    counterexample,
    forAll,
    (===),
  )
import Test.Tasty (TestTree, testGroup)

data PatchChain key value = PatchChain
  { pcKey :: !key,
    pcOldValue :: !(Maybe value),
    pcMiddleValue :: !(Maybe value),
    pcNewValue :: !(Maybe value)
  }
  deriving stock (Eq, Show)

data PatchStaleCase key value = PatchStaleCase
  { pscKey :: !key,
    pscExpectedValue :: !(Maybe value),
    pscActualValue :: !(Maybe value),
    pscReplacementValue :: !(Maybe value)
  }
  deriving stock (Eq, Show)

data PatchLaw
  = PatchCompatibleCompositionStitchesBoundary
  | PatchCompositionApplicationSequential
  | PatchStaleStateRejected
  deriving stock (Bounded, Enum, Eq, Ord, Show)

instance IsLawName PatchLaw where
  lawNameText =
    constructorLawName . show

patchLaws ::
  forall key value.
  (PatchKey key, PatchValue value, Show key, Show value) =>
  String ->
  Gen (Patch key value) ->
  Gen (PatchChain key value) ->
  Gen (PatchStaleCase key value) ->
  TestTree
patchLaws label deltaGen chainGen staleGen =
  testGroup
    label
    [ lawManifestCase label ([minBound .. maxBound] :: [PatchLaw]),
      deltaNormalizeLaws "normalize" deltaGen,
      lawProperty PatchCompatibleCompositionStitchesBoundary $ forAll chainGen patchComposition,
      lawProperty PatchCompositionApplicationSequential $ forAll chainGen patchApplicationComposition,
      lawProperty PatchStaleStateRejected $ forAll staleGen patchStaleRejection
    ]
  where
    patchComposition :: PatchChain key value -> Property
    patchComposition chain =
      compose (patchChainNewer chain) (patchChainOlder chain)
        === Right (patchChainComposed chain)

    patchApplicationComposition :: PatchChain key value -> Property
    patchApplicationComposition chain =
      case compose (patchChainNewer chain) (patchChainOlder chain) of
        Right composed ->
          apply composed (patchChainInitialState chain)
            === ( apply (patchChainOlder chain) (patchChainInitialState chain)
                    >>= apply (patchChainNewer chain)
                )
        Left err ->
          counterexample ("compatible patch chain refused composition: " <> show err) False

    patchStaleRejection :: PatchStaleCase key value -> Property
    patchStaleRejection staleCase =
      apply
        (singleton (pscKey staleCase) (cellFromEndpoints (pscExpectedValue staleCase) (pscReplacementValue staleCase)))
        (patchState (pscKey staleCase) (pscActualValue staleCase))
        === Left
          ApplyBeforeMismatch
            { mismatchKey = pscKey staleCase,
              expectedBefore = pscExpectedValue staleCase,
              actualBefore = pscActualValue staleCase
            }

patchChainOlder :: PatchChain key value -> Patch key value
patchChainOlder chain =
  singleton (pcKey chain) (cellFromEndpoints (pcOldValue chain) (pcMiddleValue chain))

patchChainNewer :: PatchChain key value -> Patch key value
patchChainNewer chain =
  singleton (pcKey chain) (cellFromEndpoints (pcMiddleValue chain) (pcNewValue chain))

patchChainComposed :: PatchChain key value -> Patch key value
patchChainComposed chain =
  singleton (pcKey chain) (cellFromEndpoints (pcOldValue chain) (pcNewValue chain))

patchChainInitialState :: Ord key => PatchChain key value -> Map key value
patchChainInitialState chain =
  patchState (pcKey chain) (pcOldValue chain)

patchState :: Ord key => key -> Maybe value -> Map key value
patchState key =
  maybe Map.empty (Map.singleton key)