packages feed

ot-0.2.1.0: src/Control/OperationalTransformation/Properties.hs

{-# LANGUAGE DataKinds, ConstraintKinds, KindSignatures, GADTs #-}
{-# LANGUAGE FlexibleInstances, FlexibleContexts, MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances, ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}

module Control.OperationalTransformation.Properties
  ( ArbitraryFor (..)
  -- , TestableOTSystem
  -- , ArbitraryOTSystem
  , Nat (..), One, Two, Three
  , DocHistory (..)
  , ConcurrentDocHistories (..)
  , prop_compose_assoc
  , prop_apply_functorial
  , prop_transform_apply_comm
  , prop_transform_comm
  , prop_transform_compose_compat_l
  , prop_transform_compose_compat_r
  , prop_transform_functorial
  ) where

import Control.OperationalTransformation
import Test.QuickCheck hiding (Result, reason)
import Test.QuickCheck.Property
import Control.Applicative ((<$>), (<*>))

{-
type ArbitraryOTSystem doc op =
  ( OTSystem doc op, OTComposableOperation op
  , Arbitrary doc, ArbitraryFor doc op --Arbitrary (GenOp doc op)
  , Show doc, Eq doc, Show op, Eq op
  )
-}

type TestableOTSystem doc op =
  ( OTSystem doc op, OTComposableOperation op
  --, Arbitrary doc, ArbitraryFor doc op --Arbitrary (GenOp doc op)
  , Show doc, Eq doc, Show op, Eq op
  )

class ArbitraryFor a b where
  arbitraryFor :: a -> Gen b

genOp :: (OTSystem doc op, ArbitraryFor doc op) => doc -> Gen (op, doc)
genOp doc = do
  op <- arbitraryFor doc
  case apply op doc of
    Left err -> fail err
    Right doc' -> return (op, doc')


data Nat = Z | S !Nat deriving (Eq, Show)

type One = S Z
type Two = S One
type Three = S Two

data DocHistory doc op :: Nat -> * where
  -- | Last state
  LS :: doc -> DocHistory doc op Z
  -- | Snapshot
  SS :: doc -> op -> DocHistory doc op n -> DocHistory doc op (S n)

deriving instance (Show doc, Show op) => Show (DocHistory doc op n)
deriving instance (Eq doc, Eq op) => Eq (DocHistory doc op n)

data ConcurrentDocHistories doc op n k =
  CDH (DocHistory doc op n) (DocHistory doc op k)

deriving instance (Show doc, Show op) => Show (ConcurrentDocHistories doc op n k)
deriving instance (Eq doc, Eq op) => Eq (ConcurrentDocHistories doc op n k)

{-
getCurrentState :: DocHistory doc op n -> doc
getCurrentState (LS doc) = doc
getCurrentState (SS _ _ dh) = getCurrentState dh

snocDocHistory :: DocHistory doc op n -> op -> doc -> DocHistory doc op (S n)
snocDocHistory (LS doc) op doc' = SS doc op (LS doc')
snocDocHistory (SS doc op dh) op' doc' = SS doc op (snocDocHistory dh op' doc')
-}

instance ArbitraryFor doc (DocHistory doc op Z) where
  arbitraryFor = return . LS

instance (OTSystem doc op, ArbitraryFor doc op, ArbitraryFor doc (DocHistory doc op n)) => ArbitraryFor doc (DocHistory doc op (S n)) where
  arbitraryFor doc = do
    (op, doc') <- genOp doc
    SS doc op <$> arbitraryFor doc'

instance (Arbitrary doc, ArbitraryFor doc (DocHistory doc op n)) => Arbitrary (DocHistory doc op n) where
  arbitrary = (arbitrary :: Gen doc) >>= arbitraryFor

instance (ArbitraryFor doc (DocHistory doc op n), ArbitraryFor doc (DocHistory doc op k)) => ArbitraryFor doc (ConcurrentDocHistories doc op n k) where
  arbitraryFor doc = CDH <$> arbitraryFor doc <*> arbitraryFor doc

instance (Arbitrary doc, ArbitraryFor doc (ConcurrentDocHistories doc op n k)) => Arbitrary (ConcurrentDocHistories doc op n k) where
  arbitrary = (arbitrary :: Gen doc) >>= arbitraryFor

(==?) :: (Eq a, Show a) => a -> a -> Result
a ==? b | a == b    = succeeded
        | otherwise = failed { reason = "expected " ++ show a ++ " to be " ++ show b }

eitherResult :: Either String a -> (a -> Result) -> Result
eitherResult (Left err) _ = failed { reason = err }
eitherResult (Right a) f  = f a

eitherProperty :: Either String a -> (a -> Property) -> Property
eitherProperty (Left err) _ = property $ failed { reason = err }
eitherProperty (Right res) prop = prop res

prop_compose_assoc
  :: TestableOTSystem doc op
  => DocHistory doc op Three
  -> Result
prop_compose_assoc (SS _doc a (SS _ b (SS _ c _))) =
  eitherResult (compose a b) $ \ab ->
  eitherResult (compose ab c) $ \abc1 ->
  eitherResult (compose b c) $ \bc ->
  eitherResult (compose a bc) $ \abc2 ->
  abc1 ==? abc2

-- | @(b ∘ a)(d) = a(b(d))@ where /a/ and /b/ are two consecutive operations
-- and /d/ is the initial document.
prop_apply_functorial
  :: TestableOTSystem doc op
  => DocHistory doc op Two
  -> Result
prop_apply_functorial (SS doc a (SS _ b (LS _))) =
  eitherResult (apply a doc) $ \doc' ->
  eitherResult (apply b doc') $ \doc''1 ->
  eitherResult (compose a b) $ \ab ->
  eitherResult (apply ab doc) $ \doc''2 ->
  doc''1 ==? doc''2

-- | @b'(a(d)) = a'(b(d))@ where /a/ and /b/ are random operations, /d/ is the
-- initial document and @(a', b') = transform(a, b)@.
prop_transform_apply_comm
  :: TestableOTSystem doc op
  => ConcurrentDocHistories doc op One One
  -> Result
prop_transform_apply_comm (CDH (SS _ a (LS docA)) (SS _ b (LS docB))) =
  eitherResult (transform a b) $ \(a', b') ->
  eitherResult (apply a' docB) $ \doc''1 ->
  eitherResult (apply b' docA) $ \doc''2 ->
  doc''1 ==? doc''2

-- | @b' ∘ a = a' ∘ b@ where /a/ and /b/ are random operations and
-- @(a', b') = transform(a, b)@. Note that this is a stronger property than
-- 'prop_transform_apply_comm', because 'prop_transform_comm' and
-- 'prop_apply_functorial' imply 'prop_transform_apply_comm'.
prop_transform_comm
  :: TestableOTSystem doc op
  => ConcurrentDocHistories doc op One One
  -> Result
prop_transform_comm (CDH (SS _ a _) (SS _ b _)) =
  eitherResult (transform a b) $ \(a', b') ->
  eitherResult (compose a b') $ \ab' ->
  eitherResult (compose b a') $ \ba' ->
  ab' ==? ba'

-- | Transformation is compatible with composition on the left. That is, if we
-- have two consecutive operations /a/ and /b/ and a concurrent operation /c/,
-- then it doesn't make a difference whether we transform /c/ against /a/ and
-- then against /b/ or transform /c/ against the composition of /a/ and /b/.
-- In other terms, @c'_1 = c'_2@ where @(_, c'_1) = transform(b ∘ a, c)@,
-- @(_, c') = transform(a, c)@ and @(_, c'_2) = transform(b, c')@.
prop_transform_compose_compat_l
  :: (OTSystem doc op, OTComposableOperation op, Arbitrary doc, Show op, Eq op)
  => (doc -> Gen op)
  -> Property
prop_transform_compose_compat_l genOperation = property $ do
  doc <- arbitrary
  a <- genOperation doc
  c <- genOperation doc
  return $ eitherProperty (apply a doc) $ \doc' -> property $ do
    b <- genOperation doc'
    let res = (,) <$> (snd <$> (compose a b >>= flip transform c))
                  <*> (snd <$> (transform a c >>= transform b . snd))
    return $ eitherProperty res $ \(c'_1, c'_2) ->
      property $ c'_1 ==? c'_2

-- | Transformation is compatible with composition on the /right/.
prop_transform_compose_compat_r
  :: (OTSystem doc op, OTComposableOperation op, Arbitrary doc, Show op, Eq op)
  => (doc -> Gen op)
  -> Property
prop_transform_compose_compat_r genOperation = property $ do
  doc <- arbitrary
  a <- genOperation doc
  c <- genOperation doc
  return $ eitherProperty (apply a doc) $ \doc' -> property $ do
    b <- genOperation doc'
    let res = (,) <$> (fst <$> (compose a b >>= transform c))
                  <*> (fst <$> (transform c a >>= flip transform b . fst))
    return $ eitherProperty res $ \(c'_1, c'_2) -> property $ c'_1 ==? c'_2

-- second functor axiom (F(f . g) = Ff . Fg) for F = transform c
prop_transform_functorial
  :: TestableOTSystem doc op
  => ConcurrentDocHistories doc op One Two
  -> Result
prop_transform_functorial (CDH (SS _ c _) (SS _ a (SS _ b _))) =
  eitherResult (compose a b) $ \ab ->
  eitherResult (transform c ab) $ \(_c''1, abPrimed1) ->
  eitherResult (transform c a) $ \(c', a') ->
  eitherResult (transform c' b) $ \(_c''2, b') ->
  eitherResult (compose a' b') $ \abPrimed2 ->
  abPrimed1 ==? abPrimed2