packages feed

sq-0.1: test/Sq/Test/Codec.hs

module Sq.Test.Codec (tree) where

import Control.Exception.Safe qualified as Ex
import Control.Monad.IO.Class
import Data.Aeson qualified as Ae
import Data.Binary qualified as Bin
import Data.Bits
import Data.ByteString qualified as B
import Data.ByteString.Lazy qualified as BL
import Data.Fixed
import Data.Functor.Contravariant
import Data.Int
import Data.Maybe
import Data.Scientific qualified as Sci
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Time qualified as Time
import Data.Time.Clock.POSIX qualified as Time
import Data.Time.Format.ISO8601 qualified as Time
import Data.Typeable
import Data.UUID.Types qualified as UUID
import Data.Word
import GHC.Generics qualified as G
import Hedgehog qualified as H
import Hedgehog.Gen qualified as H
import Hedgehog.Range qualified as HR
import Numeric.Natural
import Sq qualified
import Test.Tasty (testGroup)
import Test.Tasty.Hedgehog (testProperty)
import Test.Tasty.Runners (TestTree)

--------------------------------------------------------------------------------

tree :: IO (Sq.Pool Sq.Write) -> TestTree
tree iop =
   testGroup
      "decode . encode"
      [ t @Bool $ H.bool
      , t @UUID.UUID uuid4
      , t @Int $ H.integral HR.constantBounded
      , t @Int8 $ H.integral HR.constantBounded
      , t @Int16 $ H.integral HR.constantBounded
      , t @Int32 $ H.integral HR.constantBounded
      , t @Int64 $ H.integral HR.constantBounded
      , t @Word $ H.integral HR.constantBounded
      , t @Word8 $ H.integral HR.constantBounded
      , t @Word16 $ H.integral HR.constantBounded
      , t @Word32 $ H.integral HR.constantBounded
      , t @Word64 $ H.integral HR.constantBounded
      , t @Word64 $ H.integral HR.constantBounded
      , t @Natural $ H.integral $ HR.constant 0 maxNatural
      , t @Integer $ H.integral $ HR.constantFrom 0 minInteger maxInteger
      , t @Char H.unicode
      , t @String $ H.string (HR.constant 0 50) H.unicode
      , t @T.Text $ H.text (HR.constant 0 50) H.unicode
      , t @TL.Text $ fmap TL.fromStrict $ H.text (HR.constant 0 50) H.unicode
      , t2 @B.ByteString $ H.bytes (HR.constant 0 50)
      , t2 @BL.ByteString $ fmap BL.fromStrict $ H.bytes (HR.constant 0 50)
      , t2 @Time.UTCTime $ genUTCTime (HR.constantFrom epochUTCTime minUTCTime maxUTCTime)
      , t @Double $ H.double (HR.constantFrom 0 (fromIntegral minInteger) (fromIntegral maxInteger))
      , t @Float $ H.float (HR.constantFrom 0 (fromIntegral minInteger) (fromIntegral maxInteger))
      , t @Sci.Scientific $ genScientific (HR.constantFrom 0 (-10) 10) HR.constantBounded
      , t @(Fixed E0) $ genFixed (HR.constantFrom 0 minInteger maxInteger)
      , t @(Fixed E2) $ genFixed (HR.constantFrom 0 minInteger maxInteger)
      , t @(Fixed E9) $ genFixed (HR.constantFrom 0 minInteger maxInteger)
      , -- TODO FAIL: , testProperty "Char" $ t @Char (pure '\55296')
        tGeneric
      ]
  where
   t
      :: forall a
       . ( Typeable a
         , Eq a
         , Show a
         , Sq.EncodeDefault a
         , Sq.DecodeDefault a
         , Bin.Binary a
         , Ae.ToJSON a
         , Ae.FromJSON a
         )
      => H.Gen a
      -> TestTree
   t ga =
      testGroup
         (tyConName (typeRepTyCon (typeRep ga)))
         [ testGroup "raw" [t2 ga]
         , testGroup "binary" [t2 (WrapBinary <$> ga)]
         , testGroup "aeson" [t2 (WrapAeson <$> ga)]
         ]

   t2
      :: forall a
       . (Typeable a, Eq a, Show a, Sq.EncodeDefault a, Sq.DecodeDefault a)
      => H.Gen a
      -> TestTree
   t2 ga =
      testGroup
         (tyConName (typeRepTyCon (typeRep ga)))
         [ testProperty "pure" $ H.property do
            a0 <- H.forAll ga
            let Sq.Encode g = Sq.encodeDefault
            case g a0 of
               Left e0 -> Ex.throwM e0
               Right raw -> do
                  let Sq.Decode f = Sq.decodeDefault
                  case f raw of
                     Left e -> Ex.throwM e
                     Right a1 -> a0 H.=== a1
         , testProperty "db" $ H.property do
            p <- liftIO iop
            a0 <- H.forAll ga
            a1 <- Sq.read p $ Sq.one idStatement a0
            a0 H.=== a1
         ]
   tGeneric :: TestTree
   tGeneric =
      testGroup
         "Generic InputDefault/OutputDefault"
         [ testProperty "Foo" $ H.property do
            p <- liftIO iop
            a0 <- H.forAll genFoo
            a1 <- Sq.read p do
               Sq.one
                  ( Sq.readStatement
                     Sq.inputDefault
                     Sq.outputDefault
                     "SELECT $x AS x, $y AS y"
                  )
                  a0
            Foo0{x = a0.x, y = a0.y} H.=== a1
         ]

newtype WrapBinary a = WrapBinary a
   deriving newtype (Eq, Show)

instance (Bin.Binary a) => Sq.EncodeDefault (WrapBinary a) where
   encodeDefault = Sq.encodeBinary >$$< \(WrapBinary a) -> a

instance (Bin.Binary a) => Sq.DecodeDefault (WrapBinary a) where
   decodeDefault = WrapBinary <$> Sq.decodeBinary

newtype WrapAeson a = WrapAeson a
   deriving newtype (Eq, Show)

instance (Ae.ToJSON a) => Sq.EncodeDefault (WrapAeson a) where
   encodeDefault = Sq.encodeAeson >$$< \(WrapAeson a) -> a

instance (Ae.FromJSON a) => Sq.DecodeDefault (WrapAeson a) where
   decodeDefault = WrapAeson <$> Sq.decodeAeson

idStatement
   :: (Sq.EncodeDefault x, Sq.DecodeDefault x)
   => Sq.Statement Sq.Read x x
idStatement =
   Sq.readStatement
      (Sq.input "a" (Sq.input "b" "c"))
      (Sq.output "x" (Sq.output "y" "z"))
      "SELECT $a__b__c AS x__y__z"

uuid4 :: (H.MonadGen m) => m UUID.UUID
uuid4 =
   UUID.fromWords64
      <$> H.integral HR.constantBounded
      <*> H.integral HR.constantBounded

maxNatural :: Natural
maxNatural = 2 ^ (256 :: Int) - 1

maxInteger :: Integer
maxInteger = 2 ^ (255 :: Int) - 1

minInteger :: Integer
minInteger = complement maxInteger

minUTCTime :: Time.UTCTime
minUTCTime = fromJust $ Time.iso8601ParseM "-9999-01-01T00:00:00Z"

maxUTCTime :: Time.UTCTime
maxUTCTime = fromJust $ Time.iso8601ParseM "9999-12-31T24:00:00Z"

epochUTCTime :: Time.UTCTime
epochUTCTime = posixPicoSecondsToUTCTime 0

genUTCTime :: (H.MonadGen m) => H.Range Time.UTCTime -> m Time.UTCTime
genUTCTime =
   fmap posixPicoSecondsToUTCTime
      . H.integral
      . fmap utcTimeToPOSIXPicoSeconds

utcTimeToPOSIXPicoSeconds :: Time.UTCTime -> Integer
utcTimeToPOSIXPicoSeconds t = i
  where
   MkFixed i = Time.nominalDiffTimeToSeconds $ Time.utcTimeToPOSIXSeconds t

posixPicoSecondsToUTCTime :: Integer -> Time.UTCTime
posixPicoSecondsToUTCTime =
   Time.posixSecondsToUTCTime . Time.secondsToNominalDiffTime . MkFixed

genScientific
   :: (H.MonadGen m)
   => H.Range Integer
   -> H.Range Int
   -> m Sci.Scientific
genScientific rc re = Sci.scientific <$> H.integral rc <*> H.integral re

genFixed :: (H.MonadGen m) => H.Range Integer -> m (Fixed e)
genFixed ri = MkFixed <$> H.integral ri

-- genRational :: (H.MonadGen m) => m Rational
-- genRational = do
--    n <- genInteger
--    d <- H.integral $ H.linear 1 (10 ^ (10 :: Int))
--    pure (n % d)

data Foo
   = Foo0 {x :: Int, y :: String}
   | Foo1 {x :: Int, y :: String}
   deriving (Eq, Show, G.Generic, Sq.InputDefault, Sq.OutputDefault)

genFoo :: (H.MonadGen m) => m Foo
genFoo = do
   x <- H.integral HR.constantBounded
   y <- H.string (HR.constant 0 50) H.unicode
   H.element [Foo0 x y, Foo1 x y]