hslua-aeson-2.0.1: test/test-hslua-aeson.hs
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-|
Copyright : © 2017–2021 Albert Krewinkel
License : MIT
Tests for Aeson–Lua glue.
-}
import Control.Monad (when)
import Data.AEq ((~==))
import Data.ByteString (ByteString)
import Data.Scientific (Scientific, toRealFloat, fromFloatDigits)
import HsLua.Core as Lua
import HsLua.Marshalling
import HsLua.Aeson
import Test.QuickCheck.Monadic (assert)
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit ((@?=), (@?), testCase)
import Test.Tasty.QuickCheck
import Test.QuickCheck.Instances ()
import qualified Data.Aeson as Aeson
import qualified Data.Vector as Vector
import qualified Test.QuickCheck.Monadic as QC
#if MIN_VERSION_aeson(2,0,0)
import Data.Aeson.Key (Key, fromText)
import qualified Data.Aeson.KeyMap as KeyMap
#else
import qualified Data.HashMap.Strict as KeyMap
#endif
-- | Run this spec.
main :: IO ()
main = defaultMain tests
-- | Aeson tests
tests :: TestTree
tests = testGroup "hslua-aeson"
[ testGroup "pushNull"
[ testCase "pushes a value that is recognized as null when peeked" $ do
val <- run @Lua.Exception (pushNull *> forcePeek (peekValue top))
val @?= Aeson.Null
, testCase "pushes a non-nil value" $ do
nil <- run @Lua.Exception (pushNull *> isnil top)
not nil @? "not of type `nil`"
, testCase "pushes a single value" $ do
diff <- run $ stackDiff pushNull
diff @?= 1
, testCase "pushes two values when called twice" $ do
diff <- run $ stackDiff (pushNull *> pushNull)
diff @?= 2
]
, testGroup "Value"
[ testProperty "can be round-tripped through the stack" $
assertRoundtripEqual pushValue peekValue
, testProperty "can roundtrip a bool nested in 50 layers of arrays" $
\b -> QC.monadicIO $ do
let go _ x = Aeson.Array $ Vector.fromList [x]
mkValue a = foldr go (Aeson.Bool a) [ (1::Int) .. 50]
x <- QC.run . run @Lua.Exception $ do
pushValue $ mkValue b
forcePeek $ peekValue top
return (x === mkValue b)
, testProperty "can roundtrip a bool nested in 50 layers of objects" $
\b -> QC.monadicIO $ do
let go _ x = Aeson.Object $ KeyMap.fromList [("x", x)]
mkValue a = foldr go (Aeson.Bool a) [ (1::Int) .. 50]
x <- QC.run . run @Lua.Exception $ do
pushValue $ mkValue b
forcePeek $ peekValue top
return (x === mkValue b)
]
, testGroup "Value component"
[ testGroup "Scientific"
[ testProperty "is converted to a Lua number" $ \x ->
luaTest "type(x) == 'number'" ("x", x, pushScientific)
, testProperty "double precision numbers can be round-tripped" $ \x ->
assertRoundtripEqual pushScientific peekScientific
(luaNumberToScientific (Lua.Number x))
, testProperty "can be round-tripped and stays approximately equal"
assertRoundtripApprox
]
, testGroup "Vector"
[ testProperty "is converted to a Lua table" $ \x ->
luaTest "type(x) == 'table'" ("x", x, pushVector pushBool)
, testProperty "can contain Bools and be round-tripped" $
assertRoundtripEqual (pushVector pushBool) (peekVector peekBool)
, testProperty "can contain Text and be round-tripped" $
assertRoundtripEqual (pushVector pushText) (peekVector peekText)
, testProperty "can contain Aeson.Value and be round-tripped" $
assertRoundtripEqual (pushVector pushValue)
(peekVector peekValue)
]
, testGroup "KeyMap"
[ testProperty "is converted to a Lua table" $ \x ->
luaTest "type(x) == 'table'" ("x", x, pushKeyMap pushText)
, testProperty "can be round-tripped with Bool values" $
assertRoundtripEqual (pushKeyMap pushBool)
(peekKeyMap peekBool)
. KeyMap.fromList
, testProperty "can be round-tripped with Text values" $
assertRoundtripEqual (pushKeyMap pushText)
(peekKeyMap peekText)
. KeyMap.fromList
, testProperty "can be round-tripped with Aeson.Value values" $
assertRoundtripEqual (pushKeyMap pushValue)
(peekKeyMap peekValue)
. KeyMap.fromList
]
]
]
assertRoundtripApprox :: Scientific -> Property
assertRoundtripApprox x = QC.monadicIO $ do
y <- QC.run $
roundtrip (pushScientific @Lua.Exception)
(peekScientific @Lua.Exception)
x
let xdouble = toRealFloat x :: Double
let ydouble = toRealFloat y :: Double
assert (xdouble ~== ydouble)
assertRoundtripEqual :: Eq a
=> Pusher Lua.Exception a -> Peeker Lua.Exception a
-> a -> Property
assertRoundtripEqual pushX peekX x = QC.monadicIO $ do
y <- QC.run $ roundtrip pushX peekX x
assert (x == y)
roundtrip :: Pusher Lua.Exception a -> Peeker Lua.Exception a -> a -> IO a
roundtrip pushX peekX x = run $ do
pushX x
size <- gettop
when (size /= 1) $
failLua $ "not the right amount of elements on the stack: " ++ show size
forcePeek $ peekX top
stackDiff :: Lua a -> Lua StackIndex
stackDiff op = do
topBefore <- gettop
_ <- op
topAfter <- gettop
return (topAfter - topBefore)
luaTest :: ByteString -> (Name, a, Pusher Lua.Exception a) -> Property
luaTest luaProperty (var, val, pushVal) = QC.monadicIO $ do
result <- QC.run . run $ do
openlibs
pushVal val *> setglobal var
_ <- dostring $ "return (" <> luaProperty <> ")"
toboolean top
assert result
luaNumberToScientific :: Lua.Number -> Scientific
luaNumberToScientific = fromFloatDigits . (realToFrac :: Lua.Number -> Double)
instance Arbitrary Aeson.Value where
arbitrary = arbitraryValue 5
#if MIN_VERSION_aeson(2,0,0)
instance Arbitrary Key where
arbitrary = fmap fromText arbitrary
instance Arbitrary a => Arbitrary (KeyMap.KeyMap a) where
arbitrary = fmap KeyMap.fromList arbitrary
#endif
arbitraryValue :: Int -> Gen Aeson.Value
arbitraryValue size = frequency
[ (1, return Aeson.Null)
, (4, Aeson.Bool <$> arbitrary)
-- Note: we don't draw numbers from the whole possible range, but
-- only from the range of numbers that Lua can handle without
-- rounding errors. This is ok, as JSON doesn't define a required
-- precision, and (usually) matches the behavior of JavaScript.
, (4, Aeson.Number . luaNumberToScientific . Lua.Number <$> arbitrary)
, (4, Aeson.String <$> arbitrary)
, (2, resize (size - 1) $ Aeson.Array <$> arbitrary)
, (2, resize (size - 1) $ Aeson.Object <$> arbitrary)
]