packages feed

moonlight-delta-0.1.0.0: test/epoch/ComposeSpec.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}

module ComposeSpec
  ( composeTests,
  )
where

import Data.IntMap.Strict qualified as IntMap
import Data.IntSet qualified as IntSet
import EpochSupport.Generators
import EpochSupport.Reference
import EpochSupport.Types
import LawManifest (lawManifestCase, lawProperty)
import Moonlight.Core (IsLawName (..), SetKey, constructorLawName)
import Moonlight.Delta.Epoch
import Test.QuickCheck (Property, counterexample, forAll, (===), (.&&.))
import Test.Tasty (TestTree, testGroup)

data EpochComposeLaw
  = EpochComposeBoundaryMismatchRejected
  | EpochComposeIdentityLeft
  | EpochComposeIdentityRight
  | EpochComposeAssociative
  | EpochComposeAgreesWithSequentialReference
  | EpochComposeDirtyTargetSequential
  | EpochComposeRetireRecreateDirty
  | EpochComposeRenameIntoRetirement
  deriving stock (Bounded, Enum, Eq, Ord, Show)

instance IsLawName EpochComposeLaw where
  lawNameText = constructorLawName . show

composeTests :: TestTree
composeTests =
  testGroup
    "compose"
    [ lawManifestCase "epoch compose" ([minBound .. maxBound] :: [EpochComposeLaw]),
      lawProperty EpochComposeBoundaryMismatchRejected boundaryMismatchRejected,
      lawProperty EpochComposeIdentityLeft $
        epochDeltaIntCaseProperty composeIdentityLeft
          .&&. epochDeltaGenericCaseProperty composeIdentityLeft,
      lawProperty EpochComposeIdentityRight $
        epochDeltaIntCaseProperty composeIdentityRight
          .&&. epochDeltaGenericCaseProperty composeIdentityRight,
      lawProperty EpochComposeAssociative $
        forAll stableEpochChainIntGen composeAssociative
          .&&. forAll stableEpochChainGenericGen composeAssociative,
      lawProperty EpochComposeAgreesWithSequentialReference $
        forAll epochPairIntGen composeAgreesWithReference,
      lawProperty EpochComposeDirtyTargetSequential $
        forAll epochPairIntGen composeDirtyTargetSequential,
      lawProperty EpochComposeRetireRecreateDirty retireRecreateDirty,
      lawProperty EpochComposeRenameIntoRetirement renameIntoRetirement
    ]

boundaryMismatchRejected :: Property
boundaryMismatchRejected =
  ( composeDelta newerVersion older,
    composeDelta newerUniverse older
  )
    === ( Left (ComposeVersionMismatch (versionFromKey 1) (versionFromKey 2)),
          Left ComposeUniverseMismatch
        )
  where
    older = identityDelta (Endpoint (versionFromKey 1) (IntSet.singleton 0)) :: EpochDelta (IntMap.IntMap Int) IntSet.IntSet
    newerVersion = identityDelta (Endpoint (versionFromKey 2) (IntSet.singleton 0))
    newerUniverse = identityDelta (Endpoint (versionFromKey 1) (IntSet.singleton 1))

composeIdentityLeft ::
  (EpochKeyed keyMap observed, Eq observed, Eq keyMap, Show observed, Show keyMap, Show (SetKey observed)) =>
  EpochDeltaCase keyMap observed ->
  Property
composeIdentityLeft epochCase =
  composeDelta deltaValue (identityDelta (sourceEndpointOf deltaValue)) === Right deltaValue
  where
    deltaValue = edcDelta epochCase

composeIdentityRight ::
  (EpochKeyed keyMap observed, Eq observed, Eq keyMap, Show observed, Show keyMap, Show (SetKey observed)) =>
  EpochDeltaCase keyMap observed ->
  Property
composeIdentityRight epochCase =
  composeDelta (identityDelta (targetEndpointOf deltaValue)) deltaValue === Right deltaValue
  where
    deltaValue = edcDelta epochCase

composeAssociative ::
  (EpochKeyed keyMap observed, Eq observed, Eq keyMap, Show observed, Show keyMap, Show (SetKey observed)) =>
  EpochChainCase keyMap observed ->
  Property
composeAssociative chain =
  leftAssociated === rightAssociated
  where
    older = eccFirst chain
    middle = eccSecond chain
    newer = eccThird chain
    leftAssociated =
      composeDelta middle older >>= composeDelta newer
    rightAssociated =
      composeDelta newer middle >>= (\newerMiddle -> composeDelta newerMiddle older)

composeAgreesWithReference :: EpochPairCase (IntMap.IntMap Int) IntSet.IntSet -> Property
composeAgreesWithReference pairCase =
  fmap referenceFromDelta (composeDelta newer older)
    === referenceCompose (referenceFromDelta newer) (referenceFromDelta older)
  where
    older = epcFirst pairCase
    newer = epcSecond pairCase

composeDirtyTargetSequential :: EpochPairCase (IntMap.IntMap Int) IntSet.IntSet -> Property
composeDirtyTargetSequential pairCase =
  case (composeDelta newer older, referenceCompose (referenceFromDelta newer) (referenceFromDelta older)) of
    (Right composed, Right referenceComposed) ->
      changedKeysAcrossEpoch composed === referenceChangedKeys referenceComposed
    (Left err, _) ->
      counterexample ("boundary-compatible composition failed: " <> show err) False
    (_, Left err) ->
      counterexample ("boundary-compatible reference failed: " <> show err) False
  where
    older = epcFirst pairCase
    newer = epcSecond pairCase

retireRecreateDirty :: Property
retireRecreateDirty =
  case (epochDelta source middle IntMap.empty (IntSet.singleton 0) IntSet.empty, epochDelta middle target IntMap.empty IntSet.empty IntSet.empty) of
    (Right older, Right newer) ->
      fmap changedKeysAcrossEpoch (composeDelta newer older)
        === Right (IntSet.singleton 0)
    fixtures ->
      counterexample ("valid retire/recreate fixtures rejected: " <> show fixtures) False
  where
    source = Endpoint (versionFromKey 0) (IntSet.singleton 0)
    middle = Endpoint (versionFromKey 1) IntSet.empty
    target = Endpoint (versionFromKey 2) (IntSet.singleton 0)

renameIntoRetirement :: Property
renameIntoRetirement =
  case (epochDelta source middle (IntMap.singleton 0 1) IntSet.empty IntSet.empty, epochDelta middle target IntMap.empty (IntSet.singleton 1) IntSet.empty) of
    (Right older, Right newer) ->
      case composeDelta newer older of
        Left err ->
          counterexample ("rename into retirement refused composition: " <> show err) False
        Right composed ->
          (retiredKeys composed, transportKeys composed (IntSet.singleton 0))
            === ( IntSet.singleton 0,
                  Transport IntMap.empty (IntSet.singleton 0) IntSet.empty
                )
    fixtures ->
      counterexample ("valid rename/retire fixtures rejected: " <> show fixtures) False
  where
    source = Endpoint (versionFromKey 0) (IntSet.singleton 0)
    middle = Endpoint (versionFromKey 1) (IntSet.singleton 1)
    target = Endpoint (versionFromKey 2) IntSet.empty