packages feed

hslua-0.5.0: test/HsLuaSpec.hs

module HsLuaSpec where

import Control.Monad (forM, forM_, when)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import System.Mem (performMajorGC)

import Test.Hspec
import Test.Hspec.Contrib.HUnit
import Test.HUnit

import Test.QuickCheck
import Test.QuickCheck.Instances ()
import qualified Test.QuickCheck.Monadic as QM

import Scripting.Lua

main :: IO ()
main = hspec spec

spec :: Spec
spec = do
    describe "StackValue" $ mapM_ fromHUnitTest
      [bytestring, bsShouldLive, listInstance, nulString]
    describe "Random StackValues" $ do
      it "can push/pop booleans" $ property prop_bool
      it "can push/pop ints" $ property prop_int
      it "can push/pop doubles" $ property prop_double
      it "can push/pop bytestrings" $ property prop_bytestring
      it "can push/pop lists of booleans" $ property prop_lists_bool
      it "can push/pop lists of ints" $ property prop_lists_int
      it "can push/pop lists of bytestrings" $ property prop_lists_bytestring
    describe "luaopen_* functions" $ mapM_ fromHUnitTest $ map (uncurry testOpen) $
      [ ("table", opentable), ("io", openio), ("os", openos),
        ("string", openstring), ("math", openmath), ("debug", opendebug),
        ("package", openpackage) ]
    describe "luaopen_base returns two tables" $ fromHUnitTest $ testOpenBase

bytestring :: Test
bytestring = TestLabel "ByteString -- unicode stuff" $ TestCase $ do
    l <- newstate
    let val = T.pack "öçşiğüİĞı"
    pushstring l (T.encodeUtf8 val)
    val' <- T.decodeUtf8 `fmap` tostring l 1
    close l
    assertEqual "Popped a different value or pop failed" val val'

bsShouldLive :: Test
bsShouldLive = TestLabel "ByteString should survive after GC/Lua destroyed" $ TestCase $ do
    (val, val') <- do
      l <- newstate
      let val = B.pack "ByteString should survive"
      pushstring l val
      val' <- tostring l 1
      pop l 1
      close l
      return (val, val')
    performMajorGC
    assertEqual "Popped a different value or pop failed" val val'

listInstance :: Test
listInstance = TestLabel "Push/pop StackValue lists" $ TestCase $ do
    let lst = [B.pack "first", B.pack "second"]
    l <- newstate
    pushlist l lst
    setglobal l "mylist"
    size0 <- gettop l
    assertEqual
      "After pushing the list and assigning to a variable, stack is not empty"
      0 size0
    getglobal l "mylist"
    size1 <- gettop l
    assertEqual "`getglobal` pushed more than one value to the stack" 1 size1
    lst' <- tolist l 1
    size2 <- gettop l
    assertEqual "`tolist` left stuff on the stack" size1 size2
    close l
    assertEqual "Popped a different list or pop failed" (Just lst) lst'

nulString :: Test
nulString =
  TestLabel "String with NUL byte should be pushed/popped correctly" $ TestCase $ do
    l <- newstate
    let str = "A\NULB"
    pushstring l (B.pack str)
    str' <- tostring l 1
    close l
    assertEqual "Popped string is different than what's pushed" str (B.unpack str')


-----
-- Random Quickcheck testing for StackValue instances
-----

-- Bools
prop_bool :: Bool -> Property
prop_bool = testStackValueInstance
-- Ints
prop_int :: Int -> Property
prop_int = testStackValueInstance
-- Doubles
prop_double :: Double -> Property
prop_double = testStackValueInstance
-- Bytestrings
prop_bytestring :: ByteString -> Property
prop_bytestring = testStackValueInstance
-- Lists of bools
prop_lists_bool :: [Bool] -> Property
prop_lists_bool = testStackValueInstance
-- Lists of ints
prop_lists_int :: [Int] -> Property
prop_lists_int = testStackValueInstance
-- Lists of bytestrings
prop_lists_bytestring :: [ByteString] -> Property
prop_lists_bytestring = testStackValueInstance

-- Check that the StackValue instance for a datatype works
testStackValueInstance :: (Show t, Eq t, StackValue t) => t -> Property
testStackValueInstance t = QM.monadicIO $ do
  -- Init Lua state
  l <- QM.run newstate
  -- Get an ascending list of small (1-100) positive integers
  -- These are the indices at which we will push the value to be tested
  -- Note that duplicate values don't matter so we don't need to guard against that
  Ordered indices' <- QM.pick arbitrary
  let indices = map getPositive indices'
  let nItems = if null indices then 0 else last indices
  -- Make sure there's enough room in the stack
  QM.assert =<< QM.run (checkstack l nItems)
  -- Push elements
  QM.run $ forM_ [1..nItems] $ \n ->
    if n `elem` indices
      then push l t
      else push l n
  -- Check that the stack size is the same as the total number of pushed items
  stackSize <- QM.run $ gettop l
  QM.assert $ stackSize == nItems
  -- Peek all items
  vals <- QM.run $ forM indices $ peek l
  -- Check that the stack size did not change after peeking
  newStackSize <- QM.run $ gettop l
  QM.assert $ stackSize == newStackSize
  -- Check that we were able to peek at all pushed elements
  forM_ vals $ QM.assert . (== Just t)

  -- DEBUGGING -----------------------------------------------
  -- QM.run $ putStrLn $ "Testing value -------- " ++ show t
  -- QM.run $ putStrLn $ "Indices: " ++ show indices
  -- QM.run $ putStrLn $ "StackSize: " ++ show stackSize
  -- QM.run $ putStrLn $ "NewStackSize: " ++ show newStackSize
  -- QM.run $ putStrLn $ "Peeked Values: " ++ show vals

--------------------------------------------------------------------------------
-- luaopen_* functions

testOpen :: String -> (LuaState -> IO ())  -> Test
testOpen lib openfn = TestLabel ("open" ++ lib) $ TestCase $ do
    l <- newstate
    openlibs l
    loadInspect l

    getglobal l "print"
    getglobal2 l "inspect.inspect"
    openfn l
    call l 1 1
    call l 1 0

    close l

testOpenBase :: Test
testOpenBase = TestLabel "openbase" $ TestCase $ do
    l <- newstate
    openlibs l
    loadInspect l

    getglobal l "print"
    getglobal2 l "inspect.inspect"
    getglobal l "print"
    getglobal2 l "inspect.inspect"

    openbase l
    insert l 3

    putStrLn "--- First table ---"

    call l 1 1
    call l 1 0

    putStrLn "--- Second table ---"

    call l 1 1
    call l 1 0

    return ()

loadInspect :: LuaState -> Assertion
loadInspect l = do
    loadRet <- loadfile l "test/inspect.lua"
    assertEqual "load failed" 0 loadRet
    pcallRet <- pcall l 0 multret 0

    when (pcallRet /= 0) $ do
      msg <- tostring l 1
      close l
      assertFailure ("pcall failed with " ++ show pcallRet ++ "\n" ++
                     "error message was: " ++ B.unpack msg)

    setglobal l "inspect"