moonlight-delta-0.1.0.0: test/patch/SemanticsSpec.hs
module SemanticsSpec
( semanticTests,
)
where
import Data.Map.Strict qualified as Map
import Moonlight.Delta.Patch
import Moonlight.Delta.Patch qualified as Patch
import PatchSupport
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit ((@?=), assertFailure, testCase)
semanticTests :: TestTree
semanticTests =
testGroup
"semantics"
[ testCase "composition rejects incompatible cell boundary" $
compose
(singletonPatch (1 :: Int) (Just "x") (Just "b"))
(singletonPatch (1 :: Int) Nothing (Just "a"))
@?=
Left
ComposeBoundaryMismatch
{ boundaryKey = 1,
olderAfter = Just "a",
newerBefore = Just "x"
},
testCase "normalization preserves assertion-only cells" $
let assertion =
singletonPatch (1 :: Int) (Just "a") (Just "a")
in do
Patch.null (normalize assertion) @?= False
apply
(normalize assertion)
(Map.singleton 1 "b")
@?=
Left
ApplyBeforeMismatch
{ mismatchKey = 1,
expectedBefore = Just "a",
actualBefore = Just "b"
},
testCase "composition preserves round-trip source assertion" $
let older =
singletonPatch (1 :: Int) (Just "a") (Just "b")
newer =
singletonPatch 1 (Just "b") (Just "a")
assertion =
singletonPatch 1 (Just "a") (Just "a")
in do
compose newer older @?= Right assertion
apply assertion (Map.singleton 1 "c")
@?=
Left
ApplyBeforeMismatch
{ mismatchKey = 1,
expectedBefore = Just "a",
actualBefore = Just "c"
},
testCase "applied cell recording keeps endpoints only" $
let applied =
recordApplied
1
(cellFromEndpoints (Just "a") (Just "b"))
empty
>>= recordApplied
(1 :: Int)
(cellFromEndpoints (Just "b") (Just "c"))
in applied @?= Right (singletonPatch 1 (Just "a") (Just "c")),
testCase "applied round trip remains a checked assertion" $
let applied =
recordApplied
1
(cellFromEndpoints (Just "a") (Just "b"))
empty
>>= recordApplied
(1 :: Int)
(cellFromEndpoints (Just "b") (Just "a"))
assertion =
singletonPatch 1 (Just "a") (Just "a")
in do
applied @?= Right assertion
case applied of
Left err ->
assertFailure (show err)
Right patch ->
apply patch (Map.singleton 1 "stale")
@?=
Left
ApplyBeforeMismatch
{ mismatchKey = 1,
expectedBefore = Just "a",
actualBefore = Just "stale"
},
testCase "applied cell recording rejects discontinuous producer boundary" $
let applied =
recordApplied
1
(cellFromEndpoints (Just "a") (Just "b"))
empty
>>= recordApplied
(1 :: Int)
(cellFromEndpoints (Just "c") (Just "d"))
in applied
@?=
Left
ComposeBoundaryMismatch
{ boundaryKey = 1,
olderAfter = Just "b",
newerBefore = Just "c"
},
testCase "recordMany interprets repeated keys temporally" $
recordMany
[ (1 :: Int, replace "a" "b"),
(1, replace "b" "c")
]
@?= Right (singleton 1 (replace "a" "c")),
testCase "fromAscList falls back to canonical construction for nonascending input" $
fromAscList
[ (2 :: Int, insert "b"),
(1, insert "a"),
(2, replace "b" "c")
]
@?= fromList
[ (2, insert "b"),
(1, insert "a"),
(2, replace "b" "c")
],
testCase "absence assertion is a restricted identity" $ do
let assertion = singleton (1 :: Int) (assertAbsent :: CellPatch String)
Patch.null assertion @?= False
apply assertion Map.empty @?= Right (Map.empty :: Map.Map Int String)
apply assertion (Map.singleton 1 "present")
@?=
Left
ApplyBeforeMismatch
{ mismatchKey = 1,
expectedBefore = Nothing,
actualBefore = Just "present"
},
testCase "apply mismatch reports the patch key representative" $
let patchKey =
RepresentativeKey 1 "patch"
stateKey =
RepresentativeKey 1 "state"
patch =
singletonPatch patchKey (Just "expected") (Just "new")
in case apply patch (Map.singleton stateKey "actual") of
Left mismatch -> do
representativeKeyName (mismatchKey mismatch) @?= "patch"
expectedBefore mismatch @?= Just "expected"
actualBefore mismatch @?= Just "actual"
Right _ ->
assertFailure "expected representative-key mismatch",
testCase "apply update uses the patch key representative" $
let patchKey =
RepresentativeKey 1 "patch"
stateKey =
RepresentativeKey 1 "state"
patch =
singletonPatch patchKey (Just "old") (Just "new")
in fmap (fmap (representativeKeyName . fst) . Map.toAscList) (apply patch (Map.singleton stateKey "old"))
@?= Right ["patch"],
testCase "dense apply uses patch representatives for every matched key" $
let rowKeys :: [Int]
rowKeys = [0 .. 127]
state =
Map.fromAscList
[ (RepresentativeKey key "state", key)
| key <- rowKeys
]
patch =
fromAscList
[ (RepresentativeKey key "patch", replace key (key + 1))
| key <- rowKeys
]
labels :: Map.Map RepresentativeKey Int -> [String]
labels =
fmap (representativeKeyName . fst) . Map.toAscList
in fmap labels (apply patch state)
@?= Right (replicate 128 "patch"),
testCase "composition is independent of page layout" differentPageLayoutCase,
testCase "mask-first validation preserves least-key error ordering" maskFirstLeastMismatchCase,
testCase "disjoint composition preserves logical rows across an underfull seam" disjointSeamCase,
testCase "endpoint-native fold agrees with logical row materialization" endpointNativeFoldCase
]
maskFirstLeastMismatchCase :: IO ()
maskFirstLeastMismatchCase = do
let older :: Patch Int Int
older =
fromAscList
[ (0, replace 0 10),
(1, insert 20)
]
newer :: Patch Int Int
newer =
fromAscList
[ (0, replace 11 12),
(1, assertAbsent)
]
compose newer older
@?= Left
ComposeBoundaryMismatch
{ boundaryKey = 0,
olderAfter = Just 10,
newerBefore = Just 11
}
disjointSeamCase :: IO ()
disjointSeamCase = do
let olderRows :: [(Int, CellPatch Int)]
olderRows =
[ (key, replace key (key + 1))
| key <- [0 .. 30]
]
newerRows :: [(Int, CellPatch Int)]
newerRows =
[ (key, replace key (key + 1))
| key <- [100 .. 130]
]
older = fromAscList olderRows
newer = fromAscList newerRows
expected = fromAscList (olderRows <> newerRows)
compose newer older @?= Right expected
endpointNativeFoldCase :: IO ()
endpointNativeFoldCase = do
let patch :: Patch Int Int
patch =
fromAscList
[ (0, assertAbsent),
(1, insert 10),
(2, delete 20),
(3, replace 30 31)
]
folded =
foldWithKey'
(\ !total key -> total + key)
(\ !total key after -> total + key + after)
(\ !total key before -> total + key + before)
(\ !total key before after -> total + key + before + after)
0
patch
materialized =
foldl'
( \ !total (key, cell) ->
total
+ key
+ maybe 0 id (cellBefore cell)
+ maybe 0 id (cellAfter cell)
)
0
(toAscList patch)
folded @?= materialized
differentPageLayoutCase :: IO ()
differentPageLayoutCase = do
let rowKeys :: [Int]
rowKeys = [0 .. 256]
olderRows =
[ (key, replace key (key + 1))
| key <- rowKeys
]
newerRows =
[ (key, replace (key + 1) (key + 2))
| key <- rowKeys
]
expectedRows =
[ (key, replace key (key + 2))
| key <- rowKeys
]
initialState = Map.fromAscList [(key, key) | key <- rowKeys]
finalState = Map.fromAscList [(key, key + 2) | key <- rowKeys]
olderCanonical = fromAscList olderRows
newerCanonical = fromAscList newerRows
expected = fromAscList expectedRows
case recordMany (reverse olderRows) of
Left err ->
assertFailure ("unexpected recordMany rejection: " <> show err)
Right olderRecorded -> do
olderRecorded @?= olderCanonical
compare olderRecorded olderCanonical @?= EQ
compose newerCanonical olderRecorded @?= Right expected
replay [olderRecorded, newerCanonical] initialState @?= Right finalState