packages feed

figment-0.2.0.0: test/Figment/Test/MergeLaws.hs

-- | The algebra of 'coalesce' and of 'Figment' sequencing.
module Figment.Test.MergeLaws (tests) where

import qualified Data.Map.Strict as Map
import qualified Data.Vector as Vector
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck

import Figment
import Figment.Merge (coalesce)
import Figment.Test.Arbitrary
import Figment.Value (stripTags)

tests :: TestTree
tests =
  testGroup
    "merge laws"
    [ testProperty "duality: Join a b == Merge b a (including tags)" $
        forAll (genValue 3) $ \a -> forAll (genValue 3) $ \b ->
          coalesce Join a b === coalesce Merge b a
    , testProperty "idempotence of Merge and Join (data)" $
        forAll (genValue 3) $ \v ->
          stripTags (coalesce Merge v v) === stripTags v
            .&&. stripTags (coalesce Join v v) === stripTags v
    , testProperty "empty dictionary is a left and right identity on dictionaries" $
        forAll (genDict 3) $ \v -> conjoin
          [ stripTags (coalesce o (mkEmpty) v) === stripTags v .&&. stripTags (coalesce o v mkEmpty) === stripTags v
          | o <- [minBound .. maxBound] ]
    , testProperty "associative on same-shape trees, every order" $
        forAll genSameShape $ \(a, b, c) -> conjoin
          [ coalesce o (coalesce o a b) c === coalesce o a (coalesce o b c) | o <- [minBound .. maxBound] ]
    , testCase "not associative in general (why Value has no Semigroup)" $ do
        let a = dictV [("x", num 1)]
            b = num 5
            c = dictV [("y", num 2)]
        assertBool "counterexample" $
          stripTags (coalesce Merge (coalesce Merge a b) c) /= stripTags (coalesce Merge a (coalesce Merge b c))
    , testCase "adjoin/admerge concatenate arrays; merge/join replace" $ do
        let a = arr [num 1]
            b = arr [num 2]
        stripTags (coalesce Adjoin a b) @?= arr [num 1, num 2]
        stripTags (coalesce Admerge a b) @?= arr [num 1, num 2]
        stripTags (coalesce Merge a b) @?= arr [num 2]
        stripTags (coalesce Join a b) @?= arr [num 1]
    , testCase "container tags follow Rust: join keeps left, merge keeps right" $ do
        let tl = Tag Nothing Nothing (Just (Pos 1 1)) Nothing
            tr = Tag Nothing Nothing (Just (Pos 2 2)) Nothing
            l = Value tl (Dict Map.empty)
            r = Value tr (Dict Map.empty)
        valueTag (coalesce Join l r) @?= tl
        valueTag (coalesce Merge l r) @?= tr
    , testProperty "Figment is a monoid (observationally: same loaded data)" $
        forAll (genDict 2) $ \a -> forAll (genDict 2) $ \b -> forAll (genDict 2) $ \c ->
          ioProperty $ do
            let fa = from (Const a)
                fb = merge (Const b) new
                fc = join (Const c) new
            l1 <- loadedPlain ((fa <> fb) <> fc)
            l2 <- loadedPlain (fa <> (fb <> fc))
            l3 <- loadedPlain (new <> fa)
            l4 <- loadedPlain (fa <> new)
            l5 <- loadedPlain fa
            pure (l1 === l2 .&&. l3 === l5 .&&. l4 === l5)
    ]
  where
    mkEmpty = Value (Tag Nothing Nothing Nothing Nothing) (Dict Map.empty)
    num :: Int -> Value
    num = Value (Tag Nothing Nothing Nothing Nothing) . Num . Finite . fromIntegral
    arr = Value (Tag Nothing Nothing Nothing Nothing) . Array . Vector.fromList
    dictV = Value (Tag Nothing Nothing Nothing Nothing) . Dict . Map.fromList

-- | A provider returning a fixed dictionary.
newtype Const = Const Value

instance Provider Const where
  metadata _ = named "const"
  provide _ (Const v) = pure $ case valueNode v of
    Dict d -> Right (Map.singleton Default d)
    _ -> Right Map.empty

loadedPlain :: Figment -> IO (Maybe Value)
loadedPlain f = either (const Nothing) (Just . stripTags . selected) <$> load f