packages feed

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

{-# LANGUAGE DerivingStrategies #-}

module LawsSpec
  ( lawTests,
  )
where

import Data.Foldable qualified as Foldable
import Data.Set qualified as Set
import Moonlight.Core (IsLawName (..), constructorLawName)
import PatchLaws
  ( patchLaws,
  )
import Moonlight.Delta.Patch
import Moonlight.Delta.Patch qualified as Patch
import PatchSupport
import LawManifest
  ( lawManifestCase,
    lawProperty,
  )
import Test.QuickCheck
  ( Property,
    counterexample,
    forAll,
    (===),
  )
import Test.Tasty (TestTree, testGroup)

data PatchFacadeLaw
  = PatchFacadeComposeIdentity
  | PatchFacadeComposeAssociativityMultiKey
  | PatchFacadeApplyComposeHomomorphismMultiKey
  | PatchFacadeSupportToAscList
  | PatchFacadeLookupToAscListCoherence
  | PatchFacadeRecordManyCoherence
  deriving stock (Bounded, Enum, Eq, Ord, Show)

instance IsLawName PatchFacadeLaw where
  lawNameText =
    constructorLawName . show

lawTests :: TestTree
lawTests =
  testGroup
    "laws"
    [ patchLaws "single-cell patch" patchDeltaGen patchChainGen patchStaleCaseGen,
      lawManifestCase "patch facade" ([minBound .. maxBound] :: [PatchFacadeLaw]),
      lawProperty PatchFacadeComposeIdentity $
        forAll patchDeltaGen patchComposeIdentity,
      lawProperty PatchFacadeComposeAssociativityMultiKey $
        forAll patchFacadeChainGen patchComposeAssociativityMultiKey,
      lawProperty PatchFacadeApplyComposeHomomorphismMultiKey $
        forAll patchFacadeChainGen patchApplyComposeHomomorphismMultiKey,
      lawProperty PatchFacadeSupportToAscList $
        forAll patchDeltaGen patchSupportToAscList,
      lawProperty PatchFacadeLookupToAscListCoherence $
        forAll patchDeltaGen patchLookupToAscListCoherence,
      lawProperty PatchFacadeRecordManyCoherence $
        forAll patchAppliedEditListGen patchRecordManyCoherence
    ]

patchComposeIdentity :: Patch Int String -> Property
patchComposeIdentity patch =
  (compose emptyPatch patch, compose patch emptyPatch)
    === (Right patch, Right patch)
  where
    emptyPatch :: Patch Int String
    emptyPatch =
      empty

patchComposeAssociativityMultiKey :: PatchFacadeChain -> Property
patchComposeAssociativityMultiKey chain =
  (compose p3 =<< compose p2 p1)
    === (flip compose p1 =<< compose p3 p2)
  where
    p1 =
      diff (pfcState0 chain) (pfcState1 chain)
    p2 =
      diff (pfcState1 chain) (pfcState2 chain)
    p3 =
      diff (pfcState2 chain) (pfcState3 chain)

patchApplyComposeHomomorphismMultiKey :: PatchFacadeChain -> Property
patchApplyComposeHomomorphismMultiKey chain =
  case compose p2 p1 of
    Left err ->
      counterexample ("compatible multi-key patch chain refused composition: " <> show err) False
    Right composed ->
      apply composed (pfcState0 chain) === Right (pfcState2 chain)
  where
    p1 =
      diff (pfcState0 chain) (pfcState1 chain)
    p2 =
      diff (pfcState1 chain) (pfcState2 chain)

patchSupportToAscList :: Patch Int String -> Property
patchSupportToAscList patch =
  support patch === Set.fromList (fst <$> toAscList patch)

patchLookupToAscListCoherence :: Patch Int String -> Property
patchLookupToAscListCoherence patch =
  (presentLookups, absentLookups)
    === (expectedPresentLookups, expectedAbsentLookups)
  where
    rows =
      toAscList patch
    presentLookups =
      [(key, Patch.lookup key patch) | (key, _cell) <- rows]
    expectedPresentLookups =
      [(key, Just cell) | (key, cell) <- rows]
    presentKeys =
      Set.fromList (fst <$> rows)
    absentKeys =
      take 3 [key | key <- [-1 .. 17], Set.notMember key presentKeys]
    absentLookups =
      [(key, Patch.lookup key patch) | key <- absentKeys]
    expectedAbsentLookups =
      [(key, Nothing) | key <- absentKeys]

patchRecordManyCoherence :: [(Int, CellPatch Int)] -> Property
patchRecordManyCoherence edits =
  recordMany edits === recordAppliedFold edits

recordAppliedFold :: [(Int, CellPatch Int)] -> Either (ComposeError Int Int) (Patch Int Int)
recordAppliedFold =
  Foldable.foldl' appendApplied (Right empty)
  where
    appendApplied ::
      Either (ComposeError Int Int) (Patch Int Int) ->
      (Int, CellPatch Int) ->
      Either (ComposeError Int Int) (Patch Int Int)
    appendApplied applied (key, cell) =
      applied >>= recordApplied key cell