packages feed

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

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

module ConstructionSpec
  ( constructionTests,
  )
where

import Data.IntMap.Strict qualified as IntMap
import Data.IntSet qualified as IntSet
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import EpochSupport.Generators
import EpochSupport.Types
import LawManifest (lawManifestCase, lawProperty)
import Moonlight.Core (IsLawName (..), OrdMap (..), OrdSet (..), SetKey, constructorLawName)
import Moonlight.Delta.Epoch
import Moonlight.Delta.Normalize (deltaNull)
import Test.QuickCheck (Property, (===), (.&&.))
import Test.Tasty (TestTree, testGroup)

data EpochDeltaConstructionLaw
  = EpochDeltaRequiresVersionAdvance
  | EpochDeltaRejectsTransportDomainEscape
  | EpochDeltaRejectsTransportImageEscape
  | EpochDeltaRejectsRetirementEscape
  | EpochDeltaRejectsTransportForRetiredSource
  | EpochDeltaRejectsUnmappedSurvivor
  | EpochDeltaRejectsChangedOutsideSource
  | EpochDeltaTransportIdentityFree
  | EpochDeltaAllowsManyToOneTransport
  | EpochDeltaFreshKeysDerivation
  | EpochDeltaDirtyTargetDerivation
  | EpochDeltaIdentityIsNull
  deriving stock (Bounded, Enum, Eq, Ord, Show)

instance IsLawName EpochDeltaConstructionLaw where
  lawNameText = constructorLawName . show

constructionTests :: TestTree
constructionTests =
  testGroup
    "construction"
    [ lawManifestCase "epoch delta construction" ([minBound .. maxBound] :: [EpochDeltaConstructionLaw]),
      lawProperty EpochDeltaRequiresVersionAdvance sameVersionRejected,
      lawProperty EpochDeltaRejectsTransportDomainEscape transportDomainEscapeRejected,
      lawProperty EpochDeltaRejectsTransportImageEscape transportImageEscapeRejected,
      lawProperty EpochDeltaRejectsRetirementEscape retirementEscapeRejected,
      lawProperty EpochDeltaRejectsTransportForRetiredSource transportForRetiredSourceRejected,
      lawProperty EpochDeltaRejectsUnmappedSurvivor unmappedSurvivorRejected,
      lawProperty EpochDeltaRejectsChangedOutsideSource changedOutsideSourceRejected,
      lawProperty EpochDeltaTransportIdentityFree transportIdentityFree,
      lawProperty EpochDeltaAllowsManyToOneTransport manyToOneTransport,
      lawProperty EpochDeltaFreshKeysDerivation $
        epochDeltaIntCaseProperty freshKeysDerivation
          .&&. epochDeltaGenericCaseProperty freshKeysDerivation,
      lawProperty EpochDeltaDirtyTargetDerivation $
        epochDeltaIntCaseProperty dirtyTargetDerivation
          .&&. epochDeltaGenericCaseProperty dirtyTargetDerivation,
      lawProperty EpochDeltaIdentityIsNull identityIsNull
    ]

sameVersionRejected :: Property
sameVersionRejected =
  epochDelta endpoint endpoint IntMap.empty IntSet.empty IntSet.empty
    === Left (VersionDidNotAdvance (versionFromKey 1) (versionFromKey 1))
  where
    endpoint = Endpoint (versionFromKey 1) IntSet.empty

transportDomainEscapeRejected :: Property
transportDomainEscapeRejected =
  epochDelta source target (IntMap.singleton 0 1) IntSet.empty IntSet.empty
    === Left (TransportDomainEscapesSource 0)
  where
    source = Endpoint (versionFromKey 1) IntSet.empty
    target = Endpoint (versionFromKey 2) (IntSet.singleton 1)

transportImageEscapeRejected :: Property
transportImageEscapeRejected =
  epochDelta source target (IntMap.singleton 0 1) IntSet.empty IntSet.empty
    === Left (TransportImageEscapesTarget 0 1)
  where
    source = Endpoint (versionFromKey 1) (IntSet.singleton 0)
    target = Endpoint (versionFromKey 2) IntSet.empty

retirementEscapeRejected :: Property
retirementEscapeRejected =
  epochDelta source target IntMap.empty (IntSet.singleton 1) IntSet.empty
    === Left (RetiredKeyOutsideSource 1)
  where
    source = Endpoint (versionFromKey 1) (IntSet.singleton 0)
    target = Endpoint (versionFromKey 2) (IntSet.singleton 0)

transportForRetiredSourceRejected :: Property
transportForRetiredSourceRejected =
  epochDelta source target (IntMap.singleton 0 1) (IntSet.singleton 0) IntSet.empty
    === Left (TransportDefinedForRetiredSource 0)
  where
    source = Endpoint (versionFromKey 1) (IntSet.singleton 0)
    target = Endpoint (versionFromKey 2) (IntSet.singleton 1)

unmappedSurvivorRejected :: Property
unmappedSurvivorRejected =
  epochDelta source target IntMap.empty IntSet.empty IntSet.empty
    === Left (SurvivingKeyOutsideTarget 0)
  where
    source = Endpoint (versionFromKey 1) (IntSet.singleton 0)
    target = Endpoint (versionFromKey 2) IntSet.empty

changedOutsideSourceRejected :: Property
changedOutsideSourceRejected =
  epochDelta source target IntMap.empty IntSet.empty (IntSet.singleton 1)
    === Left (ChangedKeyOutsideSource 1)
  where
    source = Endpoint (versionFromKey 1) (IntSet.singleton 0)
    target = Endpoint (versionFromKey 2) (IntSet.singleton 0)

transportIdentityFree :: Property
transportIdentityFree =
  fmap transportOverrides (epochDelta source target (IntMap.fromList [(0, 0), (1, 2)]) IntSet.empty IntSet.empty)
    === Right (IntMap.singleton 1 2)
  where
    source = Endpoint (versionFromKey 1) (IntSet.fromList [0, 1])
    target = Endpoint (versionFromKey 2) (IntSet.fromList [0, 2])

manyToOneTransport :: Property
manyToOneTransport =
  fmap (\deltaValue -> transportKeys deltaValue querySourceKeys) (epochDelta source target transport IntSet.empty IntSet.empty)
    === Right (Transport transport IntSet.empty IntSet.empty)
  where
    querySourceKeys = IntSet.fromList [0, 1]
    source = Endpoint (versionFromKey 1) querySourceKeys
    target = Endpoint (versionFromKey 2) (IntSet.singleton 2)
    transport = IntMap.fromList [(0, 2), (1, 2)]

freshKeysDerivation ::
  (EpochKeyed keyMap observed, Eq observed, Show observed, Show keyMap, Show (SetKey observed)) =>
  EpochDeltaCase keyMap observed ->
  Property
freshKeysDerivation epochCase =
  freshKeys deltaValue
    === differenceSet (targetKeys deltaValue) transportedTargetKeys
  where
    deltaValue = edcDelta epochCase
    transportedTargetKeys =
      fromListSet (fmap snd (toAscListMap (transportedKeys (transportKeys deltaValue (sourceKeys deltaValue)))))

dirtyTargetDerivation ::
  (EpochKeyed keyMap observed, Eq observed, Show observed, Show keyMap, Show (SetKey observed)) =>
  EpochDeltaCase keyMap observed ->
  Property
dirtyTargetDerivation epochCase =
  changedKeysAcrossEpoch deltaValue
    === unionSet transportedChanged (freshKeys deltaValue)
  where
    input = edcInput epochCase
    deltaValue = edcDelta epochCase
    transportedChanged =
      fromListSet (fmap snd (toAscListMap (transportedKeys (transportKeys deltaValue (eiChanged input)))))

identityIsNull :: Property
identityIsNull =
  deltaNull (identityDelta endpoint :: EpochDelta (Map.Map GenericKey GenericKey) (Set.Set GenericKey)) === True
  where
    endpoint = Endpoint (versionFromKey 3) (Set.singleton (GenericKey 0))