packages feed

large-anon-0.3.0: test/Test/Prop/Record/Diff.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE EmptyCase           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE OverloadedLabels    #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving  #-}
{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE TypeOperators       #-}

{-# OPTIONS_GHC -fplugin=Data.Record.Anon.Plugin #-}

module Test.Prop.Record.Diff (tests) where

import Data.Kind
import Data.Map (Map)
import Data.SOP
import Data.Type.Equality
import GHC.TypeLits
import Test.QuickCheck
import Test.Tasty hiding (after)
import Test.Tasty.QuickCheck

import qualified Data.Map as Map

import Data.Record.Anon
import Data.Record.Anon.Advanced (Record)
import qualified Data.Record.Anon.Advanced as Anon

{-------------------------------------------------------------------------------
  Abstraction over the 'Diff' operations
-------------------------------------------------------------------------------}

data Op :: Row Type -> Row Type -> Type where
  -- | Insert new field into the record
  Insert :: KnownSymbol n => Field n -> Int -> Op r (n := Int : r)

  -- | Get field from the record and check that it has the correct value
  --
  -- (The test itself will have to keep track of what the correct value /is/)
  Get :: (KnownSymbol n, RowHasField n r Int) => Field n -> Op r r

  -- | Update value of a field in the record
  Set :: (KnownSymbol n, RowHasField n r Int) => Field n -> Int -> Op r r

  -- | Apply all pending changes
  Apply  :: Op r r

deriving instance Show (Op r r')

data Ops :: Row Type -> Row Type -> Type where
  Done :: Ops r r
  Op   :: IsKnownRow r' => Op r r' -> Ops r' r'' -> Ops r r''


data TestResult = TestOk | TestFailed String
  deriving (Show, Eq)

-- | Model to test the actual record against
type RecordModel = Map String Int

execute :: Ops '[] r -> TestResult
execute = go Anon.empty Map.empty
  where
    go :: Record I r -> RecordModel -> Ops r r' -> TestResult
    go _ _ Done      = TestOk
    go r m (Op o os) =
        case o of
          Insert f v ->
            go (Anon.insert f (I v) r) (Map.insert (symbolVal f) v m) os
          Get f ->
            case Map.lookup (symbolVal f) m of
              Nothing ->
                TestFailed "Unknown field (this indicates a bug in the tests"
              Just expected ->
                let actual = unI (Anon.get f r) in
                if actual == expected
                  then go r m os
                  else TestFailed $ concat [
                      "Expected " ++ show expected
                    , ", got " ++ show actual
                    ]
          Set f v ->
            go (Anon.set f (I v) r) (Map.insert (symbolVal f) v m) os
          Apply ->
            go (Anon.applyPending r) m os

{-------------------------------------------------------------------------------
  Existential wrappers
-------------------------------------------------------------------------------}

data SomeField r where
  SomeField :: (KnownSymbol n, RowHasField n r Int) => Field n -> SomeField r

data SomeOp r where
  SomeOp :: forall r r'. IsKnownRow r' => Op r r' -> SomeOp r

data SomeOps r where
  SomeOps :: forall r r'. IsKnownRow r' => Ops r r' -> SomeOps r

{-------------------------------------------------------------------------------
  Show instances

  These are not lawful (they are not valid Haskell), but try to present
  readable counter-examples.
-------------------------------------------------------------------------------}

-- | Used only for 'Show'
data UnknownOp where
  UnknownOp :: Op r r' -> UnknownOp

opsToList :: Ops r r' -> [UnknownOp]
opsToList Done        = []
opsToList (Op op ops) = UnknownOp op : opsToList ops

instance Show UnknownOp     where show (UnknownOp x) = show x
instance Show (SomeField r) where show (SomeField x) = show x
instance Show (SomeOp    r) where show (SomeOp    x) = show x
instance Show (SomeOps   r) where show (SomeOps   x) = show (opsToList x)

{-------------------------------------------------------------------------------
  Generation
-------------------------------------------------------------------------------}

-- | Known row
--
-- @large-anon@ is not designed for inductive reasoning, which makes
-- constructing a random record iteratively rather difficult. We could use the
-- (unsafe) low level API, but part of what we want to test here is the high
-- level translation the library does in the plugin. We therefore restrict our
-- attention to specific valid rows.
--
-- TODO: If we add allow for permutations, we can also test 'Anon.project'.
data KnownRow :: Row Type -> Type where
  Valid0 :: KnownRow '[]
  Valid1 :: KnownRow '[ "f1" := Int ]
  Valid2 :: KnownRow '[ "f2" := Int, "f1" := Int ]
  Valid3 :: KnownRow '[ "f3" := Int, "f2" := Int, "f1" := Int ]

class IsKnownRow (row :: Row Type) where
  isKnownRow :: proxy row -> KnownRow row

instance IsKnownRow '[]                                        where isKnownRow _ = Valid0
instance IsKnownRow '[ "f1" := Int ]                           where isKnownRow _ = Valid1
instance IsKnownRow '[ "f2" := Int, "f1" := Int ]              where isKnownRow _ = Valid2
instance IsKnownRow '[ "f3" := Int, "f2" := Int, "f1" := Int ] where isKnownRow _ = Valid3



tryPickExisting :: KnownRow r -> Maybe (Gen (SomeField r))
tryPickExisting Valid0 = Nothing
tryPickExisting Valid1 = Just $ elements [ SomeField #f1 ]
tryPickExisting Valid2 = Just $ elements [ SomeField #f1
                                         , SomeField #f2
                                         ]
tryPickExisting Valid3 = Just $ elements [ SomeField #f1
                                         , SomeField #f2
                                         , SomeField #f3
                                         ]

tryGenInsert :: KnownRow r -> Maybe (Int -> Gen (SomeOp r))
tryGenInsert Valid0 = Just $ \v -> return $ SomeOp (Insert #f1 v)
tryGenInsert Valid1 = Just $ \v -> return $ SomeOp (Insert #f2 v)
tryGenInsert Valid2 = Just $ \v -> return $ SomeOp (Insert #f3 v)
tryGenInsert Valid3 = Nothing

genOp :: forall proxy r. IsKnownRow r => proxy r -> Gen (SomeOp r)
genOp r = oneof . concat $ [
      [ do SomeField field <- genField
           return $ SomeOp (Get field)
      | Just genField <- [tryPickExisting $ isKnownRow r]
      ]
    , [ do SomeField field <- genField
           newValue <- choose (0, 100)
           return $ SomeOp (Set field newValue)
      | Just genField <- [tryPickExisting $ isKnownRow r]
      ]
    , [ do v <- choose (0, 100)
           genInsert v
      | Just genInsert <- [tryGenInsert (isKnownRow r)]
      ]
    , [ return $ SomeOp Apply ]
    ]

genOpsFrom :: IsKnownRow r => proxy r -> Int -> Gen (SomeOps r)
genOpsFrom _ 0 = return $ SomeOps Done
genOpsFrom r n = do SomeOp  op  <- genOp      r
                    SomeOps ops <- genOpsFrom op (pred n)
                    return $ SomeOps (Op op ops)

genOps :: Gen (SomeOps '[])
genOps = do
    n <- choose (0, 10)
    genOpsFrom Anon.empty n

{-------------------------------------------------------------------------------
  Shrinking

  We could be much more sophisticated in how we shrink, but now for we just pick
  the low hanging fruit only. Specifically, we do not try to omit/change any
  instructions that affect types (specifically, inserts).
-------------------------------------------------------------------------------}

sameResultRow :: Op r r' -> Maybe (r :~: r')
sameResultRow (Insert _ _) = Nothing
sameResultRow (Get _)      = Just Refl
sameResultRow (Set _ _)    = Just Refl
sameResultRow Apply        = Just Refl

isDone :: Ops r r' -> Bool
isDone Done     = True
isDone (Op _ _) = False

shrinkOp :: Op r r' -> [Op r r']
shrinkOp (Insert f n) = Insert f <$> shrink n
shrinkOp (Get _)      = []
shrinkOp (Set f n)    = Set f <$> shrink n
shrinkOp Apply        = []

shrinkOps :: IsKnownRow r' => Ops r r' -> [SomeOps r]
shrinkOps Done        = []
shrinkOps (Op op ops) = concat [
      -- Shrink the operation
      [ SomeOps $ Op op' ops
      | op' <- shrinkOp op
      ]

      -- Shrink the tail
    , [ SomeOps $ Op op ops'
      | SomeOps ops' <- shrinkOps ops
      ]

      -- Drop the operation
    , [ SomeOps $ ops
      | Just Refl <- [sameResultRow op]
      ]

      -- Drop the tail
    , [ SomeOps $ Op op Done
      | not $ isDone ops
      ]
    ]

{-------------------------------------------------------------------------------
  Arbitrary instance
-------------------------------------------------------------------------------}

instance Arbitrary (SomeOps '[]) where
  arbitrary            = genOps
  shrink (SomeOps ops) = shrinkOps ops

{-------------------------------------------------------------------------------
  Top-level

  These tests test the various ways to construct a record bit by bit, thereby
  constructing a 'Diff' (and occassionally applying that 'Diff').
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "Test.Prop.Record.Diff" [
      testProperty "diff" test_diff
    ]

test_diff :: SomeOps '[] -> Property
test_diff (SomeOps ops) = execute ops === TestOk