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