{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} {-| Module : HsLua.ObjectOrientationTests Copyright : © 2007–2012 Gracjan Polak; © 2012–2016 Ömer Sinan Ağacan; © 2017-2024 Albert Krewinkel License : MIT Maintainer : Albert Krewinkel Stability : beta Portability : non-portable (depends on GHC) Test that conversions from and to the Lua stack are isomorphisms. -} module HsLua.ObjectOrientationTests (tests) where import HsLua.Core import HsLua.ObjectOrientation import HsLua.Marshalling import HsLua.Typing import Test.Tasty (TestTree, testGroup) import Test.Tasty.HsLua ((=:), shouldBeResultOf, shouldBeErrorMessageOf) import qualified Data.ByteString.Char8 as Char8 -- | Tests for HsLua object orientation. tests :: TestTree tests = testGroup "Object Orientation" [ testGroup "Sample product type" [ "tostring" =: "Foo 7 \"seven\"" `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo 7 "seven" setglobal "foo" _ <- dostring "return tostring(foo)" forcePeek $ peekText top , "show" =: "Foo 5 \"five\"" `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo 5 "five" setglobal "foo" _ <- dostring "return foo:show()" forcePeek $ peekText top , "peek" =: Foo 37 "ananas" `shouldBeResultOf` do pushUD typeFoo $ Foo 37 "ananas" forcePeek $ peekUDGeneric typeFoo top , "unknown properties have value `nil`" =: TypeNil `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo (-1) "a" setglobal "foo" dostring "return foo.does_not_exist" >>= \case OK -> ltype top _ -> throwErrorAsException , "get number" =: (-1) `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo (-1) "a" setglobal "foo" dostring "return foo.num" >>= \case OK -> forcePeek $ peekIntegral @Int top _ -> throwErrorAsException , "get number twice" =: 8 `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo 4 "d" setglobal "foo" dostring "return foo.num + foo.num" >>= \case OK -> forcePeek $ peekIntegral @Int top _ -> throwErrorAsException , "modify number" =: Foo (-1) "a" `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo 1 "a" setglobal "foo" OK <- dostring "foo.num = -1" TypeUserdata <- getglobal "foo" forcePeek $ peekUDGeneric typeFoo top , "get string" =: "lint" `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo 0 "lint" setglobal "foo" dostring "return foo.str" >>= \case OK -> forcePeek $ peekText top _ -> throwErrorAsException , "cannot change readonly string" =: "'str' is a read-only property." `shouldBeErrorMessageOf` do openlibs pushUD typeFoo $ Foo 2 "b" setglobal "foo" ErrRun <- dostring "foo.str = 'c'" throwErrorAsException :: Lua () , "Can peek after getting read-only property" =: Foo 144 "gros" `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo 144 "gros" setglobal "foo" OK <- dostring "bar = foo.str" _ <- getglobal "foo" forcePeek $ peekUDGeneric typeFoo top , "cannot change unknown property" =: "Cannot set unknown property." `shouldBeErrorMessageOf` do openlibs pushUD typeFoo $ Foo 11 "eleven" setglobal "foo" ErrRun <- dostring "foo.does_not_exist = nil" throwErrorAsException :: Lua () , "pairs iterates over properties" =: ["num", "5", "str", "echo", "show", "function"] `shouldBeResultOf` do openlibs pushUD typeFoo $ Foo 5 "echo" setglobal "echo" OK <- dostring $ Char8.unlines [ "local result = {}" , "for k, v in pairs(echo) do" , " table.insert(result, k)" , " table.insert(" , " result," , " type(v) == 'function' and 'function' or tostring(v)" , " )" , "end" , "return result" ] forcePeek $ peekList peekText top , "absent properties are not included in `pairs`" =: [("num", "number"), ("str", "string"), ("show", "function")] `shouldBeResultOf` do openlibs pushUD typeQux $ Quux 1 "a" setglobal "a" OK <- dostring $ Char8.unlines [ "local result = {}" , "for k, v in pairs(a) do result[#result+1] = {k, type(v)} end" , "return result" ] forcePeek $ peekList (peekPair peekText peekText) top ] , testGroup "Bar type" [ "Modifying a table modifies the object" =: Bar [7, 8] `shouldBeResultOf` do openlibs pushUD typeBar $ Bar [7] setglobal "bar" OK <- dostring "table.insert(bar.nums, 8)" _ <- getglobal "bar" forcePeek $ peekUDGeneric typeBar top , "Use integer index in alias" =: 42 `shouldBeResultOf` do openlibs pushUD typeBar $ Bar [42, 5, 23] setglobal "bar" OK <- dostring "return bar.first" forcePeek $ peekIntegral @Int top ] , testGroup "initType" [ "type table is added to the registry" =: TypeTable `shouldBeResultOf` do openlibs name <- initTypeGeneric (\_ -> pure ()) typeBar getfield registryindex name , "type table is not in registry when uninitialized" =: TypeNil `shouldBeResultOf` do openlibs getfield registryindex (udName (typeBar @HsLua.Core.Exception)) , "initializing does not affect the stack" =: 0 `shouldBeResultOf` do openlibs before <- gettop _ <- initTypeGeneric (\_ -> pure ()) typeBar after <- gettop return $ after - before ] , testGroup "lazy list" [ "Access an element of a lazy list stub" =: 3 `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [1,1,2,3,5,8] setglobal "list" _ <- dostring "return (list[4])" forcePeek $ peekIntegral @Int top , "Remaining list is not evaluated" =: 2 `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [1,1,2, Prelude.error "CRASH!"] setglobal "list" _ <- dostring "return (list[3])" forcePeek $ peekIntegral @Int top , "Out-of-bounds indices return nil" =: (TypeNil, TypeNil) `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [1,4,9,16] setglobal "list" _ <- dostring "return list[0], list[5]" (,) <$> ltype (nth 1) <*> ltype (nth 2) , "Last evaled index is available in __lazylistindex" =: 3 `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [9..17] setglobal "quuz" _ <- dostring "local foo = quuz[3]; return quuz.__lazylistindex" forcePeek $ peekIntegral @Int top , "__lazylistindex becomes `false` when all items are evaled" =: False `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [1..3] setglobal "quuz" _ <- dostring "local foo = quuz[3]; return quuz.__lazylistindex" forcePeek $ peekBool top , "Input can be retrieved unchanged" =: LazyIntList [9..17] `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [9..17] setglobal "ninetofive" _ <- dostring "assert(ninetofive[3] == 11); return ninetofive" forcePeek $ peekUDGeneric typeLazyIntList top , "List is writable" =: LazyIntList [1, 4, 9, 16] `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [0,4,9,16] setglobal "list" OK <- dostring "list[1] = 1; return list" forcePeek $ peekUDGeneric typeLazyIntList top , "List can be extended" =: LazyIntList [1, 4, 9, 16, 25] `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [1,4,9,16] setglobal "list" OK <- dostring "list[5] = 25; return list" forcePeek $ peekUDGeneric typeLazyIntList top , "List can be shortened" =: LazyIntList [1, 9, 27, 81] `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [1, 9, 27, 81, 243] setglobal "list" OK <- dostring "list[5] = nil; return list" forcePeek $ peekUDGeneric typeLazyIntList top , "Setting element to nil shortenes the list" =: LazyIntList [1, 9, 27] `shouldBeResultOf` do openlibs pushUD typeLazyIntList $ LazyIntList [1, 9, 27, 81, 243] setglobal "list" OK <- dostring "list[4] = nil; return list" forcePeek $ peekUDGeneric typeLazyIntList top , "Infinite lists are ok" =: 233 `shouldBeResultOf` do openlibs let fibs = 0 : 1 : zipWith (+) fibs (tail fibs) pushUD typeLazyIntList $ LazyIntList fibs setglobal "fibs" dostring "return fibs[14]" >>= \case OK -> forcePeek $ peekIntegral @Int top _ -> failLua =<< forcePeek (peekString top) ] , testGroup "possible properties" [ "tostring Quux" =: "Quux 11 \"eleven\"" `shouldBeResultOf` do openlibs pushUD typeQux $ Quux 11 "eleven" setglobal "quux" _ <- dostring "return tostring(quux)" forcePeek $ peekText top , "show Quux" =: "Quux 11 \"eleven\"" `shouldBeResultOf` do openlibs pushUD typeQux $ Quux 11 "eleven" setglobal "quux" _ <- dostring "return quux:show()" forcePeek $ peekText top , "access Quux.num" =: "12" `shouldBeResultOf` do openlibs pushUD typeQux $ Quux 12 "twelve" setglobal "quux" _ <- dostring "return quux.num" forcePeek $ peekText top , "access Quux.str" =: "thirteen!" `shouldBeResultOf` do openlibs pushUD typeQux $ Quux 13 "thirteen" setglobal "quux" _ <- dostring "return quux.num" _ <- dostring "quux.str = quux.str .. '!'; return quux.str" forcePeek $ peekText top , testGroup "alias" [ "read subelement via alias" =: 13.37 `shouldBeResultOf` do openlibs pushUD typeQux $ Quuz (Point 13.37 0) undefined setglobal "quuz" _ <- dostring "return quuz.x" forcePeek $ peekRealFloat @Double top , "set subelement via alias" =: Point 42 1 `shouldBeResultOf` do openlibs pushUD typeQux $ Quuz (Point 1 1) undefined setglobal "quuz" _ <- dostring "quuz.x = 42; return quuz.point" -- msg <- forcePeek $ peekString top -- liftIO $ putStrLn msg forcePeek $ peekPoint top , "read subelement via integer alias" =: 13.37 `shouldBeResultOf` do openlibs pushUD typeQux $ Quuz (Point 13.37 0) undefined setglobal "quuz" _ <- dostring "return quuz[1]" forcePeek $ peekRealFloat @Double top , "set subelement via integer alias" =: Point 42 1 `shouldBeResultOf` do openlibs pushUD typeQux $ Quuz (Point 1 1) undefined setglobal "quuz" _ <- dostring "quuz[1] = 42; return quuz.point" forcePeek $ peekPoint top , "non-aliased integer fields are nil" =: TypeNil `shouldBeResultOf` do openlibs pushUD typeQux (Quuz undefined undefined) setglobal "quuz" _ <- dostring "return quuz[3]" ltype top , "absent alias returns `nil`" =: TypeNil `shouldBeResultOf` do openlibs pushUD typeQux (Quux 9 "to five") setglobal "quux" dostring "return quux.x" >>= \case OK -> ltype top _ -> failLua =<< forcePeek (peekString top) , "alias can point to the element itself" =: 9 `shouldBeResultOf` do openlibs pushUD typeLazyIntList (LazyIntList [1, 1, 1, 3, 5, 9, 17, 31]) setglobal "tribonacci" dostring "return tribonacci.seq[6]" >>= \case OK -> forcePeek $ peekIntegral @Int top _ -> failLua =<< forcePeek (peekString top) ] ] ] deftype :: LuaError e => Name -- ^ type name -> [(Operation, HaskellFunction e)] -- ^ operations -> [Member e (HaskellFunction e) a] -- ^ methods -> UDType e (HaskellFunction e) a deftype = deftypeGeneric pushHaskellFunction deftype' :: LuaError e => Name -- ^ type name -> [(Operation, HaskellFunction e)] -- ^ operations -> [Member e (HaskellFunction e) a] -- ^ methods -> Maybe (ListSpec e a itemtype) -- ^ list access -> UDTypeWithList e (HaskellFunction e) a itemtype deftype' = deftypeGeneric' pushHaskellFunction -- | Pushes a userdata value of the given type. pushUD :: LuaError e => UDTypeWithList e fn a itemtype -> a -> LuaE e () pushUD = pushUDGeneric (const (pure ())) -- | Define a (meta) operation on a type. operation :: Operation -> HaskellFunction e -> (Operation, HaskellFunction e) operation = (,) -- | Sample product type data Foo = Foo Int String deriving (Eq, Show) -- | Specify behavior of Foo values in Lua. typeFoo :: LuaError e => UDType e (HaskellFunction e) Foo typeFoo = deftype "Foo" [ operation Tostring show' ] [ property "num" "some number" (pushIntegral, \(Foo n _) -> n) (peekIntegral, \(Foo _ s) n -> Foo n s) , readonly "str" "some string" (pushString, \(Foo _ s) -> s) , methodGeneric "show" show' ] where show' = do foo <- forcePeek $ peekUDGeneric typeFoo (nthBottom 1) pushString (show foo) return (NumResults 1) newtype Bar = Bar [Int] deriving (Eq, Show) typeBar :: LuaError e => UDType e (HaskellFunction e) Bar typeBar = deftype "Bar" [] [ property' "nums" (seqType integerType) "some numbers" (pushList pushIntegral, \(Bar nums) -> nums) (peekList peekIntegral, \(Bar _) nums -> Bar nums) , alias "first" "first element" ["nums", IntegerIndex 1] ] newtype LazyIntList = LazyIntList { fromLazyIntList :: [Int] } deriving (Eq, Show) typeLazyIntList :: LuaError e => UDTypeWithList e (HaskellFunction e) LazyIntList Int typeLazyIntList = deftype' "LazyIntList" [ operation Tostring $ do lazyList <- forcePeek $ peekUDGeneric typeLazyIntList (nthBottom 1) pushString (show lazyList) return (NumResults 1) ] [ alias "seq" "sequence" [] ] (Just ( (pushIntegral, fromLazyIntList) , (peekIntegral, \_ lst -> LazyIntList lst) )) -- -- Sample sum type -- data Qux = Quux Int String | Quuz Point Int deriving (Eq, Show) data Point = Point Double Double deriving (Eq, Show) pushPoint :: LuaError e => Pusher e Point pushPoint (Point x y) = do newtable pushName "x" *> pushRealFloat x *> rawset (nth 3) pushName "y" *> pushRealFloat y *> rawset (nth 3) peekPoint :: LuaError e => Peeker e Point peekPoint idx = do x <- peekFieldRaw peekRealFloat "x" idx y <- peekFieldRaw peekRealFloat "y" idx return $ x `seq` y `seq` Point x y pointType :: TypeSpec pointType = recType [ ("x", numberType) , ("y", numberType) ] showQux :: LuaError e => HaskellFunction e showQux = do qux <- forcePeek $ peekQux (nthBottom 1) pushString $ show qux return (NumResults 1) peekQux :: LuaError e => Peeker e Qux peekQux = peekUDGeneric typeQux typeQux :: LuaError e => UDType e (HaskellFunction e) Qux typeQux = deftype "Qux" [ operation Tostring showQux ] [ methodGeneric "show" showQux , property' "num" integerType "some number" (pushIntegral, \case Quux n _ -> n Quuz _ n -> n) (peekIntegral, \case Quux _ s -> (`Quux` s) Quuz d _ -> Quuz d) , possibleProperty' "str" stringType "a string in Quux" (pushString, \case Quux _ s -> Actual s Quuz {} -> Absent) (peekString, \case Quux n _ -> Actual . Quux n Quuz {} -> const Absent) , possibleProperty' "point" pointType "a point in Quuz" (pushPoint, \case Quuz p _ -> Actual p Quux {} -> Absent) (peekPoint, \case Quuz _ n -> Actual . (`Quuz` n) Quux {} -> const Absent) , alias "x" "The x coordinate of a point in Quuz" ["point", "x"] , alias (IntegerIndex 1) "The x coordinate of a point in Quuz" ["point", "x"] ]