{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TemplateHaskell #-} {-| Module : Test.Tasty.Lua Copyright : © 2019 Albert Krewinkel License : MIT Maintainer : Albert Krewinkel Stability : alpha Portability : Requires TemplateHaskell Convert Lua test results into a tasty test trees. -} module Test.Tasty.Lua ( -- * Lua module pushModule -- * Running tests , testFileWith , testsFromFile -- * Helpers , pathFailure ) where import Control.Exception (throw, try) import Control.Monad (void) import Data.ByteString (ByteString) import Data.FileEmbed import Data.List (intercalate) import Foreign.Lua (Lua, NumResults, Peekable, StackIndex) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text.Encoding import qualified Foreign.Lua as Lua import qualified Test.Tasty as Tasty import qualified Test.Tasty.Providers as Tasty -- | Run the given file as a single test. It is possible to use -- `tasty.lua` in the script. This test collects and summarizes all -- errors, but shows generally no information on the successful tests. testFileWith :: FilePath -> (forall a . Lua a -> IO a) -> Tasty.TestTree testFileWith fp runLua = let testAction = TestCase $ do result <- runLua (runTastyFile fp) case result >>= failuresMessage of Left errMsg -> throw (Lua.Exception errMsg) Right _ -> return () in Tasty.singleTest fp testAction newtype TestCase = TestCase (IO ()) instance Tasty.IsTest TestCase where run _ (TestCase action) _ = do result <- try action return $ case result of Left (Lua.Exception message) -> Tasty.testFailed message Right () -> Tasty.testPassed "" testOptions = return [] -- | Run tasty.lua tests from the given file. testsFromFile :: FilePath -> Lua Tasty.TestTree testsFromFile fp = do result <- runTastyFile fp case result of Left errMsg -> return $ pathFailure fp errMsg Right tree -> return $ Tasty.testGroup fp $ map testTree tree -- | Run a tasty Lua script from a file and return either the resulting -- test tree or the error message. runTastyFile :: FilePath -> Lua (Either String [Tree]) runTastyFile fp = do Lua.openlibs Lua.requirehs "tasty" (void pushModule) res <- Lua.dofile fp if res == Lua.OK then Right <$> Lua.peekList Lua.stackTop else Left . toString <$> Lua.tostring' Lua.stackTop -- | Generate a single error message from all failures in a test tree. failuresMessage :: [Tree] -> Either String () failuresMessage tree = case mapM collectFailureMessages tree of Nothing -> return () Just errs -> Left $ concatMap (concatMap stringifyFailureGist) errs -- | Failure message generated by tasty.lua type LuaErrorMessage = String -- | Info about a test failure type FailureGist = ([Tasty.TestName], LuaErrorMessage) -- | Convert a test failure, given as the pair of the test's path and -- its error message, into an error string. stringifyFailureGist :: FailureGist -> String stringifyFailureGist (names, msg) = intercalate " // " names ++ ":\n" ++ msg ++ "\n\n" -- | Extract all failures from a test result tree. collectFailureMessages :: Tree -> Maybe [FailureGist] collectFailureMessages (Tree name tree) = case tree of SingleTest Success -> Nothing SingleTest (Failure msg) -> Just [([name], msg)] TestGroup subtree -> foldr go Nothing subtree where go tree' acc = case acc of Nothing -> collectFailureMessages tree' Just errs -> case collectFailureMessages tree' of Nothing -> Just errs Just x -> Just (x ++ errs) -- | Tasty Lua script tastyScript :: ByteString tastyScript = $(embedFile "tasty.lua") -- | Push the Aeson module on the Lua stack. pushModule :: Lua NumResults pushModule = do result <- Lua.dostring tastyScript if result == Lua.OK then return 1 else Lua.throwTopMessage {-# INLINABLE pushModule #-} -- | Report failure of testing a path. pathFailure :: FilePath -> String -> Tasty.TestTree pathFailure fp errMsg = Tasty.singleTest fp (Failure errMsg) -- | Convert internal (tasty.lua) tree format into Tasty tree. testTree :: Tree -> Tasty.TestTree testTree (Tree name tree) = case tree of SingleTest outcome -> Tasty.singleTest name outcome TestGroup results -> Tasty.testGroup name (map testTree results) data Tree = Tree Tasty.TestName UnnamedTree instance Peekable Tree where peek idx = do name <- Lua.getfield idx "name" *> Lua.popValue result <- Lua.getfield idx "result" *> Lua.popValue return $ Tree name result instance Tasty.IsTest Outcome where run _ tr _ = return $ case tr of Success -> Tasty.testPassed "" Failure msg -> Tasty.testFailed msg testOptions = return [] -- | Either a raw test outcome, or a nested @'Tree'@. data UnnamedTree = SingleTest Outcome | TestGroup [Tree] instance Peekable UnnamedTree where peek = peekTree peekTree :: StackIndex -> Lua UnnamedTree peekTree idx = do ty <- Lua.ltype idx case ty of Lua.TypeTable -> TestGroup <$> Lua.peekList idx _ -> SingleTest <$> Lua.peek idx -- | Test outcome data Outcome = Success | Failure String instance Peekable Outcome where peek idx = do ty <- Lua.ltype idx case ty of Lua.TypeString -> Failure <$> Lua.peek idx Lua.TypeBoolean -> do b <- Lua.peek idx return $ if b then Success else Failure "???" _ -> do s <- toString <$> Lua.tostring' idx Lua.throwException ("not a test result: " ++ s) -- | Convert UTF8-encoded @'ByteString'@ to a @'String'@. toString :: ByteString -> String toString = Text.unpack . Text.Encoding.decodeUtf8