packages feed

large-records-0.1.0.0: test/Test/Record/Sanity/Generics.hs

{-# LANGUAGE ConstraintKinds        #-}
{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE EmptyCase              #-}
{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE KindSignatures         #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE PatternSynonyms        #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TemplateHaskell        #-}
{-# LANGUAGE TypeApplications       #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeOperators          #-}
{-# LANGUAGE UndecidableInstances   #-}
{-# LANGUAGE ViewPatterns           #-}

-- {-# OPTIONS_GHC -ddump-splices #-}

-- | Sanity check that the generic functions are not totally broken
--
-- These are not proper tests, merely intended to catch egregious refactoring
-- mistakes.
module Test.Record.Sanity.Generics (tests) where

import Control.Monad.State (State, evalState, state)
import Data.Proxy
import Data.SOP (NP(..), All, Compose)

import qualified Data.SOP                   as SOP
import qualified Generics.SOP               as SOP
import qualified Generics.SOP.Metadata      as SOP
import qualified Generics.SOP.Type.Metadata as SOP.T

import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck

import Data.Record.Generic
import Data.Record.Generic.LowerBound
import Data.Record.Generic.SOP hiding (glowerBound)
import Data.Record.TH

import qualified Data.Record.Generic.SOP as SOP
import qualified Data.Record.Generic.Rep as Rep

{-------------------------------------------------------------------------------
  Small test record
-------------------------------------------------------------------------------}

largeRecord (defaultLazyOptions { generatePatternSynonym = True }) [d|
  data T a b = MkT {
        tInt   :: Word
      , tBool  :: Bool
      , tChar  :: Char
      , tA     :: a
      , tListB :: [b]
      }
    deriving (Eq, Ord, Show)
  |]

exampleT :: T () Float
exampleT = MkT 5 True 'c' () [3.14]

_silenceWarnings :: ()
_silenceWarnings = const () $ (
      tInt
    , tBool
    , tChar
    , tA
    , tListB
    )

{-------------------------------------------------------------------------------
  Handwritten SOP instance

  This allows us to compare the untyped representation used by @large-records@
  to the strongly typed version from @generics-sop@.
-------------------------------------------------------------------------------}

instance SOP.Generic (T a b) where
  type Code (T a b) = '[[Word, Bool, Char, a, [b]]]
  from (MkT i b c a bs) = SOP.SOP (SOP.Z (I i :* I b :* I c :* I a :* I bs :* Nil))
  to (SOP.SOP (SOP.Z (I i :* I b :* I c :* I a :* I bs :* Nil))) = MkT i b c a bs
  to (SOP.SOP (SOP.S x)) = case x of {}

type DefaultBang =
       'SOP.T.StrictnessInfo
          'SOP.NoSourceUnpackedness
          'SOP.NoSourceStrictness
          'SOP.DecidedLazy

instance SOP.HasDatatypeInfo (T a b) where
  type DatatypeInfoOf (T a b) =
    'SOP.T.ADT
      "Data.Record.Generic.Sanity"
      "T"
      '[ 'SOP.T.Record "MkT" '[
          'SOP.T.FieldInfo "tInt"
        , 'SOP.T.FieldInfo "tBool"
        , 'SOP.T.FieldInfo "tChar"
        , 'SOP.T.FieldInfo "tA"
        , 'SOP.T.FieldInfo "tListB"
        ]]
      '[ '[
          DefaultBang
        , DefaultBang
        , DefaultBang
        , DefaultBang
        , DefaultBang
        ]]

  datatypeInfo _ =
      SOP.T.demoteDatatypeInfo $ Proxy @(SOP.DatatypeInfoOf (T a b))

{-------------------------------------------------------------------------------
  Tests
-------------------------------------------------------------------------------}

test_from_to_id :: Assertion
test_from_to_id =
    assertEqual "from . to = id" expected actual
  where
    expected, actual :: T () Float
    expected = exampleT
    actual   = (to . from) exampleT

test_pure :: Assertion
test_pure =
    compareTyped expected actual
  where
    expected :: NP (Field (K Char)) (MetadataOf (T () Float))
    expected =
           Field (K 'a')
        :* Field (K 'a')
        :* Field (K 'a')
        :* Field (K 'a')
        :* Field (K 'a')
        :* Nil

    actual :: Rep (K Char) (T () Float)
    actual = Rep.pure (K 'a')

test_cpure :: Assertion
test_cpure =
    assertEqual "matches hand-constructed" expected actual
  where
    expected, actual :: T () Float
    expected = MkT 0 False '\x0000' () []
    actual   = glowerBound

test_sequenceA :: Assertion
test_sequenceA =
    compareTyped expected actual
  where
    expected :: NP (Field (K Int)) (MetadataOf (T () Float))
    expected =
          flip evalState 0
        $ SOP.hsequence'
        $ SOP.hmap distrib
        $ example
      where
        distrib :: Field (State Int :.: K Int) x
                -> (State Int :.: (Field (K Int))) x
        distrib (Field (Comp x)) = Comp (Field <$> x)

    actual :: Rep (K Int) (T () Float)
    actual = flip evalState 0 $ Rep.sequenceA $ SOP.fromSOP example

    example :: NP (Field (State Int SOP.:.: K Int)) (MetadataOf (T () Float))
    example =
           Field (Comp (K <$> tick))
        :* Field (Comp (K <$> tick))
        :* Field (Comp (K <$> tick))
        :* Field (Comp (K <$> tick))
        :* Field (Comp (K <$> tick))
        :* Nil

    tick :: State Int Int
    tick = state $ \i -> (i, i + 1)

test_zipWithM :: Assertion
test_zipWithM =
    compareTyped expected actual
  where
    expected :: NP (Field (K Int)) (MetadataOf (T () Float))
    expected =
          flip evalState 0
        $ SOP.hsequence'
        $ SOP.hliftA2 tick' x y
      where
        tick' :: Field (K Int) field
              -> Field (K Int) field
              -> (State Int :.: Field (K Int)) field
        tick' (Field a) (Field b) = Comp $ Field <$> tick a b

    actual :: Rep (K Int) (T () Float)
    actual = flip evalState 0 $
        Rep.zipWithM tick (fromSOP x) (fromSOP y)

    tick :: K Int x -> K Int x -> State Int (K Int x)
    tick (K a) (K b) = state $ \i -> (K (a + b + i), i + 1)

    x, y :: NP (Field (K Int)) (MetadataOf (T () Float))
    x = Field (K 10)
     :* Field (K 11)
     :* Field (K 12)
     :* Field (K 13)
     :* Field (K 14)
     :* Nil
    y = Field (K 20)
     :* Field (K 21)
     :* Field (K 22)
     :* Field (K 23)
     :* Field (K 24)
     :* Nil

test_ord :: Word -> Word -> Bool -> Bool -> Property
test_ord w w' b b'
  | w == w' && b == b' = t1 === t2
  | w == w'            = compare t1 t2 === compare b b'
  | otherwise          = compare t1 t2 === compare w w'
  where
    t1, t2 :: T () Float
    t1 = MkT w  b  'c' () [3.14]
    t2 = MkT w' b' 'c' () [3.14]

{-------------------------------------------------------------------------------
  For testing purposes, we compare against proper heterogeneous lists
-------------------------------------------------------------------------------}

compareTyped ::
     forall f a. (
       Generic a
     , Constraints a (Compose Eq f)
     , Constraints a (Compose Show f)
     , All IsField (MetadataOf a)
     )
  => NP (Field f) (MetadataOf a) -> Rep f a -> Assertion
compareTyped expected actual =
    case toSOP actual of
      Nothing ->
        assertFailure "compareTyped: incorrect number of fields"
      Just actual' ->
        case toDictAll (Proxy @f) (Proxy @a) (Proxy @Show) of
          Dict ->
            case toDictAll (Proxy @f) (Proxy @a) (Proxy @Eq) of
              Dict -> go expected actual'
  where
    go :: ( All (Compose Eq   (Field f)) fields
          , All (Compose Show (Field f)) fields
          )
       => NP (Field f) fields -> NP (Field f) fields -> Assertion
    go = assertEqual "compareTyped"

{-------------------------------------------------------------------------------
  All tests
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "Test.Record.Sanity" [
      testCase     "from_to_id" test_from_to_id
    , testCase     "pure"       test_pure
    , testCase     "sequenceA"  test_sequenceA
    , testCase     "zipWithM"   test_zipWithM
    , testCase     "cpure"      test_cpure
    , testProperty "ord"        test_ord
    ]